753 lines
26 KiB
Text
753 lines
26 KiB
Text
== 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/
|
|
ship.png
|
|
rock.png
|
|
shot.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/ship.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/ship.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/ship.png` sends the engine looking for
|
|
`singe/art/ship.png`, out beside the `Singe` folder. Your artwork is not
|
|
there. It is one level down, in `rocks/art/ship.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/ship.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 manual'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/ship.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 manual, 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/ship.png")
|
|
local rockSprite = spriteLoad(DIR .. "art/rock.png")
|
|
local shotSprite = spriteLoad(DIR .. "art/shot.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 manual 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 `ship.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.
|