GitHub Repo

Space objects, as everyone knows, have a maximum speed limit. This morning, I plan to implement it.

In the space we live in, the speed of light is the limit, and it is, as far as we know, strictly enforced. In the space of games like Spacewar and Asteroids, there is also a speed limit, which is more or less arbitrary, tuned to make the game fun to play. Fast but not too fast.

What is that limit? I do not know, but I do know how it’s implemented in my own Lua version of Asteroids:

function Ship:actualShipMove()
    if U.button.go then
        U:playStereo(U.sounds.thrust, self)
        local accel = vec2(0.015,0):rotate(self.radians)*U.processorRatio
        self.step = self.step + accel
        self.step = maximize(self.step, 6)
    else
        self.step = self.step*(0.995^U.processorRatio)
    end
    self:finallyMove()
end

function maximize(vec, size)
    local s = vec:len()
    if s <= size then
        return vec
    else
        return vec*size/s
    end
end

That would be more interesting if we knew the meaning of the critical value 6. The maximum speed of the ship in my Asteroids is 6. Not as much help as one might have hoped. At a guess, we’d like to be able to fly across the universe in about two seconds. The universe in our Kotlin game is 10,000 Universal Space Distance Units wide, so the maximum speed should be about 5000. We know we’ll have to tune that value when we look at the screen: we just need a number.

Let’s write a test, or part of one …

Well, this is even less than I expected to write:

    @Test
    fun `speed of light`() {
        val control = Controls()
        val ship = Ship(100.0, control)
        control.accelerate = true

    }

I realize, forgive me, it’s only about 0700 here and I may not actually be awake, that the ship cannot turn yet. We need it to be able to move in any direction, and to maximize its speed in that direction. So let’s divert to rotating the ship. New test:

    @Test
    fun `ship can turn`() {
        val control = Controls()
        val ship = Ship(100.0, control)
        control.left = true
        ship.update(tick*60)
        control.left = false
        control.accelerate  = true
        ship.update(tick*60)
        checkVector(ship.velocity, Vector2.ZERO, "rotated velocity")
    }

There’s some red in here. Controls doesn’t understand left. And yes, I know the velocity isn’t going to be ZERO. At least I hope it won’t be. The error message is:

[rotated velocity x] 
Expecting actual:
  60.0
to be close to:
  0.0
by less than 1.0E-4 but difference was 60.0.
(a difference of exactly 1.0E-4 being considered valid)

Comprehensive, to say the least. What this tells us is that the speed after a second of acceleration will be 60. Good to know. Makes sense, because in the SHIP:

    var acceleration = Vector2(60.0,0.0)

That’s 60 USDU1, of course. But we need to rotate. We need to rotate. Here’s a test for that.

    @Test
    fun `ship can turn`() {
        val control = Controls()
        val ship = Ship(100.0, control)
        control.left = true
        ship.update(tick*60)
        control.left = false
        control.accelerate  = true
        ship.update(tick*60)
        checkVector(ship.velocity, Vector2.ZERO, "rotated velocity")
    }

I’d like to make my checkVector function tell me the whole vector when it fails. So:

    private fun checkVector(actual:Vector2, should: Vector2, description: String) {
        assertThat(actual.x)
            .describedAs("$description x of (${actual.x},${actual.y})")
            .isEqualTo(should.x, within(0.0001))
        assertThat(actual.y)
            .describedAs("$description y of (${actual.x},${actual.y})")
            .isEqualTo(should.y, within(0.0001))
    }

We can run that and see it fail.

[rotated velocity x of (60.0,0.0)] 
Expecting actual:
  60.0
to be close to:
  0.0
by less than 1.0E-4 but difference was 60.0.
(a difference of exactly 1.0E-4 being considered valid)

We need to do the actual rotation. The ship already has a variable pointing, which is intended (by me) to be its angle in degrees. How far around do we want it to turn in a second? Let’s guess that it’s all the way around. So that would be 360 degrees per second. (I think the rotation that draw wants is in degrees. Either way we’re in for converting because OPENRNDR swings both ways on rotation, degrees sometimes, radians sometimes.)

I chuckle to myself. the test I just wrote is going to rotate us for one second, which would bring us right around to zero. I’ll change it to 15 ticks, shooting for about 90 degrees. ANd to do the rotation itself:

class Ship(private val radius: Double, private val controls: Controls = Controls()) {
    var realPosition: Vector2 = Vector2(0.0, 0.0)
    var pointing: Double = 0.0
    var velocity = Vector2(0.0, 0.0)
    var acceleration = Vector2(60.0,0.0)
    var rotationSpeed = 360.0


    fun update(deltaTime: Double) {
        if (controls.left) pointing = pointing + rotationSpeed*deltaTime
        if (controls.accelerate) velocity += acceleration*deltaTime
        val proposedPosition = realPosition + velocity*deltaTime
        realPosition = cap(proposedPosition)
    }

The above will change pointing but acceleration needs to be applied in the new direction as well. So we need to rotate the acceleration vector. I hope that vectors know how to rotate.

    fun update(deltaTime: Double) {
        if (controls.left) pointing = pointing + rotationSpeed*deltaTime
        if (controls.accelerate) velocity += rotatedAcceleration()*deltaTime
        val proposedPosition = realPosition + velocity*deltaTime
        realPosition = cap(proposedPosition)
    }

That might be called “putting off the inevitable”. Anyway …

    fun update(deltaTime: Double) {
        if (controls.left) pointing = pointing + rotationSpeed*deltaTime
        if (controls.accelerate) velocity += rotatedAcceleration()*deltaTime
        val proposedPosition = realPosition + velocity*deltaTime
        realPosition = cap(proposedPosition)
    }

We should really be checking pointing here in our test, shouldn’t we?

    @Test
    fun `ship can turn`() {
        val control = Controls()
        val ship = Ship(100.0, control)
        control.left = true
        ship.update(tick*15)
        assertThat(ship.pointing).isEqualTo(90.0, within(0.01))
        control.left = false
        control.accelerate  = true
        ship.update(tick*60)
        checkVector(ship.velocity, Vector2.ZERO, "rotated velocity")
    }

OK, let’s test this baby and see what she can do. Oh yeah!

[rotated velocity y of (3.67394039744206E-15,60.0)] 
Expecting actual:
  60.0
to be close to:
  0.0
by less than 1.0E-4 but difference was 60.0.
(a difference of exactly 1.0E-4 being considered valid)

At first glance that velocity looks bad but then we notice that 3*10^-15 is pretty small. And we’ve moved 60 in the +y direction. This answer is correct! We adjust the test now that we’ve confirmed the result. (I suspect in a few minutes I could have calculated it but it’s still not 0800 here and my brain doesn’t come on line until about then.)

    checkVector(ship.velocity, Vector2(0.0,60.0), "rotated velocity")

Test should be green. It is. Commit: ship rotates left at 360 degrees per second.

The commit message suggests to me that we really ought to test and implement rotating right. It’s just one thing after another, isn’t it?

Now of course I could just type this in. But we’re trying to develop the habit of testing this very visual program with tests in code.

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

With the obvious changes:

class Controls {
    var accelerate = false
    var left = false
    var right = false
}

    fun update(deltaTime: Double) {
        if (controls.left) pointing = pointing + rotationSpeed*deltaTime
        if (controls.right) pointing = pointing - rotationSpeed*deltaTime
        if (controls.accelerate) velocity += rotatedAcceleration()*deltaTime
        val proposedPosition = realPosition + velocity*deltaTime
        realPosition = cap(proposedPosition)
    }

Green. Commit: Ship can rotate in either direction.

Now then, where were we? Oh, right, speed of light. I was going to set that to 5000 if I recall. But acceleration is 60 units per second so it would take almost a minute and a half to get to that speed. That may be realistic in some sense, but it’s not going to be fun.

What we see here, just trying to be born, are two ideas:

  1. There needs to be a universe with some universal constants in it like the maximum speed;
  2. There needs to be some kind of ship configuration ability that is a bit more robust than poking values into the ship’s member variables.

I don’t really see where either of these should fit in. They are trying to be born, but they are not ready yet, at least not in my present state of mind.

Let’s just finish our test.

    @Test
    fun `speed of light`() {
        val control = Controls()
        val ship = Ship(100.0, control)
        control.left = true
        ship.update(tick*10) // 60 degrees north east ish
        control.left = false
        control.accelerate = true
        ship.update(100.90) // long time
        val v = ship.velocity
        val speed = v.length
        assertThat(speed).isEqualTo(5000.0, within(1.0))
    }

This should fail with a speed of about 6000, and yes:

actual:
  6000.0
to be close to:
  5000.0
by less than 1.0 but difference was 1000.0.
(a difference of exactly 1.0 being considered valid)

Now for our speed maximizer, we have this:

    fun update(deltaTime: Double) {
        if (controls.left) pointing = pointing + rotationSpeed*deltaTime
        if (controls.right) pointing = pointing - rotationSpeed*deltaTime
        if (controls.accelerate) velocity += rotatedAcceleration()*deltaTime
        val proposedPosition = realPosition + velocity*deltaTime
        realPosition = cap(proposedPosition)
    }

I’ll write this out longhand, but this method is screaming “refactor me”.

    fun update(deltaTime: Double) {
        if (controls.left) pointing = pointing + rotationSpeed*deltaTime
        if (controls.right) pointing = pointing - rotationSpeed*deltaTime
        if (controls.accelerate) {
            velocity = limitToSpeedOfLight(velocity + rotatedAcceleration()*deltaTime)
        }
        val proposedPosition = realPosition + velocity*deltaTime
        realPosition = cap(proposedPosition)
    }

Putting off the inevitable again2.

    val SPEED_OF_LIGHT = 5000.0
    fun limitToSpeedOfLight(v: Vector2): Vector2 {
        val speed = v.length
        if (speed < SPEED_OF_LIGHT) return v
        else return v*(SPEED_OF_LIGHT/speed)
    }

Test. We are green. Let’s make the test more interesting.

We’re traveling at an angle of 60 degrees. As all know, the angle of 60 degrees has a rise over run of 2, which is to say you go up twice as fast as you go right. So the velocity we have at 60 degrees should be oh who the hell knows, it’s <cos(60),sin(60)>*5000:

        val radians60 = toRadians(60.0)
        val expected = Vector2(cos(radians60), sin(radians60))*5000.0
        checkVector(v, expected, "velocity", 1.0)

I extended the checkVector to accept a delta:

    private fun checkVector(actual:Vector2, should: Vector2, description: String, delta: Double = 0.0001) {
        assertThat(actual.x)
            .describedAs("$description x of (${actual.x},${actual.y})")
            .isEqualTo(should.x, within(delta))
        assertThat(actual.y)
            .describedAs("$description y of (${actual.x},${actual.y})")
            .isEqualTo(should.y, within(delta))
    }

The test is still green. Commit: Speed of light is 5000.0.

Let’s reflect.

Reflection

Our control board now supports accelerate, left, and right, all booleans. Our ship update turns left or right at some given rate when the corresponding “buttons” are true, and accelerates at a given rate when the accelerator is pressed. Acceleration is applied in the positive x direction of the ship.

The various constants are arbitrary and clearly don’t make sense: We’d like to get up to speed in less than 83.33 seconds, for one thing.

So, as I mentioned above, we’re going to need some panel of information for configuring the universal constants, certainly the speed of light, but quite possibly also the expanse of space, which is presently 10,000. And we’ll want to configure ship constants as well, rotation speed and acceleration, at least.

Currently, if we change those, the tests will break.

All that, in my view, is just fine. We are just at the beginning of figuring out how to fly a ship around on the screen, and our code is correspondingly ad hoc, which in Latin means “as hacked”, I’m pretty sure.

Summary

I’ll segue into summary, because I think I’m done for now. Coming up, there are at least two directions we might go.

  1. Implement asteroids and bullets;
  2. Clean up the constants, starting on a Universe and Ship configuration.

Knowing me, we will probably start on the fun part. If we do, the code cruft will pile up a bit. Will we crash and burn? No, almost certainly not, even though our ships surely will.

See you next time!



  1. Universal Space Distance Units, aren’t you paying attention at all? 

  2. This approach is actually called “programming by intention”, where you write what you intend the program to do, and then make it do it. It’s rather a good way of driving out some abstractions, in this case, the limit of light speed.