497 lines
19 KiB
Text
497 lines
19 KiB
Text
== Lesson 11: Hitting Things
|
|
|
|
image::learn/11-hitting-things.png[The finished lesson, 480]
|
|
|
|
Your game from lesson ten works. Shots break rocks, rocks cost lives, and the
|
|
explosions land in the right places. Watch it closely, though, and it cheats,
|
|
in both directions. A rock that slides past the tip of a wing takes a life. A
|
|
shot that goes through the empty sky beside a rock breaks it anyway.
|
|
|
|
Two things in a game touch when the numbers say they do. Nobody looks at the
|
|
pictures. This lesson makes the numbers agree with what the player sees: it
|
|
swaps your hand-written test for the engine's own, shrinks the boxes being
|
|
tested so they fit the things inside them, and gives you a switch that shows
|
|
you those boxes. The picture above is the game with that switch on.
|
|
|
|
=== Where You Are
|
|
|
|
The game asks "do these two touch?" in exactly two places, and both use
|
|
`overlapping`, the function you typed in lesson nine. One is in `updateRocks`,
|
|
where a rock meets the ship:
|
|
|
|
[source,lua]
|
|
----
|
|
if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
----
|
|
|
|
The other is in `updateShots`, where a shot meets a rock:
|
|
|
|
[source,lua]
|
|
----
|
|
if not gone and overlapping(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
----
|
|
|
|
Everything in this lesson changes those two lines and adds a few small
|
|
functions to feed them. Nothing else in the game moves. The whole finished
|
|
script is in `learn/11-hitting-things.singe` if you lose your way.
|
|
|
|
=== Read It Once More
|
|
|
|
Lesson nine already did this part: `overlapping` is rectangle collision
|
|
written out by hand. Read it once more before you throw it away, because it is
|
|
the whole of the idea:
|
|
|
|
[source,lua]
|
|
----
|
|
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
|
|
----
|
|
|
|
The question "are these two touching?" is hard to answer directly and easy to
|
|
answer backwards. Two rectangles are *not* touching when one of them is
|
|
entirely to the left of the other, or entirely to the right, or entirely above,
|
|
or entirely below. There is no fifth way to miss. If none of those four is
|
|
true, they are touching.
|
|
|
|
Each `if` is one of the four ways to miss. `ax + aw` is the right-hand edge of
|
|
the first rectangle, so `ax + aw <= bx` reads "the first one ends before the
|
|
second one starts", which is what "entirely to the left" means in numbers. The
|
|
same shape three times more, for right, above, and below, with `y` and the
|
|
heights in place of `x` and the widths, and the only thing left is `true`.
|
|
|
|
Those are the eight numbers any collision test needs: a corner and a size for
|
|
each of two things. Keep the shape in your head, because you are about to
|
|
delete it.
|
|
|
|
=== The Engine Already Has It
|
|
|
|
You met `collideRects` in lesson seven, where it caught the falling blocks in
|
|
Dodge. Lesson nine wrote the same test out by hand so you could see inside it.
|
|
You have seen inside it. Delete `overlapping`, and change the two lines from
|
|
*Where You Are* to call the engine instead:
|
|
|
|
[source,lua]
|
|
----
|
|
if collideRects(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
----
|
|
|
|
[source,lua]
|
|
----
|
|
if not gone and collideRects(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
----
|
|
|
|
Same eight numbers, in the same order: the first rectangle's corner and size,
|
|
then the second one's. Run it, and the game plays exactly as it did, which is
|
|
the point. The only difference is at the very edge. Your `<=` said that two
|
|
rectangles meeting along an edge are not touching. `collideRects` says they
|
|
are, and a rectangle with no width or height touches nothing at all. Those
|
|
awkward cases are where hand-written versions go quietly wrong, and they are
|
|
one reason to use the engine's.
|
|
|
|
`collideRects` is one of a family of collision tests, and the family is one
|
|
part of what this book calls the *game kit*: a handful of plain functions for
|
|
the things nearly every game used to write for itself. Testing whether two
|
|
things touch is one. Saving a number for next time is another, and it is
|
|
lesson twelve. Waiting a while, sliding a number smoothly from one value to
|
|
another, and showing how long each frame took are the rest. The reference
|
|
gives each part a section of its own: Collide, Save, Timer, Tween, and Stats.
|
|
|
|
The collision tests are the plainest of the lot: plain questions, with no
|
|
handles to keep and nothing to set up or free. And none of them move anything.
|
|
`collideRects` tells you that the ship and the rock are in the same
|
|
place; what to do about it is yours to decide, and everything interesting in a
|
|
game happens in that decision.
|
|
|
|
=== The Picture Is Not the Hit Box
|
|
|
|
Lesson ten promised to explain why the rectangle around a round rock is the
|
|
wrong shape. The rectangle around the ship is wrong too, and for the same
|
|
reason.
|
|
|
|
Draw the ship's 32 by 24 rectangle on paper and shade in the ship. It is a
|
|
triangle with two fins, so there is empty sky in both top corners and between
|
|
the fins. The rock is round, so there is empty sky in all four corners of its
|
|
square. A rock that clips the top corner of the ship's rectangle has not hit
|
|
the ship. It has flown past the wing, and a player who loses a life for it
|
|
will say the game cheated -- and be right.
|
|
|
|
So the box you test is not the picture you draw. It is smaller, and it sits
|
|
inside the picture. Add three numbers to the constants at the top of your
|
|
file:
|
|
|
|
[source,lua]
|
|
----
|
|
local SHIP_PAD_X = 6
|
|
local SHIP_PAD_Y = 5
|
|
local ROCK_PAD = 4
|
|
----
|
|
|
|
The pad is how far in from each edge the real thing starts. Six pixels off
|
|
each side of the ship and five off the top and bottom leaves a box 20 by 14 in
|
|
the middle of a 32 by 24 picture, which is about where the hull is. Four off
|
|
every side of the rock leaves 16 by 16, and that is nearly all rock.
|
|
|
|
Both boxes are a little smaller than the things they stand for, and that is on
|
|
purpose. A box slightly too small gives the player near misses that feel like
|
|
skill. A box slightly too big gives them hits that feel like cheating. Players
|
|
forgive the first and never the second.
|
|
|
|
Rather than write the arithmetic out at every test, write it once for each
|
|
kind of thing:
|
|
|
|
[source,lua]
|
|
----
|
|
function rockBox(rock)
|
|
return rock.x + ROCK_PAD, rock.y + ROCK_PAD, rockWidth - ROCK_PAD * 2, rockHeight - ROCK_PAD * 2
|
|
end
|
|
|
|
|
|
function shipBox()
|
|
return shipX + SHIP_PAD_X, shipY + SHIP_PAD_Y, shipWidth - SHIP_PAD_X * 2, shipHeight - SHIP_PAD_Y * 2
|
|
end
|
|
----
|
|
|
|
The sizes come from `rockWidth` and `shipWidth` and their friends, the
|
|
measurements lesson nine took from the pictures, so there is still not one
|
|
raw pixel size in the playing code. Redraw the ship bigger and the box grows
|
|
with it.
|
|
|
|
Each of those functions hands back four numbers at once, separated by commas.
|
|
A function may return as many values as it likes, and the way to catch them is
|
|
a list of names on the left of the `=`:
|
|
|
|
[source,lua]
|
|
----
|
|
function shipHitsRock(rock)
|
|
local sx, sy, sw, sh = shipBox()
|
|
local rx, ry, rw, rh = rockBox(rock)
|
|
|
|
return collideRects(sx, sy, sw, sh, rx, ry, rw, rh)
|
|
end
|
|
----
|
|
|
|
Now the test in `updateRocks` reads like the question it is asking:
|
|
|
|
[source,lua]
|
|
----
|
|
if shipHitsRock(rock) then
|
|
----
|
|
|
|
The padding arithmetic is in exactly two places, and the numbers it uses are
|
|
at the top of the file, so you can tune how fair the game feels by changing
|
|
one number rather than by hunting through your tests.
|
|
|
|
Put the new functions in their places in the alphabet, as lesson nine asked:
|
|
`rockBox` after `onSoundCompleted`, then `shipBox` and `shipHitsRock`.
|
|
|
|
=== Firing Back
|
|
|
|
That is the ship's side done. The other collision in the game is the one you
|
|
fire back with: every shot against every rock, in `updateShots`. It has the
|
|
same problem the ship had, in a smaller size. A shot is a 4 by 10 rectangle
|
|
tested against the rock's whole 24 by 24 square, so a shot that only passes
|
|
through the empty corner beside a rock still breaks it.
|
|
|
|
You could give the shot a padded box of its own. Look at the numbers first,
|
|
though. Pad a shot four pixels wide and there is almost nothing left, and
|
|
testing a whole rectangle to find out whether a sliver of light touched
|
|
something is more machinery than the question deserves.
|
|
|
|
=== A Shot Is Almost a Point
|
|
|
|
Test the tip instead. `collidePointRect` asks whether one point falls inside
|
|
one rectangle -- two numbers for the point, then the rectangle's corner and
|
|
size:
|
|
|
|
[source,lua]
|
|
----
|
|
function shotHitsRock(shot, rock)
|
|
local rx, ry, rw, rh = rockBox(rock)
|
|
|
|
return collidePointRect(shot.x + shotWidth / 2, shot.y, rx, ry, rw, rh)
|
|
end
|
|
----
|
|
|
|
`shot.y` is the top of the shot and `shot.x + shotWidth / 2` is halfway
|
|
across it, so the point being tested is the middle of the shot's nose. That is
|
|
where a player thinks a shot is. The rock's side of the question uses the same
|
|
`rockBox` the ship does, so a rock is the same size whatever hits it.
|
|
|
|
It goes after `shipHitsRock`, and the test in `updateShots` becomes:
|
|
|
|
[source,lua]
|
|
----
|
|
if not gone and shotHitsRock(shot, rock) then
|
|
----
|
|
|
|
=== Deciding What Happened
|
|
|
|
None of the collision tests do anything. They answer, and your code decides.
|
|
Here are the two places that decide, with their new questions in them. First
|
|
`updateRocks`:
|
|
|
|
[source,lua]
|
|
----
|
|
if shipHitsRock(rock) then
|
|
local channel = soundPlay(boomClip)
|
|
|
|
newRock(rock)
|
|
lives = lives - 1
|
|
if lives <= 0 then
|
|
dying = true
|
|
deathChannel = channel
|
|
if channel < 0 then
|
|
endGame()
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
And the loop inside `updateShots`:
|
|
|
|
[source,lua]
|
|
----
|
|
for _, rock in ipairs(rocks) do
|
|
if not gone and shotHitsRock(shot, rock) then
|
|
newRock(rock)
|
|
score = score + HIT_SCORE
|
|
gone = true
|
|
soundPlay(boomClip, 0, 45)
|
|
end
|
|
end
|
|
----
|
|
|
|
Everything under the questions is exactly what lesson ten left there. A rock
|
|
that hits the ship bangs, goes back to the top, and costs a life. A rock that
|
|
is shot bangs, goes back to the top, and scores. The shot goes too, and the
|
|
`not gone` stops one shot from breaking two rocks. Change any of that and you
|
|
change the game, without touching a single collision test, which is the reason
|
|
the questions and the answers are kept apart.
|
|
|
|
Run it. Fly close past a rock and let it brush a fin: you keep the life. Shoot
|
|
just beside a rock and the shot sails past. Shoot into the middle of one and it
|
|
breaks, the same as ever.
|
|
|
|
=== Seeing the Boxes
|
|
|
|
The hit boxes are invisible, which makes a mistake in them invisible too. Draw
|
|
them while you are working. One more constant at the top:
|
|
|
|
[source,lua]
|
|
----
|
|
local SHOW_BOXES = false
|
|
----
|
|
|
|
Two more functions, which go at the very top of the alphabet:
|
|
|
|
[source,lua]
|
|
----
|
|
function drawBox(x, y, width, height)
|
|
overlayBox(x, y, x + width - 1, y + height - 1)
|
|
end
|
|
|
|
|
|
function drawBoxes()
|
|
colorForeground(0, 255, 0)
|
|
drawBox(shipBox())
|
|
for _, rock in ipairs(rocks) do
|
|
drawBox(rockBox(rock))
|
|
end
|
|
end
|
|
----
|
|
|
|
And in `onOverlayUpdate`, after the ship is drawn and before the score goes on
|
|
top:
|
|
|
|
[source,lua]
|
|
----
|
|
spriteDraw(shipSprite, shipX, shipY)
|
|
if SHOW_BOXES then
|
|
drawBoxes()
|
|
end
|
|
drawHud()
|
|
----
|
|
|
|
Change `SHOW_BOXES` to `true` and green outlines appear over the artwork, as
|
|
in the picture at the top of this lesson. Change it back when you are done. A
|
|
switch like this, one word at the top of a file that turns on a picture of
|
|
what the program believes, is worth more than any amount of staring.
|
|
|
|
=== What Just Happened
|
|
|
|
[source,lua]
|
|
----
|
|
function rockBox(rock)
|
|
return rock.x + ROCK_PAD, rock.y + ROCK_PAD, rockWidth - ROCK_PAD * 2, rockHeight - ROCK_PAD * 2
|
|
end
|
|
----
|
|
|
|
Four values from one `return`, separated by commas. Lua is happy to hand back
|
|
as many as you want, and the caller decides how many to keep. When a call like
|
|
this is the *last* thing inside another call's parentheses, all four are passed
|
|
along, which is why `drawBox(rockBox(rock))` works and gives `drawBox` its four
|
|
arguments. Anywhere else in an argument list, only the first value survives.
|
|
That rule bites exactly once, and it bites at the end of this lesson.
|
|
|
|
[source,lua]
|
|
----
|
|
return collideRects(sx, sy, sw, sh, rx, ry, rw, rh)
|
|
----
|
|
|
|
`collideRects` wants eight numbers: corner and size, corner and size. It gives
|
|
back `true` or `false` and changes nothing. Because the answer comes straight
|
|
back out of the function with `return`, `shipHitsRock(rock)` reads like a
|
|
question wherever it is used.
|
|
|
|
[source,lua]
|
|
----
|
|
return collidePointRect(shot.x + shotWidth / 2, shot.y, rx, ry, rw, rh)
|
|
----
|
|
|
|
`collidePointRect` wants six: a point, then a rectangle. The edge counts as
|
|
inside, so a shot exactly on the boundary hits. Use it whenever one side of the
|
|
question is small enough that its size does not matter.
|
|
|
|
[source,lua]
|
|
----
|
|
colorForeground(0, 255, 0)
|
|
overlayBox(x, y, x + width - 1, y + height - 1)
|
|
----
|
|
|
|
Both of these are from lesson seven. `overlayBox` takes two *corners*, not a
|
|
corner and a size -- the one place in this lesson where the numbers change
|
|
shape -- and the `- 1` is because both corners are included. `colorForeground`
|
|
is a switch that stays flipped, so everything `drawBoxes` draws comes out
|
|
green. Sprites and `overlayPrint` ignore it, so the rest of the game looks the
|
|
same.
|
|
|
|
=== The Rest of the Family
|
|
|
|
`collideRects` and `collidePointRect` will carry most 2D games. The others are
|
|
there for the shapes rectangles describe badly, and each is the same kind of
|
|
plain question:
|
|
|
|
* `collideCircles` and `collidePointCircle`, for anything round. A circle is a
|
|
center and a radius.
|
|
* `collideRectCircle`, for a round thing against a square one.
|
|
* `collidePointPolygon`, for a shape with corners of its own -- a dragon, a
|
|
continent, an odd-shaped button. You give it a flat list of `x`, `y`, `x`,
|
|
`y` numbers.
|
|
* `collideSegments`, for two line segments crossing.
|
|
|
|
If you are honest, your rock is round, and the circle tests fit it better than
|
|
any box. Padding is the cheap answer and it is good enough for most games,
|
|
which is why it comes first. The exact answer is one of the things to try
|
|
below.
|
|
|
|
`collideSegments` matters more than it sounds. Your shot moves six pixels a
|
|
frame and the rocks are sixteen pixels of hit box, so a shot cannot get past
|
|
one without being tested inside it. Make the shot fast enough -- forty pixels a
|
|
frame -- and it jumps from below a rock to above it, never once being in the
|
|
same place as the rock, and it never hits anything. Drawing the line from
|
|
where the shot was to where it is now, and asking `collideSegments` whether
|
|
that line crosses an edge, is the cure. Slow things do not need it.
|
|
|
|
There is one collision callback in Singe, `onCollision`, and it is not for
|
|
this. It belongs to the 3D physics engine: bodies with mass, falling and
|
|
bouncing off one another in three dimensions, telling you where they touched
|
|
and how hard. You will meet it in lesson twenty-four. Nothing on the overlay
|
|
reports itself, and nothing needs to. Your rocks are in a list, you know how
|
|
many there are, and asking is cheap.
|
|
|
|
And when you want to know not just *whether* two things met but where the
|
|
moving one should end up -- sliding along a wall instead of stopping dead
|
|
inside it -- the collide calls deliberately do not answer that. A library
|
|
called `bump` is bundled with Singe for exactly that job, and the reference's
|
|
Included Libraries section says how to reach it.
|
|
|
|
=== Try It
|
|
|
|
. *Take the padding out.* Set `ROCK_PAD` to `0` and play for a minute. Then set
|
|
it to `8` and play again. One of them feels like the game is lying to you.
|
|
. *Look at what you are testing.* Set `SHOW_BOXES` to `true` and watch the
|
|
green outlines while a rock passes the ship. Now set `SHIP_PAD_X` to `16`,
|
|
play, and notice that nothing can hit you any more. Work out why from the
|
|
arithmetic in `shipBox`.
|
|
. *Make the shot too fast.* Set `SHOT_SPEED` to `40`. Count how many rocks you
|
|
hit out of ten. Work out from the numbers why that happens before you change
|
|
it back.
|
|
. *Use the whole shot.* Rewrite `shotHitsRock` to use `collideRects` with the
|
|
shot's full rectangle instead of `collidePointRect` with its nose. Play both
|
|
and decide which one you prefer; there is no right answer, which is worth
|
|
knowing.
|
|
. *Make the rock round.* Give the rock a circle instead of a box: its center is
|
|
`rock.x + rockWidth / 2` across and `rock.y + rockHeight / 2` down, and a
|
|
radius of `10` is about right. Use `collideRectCircle` for the ship and
|
|
`collidePointCircle` for the shot, and draw the circle in `drawBoxes` with
|
|
`overlayCircle`, which takes a center and a radius.
|
|
|
|
=== Break It on Purpose
|
|
|
|
The two boxes are already four values each, so it is tempting to feed them
|
|
straight in and skip the eight local names:
|
|
|
|
[source,lua]
|
|
----
|
|
function shipHitsRock(rock)
|
|
return collideRects(shipBox(), rockBox(rock))
|
|
end
|
|
----
|
|
|
|
That looks right and it is wrong. Run it and the game stops before it draws a
|
|
thing:
|
|
|
|
----
|
|
186:collideRects: Expected 8 argument(s), got 5.
|
|
----
|
|
|
|
The line is the one you just typed, so yours may not be 186. Five, though.
|
|
`rockBox(rock)` is the last thing in the parentheses, so all four of its values
|
|
went in. `shipBox()` is not, so it was cut down to one. One plus four is five,
|
|
and `collideRects` will not guess at the other three.
|
|
|
|
This error comes from the engine rather than from Lua, so it is shaped like the
|
|
ones in lessons nine and ten: the number at the front is the line, then the
|
|
name of the function that refused, then the complaint. Put the eight local
|
|
names back.
|
|
|
|
=== What You Learned
|
|
|
|
* Collision is arithmetic on rectangles, and you can write it yourself.
|
|
* Two rectangles miss in exactly four ways; if none of them is true, they hit.
|
|
* `collideRects` takes a corner and a size for each of two rectangles and
|
|
answers `true` or `false`. It counts edges that only touch as touching.
|
|
* The collision tests are part of the game kit, plain functions with nothing
|
|
to set up. Save, Timer, Tween, and Stats are the rest of it.
|
|
* `collidePointRect` asks whether a point is inside a rectangle, which is the
|
|
right question for anything small.
|
|
* The box you test is smaller than the picture you draw, or the game feels
|
|
unfair.
|
|
* A function can return several values, and only the last call in an argument
|
|
list passes all of them on.
|
|
* The collision tests only answer. What happens next is your code, and keeping
|
|
the two apart lets you change either one alone.
|
|
* Drawing your hit boxes on screen turns an invisible bug into a visible one.
|
|
* `onCollision` is for 3D physics bodies, not for sprites on the overlay.
|
|
|
|
=== Next Time
|
|
|
|
The game counts your score and takes your lives, and the moment you quit, the
|
|
best score anybody ever got is gone. It starts in the middle of the action the
|
|
instant it loads. And it keeps track of what it is doing with two true-or-false
|
|
variables, `over` and `dying`, which is exactly the trap lesson seven warned
|
|
you about. Lesson twelve gives it a title screen, one variable in place of
|
|
those two, and a high score that is still there tomorrow.
|