== Lesson 16: Branching image::learn/16-branching.png[The finished lesson, 480] A branching video game is a film with forks in it. A scene plays, the player chooses, and a different scene plays next. Do that twenty or thirty times and you have a game people will replay for years trying to find the ending they have not seen. In this lesson you will build one. It has four scenes, two choices, a timer on each choice, a death, and three lives. The code that plays it is about thirty lines, and it would still be about thirty lines if the game had two hundred scenes -- because the game itself will not be written in code at all. === Imagine It Is a Dragon The video everybody has is `Singe/menuBackground.mkv`, the same 420 frames you have been driving since lesson fourteen. It is a ball of fire, a dragon made of paper, the word `SINGE`, and then a magenta grid rolling away under an orange sun forever. It is not a drama. There is nobody in it to be in peril, and the sunset does not care what you decide. Play along anyway. The dragon is real enough for two minutes, and everything you are about to write works exactly the same when the footage is a stunt performer falling off a cliff. Here is the map, and the frame numbers are the ones you found for yourself in lesson fourteen: [cols="1,1,3",options="header"] |=== | Scene | Frames | What happens | `arrive` | 0 to 119 | Fire, and the dragon lands. *Left or Right?* | `sunset` | 120 to 259 | It breathes fire. *Duck left, or run right?* | `pit` | 260 to 339 | You are eaten. You lose a life. | `escape` | 340 to 410 | The grid rolls away and you got out. |=== Left is always right, so to speak: left from `arrive` goes to `sunset`, left from `sunset` gets you to `escape`. Right goes to the `pit` both times, and so does taking too long. === The Map Is Data, Not Code Here is the whole game, and it is a table. [source,lua] ---- local scenes = { arrive = { first = 0, last = 119, ask = 30, prompt = "The dragon lands. Left or Right?", left = "sunset", right = "pit", goesTo = "pit" }, sunset = { first = 120, last = 259, ask = 170, prompt = "It breathes fire. Duck Left or run Right?", left = "escape", right = "pit", goesTo = "pit" }, pit = { first = 260, last = 339, death = true, goesTo = "arrive" }, escape = { first = 340, last = 410 } } ---- A table with names down the left instead of numbers, exactly as in lesson six, and every value in it is another table. Each of those inner tables is a *record*: one scene, described field by field. `scenes.arrive` is the first scene. `scenes.arrive.first` is `0`. `scenes.arrive.left` is the string `"sunset"`, which is the name of another scene in the same table -- which is how the forks are written down. A scene does not contain the scene after it. It contains the *name* of the scene after it, and the name is looked up when the time comes. The fields mean this: `first` and `last`:: The frames the scene runs between. `ask`:: The frame the question appears on. The player has from `ask` to `last` to answer, which is the timer. `prompt`:: What the question says. `left` and `right`:: The scene each answer leads to. `goesTo`:: Where the scene goes when nobody chose -- because the player ran out of time, or because there was no question in the first place. For `pit`, which is a death, it is where the retry goes back to. `death`:: Present, and `true`, only on a scene that costs a life. Three things are worth noticing about that table. The first is that it is the design document. You can hand it to somebody who has never written a line of Lua and they can read the game out of it. When you want the dragon to fork three ways, you add a field. When you want another scene, you add another record. You do not go looking for the code that plays scenes, because you do not change it. The second is that the records are not all the same shape. `pit` has no `ask`, no `prompt`, and no `left` -- there is nothing to choose in it. `escape` has no `goesTo`, because it is the end and goes nowhere. A field that is not in a table reads as `nil`, which is the word you met in lesson six for "nothing is there", and asking for `scenes.escape.goesTo` is not an error. It is `nil`, and `nil` is an answer. Code that reads these records has to be ready for that, and in a minute you will see the one line that is. The third is what the table does *not* contain: nothing in it draws, plays, or waits. It is a description. That separation is the entire trick of this lesson. === One Function That Plays Any Scene This is the code half, and there is only one function of any size. Written in the threaded model from lesson fifteen, because a branching game is the most sequential thing there is. [source,lua] ---- local function playScene(name) local scene = scenes[name] discSkipToFrame(scene.first) while discGetFrame() < scene.last do local frame = discGetFrame() local asking = scene.ask ~= nil and frame >= scene.ask if not asking then answer = nil elseif answer == "left" or answer == "right" then return scene[answer] end drawScene(scene, frame, asking) singeYield() end discPause() if scene.death then lives = lives - 1 if lives == 0 then ending = "The dragon wins. Game over." return nil end waitForGo("Press the space bar to try again.") end return scene.goesTo end ---- It takes the name of a scene, plays it, and returns the name of the scene that comes next. That is its whole contract, and the game is then three lines: [source,lua] ---- function singeMain() local scene = "arrive" while scene ~= nil do scene = playScene(scene) end waitForGo(ending .. " Space to quit.") end ---- Start at `arrive`. Play whatever scene you are holding, and hold whatever it hands back. When it hands back `nil` there is no next scene, the loop ends, and the game is over. === The Whole Thing Make `dragon.singe` in your `movie` folder and type this in. It is the longest script in the book so far, and every piece of it has been explained except the two small helpers, which come after. [source,lua] ---- local FPS = 30 local scenes = { arrive = { first = 0, last = 119, ask = 30, prompt = "The dragon lands. Left or Right?", left = "sunset", right = "pit", goesTo = "pit" }, sunset = { first = 120, last = 259, ask = 170, prompt = "It breathes fire. Duck Left or run Right?", left = "escape", right = "pit", goesTo = "pit" }, pit = { first = 260, last = 339, death = true, goesTo = "arrive" }, escape = { first = 340, last = 410 } } local answer = nil local ending = "You got out alive." local lives = 3 local function drawScene(scene, frame, asking) overlayClear() overlayPrint(2, 2, "Lives: " .. lives) if scene.death then overlayPrint(2, 4, "That did not go well.") elseif asking then overlayPrint(2, 4, scene.prompt) overlayPrint(2, 6, math.ceil((scene.last - frame) / FPS) .. " seconds left.") end end local function waitForGo(text) answer = nil while answer ~= "go" do overlayClear() overlayPrint(2, 2, "Lives: " .. lives) overlayPrint(2, 4, text) singeYield() end end local function playScene(name) local scene = scenes[name] discSkipToFrame(scene.first) while discGetFrame() < scene.last do local frame = discGetFrame() local asking = scene.ask ~= nil and frame >= scene.ask if not asking then answer = nil elseif answer == "left" or answer == "right" then return scene[answer] end drawScene(scene, frame, asking) singeYield() end discPause() if scene.death then lives = lives - 1 if lives == 0 then ending = "The dragon wins. Game over." return nil end waitForGo("Press the space bar to try again.") end return scene.goesTo end function onInputPressed(what) if what == SWITCH_LEFT then answer = "left" elseif what == SWITCH_RIGHT then answer = "right" elseif what == SWITCH_BUTTON1 then answer = "go" end end function singeMain() local scene = "arrive" while scene ~= nil do scene = playScene(scene) end waitForGo(ending .. " Space to quit.") end dofile("Singe/Framework.singe") ---- Run it: ---- Singe -R -v Singe/menuBackground.mkv dragon ---- Play it four or five times. Go left twice and get out. Go right and get eaten. Sit on your hands and get eaten anyway. Lose all three lives. === What Just Happened [source,lua] ---- discSkipToFrame(scene.first) while discGetFrame() < scene.last do ---- A scene is a jump followed by a wait, which is the pattern from lesson fifteen with the numbers coming out of the record instead of being typed in. `discSkipToFrame` goes to a frame and plays from it, which is what a scene change is. The frame number is right the instant the call returns, so the `while` never thinks it is still in the scene you just left. [source,lua] ---- local asking = scene.ask ~= nil and frame >= scene.ask ---- The one line that is ready for a missing field, and the reason the death scene can leave `ask` out. `and` means both halves have to be true, as it did in lesson three, and Lua checks the left half first. If `scene.ask` is `nil` the left half is false, Lua stops there, and the comparison on the right never happens. Which is just as well, because comparing `nil` to a number is an error and you will meet it at the end of this lesson. `asking` is the answer to "is the question on screen right now?", worked out once a frame and used three times. [source,lua] ---- if not asking then answer = nil elseif answer == "left" or answer == "right" then return scene[answer] end ---- While the question is not up, anything the player presses is thrown away. It has to be, or a player leaning on the left arrow during the film answers a question they have not read. Once the question is up, an answer ends the scene at once. `return scene[answer]` is worth a second look: `answer` holds the string `"left"` or `"right"`, and `scene["left"]` is the same thing as `scene.left`. Putting the name in brackets instead of after a dot lets you look up a field whose name you are holding in a variable. Two lines of code for both answers, and for five answers it would still be two lines. The `elseif` also explains why only `"left"` and `"right"` are allowed through. The space bar sets `answer` to `"go"`, and `scene["go"]` is `nil`, so a stray space bar during a question would end the game in silence. Say what you accept. [source,lua] ---- discPause() ---- Right after the loop, and easy to forget. The scene has reached its last frame but the disc has no idea the scene is over -- it plays straight on into whatever footage happens to sit next in the file, which in a real game is the middle of a scene from somewhere else entirely. Stop it the moment you stop wanting it. [source,lua] ---- if scene.death then lives = lives - 1 ---- `scene.death` is `nil` for every scene except `pit`, and `nil` counts as false in an `if`, so this reads as "if this scene was a death". You do not have to write `death = false` in the other three records, and you should not: the absence of the field says it just as well, and there is then only one place that decides which scenes are deadly. The retry is the two lines under it. Take a life, and if there are any left, hold on the frame and wait for the space bar. Then `return scene.goesTo` sends the player back to `arrive`, and the record is what knows that. Change `goesTo` to `"sunset"` and the retry becomes a checkpoint instead of a restart -- one word, and the game's difficulty changes. [source,lua] ---- overlayPrint(2, 6, math.ceil((scene.last - frame) / FPS) .. " seconds left.") ---- The timer. `scene.last - frame` is how many frames are left, and dividing by the frames in a second turns that into seconds. `math.ceil` rounds a number up to the next whole one, so the countdown reads `3`, `2`, `1` and never `0.4` or `2.966666`. `FPS` is `30` because this video runs at a little over thirty frames a second. It is the one number in the script that is a guess, and it is a named guess at the top of the file rather than a `30` buried in the middle of a line, so that when you change videos there is exactly one thing to change. Notice what the timer is measured against: the film. The player is racing the video, not a clock, so the question can never outstay the shot it belongs to. That is the reason branching games count in frames. === Why Not Write It Out by Hand You could write this game as one long function. Play frames 0 to 119; if left, play 120 to 259; if left again, play 340 to 410; else play 260 to 339, take a life, go back to the top. For four scenes it would be shorter than what you have. For forty scenes it would be a disaster, and it is worth being precise about why. The frame numbers would be scattered through the code, each written once and never named, so changing where a scene starts means finding every place it is mentioned. The shape of the story would be buried in the shape of the `if` statements, so nobody could see the game without reading all of it. And adding a scene would mean editing the playing code, which is the code that already works -- every new scene another chance to break an old one. The version you wrote has one piece of code that plays scenes and one piece of data that says what the scenes are. Adding a scene touches only the data. That is worth the extra half hour it took, and it is the same idea underneath the `games.dat` file from lesson thirteen: describe the thing, then write one piece of code that reads the description. === Try It . *Add an ending.* Give `escape` a question and a second ending scene that reuses frames from somewhere in the film. Only the table changes. . *Make it fair.* Lower `ask` in both scenes so the player has five seconds instead of three, then raise it so they have one. Find the number you would actually ship. . *Move the checkpoint.* Change `pit`'s `goesTo` to `"sunset"` and play it. Then think about what happens if a player dies in `arrive` -- and whether the death scene should know where it came from rather than being told. . *Count what the player has seen.* Add a table beside `scenes` that records which scene names have been played, and show the count in the corner. Now you have a game that can say "you have found three of the five endings". . *Break the map.* Change `arrive`'s `left` to `"cave"`, a scene that does not exist. Run it, go left, and read what Lua says. That error is the most common one a data-driven game has, and knowing its shape saves an hour. === Break It on Purpose Take the guard off the `asking` line, so it reads: [source,lua] ---- local asking = frame >= scene.ask ---- Run it and go right. The `arrive` scene plays fine, because `arrive` has an `ask`. Then the `pit` scene starts, which does not, and the game stops with this: ---- Error executing function 'onOverlayUpdate': dragon.singe:68: attempt to compare nil with number ---- followed by a traceback -- a list of the functions that were running at the moment it went wrong, innermost first. Read it the way lesson one taught. The file and the line come first, and they are exactly right: line 68 is the line you just changed. The complaint is the rest: you asked Lua whether a number was greater than `nil`, and Lua has no idea. `scene.ask` is `nil` because the `pit` record has no `ask` field, and a field that is not there is `nil`. One part of that message deserves an explanation, because it is a lie of omission. The function named is `onOverlayUpdate`, which you did not write. That is the framework's own, the one lesson fifteen said was installed to drive your `singeMain`. The error came out through it, so that is the name the engine had to hand. Ignore it. The file and the line are yours, and they are the ones that are true. === What You Learned * A branching game is a set of scenes, each of which names the scenes that come after it. * Keep the scenes as data -- a table of records -- and the code that plays them stays one function however big the game gets. * A record holds the name of the next scene, not the scene, and the name is looked up when it is needed. * A field that is not in a record is `nil`, which is not an error, and code that reads records must expect it. * `scene[answer]` looks up the field whose name is in a variable; `scene.left` and `scene["left"]` are the same field. * Put the timer on the video: a choice lasts from one frame to another, so it can never outlast its own shot. * Throw away anything the player pressed before the question appeared. * Pause the disc the moment a scene ends, or it plays on into the next scene's footage. * A missing field standing for false -- `death` on three records out of four -- keeps the description short and honest. * Comparing `nil` with a number is an error, and `and` is how you avoid it. === Next Time Your game asks left or right. The arcade machines this kind of game grew up in asked something harder: point at the thing on screen and shoot it. Lesson seventeen is about light guns -- where the player is aiming, what the game does with that, and the calibration a real cabinet cannot do without.