GitHub Repo

It’s about time … to work on time. I think the code is telling me something, but I’m not sure just what it is. Shadows in the mist.

And if everything else wasn’t enough, my cable is down, so if I have any questions, I’m on my own. No decent cell coverage here either, so I can’t even go out on the 5G, braving the dangers thereof. I hope I don’t have any big questions.

Anyway, time. Missiles live forever, which means that even if you do manage to destroy all the asteroids, you’re in a never-ending barrage of missiles (unless of course you never miss a shot) and they keep flying about willy-nilly1 killing you over and over again. Tedious to say the least.

In my Codea game, missiles have a lifetime of 3. I imagine that’s three seconds, but I didn’t look. It makes sense to have them terminate based on time, not distance, so that if you’re really wheeling along and fire a missile, it’ll go a longer distance in its oh too brief brief existence.

I’m sure that we’ll have other needs for an elapsed time measure. For example, the ShipMonitor should really wait a respectful interval before spawning a ship after one has met its tragic and inevitable demise. So I think I’ll just give every object a clock. But how?

My friend and colleague GeePaw Hill is working on a thing and he has an actual clock object. Now, to be honest, I think that if our objects had a couple of members, say elapsed time and lifetime, we could add deltatime into everyone’s elapsed time and they’d know how long they had been around, and if an object wanted to time out, it would happen when elapsed time exceeds lifetime. We’d set the default lifetime to BigNum and most folks would never time out. And, if some object wanted to keep track of time in the middle of things, it could just zero out elapsed time and pay attention to it thereafter.

So it’s not much of an object, is it?

Let’s build this in the naive way, then observe that it makes to our mess, and then improve the code, probably by creating a clock. But maybe not: maybe we’ll see something different.

One question that comes to mind is this: to remove itself from the world, using only the current mechanism, an object has to be returned as a discard during collision detection:

    fun processInteractions() {
        val toBeRemoved = colliders()
        flyers.removeAll(toBeRemoved)
        for (removedObject in toBeRemoved) {
            val addedByFinalize = removedObject.finalize()
            flyers.addAll(addedByFinalize)
        }
    }

I am reliably informed by IDEA that that’s the only call to removeAll in the system. So one way this could be done, and it would be consistent with our architecture, would be to have an object in the mix that checks other objects for being out of date, and if they are, removes them. For this to work, we need … let’s make a short list:

  1. Objects must have a property, elapsedTime.
  2. elapsedTime starts at 0 when the object is created.
  3. elapsedTime must be ticked up on each deltaTime.
  4. Objects must have a lifetime, the maximum number of seconds they can live.

Now there’s no reason why the elapsedTime and lifeTime can’t be virtual. Only true Flyers need them. But at present, we do not even have a place to put code that each flyer must execute. It would seem that the update method is the right place for such a thing.

Aside
I am making a bit of a mess here. The notion of the independent flyers in the mix is working out well, but the fact that ship, missile, and asteroid are all the same class is making that class, Flyer a bit nasty. And what we’re doing today is going to make it worse. Because I like to get in trouble and then see if I can get back out without needing to rewrite the world, I’m going to let the cruft grow a bit more. This simulates what often happens in real projects: we let things go and they get worse and worse until finally we have to do something. One of my chosen missions is to show that the only answer isn’t always “scrap this and start over”.

So, for now, in Flyer:

    override fun update(deltaTime: Double): List<Flyer> {
        val result: MutableList<Flyer> = mutableListOf()
        val additions = controls.control(this, deltaTime)
        result.addAll(additions)
        move(deltaTime)
        return result
    }

We’ll tick an elapsedTime member here. My plan is to have that default to a zero in the interface IFlyer and to be real here.

    override fun update(deltaTime: Double): List<Flyer> {
        tick(deltaTime)
        val result: MutableList<Flyer> = mutableListOf()
        val additions = controls.control(this, deltaTime)
        result.addAll(additions)
        move(deltaTime)
        return result
    }

I made it a method because while I am a monster, I’m not a terrible one, and clearly ticking the clock has nothing to do with this other stuff in here. It, too, probably needs to be broken out. Not our present job, though.

So, tick:

    fun tick(deltaTime: Double) {
        elapsedTime += deltaTime
    }

Now elapsed time needs to be set to zero in creation:

    var elapsedTime = 0.0

This will soon need an override notation, I think. We’ll see how this goes. And I really could have written a test. Let’s do so now.

    @Test
    fun `Flyer clock ticks on update`() {
        val ship = Flyer.ship(Vector2.ZERO)
        assertThat(ship.elapsedTime).isEqualTo(0.0)
        ship.update(5.0)
        assertThat(ship.elapsedTime).isEqualTo(5.0)
        ship.update(3.0)
    }

That runs green. Could commit, but I think there’s a bit more to do before we set this free. I’d like all the IFlyer objects to have a property elapsedTime, which can be virtual and unchanging. That will make it easier to build our upcoming clock. I would like to make tick mandatory but I don’t see at this instant how to do it. So …

interface IFlyer {
    val killRadius: Double
        get() = -Double.MAX_VALUE
    val position: Vector2
        get() = Vector2(-666.0, -666.0)
    val ignoreCollisions: Boolean
        get() = true
    val score: Int
        get() = 0
    val velocity
        get() = Vector2(0.0, 100.0)
    val elapsedTime
        get() = 0.0

And now I’ll need to override in Flyer, I expect. Yes. Test again. Green. Commit: Flyer objects know elaspedTime since creation.

Ah. Push failed: I have no Internet. I think I mentioned that. Anyway committed to my local repo.

Now the clock. We should test it. What should it do? Create some Flyer, a missile. Create the LifetimeClock. Interact the two. See that the flyer comes back to be deleted only after its time has expired.

Ah. What time? We forgot that, didn’t we? The test will drive it out, no problem.

    @Test
    fun `lifeetime clock removes oldbies`() {
        val missileKillRadius = 10.0
        val missilePos = Vector2.ZERO
        val missileVel = Vector2.ZERO
        val missile =  Flyer(missilePos, missileVel, missileKillRadius, 0, false, MissileView())
        val clock = LifetimeClock()
        var discards = missile.collisionDamageWith(clock)
        assertThat(discards).isEmpty()
        discards = clock.collisionDamageWith(missile)
        assertThat(discards).isEmpty()
        missile.update(4.0)
        missile.collisionDamageWith(clock)
        assertThat(discards).contains(missile)
        discards = clock.collisionDamageWith(missile)
        assertThat(discards).contains(missile)
    }

I think that tells the story. We try the collision both ways and when time is 0 it lives and when it’s 4, it dies. If only we had a LifetimeClock to test. Let’s create it.

IDEA kindly builds me this much:

class LifetimeClock : IFlyer {
    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        TODO("Not yet implemented")
    }

    override fun collisionDamageWithOther(other: IFlyer): List<IFlyer> {
        TODO("Not yet implemented")
    }

    override fun update(deltaTime: Double): List<IFlyer> {
        TODO("Not yet implemented")
    }

}

No point running the test yet, with all those TODO in there.

Our convention is to do the work in the With method and for the WithOther to invoke it. So, this:

    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        if (other.elapsedTime > other.lifetime)
            return listOf(other)
        else
            return emptyList()
    }

    override fun collisionDamageWithOther(other: IFlyer): List<IFlyer> {
        return collisionDamageWith(other)
    }

    override fun update(deltaTime: Double): List<IFlyer> {
        return emptyList()
    }

We need lifetime. I’ll put that into the interface and my object.

interface IFlyer ...
    val elapsedTime
        get() = 0.0
    val lifetime
        get() = Double.MAX_VALUE

class Flyer ...
    var heading: Double = 0.0
    override var elapsedTime = 0.0
    override var lifetime = Double.MAX_VALUE

And in my test, I’ll set the lifetime:

...
        val missile =  Flyer(missilePos, missileVel, missileKillRadius, 0, false, MissileView())
        missile.lifetime = 3.0
        val clock = LifetimeClock()

Test, with high hopes. Test fails:

Expecting EmptyList:
  []
to contain:
  [Flyer Vector2(x=0.0, y=0.0) (10.0)]
but could not find the following element(s):
  [Flyer Vector2(x=0.0, y=0.0) (10.0)]

That’s on the first call to the collision code after updating the time.

I add a couple of assertions:

        missile.update(4.0)
        assertThat(missile.elapsedTime).isEqualTo(4.0)
        assertThat(missile.elapsedTime).isGreaterThan(missile.lifetime)
        missile.collisionDamageWith(clock)
        assertThat(discards).contains(missile)

The new ones pass. What’s up with clock? Nothing. I am a fool: I’m not saving the result of the call, in my test. Fixing that, I’m green.

        missile.update(4.0)
        assertThat(missile.elapsedTime).isEqualTo(4.0)
        assertThat(missile.elapsedTime).isGreaterThan(missile.lifetime)
        discards = missile.collisionDamageWith(clock) // missed storing this one
        assertThat(discards).contains(missile)
        discards = clock.collisionDamageWith(missile)
        assertThat(discards).contains(missile)

We are green. Commit: Lifetime clock tested and works.

Writing this test has shown me that we do not have an official centralized way to create a missile, where we could plant a suitable timer. Here’s what happens when we fire:

    private fun createMissile(obj: Flyer): Flyer {
        val missileKillRadius = 10.0
        val missileOwnVelocity = Vector2(SPEED_OF_LIGHT / 3.0, 0.0).rotate(obj.heading)
        val missilePos = obj.position + Vector2(obj.killRadius + 2 * missileKillRadius, 0.0).rotate(obj.heading)
        val missileVel = obj.velocity + missileOwnVelocity
        return Flyer(missilePos, missileVel, missileKillRadius, 0, false, MissileView())
    }

Let’s add a missile helper to our companion, which already knows how to create asteroids and ships:

    companion object {
        fun asteroid(pos:Vector2, vel: Vector2, killRad: Double = 500.0, splitCt: Int = 2): Flyer {
            return Flyer(
                position = pos,
                velocity = vel,
                killRadius = killRad,
                splitCount = splitCt,
                ignoreCollisions = true,
                view = AsteroidView()
            )
        }

        fun ship(pos:Vector2, control:Controls= Controls()): Flyer {
            return Flyer(
                position = pos,
                velocity = Vector2.ZERO,
                killRadius = 150.0,
                splitCount = 0,
                ignoreCollisions = false,
                view = ShipView(),
                controls = control,
            )
        }

Now missiles are rather special. Their creation is dependent on a ship, from which the createMissile code above concocts the right position and velocity for the missile. So let’s, in the companion, create a missile given only a ship. I scarf the code from createMissile in Controls (which will shortly use what we’re writing here):

        fun missile(ship: Flyer): Flyer {
            val missileKillRadius = 10.0
            val missileOwnVelocity = Vector2(SPEED_OF_LIGHT / 3.0, 0.0).rotate(ship.heading)
            val missilePos = ship.position + Vector2(ship.killRadius + 2 * missileKillRadius, 0.0).rotate(ship.heading)
            val missileVel = ship.velocity + missileOwnVelocity
            return Flyer(missilePos, missileVel, missileKillRadius, 0, false, MissileView())
        }

This isn’t quite right yet. We have to set the time.

        fun missile(ship: Flyer): Flyer {
            val missileKillRadius = 10.0
            val missileOwnVelocity = Vector2(SPEED_OF_LIGHT / 3.0, 0.0).rotate(ship.heading)
            val missilePos = ship.position + Vector2(ship.killRadius + 2 * missileKillRadius, 0.0).rotate(ship.heading)
            val missileVel = ship.velocity + missileOwnVelocity
            val flyer =  Flyer(missilePos, missileVel, missileKillRadius, 0, false, MissileView())
            flyer.lifetime = 3.0
            return flyer
        }

We should certainly be seeing that the creation of Flyers is a mess. It’s getting to be time to sort that out. Meanwhile, I change the controls to use this function and in the game I expect to see my missiles time out … oh … as soon as we add a clock to the mix:

    fun createContents(controls: Controls) {
        val ship = newShip(controls)
        add(ship)
        add(ShipMonitor(ship))
        add(ScoreKeeper())
        add(LifetimeClock())
        for (i in 0..7) {
            val pos = Vector2(random(0.0, 10000.0), random(0.0,10000.0))
            val vel = Vector2(1000.0, 0.0).rotate(random(0.0,360.0))
            val asteroid = Flyer.asteroid(pos,vel )
            add(asteroid)
        }
    }

That does in fact work. I learn two things. First of all, three seconds is the time it takes to cross from center of screen almost to the edge, and that’s not quite long enough. We’ll have to fiddle with that number a bit for best game play. Second, when the ship is traveling even at a reasonable forward speed, it runs over its own missiles and dies. This happens even if the ship is coasting at well below light speed.

I spend some time futzing with that. I ascertain that the missile does appear just barely past the kill radius of the ship (which is somewhat short of the nose of the drawn ship, because it is longer than it is wide). I find that in my Codea version, if you travel straight ahead at a speed larger than some amount, the same thing happens. Curious. I find that starting the missile further away makes the problem go away. It seems to me that it should work now, so I’ll chase the issue further when my brain is more clear. It might have to do with when new items show up and when they first get to move. We’ll find out, probably.

I still have no Internet, though it has been back for brief periods. Meh. I’ll restart the modem just in case, without much optimism. Let’s sum up and get outa here.

Summary

The mission was to add an elapsed timer to flyers, and to have a lifetime clock that can be used to time missile lifetimes. Both those went smoothly, and I should have never looked at the screen.

The idea of these sweeper objects in the mix is holding up well. The design for the basic flyers is getting more and more messy. Someone needs to do something about that, real soon now. The Flyer is probably the largest and most complex of our objects:

class Flyer(
    override var position: Vector2,
    override var velocity: Vector2,
    override var killRadius: Double,
    var splitCount: Int = 0,
    override val ignoreCollisions: Boolean = false,
    val view: FlyerView = NullView(),
    val controls: Controls = Controls()
) : IFlyer {
    var heading: Double = 0.0
    override var elapsedTime = 0.0
    override var lifetime = Double.MAX_VALUE

    fun accelerate(deltaV: Vector2) {
        velocity = (velocity + deltaV).limitedToLightSpeed()
    }

    private fun asSplit(): Flyer {
        splitCount -= 1
        killRadius /= 2.0
        velocity = velocity.rotate(random() * 360.0)
        return this
    }

    private fun asTwin() = asteroid(
        pos = position,
        vel = velocity.rotate(random() * 360.0),
        killRad = killRadius,
        splitCt = splitCount
    )

    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        return other.collisionDamageWithOther(this)
    }

    override fun collisionDamageWithOther(other: IFlyer): List<IFlyer> {
        if ( this === other) return emptyList()
        if ( this.ignoreCollisions && other.ignoreCollisions) return emptyList()
        val dist = position.distanceTo(other.position)
        val allowed = killRadius + other.killRadius
        return if (dist < allowed) listOf(this,other) else emptyList()
    }

    override fun draw(drawer: Drawer) {
        val center = Vector2(drawer.width/2.0, drawer.height/2.0)
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.translate(position)
        view.draw(this, drawer)
    }

    override fun move(deltaTime: Double) {
        position = (position + velocity * deltaTime).cap()
    }

    override fun finalize(): List<IFlyer> {
        val result: MutableList<IFlyer> = mutableListOf()
        val score = getScore()
        if (score.score > 0 ) result.add(score)
        if (splitCount >= 1) {
            val meSplit = asSplit()
            result.add(meSplit.asTwin())
            result.add(meSplit)
        }
        return result
    }

    private fun getScore(): Score {
        val score = when (killRadius) {
            500.0 -> 20
            250.0 -> 50
            125.0 -> 100
            else -> 0
        }
        return Score(score)
    }

    fun tick(deltaTime: Double) {
        elapsedTime += deltaTime
    }

    override fun toString(): String {
        return "Flyer $position ($killRadius)"
    }

    fun turnBy(degrees:Double) {
        heading += degrees
    }

    override fun update(deltaTime: Double): List<Flyer> {
        tick(deltaTime)
        val result: MutableList<Flyer> = mutableListOf()
        val additions = controls.control(this, deltaTime)
        result.addAll(additions)
        move(deltaTime)
        return result
    }

We see some obvious things, like draw, because everyone needs to draw. And all the details of drawing are handled by the view, which is provided as a sort of strategy, so that’s not bad.

Both draw and tick are really applicable to all the flyers, at least the ones that fly. But other methods are not so applicable. turnBy, move, and accelerate only apply to one kind of ship. The closest to another one is the Saucer, and I don’t think it’ll use those methods.

The getScore method really only applies to the Asteroid, and relies, oddly, on the killRadius of the object in question. The “real” question would be something about what size asteroid you are in a more generic sense, or perhaps some kind of built-in destruction score?

And so on and so on. I want to avoid subclasses but there seems to be a “type” thing going on. Maybe we’ll have strategies?

Overall, I like how it’s going, but cruft is building up. That cannot long endure.

What will we do? It remains to be seen, but it’s getting past time to clean it up. Come find out!



  1. Usually, I style that phrase as will ‘e, nil ‘e, but today I’m not going to do that. I found that in some book I read. It may or may not be part of the source of the phrase. Why are we even here, let’s get back to time.