== Lesson 11: Hitting Things image::learn/11-hitting-things.png[The finished lesson, 480] Your ship slides along the bottom of the screen and the rocks fall past it. Nothing has ever touched anything. In this lesson the ship fires, the rocks break, and the game finally pushes back. Two things in a game touch when the numbers say they do. Nobody looks at the pictures. Working out what the numbers have to say is the whole of collision, and you can do it yourself in a dozen lines before the engine does it for you in one. === Where You Are The script from lesson ten loads its artwork from the `art` folder beside it, draws the ship near the bottom, and drops rocks down the screen. It looks like this, with the details of the rocks left out: [source,lua] ---- local shipSprite = spriteLoad(DIR .. "art/spaceship.png") local rockSprite = spriteLoad(DIR .. "art/asteroid.png") local shootSound = soundLoad(DIR .. "art/shoot.wav") local boomSound = soundLoad(DIR .. "art/boom.wav") ---- Everything in this lesson is added to that script. The whole finished thing is in `learn/11-hitting-things.singe` if you lose your way. === Work It Out Yourself First The ship is a picture 32 wide and 24 tall, drawn with its top left corner at `shipX`, `shipY`. A rock is 24 by 24 at `rock.x`, `rock.y`. Are they touching? That question 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. Type this in, above your other functions, and read it rather than trusting it: [source,lua] ---- local function overlap(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 ---- 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 then 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 have now written it once and will never write it again. === The Engine Already Has It Delete `overlap`. Singe ships the same test, and it is called `collideRects`: [source,lua] ---- if collideRects(shipX, shipY, 32, 24, rock.x, rock.y, 24, 24) then soundPlay(boomSound) end ---- Eight numbers in, `true` or `false` out, in the same order you just wrote them: the first rectangle's corner and size, then the second one's. It is one of a small family of collision functions, all of them plain questions with no handles to keep and nothing to set up, and the reference's Collide section lists every one. Two edges that touch exactly count as touching, and a rectangle with no width or height touches nothing at all. 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 Draw the ship's 32 by 24 rectangle on paper and shade in the ship. There is sky in the corners. A rock that clips the top right corner of that 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: [source,lua] ---- local SHIP_WIDTH = 32 local SHIP_HEIGHT = 24 local SHIP_PAD_X = 6 local SHIP_PAD_Y = 5 local ROCK_WIDTH = 24 local ROCK_HEIGHT = 24 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. Rather than write that arithmetic out at every test, write it once per kind of thing: [source,lua] ---- local function rockBox(rock) return rock.x + ROCK_PAD, rock.y + ROCK_PAD, ROCK_WIDTH - ROCK_PAD * 2, ROCK_HEIGHT - ROCK_PAD * 2 end local function shipBox() return shipX + SHIP_PAD_X, shipY + SHIP_PAD_Y, SHIP_WIDTH - SHIP_PAD_X * 2, SHIP_HEIGHT - SHIP_PAD_Y * 2 end ---- Each of those 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] ---- local 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 ---- The padding arithmetic is now in exactly two places, and the numbers it uses are at the top of the file, so you can tune the game's fairness by changing one number rather than by hunting through your tests. === Firing Back A shot is a small record in a list, exactly like a rock. `bullet.png` is 4 wide and 10 tall, and shots travel up the screen instead of down: [source,lua] ---- local SHOT_WIDTH = 4 local SHOT_HEIGHT = 10 local SHOT_SPEED = 6 local shotSprite = spriteLoad(DIR .. "art/bullet.png") local shots = {} local function fireShot() local shot = {} shot.x = shipX + SHIP_WIDTH / 2 - SHOT_WIDTH / 2 shot.y = shipY - SHOT_HEIGHT shots[#shots + 1] = shot soundPlay(shootSound) end local function moveShots() for i = #shots, 1, -1 do local shot = shots[i] shot.y = shot.y - SHOT_SPEED if shot.y + SHOT_HEIGHT < 0 then table.remove(shots, i) end end end ---- The fire button calls it, alongside the movement you already have: [source,lua] ---- function onInputPressed(what) if what == SWITCH_LEFT then movingLeft = true elseif what == SWITCH_RIGHT then movingRight = true elseif what == SWITCH_BUTTON1 then fireShot() end end ---- And the shots are drawn in `onOverlayUpdate` the way the rocks are: [source,lua] ---- for _, shot in ipairs(shots) do spriteDraw(shotSprite, shot.x, shot.y) end ---- === A Shot Is Almost a Point A shot is four pixels wide. Shrinking that into a hit box would leave nearly nothing, and testing a whole rectangle against a rock to find out whether a sliver of light touched it is more machinery than the question deserves. 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] ---- local function shotHitsRock(shot, rock) local rx, ry, rw, rh = rockBox(rock) return collidePointRect(shot.x + SHOT_WIDTH / 2, shot.y, rx, ry, rw, rh) end ---- `shot.y` is the top of the shot and `shot.x + SHOT_WIDTH / 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. === Deciding What Happened One function, called once a frame, asks every question and acts on the answers: [source,lua] ---- local function checkHits() for s = #shots, 1, -1 do for r = #rocks, 1, -1 do if shotHitsRock(shots[s], rocks[r]) then table.remove(rocks, r) table.remove(shots, s) soundPlay(boomSound) break end end end for r = #rocks, 1, -1 do if shipHitsRock(rocks[r]) then table.remove(rocks, r) soundPlay(boomSound) end end end ---- Call it from `onOverlayUpdate`, after everything has moved and before anything is drawn: [source,lua] ---- moveShip() moveRocks() moveShots() checkHits() ---- Run it. Rocks fall, shots rise, and where they meet there is a bang and a gap. The rock that reaches the ship makes the same noise and disappears, which is not yet a punishment; lesson twelve turns it into one. === Seeing the Boxes The hit boxes are invisible, which makes a mistake in them invisible too. Draw them while you are working: [source,lua] ---- local SHOW_BOXES = false local function drawBox(x, y, width, height) overlayBox(x, y, x + width - 1, y + height - 1) end local function drawBoxes() colorForeground(0, 255, 0, 255) drawBox(shipBox()) for _, rock in ipairs(rocks) do drawBox(rockBox(rock)) end end ---- And at the end of the drawing in `onOverlayUpdate`: [source,lua] ---- if SHOW_BOXES then drawBoxes() end ---- Change `SHOW_BOXES` to `true` and the green outlines appear over the artwork. Change it back when you are done. A switch like this, one word at the top of a file that turns a picture of what the program believes on and off, is worth more than any amount of staring. === What Just Happened [source,lua] ---- local function rockBox(rock) return rock.x + ROCK_PAD, rock.y + ROCK_PAD, ROCK_WIDTH - ROCK_PAD * 2, ROCK_HEIGHT - 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) end ---- `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 + SHOT_WIDTH / 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] ---- for s = #shots, 1, -1 do ---- The loop runs backwards, from the last shot down to the first, because it removes things as it goes. `table.remove(shots, 3)` shuffles every later shot down one place, so a forward loop would step straight over the shot that moved into the gap. Counting down, everything you have not looked at yet is below you, and removing something above you cannot disturb it. You met this in lesson six; this is where it earns its keep. [source,lua] ---- break end end end ---- `break` leaves the loop it is inside immediately. One shot destroys one rock, so once a rock is gone there is no point comparing the same shot against the rest of them -- and the shot itself has been removed, so carrying on would compare a shot that no longer exists. `break` stops the inner loop over rocks; the outer loop over shots carries on with the next shot. [source,lua] ---- colorForeground(0, 255, 0, 255) overlayBox(x, y, x + width - 1, y + height - 1) ---- `overlayBox` draws the outline of a rectangle, and it takes two *corners*, not a corner and a size -- the one place in this lesson where the numbers change shape. The `- 1` is because both corners are included: a box 20 wide that starts at 100 ends at 119. The colour is not an argument. `colorForeground` sets it, and every shape drawn afterwards uses it until something changes it again. === 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 centre 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. That last one 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 teleports from above a rock to below 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 `20` and look again. . *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 4 by 10 rectangle instead of `collidePointRect` with its nose. Play both and decide which one you prefer; there is no right answer, which is worth knowing. . *Rocks against rocks.* Write a loop that compares every rock with every other rock and prints a line with `debugPrint` when two of them overlap. Watch how many comparisons that is, and why nobody does it for a thousand rocks. === 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] ---- local function shipHitsRock(rock) return collideRects(shipBox(), rockBox(rock)) end ---- That looks right and it is wrong. Run it and the game stops with: ---- 62:collideRects: Expected 8 argument(s), got 5. ---- Five. `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 a little differently from the ones in lesson eight: 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`. * `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. * Loop backwards through a list you are removing things from, and `break` out of a loop that has nothing left to find. * 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 Rocks break and the ship gets hit, and neither costs anything. Lesson twelve adds a score that goes up, lives that run out, a title screen to come back to, and a high score that is still there tomorrow.