Skip to content

Unity3D integration

eugeniusfox edited this page Apr 12, 2017 · 4 revisions

What follows is the full tutorial included in the Unity3D plugin package.

VGPrompter

VGPrompter takes a script and feeds dialogue lines to game characters and interactive menus to the player.

This plugin is currently in beta.

VGPrompter is a C# library which parses an extended subset of Ren'Py script syntax and allows to iterate over the lines according to:

  • flow control statements (conditions are C# parameterless methods referenced in the script);
  • user input provided to interactive menus.

The source code for this plugin is available on GitHub. Full documentation is available here.

Quickstart

We'll go through the demo project included with the plugin (or a slightly simplified version thereof). It's a really, really simple visual novel in which the player is asked to change the color of a spinning cube via an interactive menu.

Writing the script

If you're familiar with the Ren'Py script syntax, this will require no other explanation than "it's the same only without Python expression evaluation" (with a few caveats).

Take a look at this example:

# A random comment

label start:
    eu "Hi there."
    eu "Let me show you the power of spinning cubes!"
    eu "Pick a color, would you?"

    while True:
        menu:
            "Green" if CurrentColorNotGreen:
                TurnCubeGreen
            "Blue":
                TurnCubeBlue
            "The prohibited color!" if False:
                DoNothing
            "Not interested, thanks":
                eu "That's enough for today."
                jump another_scene

        "Come on, another one!"

    eu "Cool, ah?"

label another_scene:
    return

Here we have two 'scenes', start and another_scene; by default, VGPrompter will start from the one named 'start'.

The first three lines are dialogue lines spoken by the character that we are going to associate with the dialogue attribution tag eu (Euphrasius maybe?).

Then we have a theoretically infinite loop over a menu; the player will be presented with a few choices, and depending on which one she'll choose, she'll get a different outcome. Choices can be made available only if a certain condition is met: e.g., the green choice requires CurrentColorNotGreen to be true, that is to say, the player should not be allowed to turn the cube green if it already is green; later on we'll see how to define said condition and how to define what happens if the condition is not met.

The prohibited choice will never be available to the player.

The green and blue choices bring the player to lines which are not dialogue lines, but references to arbitrary actions that can be defined pretty much like conditions; we'll discuss these together later on.

Since we are in a loop, after each choice, the player will be presented with the "Come on, another one!" line (in the loop but out of the menu) and then again with the same menu.

Since the while condition will never turn false and, as in Ren'Py, there is no break statement, the only way to move forward is to use a go-to statement. Here we use jump, which simply brings the player to the label it points to. If you wanted to go back to the line following the go-to after the target label has returned (simply by exhausting its lines), you'd need to use call instead.

When the player will say she's not interested anymore, she'll get to the second (and last) label, which does absolutely nothing. The return keyword at the end of a label is completely unnecessary, since it works as a global break statement of sorts.

An important consequence of all that has been discussed thus far is that Euphrasius will never utter the word 'cool'.

The syntax is more thoroughly discussed here.

Script files must have the rpy extension.

Using the script

Now that the script is complete, we must turn to a behaviour script to load it in Unity3D:

using VGPrompter;

public class ScriptManager : MonoBehaviour {

    public string script_path;

    Script script;

    void Start() {

        script = Script.FromSource(script_path);

        var actions = new Dictionary<string, Action>() {
            { "DoNothing", () => { } },
            { "TurnCubeGreen", () => ChangeCubeColor(Color.green) },
            { "TurnCubeBlue", () => ChangeCubeColor(Color.blue) }
        };

        var conditions = new Dictionary<string, Func<bool>>() {
            { "True", () => true },
            { "False", () => false },
            { "CurrentColorNotGreen", () => cube.CurrentColor != Color.green }
        };

        script.SetDelegates(conditions, actions);

        script.Prime();
        script.Validate();

        StartCoroutine(script.GetCurrentEnumerator(OnLine, SelectChoice, OnChoiceSelected, OnReturn));
    }
}

Loading

First, you need to provide a path to the folder containing your .rpy script files (or to a single .rpy file; the FromSource method accepts either).

script_path = @"Assets/Demo/VGPScript/";
script = Script.FromSource(script_path);

The Script object now stored in the 'script' variable can be serialized to a binary file and reloaded from there; for further information, see the documentation here and here.

Initialization

Now it's the time to define the conditions and actions we referenced in the script. The aliases used in the script must be associated to corresponding methods with the appropriate signature.

Conditions

Conditions are methods with no arguments that return a bool; e.g.:

var conditions = new Dictionary<string, Func<bool>>() {
    { "True", () => true },
    { "False", () => false },
    { "CurrentColorNotGreen", ColorNotGreen }
};
bool ColorNotGreen() {
    return cube.CurrentColor != Color.green
}

Remember that True and False must be included in the dictionary if you intend to use them in the script (they get no special treatment).

Actions

Simpler still, actions are methods with no arguments that return nothing.

var actions = new Dictionary<string, Action>() {
    { "DoNothing", () => { } },
    { "TurnCubeGreen", () => ChangeCubeColor(Color.green) },
    { "TurnCubeBlue", () => ChangeCubeColor(Color.blue) }
};

Setup

Now that everything is in place, a few magic lines to put everything together.

First, the dictionaries we just defined must be passed to the script:

script.SetDelegates(conditions, actions);

Then, the methods we defined must be bound to the corresponding script lines:

script.Prime();

Finally, we check that the Script object has all that it needs to run:

script.Validate();

Execution

Now the Script object is ready to run:

StartCoroutine(script.GetCurrentEnumerator(OnLine, SelectChoice, OnChoiceSelected, OnReturn));

The GetCurrentEnumerator method requires four coroutines to define what happens when the Script object returns either a dialogue line or a menu, or simply reaches its end (not necessarily because of a return statement).

OnLine

This coroutine allows to customize how a dialogue line gets processed in your game:

IEnumerator OnLine(Script.DialogueLine line) {
    textbox.text = line.Text;
    yield return new WaitForSecondsRealtime(2f);
}

In this demo, the OnLine method receives a dialogue line and displays its text in a textbox; then, after two seconds, the iteration will move to the next line.

The Script.DialogueLine class has two public properties:

  • Tag (string): dialogue attribution tag;
  • Text (string): line.

SelectChoice

This coroutine is about showing the player the menu and letting her pick one choice. It must return a nullable integer, representing the index of the choice (which may not be the same as its index in the list that gets displayed to the player; always return the Index property of the Choice object to be sure).

IEnumerator<int?> SelectChoice(Script.Menu menu) {

    // Show the menu and return the choice index once selected

    int n = 0;

    textbox.text = string.Join("\n",
        menu.TrueChoices
            .Select((x, i) => string.Format("[{0}] {1}", i, x.Text))
            .ToArray());

    while (!(DigitWasPressed(ref n) && n >= 0 && n < menu.TrueCount))
        yield return null;

    yield return menu.TrueChoices[n].Index;
}

Here we use the same old textbox to display all the available choices (i.e., they either have no condition or their condition evaluates to true) and then we wait until the player presses a number key (as long as it does not go out of range). When she does, we return the index of the selected choice.

Alternatively, we could have displayed all the choices but have grayed out the unavailable ones.

The Script.Menu class has four public properties:

  • Choices (List<Script.Choice>): all choices;
  • TrueChoices (List<Script.Choice>): all available choices;
  • Count (int): number of choices;
  • TrueCount (int): number of available choices.

OnChoiceSelected

This coroutine allows to define what happens once the player makes her choice.

IEnumerator OnChoiceSelected(Script.Menu menu, Script.Choice choice) {
    textbox.text = choice.ToString();
    yield return new WaitForSecondsRealtime(2f);
}

Here we simply set the textbox to the choice text and wait two seconds.

The Script.Choice class has four public properties:

  • Tag (string): tag;
  • Text (string): text;
  • IsTrue (bool): whether the choice is available or not;
  • Index (int): index in the menu.

OnReturn

This coroutine defines what happens after the iteration over the Script object has ended.

IEnumerator OnReturn() {
    textbox.text = "The End";
    yield return new WaitForSecondsRealtime(2f);
}

Here we just set the textbox to "The End" and then wait a bit and finally we're done.

Logging and Debugging

If the parser is unhappy with your script, you can let it tell you why in detail (hopefully) by setting up its logger so that it outputs to the Unity3D console:

Script.Parser.Logger = new VGPrompter.Logger("Parser", Debug.Log);

The script carries a logger as well:

script.Logger = new VGPrompter.Logger("Debug", Debug.Log);

Logger objects may be disabled by setting their Enabled property to false.

By default, the loggers are enabled and use the System.Console.WriteLine method. Currently, the name assigned to the logger is not relevant.