== Lesson 14: Playing Video image::learn/14-video.png[The finished lesson, 480] Singe can play a film and run your game inside it. The picture becomes the world, and the overlay you have been drawing on since lesson one is laid over the top of it. In this lesson you will start a video, find out which frame is showing, stop it, and jump to any moment you like. Before any of that, one thing needs saying plainly, because it decides whether you should read this part of the book at all. === A Game Plays Video Only If It Asks To Singe is a game engine. Everything you built in parts one and two -- the sprites, the sound, the collisions, the score, the game folder -- is the whole of Singe, and none of it involves video. What part three adds is an extra: a game may ask for a film to be playing behind it, and then the film is the world instead of a black canvas. Nothing else changes. `onOverlayUpdate` is still called sixty times a second. `overlayPrint` still counts in character cells. Your sprites draw where they drew before. A game with video and a game without it are written the same way, with the same functions, and share the same book. So if the game you want to make has no video in it, you may close this part now and go to part four. Nothing in part four depends on anything in part three, and the only lessons that need this one are the four that follow it. Still here? Good. === You Already Have a Video You do not need to find one, buy one, or make one. The first time you ran Singe in lesson one, a folder called `Singe` appeared in your work folder. Open it and look: among the files the engine unpacked there is `menuBackground.mkv`. It is the backdrop the engine's own menu uses, it is 720 by 480 pixels, and it is 420 frames long -- about fourteen seconds. That is the video for this lesson and the two after it. Make a folder called `movie` next to your other work, and a file inside it called `movie.singe`. Then run Singe once in that folder, the plain way, so that the engine unpacks its `Singe` folder there too: ---- Singe movie ---- A black window opens and sits there doing nothing, because your script is still empty and there is nothing for the engine to call. That is fine. Press Escape, and look in the folder: the `Singe` folder is there now, with the video inside it. === Type This In [source,lua] ---- dofile("Singe/Framework.singe") function onOverlayUpdate() overlayClear() overlayPrint(2, 2, "Frame " .. discGetFrame()) return OVERLAY_UPDATED end discPlay() ---- Save it. === Run It The video is not part of your script, so you do not load it in your script. You hand it to the engine on the command line, with `-v`: ---- Singe -R -v Singe/menuBackground.mkv movie ---- `-v` is short for `--framefile`, and it names the video the game is to play. The name of your script comes last, as it always has. `-R` is the same `-R` you have been using since lesson one, and it still restarts the game when you save. Asking is the whole point of `-v`. If you drop a video file next to your script and say nothing, the engine notices it, says so on the console, and runs your game without it. A game is a video game only when somebody says it is. A window opens. A ball of orange fire fills it, a dragon forms out of the fire, the word `SINGE` appears under it, and then a magenta grid rolls away under an orange sun. In the top left corner, over all of it, is a number counting up. Watch it to the end. Then watch what happens at the end. === What Just Happened [source,lua] ---- discPlay() ---- Singe calls the video *the disc*, and every function that drives it begins with `disc`. The name is historical: the engine began life playing games that kept their film on a laserdisc, and the name stuck to the idea. It is a video file on your hard drive and nothing more. A game has at most one disc, so none of the `disc` functions takes an argument saying which one. There is only the one. This line is not inside any function. It sits at the bottom of the file, on its own, and it runs once when Singe loads your script -- before the first `onOverlayUpdate`, before anything else. Singe has no `onStartup` callback because it does not need one: a Lua script *is* a list of instructions, and anything you write outside a function happens as the file is read. Startup code goes at the bottom, below the functions, by convention. You need `discPlay()` because the disc does not start itself. When the game begins, the engine parks the disc on frame one and pauses it there. Nothing moves until you ask. [source,lua] ---- overlayPrint(2, 2, "Frame " .. discGetFrame()) ---- `discGetFrame` answers one question: which frame is showing right now? It takes no arguments and hands back a number. Notice where that number is drawn. Column 2, row 2 -- exactly where `Hello!` went in lesson one, and it lands in the same place on screen. The overlay you draw on is half the size of the video on each side, which for a 720 by 480 video is 360 by 240, which is exactly the size of the canvas you have been drawing on for thirteen lessons. Your coordinates did not move. If you ever play a video of some other size, the overlay changes size with it, and `overlaySetResolution` sets it to whatever you want; the reference's entry has the details. This is the whole idea of the overlay. The video is underneath. Your drawing is on top, and wherever you have not drawn, the film shows through. That is why `overlayClear` matters even more now than it did: clear the overlay and you are looking at the film. === A Frame Is an Address The number `discGetFrame` returns is a *frame* number. A frame is one still picture. This video holds 420 of them and shows about thirty every second, so it lasts about fourteen seconds. Frames are counted from zero, so this video has frames 0 to 419. The count and the last number are not the same, which catches everybody once. `discGetFrameCount` tells you the count -- 420 -- and the highest frame you can ask for is one less than that. The important thing about a frame number is that it never changes and never drifts. Frame 110 is the moment the dragon breathes fire, in this copy of the file, on this machine, today and next year. Seconds drift, because a video does not always play at exactly the rate you think. A frame number is the address of a moment, and addresses are what a video game is built out of: you write down the frame where the dragon appears, and from then on you can send the film there whenever you like. Finding those numbers is a job you do once per video, and the script you wrote is the tool for it: run the film, watch the counter, write down the frames where things happen. Everything in lesson sixteen is built on a list of numbers found exactly that way. === What Happened at the End The counter reached 419, and then it was 0 again and the fire was back. When the disc runs off the end, the engine starts it again from frame zero. It does not stop, and it does not tell you. A real game never lets it get there: it watches the frame number and sends the disc somewhere else before the end arrives. Forgetting to do that is why a half finished video game suddenly plays its own title sequence in the middle of a fight. === Stopping, Jumping, and Asking Add this function above `onOverlayUpdate`: [source,lua] ---- function onInputPressed(what) if what == SWITCH_BUTTON1 then if discGetState() == DISC_PLAYING then discPause() else discPlay() end elseif what == SWITCH_LEFT then discSearch(0) elseif what == SWITCH_RIGHT then discSkipForward(30) elseif what == SWITCH_START1 then discStop() end end ---- Save, and try each one. Space plays and pauses. Left goes back to the start. Right jumps on about a second. The `1` key stops the disc altogether, and what you get is a blue screen -- the colour a laserdisc player showed when it had stopped, kept because a great many games drew their score table on it. Five new calls there, and they divide into two kinds. *Asking.* `discGetState` hands back one of four names the engine has already defined for you: `DISC_PLAYING`, `DISC_PAUSED`, `DISC_STOPPED`, and `DISC_EJECTED`. They are like `OVERLAY_UPDATED` -- no quotes, because they are names for values rather than text. Compare with `==` as you compared numbers in lesson three. *Telling.* `discPlay` starts or resumes. `discPause` freezes on the frame that is showing and leaves it on screen. `discStop` is the heavier one: the picture is replaced by the blue screen, `discGetFrame` answers `0` until the disc runs again, and `discPause` and the skip calls are ignored while it is stopped. Any of `discPlay`, `discSearch`, or `discSkipToFrame` un-stops it. Two calls send the disc to a frame, and the difference between them is the only thing in this lesson worth memorising: * `discSearch(frame)` goes there and *pauses* on it. * `discSkipToFrame(frame)` goes there and *plays* from it. Search when you want to park on a picture and wait. Skip when you want a scene to start. `discSkipForward(30)` and `discSkipBackward(30)` are the same idea measured from where you are rather than from the beginning, and they leave the disc playing if it was playing and paused if it was paused. There is also `discPauseAtFrame`, which does exactly what `discSearch` does under an older name. The manual marks it as a legacy alias. You will see it in other people's scripts; write `discSearch` in your own. === Waiting Is Harder Than It Sounds Here is the sentence every video game is made of: ____ Play from frame 0 to frame 120, then ask the player a question. ____ Try to write it. You cannot write "play, then wait, then ask", because you do not have anywhere to write it. You have `onOverlayUpdate`, which Singe calls sixty times a second and which must return immediately every single time. A function that sat and waited would stop the engine dead: no drawing, no input, no video. So you cannot say "then". You have to turn "then" into a variable that remembers where you have got to, and check it on every one of those sixty calls: [source,lua] ---- local asking = false function onOverlayUpdate() overlayClear() if not asking and discGetFrame() >= 120 then discPause() asking = true end if asking then overlayPrint(2, 4, "Left or right?") end return OVERLAY_UPDATED end ---- That works. Read it and you can see it works. But notice what it cost: one variable, whose only job is to remember which part of the sentence you are in, and two `if` statements to keep it honest. Now add the rest of the scene. The player answers, so one of two things plays next. Each of those ends somewhere and asks another question. A modest branching game has forty of these moments, and every one of them wants its own variable and its own `if`. You end up with a script where the story is not written down anywhere -- it is spread across a dozen flags, and to find out what happens after the dragon you have to read all of them. Hold that thought. Lesson fifteen is nothing but the fix for it. === When There Is No Video Run your script the plain way again, with no `-v`: ---- Singe -R movie ---- Nothing crashes. You get a black window with `Frame 0` in the corner. This is worth understanding, because it is the reassuring half of the promise at the top of this lesson. A game without a disc still runs every `disc` call you make; they do nothing useful. The engine documents exactly what they answer, and it never changes: `discGetFrame` and `discGetFrameCount` return `0`, `discGetState` returns `DISC_EJECTED`, `discGetWidth` and `discGetHeight` return the size of the canvas, and `discPlay`, `discPause`, `discSearch`, and the rest are recorded in the log and otherwise ignored. You cannot break a script by calling a disc function when there is no disc. When a script needs to *know*, it asks `SINGE_DISC`. That is another name the engine defines before your script runs, and it is `true` when the game has a video and `false` when the world is a plain canvas. It is the right way to write a game that can run both ways: [source,lua] ---- if not SINGE_DISC then overlayPrint(2, 2, "This game has no video.") overlayPrint(2, 4, "Run it again with -v Singe/menuBackground.mkv") return OVERLAY_UPDATED end ---- `not` flips a true or false value over, as it did in lesson three. === The Whole Thing Put together, with a small function that turns the state into words and a frame to hold at, this is the finished script. It is a frame finder: the tool you will use every time you meet a new video. [source,lua] ---- dofile("Singe/Framework.singe") local stopAt = 400 local held = false local function stateName() local state = discGetState() if state == DISC_PLAYING then return "playing" elseif state == DISC_PAUSED then return "paused" elseif state == DISC_STOPPED then return "stopped" end return "not there" end function onInputPressed(what) if what == SWITCH_BUTTON1 then if discGetState() == DISC_PLAYING then discPause() else discPlay() end elseif what == SWITCH_LEFT then discSearch(0) held = false elseif what == SWITCH_RIGHT then discSkipForward(30) elseif what == SWITCH_START1 then discStop() held = false end end function onOverlayUpdate() local frame = discGetFrame() overlayClear() if not SINGE_DISC then overlayPrint(2, 2, "This game has no video.") overlayPrint(2, 4, "Run it again with -v Singe/menuBackground.mkv") return OVERLAY_UPDATED end if not held and frame >= stopAt then discPause() held = true end overlayPrint(2, 2, "Frame " .. frame .. " of " .. discGetFrameCount()) overlayPrint(2, 4, "The disc is " .. stateName() .. ".") overlayPrint(2, 6, "Space plays and pauses. Left rewinds.") overlayPrint(2, 7, "Right jumps on a second. 1 stops the disc.") if held then overlayPrint(2, 9, "Holding at frame " .. stopAt .. ".") end return OVERLAY_UPDATED end discPlay() ---- `held` is the same kind of variable as `asking` above, and it is here for a reason you can test: take it out, leave `if frame >= stopAt then discPause() end`, and then try to press space to carry on past frame 400. You cannot. The disc resumes for a sixtieth of a second and the next `onOverlayUpdate` pauses it again, forever. `held` is how you say "I have already done this once". === Try It . *Find the dragon.* Run it, watch the counter, and write down the frame where the dragon first appears, the frame where it breathes fire, and the frame where the grid takes over the screen. You will want these three numbers in lesson sixteen. . *Step through it.* Change `discSkipForward(30)` to `discSkipForward(1)` and hold the right arrow down. Now change `stopAt` to a frame in the middle of the dragon and use `discSearch` and the arrow key to land on it exactly. . *Make it loop.* Change the hold so that instead of pausing at frame 400 it calls `discSkipToFrame(0)`. You have built an attract mode: a scene that plays forever until somebody presses a button. . *Ask for a frame that is not there.* Change the `SWITCH_RIGHT` line to `discSkipToFrame(100000)`. It does not crash. Read the reference's entry for `discSkipToFrame` and find the sentence that says why. . *Play it without the video.* Run `Singe -R movie` and then press every key. Nothing you press can break it. === Break It on Purpose The mistake that costs the most time here is not a Lua mistake. Run this: ---- Singe -R -v menuBackground.mkv movie ---- The path is wrong -- the video is in the `Singe` folder, not beside your script -- and Singe will not start. What it does instead is print its entire list of command line options, which is alarming the first time, and then this as the very last line: ---- Error: Unable to locate video. ---- A wall of options like that always means the same thing: the engine never got as far as your game. Ignore the wall and read the last line. That one is easy. The unhelpful version of it is running with no `-v` at all and forgetting you did. There is no error, because a game without a video is a perfectly good game. You get a black screen and a counter stuck on `0`, and you go looking for the bug in your script for twenty minutes. That is what the `SINGE_DISC` block in the finished script is for. Three lines of it, and the game tells you what is wrong instead of sulking. Put a check like that in every script you write that needs a video. === What You Learned * A game plays video only when it asks to, and everything else in Singe works the same either way. * Singe calls the video the disc. A game has one, and the `disc` functions take no handle because there is nothing to choose between. * The video goes on the command line with `-v`, not in the script. * The overlay is drawn over the picture, and the film shows through wherever you have not drawn. * A frame is one still picture, counted from zero, and a frame number is the address of a moment. * `discGetFrame` asks where the disc is; `discGetFrameCount` says how many frames there are; `discGetState` says what the disc is doing. * `discPlay`, `discPause`, and `discStop` drive it. `discSearch` goes to a frame and pauses; `discSkipToFrame` goes to a frame and plays. * A disc that runs off the end starts again at frame zero without telling you. * Without a disc every `disc` call is harmless, and `SINGE_DISC` is how a script tells which kind of game it is in. * You cannot wait inside `onOverlayUpdate`, so "then" has to be spelled out as a variable. === Next Time That last point is the sore one. A video game is a list of steps -- play this, ask that, play the other -- and you have just seen that the callback model cannot say "then" without a flag and an `if` for every step. Lesson fifteen shows you the other way Singe lets you write a script, in which "then" is a new line.