== Lesson 9: Pictures image::learn/09-pictures.png[The finished lesson, 480] Everything you have drawn so far, you drew with boxes and lines and the engine's own plain text. That was on purpose: eight lessons with no artwork meant eight lessons where nothing could go wrong except your program. Now the boxes become a ship and some rocks. A picture the engine draws onto the overlay is called a *sprite*. This lesson loads them, draws them, measures them, puts them in the right order, and steps through a picture that holds several frames of an animation. By the end, the game from lesson seven looks like a game. === The Art Kit You do not have to draw anything. This book ships a small folder of artwork, and everything in this lesson and the next uses it. Find the `art` folder that came with the book and copy the whole folder into this lesson's folder, beside your script. You should end up with this: ---- singe/ <- your work folder Singe/ data/ rocks/ rocks.singe art/ spaceship.png asteroid.png bullet.png star.png walk.png shoot.wav boom.wav ---- Your artwork goes inside your game's own folder, and the engine's `Singe` folder stays where it is, out in the work folder. That matters in a moment, because it decides how you name a file when you load it. The `Singe` folder is the one the engine unpacked for you on your very first run, back in lesson one. It is worth knowing what is in it, because those files are yours to use too and every reader of this book has them: * `Singe/click.wav` -- a short click, which the engine's own menu uses for its sound test. * `Singe/FreeSansBold.ttf` -- a TrueType font, for lesson nineteen. * `Singe/menuIntro.flac` -- the music the engine's menu plays. You will use it in lesson ten. * `Singe/menuBackground.mkv` -- a video, for lesson fourteen. * `Singe/DragonModel.glb` and `Singe/SingeText.glb` -- two 3D models, for lesson twenty-three. * `Singe/missing.png` -- the picture the engine draws when a game asks for one it cannot find. The kit's pictures are small on purpose. The ship is 32 pixels across and 24 down, a rock is 24 by 24, a shot is 4 by 10, and a star is 8 by 8. That sounds tiny until you remember how big the screen actually is, which is the next thing to sort out. === How Big the Screen Is Without a video to play, Singe gives your game a canvas 720 pixels across and 480 down, and the surface you draw on -- the *overlay* -- is half that in each direction: *360 by 240*. Singe stretches it to fill the window for you, so a 32 pixel ship is about a tenth of the width of the screen, which is roughly what a ship in an arcade game looks like. Never type 360 and 240 into your program. Ask: [source,lua] ---- local screenWidth = overlayGetWidth() local screenHeight = overlayGetHeight() ---- Those two report the size of the overlay, and a game that asks is a game that still works when somebody runs it at a different size. There is one thing that does not count in those pixels: `overlayPrint`, from lesson one, counts in character cells. Each cell is 6 pixels across and 13 down, so a 360 by 240 overlay is 60 columns by 18 rows of text. Sprites are in pixels, `overlayPrint` is in cells, and mixing the two up is worth one confused afternoon if you let it be. === One Picture on Screen Start a new file next to your `art` folder and type this in. [source,lua] ---- dofile("Singe/Framework.singe") local ship = spriteLoad(DIR .. "art/spaceship.png") function onOverlayUpdate() overlayClear() spriteDraw(ship, 160, 100) return OVERLAY_UPDATED end function onShutdown() spriteUnload(ship) end ---- Run it. A small blue ship sits near the middle of a black screen, pointing up. === What Just Happened Four new things, one at a time. [source,lua] ---- local ship = spriteLoad(DIR .. "art/spaceship.png") ---- `spriteLoad` reads an image file off the disk, decodes it, and keeps the picture in memory. What it hands back is not the picture. It is a *handle*: a plain number, an entry ticket, the engine's way of saying "that one". You give that number back to the engine every time you want to do something with the picture, and you never need to know or care what the number is. Print it if you like -- `debugPrint(ship)` from lesson eight will show you a small number -- but the number itself is not yours to reason about, and nothing in your program should ever depend on what it turns out to be. The only rule is that you keep it somewhere you can get at it again, which is exactly what a variable is for. `DIR` is new too, and it solves a problem the layout you have been using since lesson one creates. Singe looks for files relative to the folder you were standing in when you started it, *not* relative to your script. You start it from your work folder, so a plain `art/spaceship.png` sends the engine looking for `singe/art/spaceship.png`, out beside the `Singe` folder. Your artwork is not there. It is one level down, in `rocks/art/spaceship.png`, with your script. `DIR` closes that gap. It is a piece of text holding the folder your script is in, with the slash already on the end, so `DIR .. "art/spaceship.png"` -- joining two pieces of text with `..`, which you have been doing since part one -- names the picture correctly however the game was started, and keeps naming it correctly when you move the whole folder somewhere else. `DIR` is one of the names that `Singe/Framework.singe` defines, which is what the `dofile` line at the top of your script is for. You met that line and what is in it in lesson seven, and this is the moment it earns its place: without it there is no `DIR`, and without `DIR` the engine cannot find your ship. Use `DIR` on every file your game loads, from here to the end of the book. [source,lua] ---- spriteDraw(ship, 160, 100) ---- Draw the sprite with that handle, at that position. Like every drawing call, it only works inside `onOverlayUpdate`. The position is the *top left corner* of the picture, not its middle. Ask for `(160, 100)` and the topmost, leftmost pixel of the ship lands there and the rest of it hangs down and to the right. That trips up everybody once, usually when they try to centre something and it comes out low and to the right by half its own size. If you would rather give the middle, say so: [source,lua] ---- spriteDraw(ship, 160, 100, true) ---- A fourth argument of `true` means "treat that point as the centre". Both forms are in the reference's entry for `spriteDraw`, along with two more that stretch the picture into a rectangle. [source,lua] ---- function onShutdown() spriteUnload(ship) end ---- `onShutdown` is a callback, like `onOverlayUpdate` and the two input callbacks from lesson three. The engine calls it once, when your game is ending. It is where you give back what you took: `spriteUnload` frees the picture and throws the handle away. Strictly, you could leave it out. The engine frees everything your script loaded when the game ends, so a missed `spriteUnload` is not a leak. Write it anyway. It costs one line, it says what your program owns, and in a minute you will see the case where forgetting to unload really does matter. One warning about that handle: once you have unloaded a sprite, the number is dead. Drawing with it does not draw nothing, it ends your game with an error. === Transparency, and Why It Is a PNG The ship is a triangle with two fins. The file it lives in is a rectangle, 32 by 24, because every image file is a rectangle. So what happened to the corners? They are *transparent*. A PNG can say, for every single pixel, how solid it is, and the kit's pictures say "not there at all" for every pixel outside the shape. Singe honours that, so the black background shows through the corners and the ship has an outline instead of a box around it. This is the reason the kit is PNG and not JPEG. A JPEG cannot store transparency at all -- it would give you the ship in a grey box -- and it also smudges hard edges, which is the last thing small artwork needs. For game artwork, PNG. For a photograph that fills the whole screen and has no transparent parts, JPEG is fine and much smaller. The manual's entry for `spriteLoad` lists every format Singe will read, and there are a lot of them. Try it yourself: change the black to something else by drawing a box behind the ship before you draw the ship. The corners will show whatever is behind them, which is the whole point. === Do Not Load While You Draw Here is the mistake this section exists to stop. It looks completely reasonable: [source,lua] ---- function onOverlayUpdate() overlayClear() local ship = spriteLoad(DIR .. "art/spaceship.png") spriteDraw(ship, 160, 100) return OVERLAY_UPDATED end ---- Run that and it works. It draws the ship. Nothing complains. You would have no reason to think anything was wrong. `onOverlayUpdate` runs about sixty times a second. That is sixty fresh copies of the ship loaded into memory every second, three thousand six hundred a minute, and not one of them ever unloaded. The game runs beautifully for two minutes, gets slower, and then the machine runs out of memory and it dies. Worse, it dies during the demo and not while you were testing. The rule: *load once, draw many times*. Loading is slow and it takes memory. Drawing is fast and takes none. So `spriteLoad` goes at the top of your file, outside every function, where it runs one time as the script starts. This is not only about sprites. Every `somethingLoad` in the engine works this way -- sounds in lesson ten, fonts in lesson nineteen, models in lesson twenty-three. If you find yourself typing `Load` inside a function that runs every frame, stop and move it out. === How Big Is It? You know the ship is 32 by 24 because this book told you. Your program does not, and it should not have to. Ask: [source,lua] ---- local shipWidth = spriteGetWidth(ship) local shipHeight = spriteGetHeight(ship) ---- Now you can keep the ship on the screen, because the rightmost position it may sit at is `screenWidth - shipWidth`, and put it in the middle, because the middle is `(screenWidth - shipWidth) / 2`. Neither line has a number in it that would have to change if somebody redrew the ship a bit bigger. Both of these report the size *as the sprite would be drawn right now*. If you ever scale or rotate a sprite, with `spriteScale` or `spriteRotate`, the answer changes to match. That is usually what you want, and it will surprise you exactly once. === Drawing Order Sprites do not merge. Later covers earlier, pixel for pixel, exactly like sticking paper cutouts on a wall. So the order of your `spriteDraw` calls is the order from back to front: [source,lua] ---- drawStars() drawRocks() drawShots() spriteDraw(shipSprite, shipX, shipY) drawHud() ---- Stars are furthest back, so they go down first. The ship is in front of everything in the playfield. The score sits on top of all of it. Get this backwards and your carefully drawn ship spends the game hiding behind the star field, and there is no error to tell you so -- only a ship you cannot see. `overlayClear` still comes first, before all of it. Drawing never replaces, it only covers, so the frame starts empty every time. === Frames Open `art/walk.png` in any image viewer. It is not a picture of a person. It is a picture of four people, side by side: the same figure at four points of a walk, each one 24 pixels across in a strip 96 wide. One picture holding several frames of an animation is a *sprite sheet*, and it is how nearly all 2D animation is stored. One file, one load, one handle, and the game picks which slice to draw. Make a second file, `walk.singe`, beside the first one: [source,lua] ---- dofile("Singe/Framework.singe") local walk = spriteLoadFrames(4, DIR .. "art/walk.png") local walkFrame = 1 local walkTick = 0 local walkX = 0 function onOverlayUpdate() overlayClear() walkTick = walkTick + 1 if walkTick >= 8 then walkTick = 0 walkFrame = walkFrame + 1 if walkFrame > 4 then walkFrame = 1 end end walkX = walkX + 1 if walkX > overlayGetWidth() then walkX = -spriteFrameWidth(walk) end spriteDrawFrame(walk, walkX, 100, walkFrame) return OVERLAY_UPDATED end function onShutdown() spriteUnload(walk) end ---- Run it with `Singe walk`. A small figure walks steadily across the screen, disappears off the right edge, and comes back on from the left. `spriteLoadFrames` is `spriteLoad` with one extra thing to say: how many frames are in the strip. *The count comes first*, before the file name, which is the opposite of what most people guess. It divides the width of the image by the count and remembers the slices. Four frames in a 96 pixel strip means four frames 24 pixels wide. Get the count wrong -- say 3 -- and you get three 32 pixel slices, each showing bits of two figures, which is a memorably silly way to find out you typed the wrong number. `spriteDrawFrame` draws one slice. It takes the handle, where to put it, and which frame, *in that order*, and frames are numbered *from 1*: frame 1 is the leftmost figure and frame 4 the rightmost. Watch out for that 1. Elsewhere in the sprite family -- `spriteSetFrame` and `spriteGetFrame` -- frames are numbered from 0. That is an accident of history, both are in the reference, and this book uses `spriteDrawFrame` and counts from 1. `spriteFrameWidth` reports the width of one frame, 24 here, rather than the 96 of the whole strip. `spriteFrameHeight` is its partner. When you want to know how wide a slice of a sheet is, those are the two to ask, and not `spriteGetWidth`. The frame number is a number in a variable like any other, so the walking is arithmetic: [source,lua] ---- walkTick = walkTick + 1 if walkTick >= 8 then walkTick = 0 walkFrame = walkFrame + 1 if walkFrame > 4 then walkFrame = 1 end end ---- Count the frames going by. Every eighth one, step to the next picture, and after the fourth picture go back to the first. Sixty frames a second divided by eight is between seven and eight steps a second, which for a four frame cycle is a brisk but believable walk. Change the 8 and you change the speed: bigger is slower. You may wonder why you are counting at all, when the engine has `spritePlay` to run an animation for you. It does, and for an animated GIF it is the right answer, because a GIF carries the timing for each of its frames inside the file. A plain strip like `walk.png` carries no timing, so the engine runs it as fast as it is allowed to -- a hundred frames a second, twenty-five complete walk cycles every second, a blur. When the frames come from a strip, do the counting yourself. === The Game Gets Its Artwork Now put it together. The game from lesson seven steered a box along the bottom of the screen and dropped other boxes on it. Every box becomes a picture, and while you are in there the ship gets something to shoot with. The changes are all of a piece, so here is the whole script. It is longer than anything you have written, and there is nothing in it you have not met. [source,lua] ---- dofile("Singe/Framework.singe") local SHIP_SPEED = 3 local SHOT_SPEED = 6 local ROCK_COUNT = 6 local STAR_COUNT = 40 local START_LIVES = 3 local HIT_SCORE = 10 local shipSprite = spriteLoad(DIR .. "art/spaceship.png") local rockSprite = spriteLoad(DIR .. "art/asteroid.png") local shotSprite = spriteLoad(DIR .. "art/bullet.png") local starSprite = spriteLoad(DIR .. "art/star.png") local screenWidth = overlayGetWidth() local screenHeight = overlayGetHeight() local shipWidth = spriteGetWidth(shipSprite) local shipHeight = spriteGetHeight(shipSprite) local rockWidth = spriteGetWidth(rockSprite) local rockHeight = spriteGetHeight(rockSprite) local shotWidth = spriteGetWidth(shotSprite) local shotHeight = spriteGetHeight(shotSprite) local shipX = (screenWidth - shipWidth) / 2 local shipY = screenHeight - shipHeight - 4 local goLeft = false local goRight = false local rocks = {} local shots = {} local stars = {} local score = 0 local lives = START_LIVES local over = false function drawHud() overlayPrint(1, 1, "SCORE " .. score) overlayPrint(50, 1, "LIVES " .. lives) if over then overlayPrint(25, 8, "GAME OVER") overlayPrint(17, 10, "PRESS 1 TO PLAY AGAIN") end end function drawRocks() for _, rock in ipairs(rocks) do spriteDraw(rockSprite, rock.x, rock.y) end end function drawShots() for _, shot in ipairs(shots) do spriteDraw(shotSprite, shot.x, shot.y) end end function drawStars() for _, star in ipairs(stars) do spriteDraw(starSprite, star.x, star.y) end end function newRock(rock) rock.x = math.random(0, screenWidth - rockWidth) rock.y = -rockHeight - math.random(0, 160) rock.speed = math.random(8, 20) / 10 end function onInputPressed(what) if over then if what == SWITCH_START1 then startGame() end return end if what == SWITCH_LEFT then goLeft = true elseif what == SWITCH_RIGHT then goRight = true elseif what == SWITCH_BUTTON1 then shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY } end end function onInputReleased(what) if what == SWITCH_LEFT then goLeft = false elseif what == SWITCH_RIGHT then goRight = false end end function onOverlayUpdate() overlayClear() updateStars() if not over then updateShip() updateShots() updateRocks() end drawStars() drawRocks() drawShots() spriteDraw(shipSprite, shipX, shipY) drawHud() return OVERLAY_UPDATED end function onShutdown() spriteUnload(shipSprite) spriteUnload(rockSprite) spriteUnload(shotSprite) spriteUnload(starSprite) end function overlapping(ax, ay, aw, ah, bx, by, bw, bh) if ax + aw <= bx then return false end if bx + bw <= ax then return false end if ay + ah <= by then return false end if by + bh <= ay then return false end return true end function startGame() shipX = (screenWidth - shipWidth) / 2 score = 0 lives = START_LIVES over = false shots = {} for i = 1, ROCK_COUNT do rocks[i] = {} newRock(rocks[i]) end end function updateRocks() for _, rock in ipairs(rocks) do rock.y = rock.y + rock.speed if rock.y > screenHeight then newRock(rock) end if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then newRock(rock) lives = lives - 1 if lives <= 0 then over = true end end end end function updateShip() if goLeft then shipX = shipX - SHIP_SPEED end if goRight then shipX = shipX + SHIP_SPEED end if shipX < 0 then shipX = 0 end if shipX > screenWidth - shipWidth then shipX = screenWidth - shipWidth end end function updateShots() for i = #shots, 1, -1 do local shot = shots[i] local gone = false shot.y = shot.y - SHOT_SPEED if shot.y + shotHeight < 0 then gone = true end for _, rock in ipairs(rocks) do if not gone and overlapping(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then newRock(rock) score = score + HIT_SCORE gone = true end end if gone then table.remove(shots, i) end end end function updateStars() for _, star in ipairs(stars) do star.y = star.y + star.speed if star.y > screenHeight then star.y = 0 star.x = math.random(0, screenWidth - 1) end end end for i = 1, STAR_COUNT do stars[i] = { x = math.random(0, screenWidth - 1), y = math.random(0, screenHeight - 1), speed = math.random(1, 3) / 4 } end startGame() ---- Left and right arrows steer. Space fires. When the rocks have taken your last life, `1` starts a new game. A few things in there are worth pointing at. The four `spriteLoad` calls and the eight measurements are at the top, outside every function, so they happen once. Everything after them is written in terms of `screenWidth`, `shipWidth`, and their friends, and there is not one raw pixel count in the whole of the playing code. The functions are in alphabetical order, which is a habit worth picking up now. It does not matter to Lua in the slightest -- it is for you, six months from now, looking for `updateShots` in a file with forty functions in it. The last two things in the file are not functions at all. They are plain instructions at the bottom of the script: build the star field, then call `startGame` to set up the rocks and zero the score. They run once, in order, as the script loads, and they have to come after the functions they use. `overlapping` is the same idea you wrote in lesson seven, moved into a function of its own and given proper arguments. Two rectangles miss each other if either one is entirely left of, right of, above, or below the other; if none of those four is true, they overlap. Lesson eleven replaces it with something the engine provides, and explains why the rectangle is the wrong shape for a ship. One last note for when you read other people's Singe code. Every sprite call in this book takes the handle *first*. Singe 2.10 put it last, and a game can ask the engine to keep doing that, so code you find online may read `spriteDraw(x, y, ship)`. Do not copy it. Handle first is the current order and the one the reference documents. === Try It . *Centre the ship on its position.* Add `true` as a fourth argument to the ship's `spriteDraw`. Watch where the ship jumps to, and work out from the distance it moved what the fourth argument actually did. . *More rocks.* Change `ROCK_COUNT` to 20. Then to 200. Somewhere between those two the game stops being playable, and somewhere well past it the game starts to slow down. Find both. . *Grow the ship.* Add `spriteScale(shipSprite, 2)` at the very bottom of the file, on the line above `startGame()`. Run it and steer into the right-hand edge: the ship is twice the size but it still stops where the small one stopped, and it hangs off the right edge and off the bottom. Work out why, then move that one line up above the `spriteGetWidth` calls and watch it come right. The manual's entry for `spriteScale` explains what scaling does to the measurements. . *Put the walker in.* Load `walk.png` in the game, and draw the walking figure across the top of the screen as scenery. You will need the frame counting from the walk script, and you will have to decide where in `onOverlayUpdate` the draw goes -- in front of the stars, behind the rocks. . *Unload something you are still using.* Add `spriteUnload(starSprite)` at the bottom of the file, after `startGame()`, and run it. Read what you get. === Break It on Purpose Capital letters matter, and artwork is where that bites hardest. Change the ship's load to use a capital S: [source,lua] ---- local shipSprite = spriteLoad(DIR .. "art/Ship.png") ---- The game refuses to start: ---- 10:spriteLoad: Couldn't open art/Ship.png: No such file or directory ---- Read it the same way as the error in lesson one, but note that the shape is a little different. `10` is the line. `spriteLoad` is the engine function that gave up, rather than a file name -- when the complaint comes from Singe rather than from Lua, this is what you get. Then the reason, which here is your operating system's own wording and will differ slightly from machine to machine. `No such file or directory` is the computer being precise rather than unkind. There genuinely is no file called `Ship.png`; there is one called `spaceship.png`, and to a Linux or macOS machine those are two different names. On Windows it would have worked, which is worse, because it means the bug travels to somebody else's machine and appears there for the first time. The other half of this error you will meet is the folder. If you put the art kit in the wrong place, or started Singe from the wrong directory, the message is exactly the same and the fix is not. Check three things, in this order: is the file named exactly what you typed, is it in the `art` folder, and is that `art` folder beside your script. === What You Learned * A picture drawn onto the overlay is a sprite, loaded with `spriteLoad`. * `spriteLoad` hands back a handle: a number that means "that picture" to the engine, which you keep in a variable. * `DIR` is the folder your script is in, and every file your game loads should be named with it. * `spriteDraw(handle, x, y)` draws it, from its top left corner, and only inside `onOverlayUpdate`. A fourth argument of `true` means the centre instead. * Load once, at the top of the file. Loading inside `onOverlayUpdate` eats memory until the game dies. * `spriteUnload` gives a picture back, usually from `onShutdown`, and the handle is dead afterwards. * PNG carries transparency, which is why the corners of the ship are not black boxes. * `spriteGetWidth` and `spriteGetHeight` measure a sprite as it would be drawn now, so you never have to type its size into your program. * Sprites cover each other. The order you draw in is back to front. * `spriteLoadFrames(count, file)` slices a strip into frames, and `spriteDrawFrame(handle, x, y, frame)` draws one of them, counting from 1. === Next Time The game looks right and sounds like nothing at all. In lesson ten the shots get a noise, the rocks get an explosion, and something plays underneath the whole thing -- and you meet your third callback, the one the engine uses to tell you a sound has finished.