== Lesson 15: The Other Way to Write It image::learn/15-threaded.png[The finished lesson, 480] Every lesson so far has been written the same way. You write functions whose names begin with `on`, Singe calls them when it feels like it, and you never say what order anything happens in. That is the *event driven* model, and it is how most game engines work. Singe has a second model. In it you write your game as a list of steps, from the first line to the last, and you are allowed to wait. In this lesson you will write the same short video sequence both ways and see the difference with your own eyes. === The Sequence Here is what the game does, in English: . Start the film at the beginning and play it up to frame 120. . Stop there and ask the player to press the space bar. . When they press it, play on to frame 300. . Stop there, say "The end.", wait four seconds, and quit. Four steps. Read them again and notice that every one of them contains the word "then", spoken or not. === The Way You Know Make a file called `events.singe` in the `movie` folder from lesson fourteen, and type this: [source,lua] ---- dofile("Singe/Framework.singe") local step = 1 local endedAt = 0 function onInputPressed(what) if what == SWITCH_BUTTON1 and step == 2 then step = 3 discPlay() end end function onOverlayUpdate() local frame = discGetFrame() overlayClear() overlayPrint(2, 2, "Frame " .. frame) if step == 1 then if frame >= 120 then discPause() step = 2 end elseif step == 2 then overlayPrint(2, 4, "Press the space bar to go on.") elseif step == 3 then if frame >= 300 then discPause() endedAt = singeGetTicks() step = 4 end elseif step == 4 then overlayPrint(2, 4, "The end.") if singeGetTicks() - endedAt > 4000 then singeQuit() end end return OVERLAY_UPDATED end discSearch(0) discPlay() ---- Run it: ---- Singe -R -v Singe/menuBackground.mkv events ---- It works. It does exactly the four steps. `singeGetTicks` is new -- it hands back the number of milliseconds since the engine started, which is the ordinary way to measure a stretch of time -- and `singeQuit` ends the game, which is what the engine does for you when you press Escape. Now read the script and try to find the four steps in it. They are there, but they are inside out. The order of the story is written in the *values* of `step`, not in the order of the lines, and `step` is set in one function and read in another. To answer "what happens after the player presses space?" you have to hold the whole thing in your head at once. And this is the easy version. There are two decisions in it. === The Other Way Start a new file, `steps.singe`, and type this: [source,lua] ---- local message = "" local pressed = false local function drawFrame() overlayClear() overlayPrint(2, 2, "Frame " .. discGetFrame()) overlayPrint(2, 4, message) end local function playTo(frame) while discGetFrame() < frame do drawFrame() singeYield() end end local function waitForButton() pressed = false while not pressed do drawFrame() singeYield() end end local function waitSeconds(seconds) local stopTime = singeGetTicks() + seconds * 1000 while singeGetTicks() < stopTime do drawFrame() singeYield() end end function onInputPressed(what) if what == SWITCH_BUTTON1 then pressed = true end end function singeMain() discSearch(0) discPlay() playTo(120) discPause() message = "Press the space bar to go on." waitForButton() message = "" discPlay() playTo(300) discPause() message = "The end." waitSeconds(4) end dofile("Singe/Framework.singe") ---- Run it the same way. It does the same four things. Now read `singeMain`. The four steps are the four paragraphs, in order, top to bottom, and there is no `step` variable anywhere because the position in the story *is* the position in the function. That is the whole point of this lesson. === What Just Happened [source,lua] ---- function singeMain() ---- `singeMain` is a name Singe looks for, like `onOverlayUpdate`. If your script defines it, the framework runs your game from it: it is called once, at the start, and your game lasts as long as it lasts. When it returns, the game quits. [source,lua] ---- singeYield() ---- This is the call that makes it possible, and it means "let a frame happen". Singe is not doing two things at once -- nothing here is running in parallel, whatever the word *threaded* suggests. What happens is that `singeYield` stops your function where it stands, hands control back to the engine, and remembers the exact spot. The engine draws the frame, reads the input, moves the video on, and then starts your function again from the line after the `singeYield`, with every variable exactly as you left it. So a `while` loop with a `singeYield` in it is a way of waiting. It is the only way of waiting, and you must put one in every loop that is waiting for something. This is a deal you are making with the engine: you may sit and wait, as long as you give the frame back sixty times a second. [source,lua] ---- local function playTo(frame) while discGetFrame() < frame do drawFrame() singeYield() end end ---- Singe has no "wait until the video reaches frame 120" function. It does not need one, because you can write it in four lines, and you just did. Keep going while the disc is short of the frame you want; give a frame back each time round. `waitForButton` is the same shape with a different question, and `waitSeconds` is the same shape again with a clock. Three small functions, written once, and `singeMain` reads like the English sentences at the top of the lesson because of them. This is exactly what lesson five said your own functions were for. [source,lua] ---- drawFrame() singeYield() ---- Every wait loop draws before it yields, and this is the part that surprises people. In the threaded model there is no `onOverlayUpdate` of your own, so nothing draws unless you draw it. Whatever you draw before a `singeYield` is what appears on that frame. Stop drawing and the overlay stops changing. That is why `message` exists. `singeMain` sets it, and `drawFrame` prints it, and the three wait loops all call `drawFrame`. It is a small price for getting the story back in order. [source,lua] ---- dofile("Singe/Framework.singe") ---- At the *bottom*. This matters more than anything else in the lesson, so it is worth knowing why. `dofile` runs another script inside yours, and one of the things the framework does as it runs is look for a function called `singeMain`. If it finds one it wraps your game around it and writes an `onOverlayUpdate` of its own, whose entire job is to start your `singeMain` again once per frame. If it does not find one, it does nothing of the sort, and your game is an ordinary event driven game. So the framework has to be loaded *after* `singeMain` exists. Load it at the top, the way you have every lesson since the third, and at the moment it looks there is no `singeMain` yet, and you get a game with no game in it. === The Rules There are four, and between them they cover every mistake you can make here. *The framework goes last.* For the reason above. *Do not write your own `onOverlayUpdate`.* The framework has written one, and there can only be one: whichever of the two is written last wins, and the other is thrown away without a word. Above the `dofile`, yours goes and your drawing never appears. Below it, the framework's goes and your `singeMain` never runs again. Draw from inside `singeMain` instead. *Every waiting loop needs a `singeYield`.* A loop without one never gives the frame back. The window stops redrawing, stops responding, and the game is gone; you close the terminal to get out of it. There is no error message, because from the engine's point of view your function has not finished yet. *Returning from `singeMain` quits the game.* Which is tidy when you mean it and a surprise when you do not. A game that should keep going forever ends with a loop that never exits: [source,lua] ---- while true do drawFrame() singeYield() end ---- === Input Still Arrives Look again at where `pressed` is set: [source,lua] ---- function onInputPressed(what) if what == SWITCH_BUTTON1 then pressed = true end end ---- That is a callback, in a script that is written as a list of steps. Both models are running at once, and they are meant to be. When `singeMain` exists, only `onOverlayUpdate` is taken away from you. Everything else -- `onInputPressed`, `onInputReleased`, `onKeyPressed`, `onSoundCompleted`, `onShutdown`, all of them -- fires exactly as it always did, in the gaps between your yields. So the usual way to write a game in this model is the way you just did: the story in `singeMain`, and a small callback that catches an event and leaves a note for the story to find. `waitForButton` sets `pressed` to `false` before it starts waiting, which throws away anything the player mashed earlier. Leave that line out and a button pressed during the video counts as the answer to a question that has not been asked yet. === Which One to Use Neither model is the better one. They suit different jobs, and a real game uses whichever fits. The threaded model suits anything that is a *sequence*. Play this, then ask, then play that. An opening. A cut scene. A tutorial. A boss fight with three phases. Anything you would naturally describe as a numbered list is a `singeMain` waiting to be written, and video is the most sequential thing there is, which is why part three arrives at it here. The event driven model suits anything that is a *situation*. Ten enemies and a player, all moving, all colliding, none of them in any order. The game you wrote in lesson seven has no sequence in it at all: every frame is the same question asked again, which is exactly the shape of `onOverlayUpdate`. Write that as a list of steps and you gain nothing and lose the clarity. When a game is both -- and most are -- it is written as both. `singeMain` walks through the levels in order, and inside each level a loop does the lesson seven work sixty times a second until the level ends. === Try It . *Add a step.* After `waitForButton`, make the film jump backwards instead: `discSkipToFrame(0)`, then `playTo(60)`, then carry on as before. One new line in one place, and nothing else in the script has to know. . *Add the same step to the other one.* Now do it to `events.singe`. Count how many places you had to touch. . *Write `waitFrames`.* A fourth helper, next to the other three, that waits for a given number of frames of video rather than a given number of seconds. Use `discGetFrame` and remember where you started. . *Take a yield out.* Delete the `singeYield()` inside `playTo` and run it. Be ready to close the window. Now put it back and delete `drawFrame()` instead, and work out from what you see why the frame counter stops but the film does not. . *Make it a loop.* Put the whole of `singeMain` inside `while true do ... end` so the sequence starts over instead of quitting. Escape still gets you out, because that has never been your job. === Break It on Purpose Move the `dofile("Singe/Framework.singe")` line from the bottom of `steps.singe` to the top, and run it. There is no error. Nothing is printed. The window opens, the first frame of the film sits there, and that is all that ever happens. This is the worst kind of bug and the reason it is worth meeting on purpose. Nothing has gone wrong, in the engine's view: your script loaded, the framework loaded, and neither of them defined an `onOverlayUpdate`, so the engine has nothing to call. The disc is parked on frame one, paused, exactly as the engine left it, because the `discPlay` that would have started it is inside a `singeMain` that nobody ever runs. When a threaded game does nothing at all, look at the last line of the file first. It is almost always that. === What You Learned * Singe has two ways to write a script, and you choose by defining `singeMain` or not. * In the threaded model your game is a list of steps and runs from top to bottom. * `singeYield()` gives one frame back to the engine and carries on from the next line. * Waiting is a `while` loop with a `singeYield` in it. You write the waiting function you need; the engine does not provide one. * Nothing draws unless you draw it, before a yield, on every frame. * `dofile("Singe/Framework.singe")` goes at the *end* of a threaded script. * Do not write your own `onOverlayUpdate` when `singeMain` exists. * Every other callback still fires, so events and steps mix freely. * Returning from `singeMain` ends the game. * Sequences want the threaded model; situations want the event driven one. === Next Time You can now write "play this, then ask, then play that" as three lines. A branching video game is nothing but that sentence, a few hundred times, with the answers deciding which line comes next. Lesson sixteen builds one, and the interesting part turns out not to be the video at all. It is keeping the map of what leads where in a form you can still read when the game has forty scenes in it.