405 lines
13 KiB
Text
405 lines
13 KiB
Text
== 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.
|