jcl - 11/25/01
The Ren'ai engine is a program that interprets a Lua program and carries out its various graphics, audio, and input commands. By itself, the Ren'ai engine can be used to implement many different kinds of games, but creating any specific genre of game, such as, say, a dating simulation, would be quite difficult because the engine doesn't provide any genre-specific support. The Ren'ai scene interpreter is a library written in Lua to make the job of creating a dating simulation-type game much more intuitive.
At any time a game using the scene interpreter is running, the game is in a "scene". A scene could represent the different things that happen on a date with an NPC, but it could also represent a room with several different exits, or a menu with several options. The actions that the user performs during a scene can trigger other scenes, which is how gameplay progresses.
The scene interpreter begins interpreting a special scene named "introduction". After the interpreter is finished interpreting a scene:
The current game state is stored in a global variable called state. This includes things like the current location and time as well as the status variables of the player and NPCs. When a game is saved, the information in the state variable is what gets saved into a file. Likewise, when the game is loaded, the contents of the state variable are restored from this file.
Another special global variable, settings, contains game settings that persist between games, like window or sound options. The contents of this variable are saved just before quitting and are restored when the interpreter is run again.
NOTE: The settings and state variables should only contain "simple values", such as numbers, strings, or tables of "simple values". Also, circularly referenced tables should not be stored in these variables. (If you don't know what that means, you don't need to worry about it.)
Special state attributes:
|A string indicating the current location.|
|A string indicating the current time.|
|The name of the previous script.|
|The name of the current script.|
|The name of the next script to interpret.|
|A table of character tables, indexed by name. The variable NPCs in a scene can also be indexed by number, e.g. state.npcs gives the state of NPC1.|
|The character's name.|
|Indicates how to draw a character.|
|A table of locations indexed by name.|
|The location's name.|
|A table counting the number of times a scene has played, indexed by scene name. Unplayed scenes are not in the table.|
A scene can specify several built-in prerequisites (location, time, timeline, and freq), and it can specify a general prerequisite function (prereq):
What if the interpreter is searching for scene to execute, and there is more than one scene whose prerequisites are satisfied? In this case, the interpreter uses the priority and probability attributes of a scene to break the tie.
First, a little Lua syntax. Indentation isn't required, but it is recommended for making the script easier to read. Identifiers (function names, variables) can contain upper and lowercase letters, numbers, and underscores, although they can't start with a digit. Strings are delimited with double quotes ("string") or single quotes ('string'); if the string contains a delimiting character, it must be "escaped" with a backslash ('It\'s a beatiful day'). Double- and single-quote-delimited strings can't contain line breaks, but another delimiting method, double brackets ([[string]]), can. Arbitrary whitespace (line breaks, spaces, and tabs) are allowed between Lua tokens. Any double hyphens (--) outside of a string mean that the rest of the line is a comment:
x = "hello!" -- This is a comment.
The scene is defined by calling Lua functions in the scene interpreter library. The general method for making a function call in Lua is:
functionName(param1, param2, param3)
Lua also, however, allows some shortcuts that make the scene definition clearer. For instance, if you only have to pass one string to a function, this:
is equivalent to this:
location "behind school"
The latter is easier to read, so we will use it whenever possible.
The general format of a scene definition is:
startScene "Scene name" -- Scene setup (location, etc.) startScript() -- Scene script (dialogue, decisions, etc.) endScene()
In the setup portion of the scene, you must declare the actors that appear in the scene, including actors that only appear partway through the scene. This is done by calling the actor function with the nickname of the character (specification to be written):
actor "Nanami" actor "Hiro" actor "Takashi"
NOTE: Names are case-sensitive.
You can also declare multiple actors at once by passing multiple actor names. For instance, the following is equivalent to the above:
actor("Nanami", "Hiro", "Takashi")
You can also define up to four "variable" non-player characters, for scenes where one of several characters might participate, by using one of the actorNPC functions:
You can specify the actor's name to be a constant string, as above, but that defeats the purpose of having variable NPCs. Rather, you want to be able to select the character dynamically, using a Lua function, as described later:
actorNPC2(function () if state.friend == "Yui" then return "Yui" else return "Ikue" end end)
This declaration sets NPC2 to be "Yui", if you have set the "friend" member of the game state equal to "Yui"; otherwise, it sets NPC2 to "Ikue". Later in the script, when you write lines for NPC2, Yui or Ikue will speak the lines, depending on which you chose. Likewise, if you make modifications to variables in state.npcs, the changes will be reflected in state.npcs.Yui or state.npcs.Ikue, respectively.
If you try to use an actor in the scene that you have not declared using the actor or actorNPC functions, the interpreter will raise an error when it tries to read the script.
NOTE: Declaring the same actor using both actor and actorNPC statements in the same scene may have unpredictable results and should be avoided.
After declaring the scene attributes (using location, priority, etc.) and declaring the actors, you can use several functions to set up the scene prior to actually displaying it on the screen. These functions can also be used within the scene itself:
The enter and leave functions allow you to specify which actors are visible to the player. When placed prior to the start of the script, they specify which actors will be visible when the script starts. When placed in the body of the script, they indicate that the specified actor should move onto or off of the screen (with an appropriate delay). The default transition is for the actor to immediately appear or disappear; transition options may be added later.
During the game, you can make changes to game state variables with the set command:
-- Sets state.npcs.Nanami.love to 5: set("npcs.Nanami.love", 5)
The set function works both for numeric and string values. You can also add to a numeric state variable using the add function:
-- Adds 10 to Nanami's love: add("npcs.Nanami.love", 10)
Likewise, you can use the sub function to perform subtraction:
-- Subtracts 10 from Nanami's appearance: sub("npcs.Nanami.appearance", 10)
The set, add, and sub functions are valid both before and during the script, and they can be used to adjust any state values, not just the values associated with characters declared with the actor and actorNPC functions.
The set function performs actions for some special state values:
-- Always restart the "love theme" music: set("music", nil) set("music", "love theme")
Within the body of the script, you can prompt a character to say something by typing their name, then the string they should say:
Nanami "Hi! How are you doing?"
You can prompt characters declared with the actorNPC function to say things by using the corresponding NPC function:
NPC1 "I need a hug."
There are also two "special" characters, PC and Narr. PC's lines are those said by the character, while Narr's lines are those said by the game to the player:
PC "Hmm, I wonder if I added too much..." Narr "The chemistry lab blows up!"
NOTE: Aside from PC and Narr, only characters who have been declared via actor or actorNPC are allowed to say lines in a scene. Characters that are offstage are allowed to say lines. Character names are case-sensitive.
(Specification for changing font colors, styles, and inserting pauses is yet to be written.)
Most of the arguments to the scene definition functions can be passed either values or arbitrary Lua functions that are interpreted at run time. This allows you to implement scenes that adjust themselves to the current game state:
Nanami(function() local aspirinLeft = state.npcs.Nanami.aspirin return format("OK, but I only have %d left.", aspirinLeft) end)
You cannot pass functions to freq, location, time, timeline, or actor (maybe more?). Also, the functions that are passed should not have side effects. That is, they should not modify the game state; all variables set within them should be local (as above), and they should not call other functions that modify game state.
You can use the doFunc function to specify the execution of arbitrary functions with side effects:
-- Nanami eats. doFunc(function() local Nanami = state.npcs.Nanami if Nanami.hunger < Nanami.food then Nanami.food = Nanami.food - Nanami.hunger Nanami.hunger = 0 else Nanami.hunger = Nanami.hunger - Nanami.food Nanami.food = 0 end end)
NOTE: The functions used for defining a scene (such as set, PC, startScene, etc.) should not be called from within a Lua function!
If you are writing a function that simply returns the value of a Lua expression, you can use the f function to simplify it. The f function takes a string that is a Lua expression and returns a function that returns that expression. For example, this:
PC(function() return format("I only have $%d left.", state.cash) end)
is equivalent to any of these:
PC(f("format(\"I only have $%d left.\", state.cash)")) PC(f 'format("I only have $%d left.", state.cash)') PC(f [[format("I only have $%d left.", state.cash]])
You can create sections of scripts that only get executed if a Lua function evaluates to true.
If startCase is passed a string, it turns it into a function like the f function does.
PC "Wanna grab something to eat?" startCaseList() startCase [[state.npcs.Nanami.love > 15]] Nanami "Sure!" set("nextScene", "Coffeeshop date with Nanami") endCase() startCase [] -- This case will always execute if the above case didn't. Nanami "Some other time, perhaps." sub("npcs.Nanami.love", 10) endCase() endCaseList()
Script choices are the primary means of player input. There are two types of choices possible: graphical and textual. Graphical choices involve the user clicking on a character or on an object in the background, while textual choices involve the user selecting a text string from a collection of text strings. Both types of choices can provide the user with an additional prompt string.
An example of a set of text choices:
Nanami "Want to go out?" startTextChoices "You reply:" startChoice "Sure." set("nextScene", "Diner with Nanami") Nanami "Great!" endChoice() startChoice "Maybe later." Nanami "OK, see you later, then." sub("npcs.Nanami.interest", 10) set("nextScene", "At home") endChoice() endTextChoices()
Graphical choices are specified largely the same way. The prompt strings correspond to named clickable regions defined in the location specification (specification to be written). The strings can also indicate, via a character name or NPC1 to NPC4, the result of clicking on a character.
startClickChoices "Where do you want to go?" startChoice "Left door" set("location", "School roof") endChoice() startChoice "Right door" set("location", "Cafeteria") endChoice() endClickChoices()
There are situations where you might want to stop the interpretation of the current scene and move on to the next scene (inside a conditional section, for instance). You can use the nextScene function to do this:
startCaseList() startCase [[state.npcs.Nanami.stamina < 20]] Nanami "I think we should go home now." nextScene() endCase() endCaseList() Nanami "Where to next?"
The nextScene function can take an optional argument specifying the name of the next scene; this sets the value of state.nextScene. For example, this:
is equivalent to this:
set("nextScene", "The playground") nextScene()
To introduce a pause in a script, use the delay function. Its parameter is the delay time in milliseconds (thousandths of a second):
-- Delay two and a half seconds: delay(2500)
Some of the scripting functions introduce default delays or transitions during execution, such as the changing of a location or the entrance or exit of a character. To tell the interpreter to make these delays or transitions instantaneous, you can enclose a section of a script in disablePause and enablePause functions:
disablePause() leave "Nanami" enter "Emiko" enablePause() Narr "Nanami is replaced by Emiko!"
If we didn't have the disablePause and enablePause functions in this example, the player would see Nanami leave, a pause, then Emiko entering. By including the disablePause and enablePause functions, the player will see Nanami immediately replaced by Emiko (although the desired effect is dependent on the characters' locations on the screen, which has yet to be specified).
NOTE: You should not place script lines or player choices within disablePause and enablePause functions.
You can set the current background music using set. See the section on setting attributes for details.
Sounds are played using the playSound function. The function takes a second parameter indicating the time to delay interpretation after the sound starts, as measured in milliseconds. If the delay time is negative, the interpreter waits until the sound ends before continuing.
-- Wait half a second before playing a cheer. playSound("School bell", 500) playSound "Cheer" -- No wait time specified, so Nanami starts talking as the cheer starts. Nanami "Yay! School's out!"
The interpreter has standard routines for saving and loading games that handle all the complicated stuff like choosing a slot or prompting for a description. As described in the game state section, the saveGame function saves the contents of the state global variable, while the loadGame function restores it. After loading, the interpreter resumes interpretation at the start of the scene given in state.nextScene, so be sure it is set to the desired value before calling saveGame. To run the save or load routine, call the function saveGame or loadGame, respectively:
set("nextScene", "In the dragon's lair") startChoiceList "Want to save the game?" startChoice "Yes" saveGame() Narr "Game saved." endChoice() startChoice "No" endChoice() endChoiceList()
You can tell the interpreter to quit by calling quitGame.Other specifications: - Location - Characters/Outfits - "Freezing" attributes - Sound/Music