-
Notifications
You must be signed in to change notification settings - Fork 4
Unity3D integration
What follows is the full tutorial included in the Unity3D plugin package.
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.
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.
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.
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));
}
}
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.
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 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).
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) }
};
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();
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).
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.
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.
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.
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.
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.