Skip to navigation


Collisions and bullets

Detecting when the player has crashed or shot down an object

Probably the most familiar part of Lander is the destruction sequence when you nudge the mouse on the launchpad. But this is just one example of an explosive demise; you can also collide with objects on the ground, faceplant into the landscape, or get smashed to pieces by falling rocks (though the latter only happens if you're good enough to reach a score of 800, something most of us can only dream of).

Destroying trees in Acorn Archimedes Lander

Revenge - if you can call it that - can be served in the form of bullets, which you can use to destroy all those pesky trees and gazebos and buildings and rockets that would otherwise just sit there, minding their own business. In this deep dive, we'll take a look at all these different types of collision, as well as the code behind the bullet system.

Hitting the ground
------------------

Let's start by talking about the most popular pastime in Lander - hitting the ground. Collisions with the ground are checked in part 2 of the DrawObject routine, using a simple trick: if the shadow of any one of the vertices of an object has a lower projected y-coordinate than the projected y-coordinate of the vertex itself, then that means the shadow appears higher up the screen than the object casting the shadow. The game's light source is high above the landscape, so shadows are calculated by working out the coordinate of the landscape that is directly beneath the object. Having a shadow that is higher up the screen than the object can only mean one thing: that vertex has dropped below ground level. This means the object has crashed into the ground, so the routine sets the crashedFlag variable to &FF to indicate the crash.

In Lander, all objects potentially set crashedFlag, but we only care about the result when we are drawing the player's ship. In this case we check the value of crashedFlag in part 3 of the MoveAndDrawPlayer routine, just after calling DrawObject to draw the player's ship. If it's non-zero, then we jump to LoseLife to do just that.

There is another check for hitting the ground, on top of the shadow check when drawing the object. Part 3 of the MoveAndDrawPlayer routine also fetches the altitude of the landscape below the ship, but this time it's the point directly beneath the centre of the ship. If that point on the ground has a higher altitude than the bottom of the undercarriage of the ship, then that means the bottom of our ship has gone past ground level, and we have hit the ground (the bottom of the undercarriage is defined as the point that's UNDERCARRIAGE_Y coordinates down from the centre of the ship, along the y-axis).

If this happens, then we jump to the LandOnLaunchpad routine to check whether we have landed on the launchpad, or whether we have hit the ground. If we have landed on the launchpad then we check that our speed is less than the maximum allowed landing speed, which is defined in the LANDING_SPEED configuration variable, and if we've landed softly enough, we touchdown and start the refuelling process. However, if our speed is too high, or we aren't above the launchpad, then that's another call to LoseLife, and another fiery end.

Hitting an object
-----------------

Part 3 of the MoveAndDrawPlayer routine contains yet another collision check - whether the player has hit a 3D object. In Lander, almost all objects are ground-based, and their positions on the landscape are stored in the object map (see the deep dive on placing objects on the map for details of how this works). This makes the check for object collisions quite easy.

As part of the ground-collision checks that we looked at above, we fetched the altitude of the point on the landscape directly beneath the centre of the player's ship. We can work out the clearance between the bottom of the ship's undercarriage and the ground by taking working out the difference between the ship's altitude and the altitude of the landscape, and then subtracting UNDERCARRIAGE_Y to give us the distance between the bottom of the ship and the ground. We can now compare this to the SAFE_HEIGHT configuration variable, which contains the minimum safe height, above which we are guaranteed not to be hitting anything (this is set to a height of 1.5 tile sizes).

If we are below the safe height but have not hit the ground, then we need to check whether there is an object below the ship. We do this by clipping our ship's (x, z) coordinate down to the nearest tile corner, and checking the object map. If the object map for this tile corner contains a value in the range 1 to 11, then there is an object below the ship, and because we are below the safe height we are deemed to have hit that object, so we jump to LoseLife to process another crash. If there is an object with a value of 12 or more, then that object is a destroyed object, so we ignore it as we can't die by crashing into destroyed objects.

The clipping of the ship coordinate to the nearest tile is not that accurate, and the collision check doesn't take the object's height into consideration. This means that we collide with tall trees and rockets at the same altitude as we collide with squat buildings (i.e. at an undercarriage clearance of less than SAFE_HEIGHT, or 1.5 tiles). Perhaps that's why it's so easy to crash in Lander...

Rock collisions
---------------

Lander has another way of killing us. If we reach a score of 800, then rocks can randomly drop from the sky, with a higher chance of a rock falling with higher scores. If a rock hits our ship, then that's another way to go, so not surprisingly there is a collision detection routine for that too.

Although rocks are drawn as 3D objects, they are stored and moved as particles, so the collision logic for falling rocks appears in part 3 of the MoveAndDrawParticles routine. Rocks are diamond-shaped objects that are one tile wide in each dimension, so the rock-collision code starts by checking if either of the x-coordinate or z-coordinate of the ship are less than one tile size away from the rock's x-coordinate or z-coordinate.

If neither distance is close enough then the rock can't be hitting the ship, but if either calculation is less than one tile away from the rock in that axis, then we move on to checking whether the ship is less than one tile size away from the rock in the y-axis. If the rock is too close, then we know the rock has hit us, so it's time to lose a life, this time by jumping to the LoseLifeFromParticleLoop routine, which is just like the LoseLife routine, except it pulls a couple of values off the stack before exploding our ship.

Firing bullets
--------------

There are lots of ways to die, but our ship is not defenceless. We can fire laser bullets out of the nose of our ship, which we can use to destroy objects on the ground to score points. Let's first take a look at how bullets are fired, before looking at bullet collision detection in the next section.

Bullets are fired in part 5 of the MoveAndDrawPlayer routine. This routine starts by checking whether the right mouse button (the fire button) has been pressed, and if it has, it deducts a point from the current score, as that's the cost of firing each bullet.

It then fetches the nose vector from the ship's rotation matrix. The ship's rotation matrix defines the ship's rotation in 3D space, and it can also be thought of as consisting of three orientation vectors, each of which points along one axis of the object - on out of the object's nose, one out of the object's roof and one out of the object's right side. Specifically, the ship's rotation matrix can be thought of like this:

  [ xNoseV xRoofV xSideV ]
  [ yNoseV yRoofV ySideV ]
  [ zNoseV zRoofV zSideV ]

where the nose, roof and side vectors are these three orientation vectors. See the deep dive on flying by mouse for more information about how the rotation matrices work, but for our purposes, the vector [xNoseV yNoseV zNoseV] points out of the nose of the ship, so we can use this vector to fire bullets in that direction.

Bullets are particles, so we can fire a bullet by taking this nose vector and adding the player's current velocity, to get a 3D vector that we can use as the initial velocity of our bullet. We have to add the player's velocity as the bullets are already travelling in that direction inside the ship, so if we didn't add the ship's velocity, then the bullets would be left behind by the moving ship.

This gives us a velocity vector to pass to the AddBulletParticleToBuffer routine, which adds the bullet particle to the particle data buffer. We also need to pass the initial coordinates of the bullet, which we calculate by taking the coordinates of the ship and adding the nose vector, so the bullet fires from the end of the gun. We also subtract the ship's velocity because the first thing that happens when we process the particle in MoveAndDrawParticles is that we add the velocity, so this cancels that out to ensure the particle starts out along the line of the exhaust plume.

Bullet particles have bit 21 of their particle flags set, which denotes that they destroy objects on impact, so let's look at this next. For more information on particles and the particle flags, see the deep dive on particles and particle clouds.

Detecting bullet collisions
---------------------------

The MoveAndDrawParticles routine is responsible for moving and processing all the particles in the game on each iteration around the main loop. This includes any bullets that are flying around.

Bullets have bit 21 of their particle flags set, and when we process a particle with this flag set, we call the ProcessObjectDestruction routine to see if the bullet is anywhere near the ground, and if it is, whether it is hitting anything.

The checks in ProcessObjectDestruction are pretty similar to the checks we do when testing whether the player's ship is colliding with an object, but they're a bit simpler this time as the bullet is just a point in space, rather than a 3D ship with an undercarriage.

First we check whether the particle is within a distance of SAFE_HEIGHT of the ground, and if it is further away than this, we know there can't be a collision. If it is closer than the safe height, then we clip the bullet's x- and z-coordinates down to the nearest tile corner, and check the object map to see if there is an object on the ground at these coordinates.

If there is no object, then there is no collision, so we return from the subroutine there and then. If there is a destroyed object there, with an object type of 12 or above, then we add a small explosion to the particle data buffer via the AddSmallExplosionToBuffer routine, and that's that.

But if there is an intact object in the object map, with an object type between 1 and 11, then we add a medium-sized explosion to the particle data buffer by calling AddExplosionToBuffer, passing a value of 20 in R8 so the explosion consists of 20 clusters, or 80 particles (see the deep dive on particles and particle clouds for more on explosion clouds).

It's worth noting that rocks also have bit 21 of the particle flags set, so if they fall onto objects on the ground, they will destroy them. However, the last part of the ProcessObjectDestruction routine awards points for destroying objects, and it only does this if the particle is a bullet; if rocks destroys objects then that doesn't count towards our score, of course.

It's worth noting that we can't shoot falling rocks. Bullets are only tested to see if they have hit objects in the object map, and rocks are stored as airborne particles, not objects. You should just get out of the way of rocks, rather than trying to engage them.

The final act of the routine is to delete the bullet or rock particle from the particle data buffer, as it has hit the ground and served its purpose.

Much like our ship keeps doing...