GitHub Repo

It’s lonely in outer space. We need some other stuff up here.

If we’re doing Asteroids, and we seem to be, we are going to want some asteroids and missiles flying about. It would be a really boring game otherwise. Let’s see what we should do about that.

Planning

Bullets and asteroids will have constant velocity once created. The bullets receive some forward velocity in addition to that of the ship. (I think they therefore must have a slightly larger maximum speed, lest we run over them. We’ll think about that.) Asteroids start at a random position and velocity. When they are split, the two fragments get an adjusted velocity. I think that the official game details may be included in my Codea Lua version. In both cases, once assigned their velocities, they do not change. Bullets have a finite distance limit, or time limit, I forget which. Time would probably be more interesting, since if you really got rolling, they might go further. Again, we’ll decide on the details in due time.

So it seems to me that the update on a missile or asteroid is similar to that of the ship, but that they won’t have a controller to adjust them. That may lead to some interesting design issues.

The universe will need to know all the objects, because, while asteroids do not collide with each other they do collide with missiles or the ship. Missiles may be allowed to kill the ship, which could in principle happen if you were to drive in front of one.

This seems to be calling for some kind of Interface, since in Kotlin, collections kind of all want to be of all the same type. Probably IFO, Identified Flying Object.

Let’s add an Asteroid class and dig out the commonalities.

class AsteroidTest {
    private val tick = 1.0/60.0
    @Test
    fun `Asteroids Exist and Move`() {
        val asteroid = Asteroid(Vector2(15.0,30.0))
        asteroid.update(tick*60)
        checkVector(asteroid.position, Vector2(15.0, 30.0),"asteroid position")
    }
}

I implement Asteroid thus:

class Asteroid(val velocity: Vector2, var position:Vector2 = Vector2.ZERO) {

    fun update(deltaTime: Double) {
        position = cap(position+velocity*deltaTime)
    }

    fun cap(v: Vector2): Vector2 {
        return Vector2(cap(v.x), cap(v.y))
    }

    fun cap(coord: Double): Double {
        return (coord+10000.0)%10000.0
    }
}

We see some commonality. And, my dear colleague Hill notwithstanding, I see the possibility for some inheritance as well. For now, let’s just do an Interface, and allow duplication to crop up. No, wait, we don’t have a need for the interface yet. Let’s test our way to that need. I think I’ll create a UniverseTest next.

    @Test
    fun `collision calculation`() {
        val ship = Ship(100.0)
        val asteroid = Asteroid(Vector2.ZERO) // very slow moving
        assertThat(ship.collides(asteroid)).isEqualTo(true)
    }

I figure they’re both at zero but anyway I’ll fake it till I make it.

    fun collides(asteroid: Asteroid):Boolean {
        return false
    }

That should be enough to fail.

expected: true
 but was: false

Perfect. Now make it pass:

    fun collides(asteroid: Asteroid):Boolean {
        return true
    }

Green. This is easy! Maybe make the test harder.

    @Test
    fun `collision calculation`() {
        val ship = Ship(100.0)
        val asteroid = Asteroid(Vector2.ZERO) // very slow moving
        assertThat(ship.collides(asteroid)).describedAs("on top").isEqualTo(true)
        val tooFar = Vector2(ship.killRadius + asteroid.killRadius + 1, 0.0)
        var rotated = tooFar.rotate(37.0)
        ship.position = rotated
        assertThat(ship.collides(asteroid)).describedAs("too far").isEqualTo(false)
        val closeEnough = Vector2(ship.killRadius + asteroid.killRadius - 1, 0.0)
        rotated = closeEnough.rotate(37.0)
        ship.position = rotated
        assertThat(ship.collides(asteroid)).describedAs("too close").isEqualTo(true)
    }

If we’re 1101 away, no collision. If we’re 1099, collide. I’m not checking exactly 1100 because rounding and I don’t care.

    fun collides(asteroid: Asteroid):Boolean {
        val dist = position.distanceTo(asteroid.position)
        val allowed = killRadius + asteroid.killRadius
        return dist < allowed
    }

Test passes. I implemented killRadius as = radius on Ship and 1000 on Asteroid, since it has no radius at present. In my Lua game, the ship kR is 12, and the Asteroids are 64, 32, and 16. They halve when they split.

We’re getting to the point where we really need some constants and meta-variables. And we have quite a bit of duplication. How should we deal with that? In the Lua version, I maintain Ship and Asteroid and Bullet objects at the top. Let’s do something different here, and just have one kind of moving object, with varying parameters. Each one will have to know how to draw itself differently. In due time.

So, let’s see. they all have a position and a velocity. They do not all have the ability to accelerate, and some of them will be given controls that aren’t hooked up. Let’s just make them all have position, velocity and acceleration, but some of them accelerate very poorly.

I think I’d like to have a sort of control card, but for now we’ll just pass in all the parameters.

Let’s break everything, by renaming Ship to FlyingObject and fix things up as needed. Shouldn’t take long: it’s 1320 now.

If I use rename, Kotlin obligingly renames my tests and variable names, as you’ll see below.

But I want to change the primary constructor signature to require:

  1. Radius: Double (killRadius)
  2. Control: Controls (defaults to create one)
  3. Velocity: Vector2 (zero for ship, typically some value for asteroid or missile)
  4. Acceleration: Double (zero except for ships)

I’ll start with all of these required. Then I think we’ll create some constructor or factory methods.

Once I look at the initial change, I think we’ll want to provide position as well.

It really seems that the order should be position, velocity, acceleration, radius, controls.

class FlyingObject(
    var position: Vector2,
    var velocity: Vector2,
    private val acceleration: Vector2,
    private val killRadius: Double,
    private val controls: Controls = Controls()
) {

Test to see what we need to clean up. As I look at these tests, I want to create my ship constructor ASAP.

class FlyingObjectTest {
    private val tick = 1.0/60.0
    @Test
    fun `Ship Happens`() {
        val ship = FlyingObject(100.0)
        ship.velocity = Vector2(120.0, 120.0)
        ship.update(tick)
        assertThat(ship.position).isEqualTo(Vector2(2.0,2.0))
    }

I could fill them all in to get to green. That might be best. I’ll do that.

That took me longer than I expected, but I was moving rather sluggishly. It’s 1357. We have this for FlyingObject, with the methods as before:

class FlyingObject(
    var position: Vector2,
    var velocity: Vector2,
    private val acceleration: Vector2,
    val killRadius: Double,
    private val controls: Controls = Controls()
) {
    var pointing: Double = 0.0
    var rotationSpeed = 360.0

It looks as if we may need to add rotationSpeed as another parameter, but for now we have more than enough.

Tests are green. Commit: Convert Ship to FlyingObject with explicit parameters.

Now let’s see if we can convert the Asteroid to a FlyingObject. We’ll just edit all the references:

    @Test
    fun `collision calculation`() {
        val ship = FlyingObject(
            Vector2.ZERO,
            Vector2.ZERO,
            Vector2.ZERO,
            100.0
        )
        val asteroid = FlyingObject(
            position = Vector2.ZERO,
            velocity = Vector2.ZERO,
            acceleration = Vector2.ZERO,
            killRadius = 1000.0
        )
        assertThat(ship.collides(asteroid)).describedAs("on top").isEqualTo(true)
        val tooFar = Vector2(ship.killRadius + asteroid.killRadius + 1, 0.0)
        var rotated = tooFar.rotate(37.0)
        ship.position = rotated
        assertThat(ship.collides(asteroid)).describedAs("too far").isEqualTo(false)
        val closeEnough = Vector2(ship.killRadius + asteroid.killRadius - 1, 0.0)
        rotated = closeEnough.rotate(37.0)
        ship.position = rotated
        assertThat(ship.collides(asteroid)).describedAs("too close").isEqualTo(true)
    }

The tests are green. I think I can remove Asteroid class. Yes. Test. Green. Commit: Both ship and asteroid are now instances of FlyingObject.

Now let’s see about reasonable constructors or factory methods or something.

class FlyingObject ...
    companion object {
        fun asteroid(pos:Vector2, vel: Vector2): FlyingObject {
            return FlyingObject(pos, vel, Vector2.ZERO, 1000.0 )
        }
    }

That seems to do the trick. Let’s do one for ship also.

        fun ship(pos:Vector2, control:Controls= Controls()): FlyingObject {
            return FlyingObject(
                position = pos,
                velocity = Vector2.ZERO,
                acceleration = Vector2(60.0, 0.0),
                killRadius = 100.0,
                controls = control
            )
        }

And the tests convert nicely, looking like this:

    @Test
    fun `Asteroids Exist and Move`() {
        val asteroid = FlyingObject.asteroid(
            Vector2.ZERO,
            Vector2(15.0,30.0)
        )
        asteroid.update(tick*60)
        checkVector(asteroid.position, Vector2(15.0, 30.0),"asteroid position")
    }

    @Test
    fun `ship can turn right`() {
        val control = Controls()
        val ship = FlyingObject.ship(Vector2.ZERO, control)
        control.right = true
        ship.update(tick*10)
        assertThat(ship.pointing).isEqualTo(-60.0, within(0.01))
    }

All the ship creations are down to just one or two parameters, since I’m defaulting the controls. I could imagine even simpler tests, but these aren’t bad.

Let’s sum up.

Summary

I set out to add an Asteroid class, did so, then implemented a starting collides method, then removed the duplication by making the Ship class into FlyingObject, then converting my only asteroid code to use FlyingObject. Then I created two convenience constructors, FlyingObject.ship and FlyingObject.asteroid, which went easily once I realized that companion object is how you do that. Converting to use them was tedious and error-prone but of course the changes were all in tests, so the immediate failure of a test told me I’d done it wrong.

We’ll need a few tweaks to collide, depending on how we manage the universe, most likely just making sure that objects don’t collide with themselves. That would be awkward.

The code is simpler. I’ve removed an entire class (all three methods of it) and simplified the tests.

Nice little exercise. See you next time!