GitHub Decentralized Repo
GitHub Centralized Repo

Let’s keep simplifying and see what we can find. Let’s work to eliminate the Collidable collection.

It turns out, he said as if software just happens, that all the space objects implement Collidable except for Splat, because, he explained, Splats don’t collide anyway, so why should they implement the interface?

However, let’s see about making Splats into collidables and see what happens. I think it might be good.

interface Collidable {
    val position: Point
    val killRadius: Double
    fun interactWith(other: Collidable, trans: Transaction)
    val collisionStrategy: Collider
}


class Splat(
    var position: Point,
    val scale: Double = 1.0,
    val color: ColorRGBa = ColorRGBa.WHITE,
    val velocity: Velocity = Velocity.ZERO
) : SpaceObject {
    constructor(ship: Ship) : this(ship.position, 2.0, ColorRGBa.WHITE, ship.velocity*0.5)
    constructor(missile: Missile) : this(missile.position, 0.5, missile.color, missile.velocity*0.5)
    constructor(saucer: Saucer) : this(saucer.position, 2.0, ColorRGBa.GREEN, saucer.velocity*0.5)
    constructor(asteroid: Asteroid) : this(asteroid.position, 2.0, ColorRGBa.WHITE, asteroid.velocity*0.5)

    var elapsedTime = 0.0
    private val lifetime = U.SPLAT_LIFETIME
    private val view = SplatView(lifetime)

    override fun update(deltaTime: Double, trans: Transaction) {
        elapsedTime += deltaTime
        if (elapsedTime > lifetime) trans.remove(this)
        position = (position + velocity * deltaTime).cap()
    }

    override fun draw(drawer: Drawer) {
        drawer.translate(position)
        view.draw(this, drawer)
    }
}

I’ll just add Collidable and follow IDEA’s inevitable whining until everyone is happy. I start by making Splat be its own Collider:

class Splat(
    override var position: Point,
    val scale: Double = 1.0,
    val color: ColorRGBa = ColorRGBa.WHITE,
    val velocity: Velocity = Velocity.ZERO
) : SpaceObject, Collidable, Collider {
    constructor(ship: Ship) : this(ship.position, 2.0, ColorRGBa.WHITE, ship.velocity*0.5)
    constructor(missile: Missile) : this(missile.position, 0.5, missile.color, missile.velocity*0.5)
    constructor(saucer: Saucer) : this(saucer.position, 2.0, ColorRGBa.GREEN, saucer.velocity*0.5)
    constructor(asteroid: Asteroid) : this(asteroid.position, 2.0, ColorRGBa.WHITE, asteroid.velocity*0.5)

    override val collisionStrategy: Collider
        get() = this
    override val killRadius: Double
        get() = 0.0

That demands that I implement this interface:

interface Collider {
    val position: Point
    val killRadius: Double
    fun interact(asteroid: Asteroid, trans: Transaction)
    fun interact(missile: Missile, trans: Transaction)
    fun interact(saucer: Saucer, trans: Transaction)
    fun interact(ship: Ship, trans: Transaction)
}

I’l also have to implement interactWith, part of Collidable.

    override fun interactWith(other: Collidable, trans: Transaction) = Unit
    override fun interact(asteroid: Asteroid, trans: Transaction) = Unit
    override fun interact(missile: Missile, trans: Transaction) = Unit
    override fun interact(saucer: Saucer, trans: Transaction) = Unit
    override fun interact(ship: Ship, trans: Transaction) = Unit

All this is just a fancy way of implementing “does not interact”. All this should work, I think. It does. Commit: Splat is a collider that collides with nothing. My intermittent test failed. Try again, all good. Commit.

Now in SpaceObjectCollection we don’t need to create the colliders collection:

    fun add(spaceObject: SpaceObject) {
        spaceObjects.add(spaceObject)
        if (spaceObject is Collidable) colliders.add(spaceObject)
    }

Not the best naming either. Find senders of colliders. They are all local to SpaceObjectCollection. The sole actual use is here:

    fun pairsToCheck(): List<Pair<Collidable, Collidable>> {
        val pairs = mutableListOf<Pair<Collidable, Collidable>>()
        colliders.indices.forEach { i ->
            colliders.indices.minus(0..i).forEach { j ->
                pairs.add(colliders[i] to colliders[j])
            }
        }
        return pairs
    }

We’ll just refer to spaceObjects here:

    fun pairsToCheck(): List<Pair<SpaceObject, SpaceObject>> {
        val pairs = mutableListOf<Pair<SpaceObject, SpaceObject>>()
        spaceObjects.indices.forEach { i ->
            spaceObjects.indices.minus(0..i).forEach { j ->
                pairs.add(spaceObjects[i] to spaceObjects[j])
            }
        }
        return pairs
    }

Test, should be fine. We’re good. Commit: pairs uses spaceObjects, not colliders.

Now just remove all the code that maintained the list. Test again.

Can’t quite do this. In fact the previous commit was a bit mistaken. We need to move the Collidable stuff into SpaceObject.

Arrgh, can’t do that if DeferredAction is a SpaceObject. I’ve got to roll back and fix this up.

I think I’ll have to fix pairs with a cast, which I do not like:

    fun pairsToCheck(): List<Pair<Collidable, Collidable>> {
        val pairs = mutableListOf<Pair<Collidable, Collidable>>()
        spaceObjects.indices.forEach { i ->
            spaceObjects.indices.minus(0..i).forEach { j ->
                pairs.add(spaceObjects[i] as Collidable to spaceObjects[j] as Collidable)
            }
        }
        return pairs
    }

We’ll get rid of that soon, I hope. Maybe. Tests are running again but I am suspicious. Game seems solid.

Now can I remove the colliders? I have a test that refers to the colliders() function, but it will no longer be applicable:

    @Test
    fun `clear clears all sub-collections`() {
        val s = SpaceObjectCollection()
        s.add(Missile(Ship(U.CENTER_OF_UNIVERSE)))
        s.add(Asteroid(Point.ZERO))
        val deferredAction = DeferredAction(3.0, Transaction()) {}
        s.add(deferredAction)
        s.clear()
        assertThat(s.spaceObjects()).isEmpty()
        assertThat(s.deferredActions()).isEmpty()
        assertThat(s.colliders()).isEmpty()
    }

That last line can go. Now can I get rid of the collection?

    private val colliders = mutableListOf<Collidable>()


    fun add(spaceObject: SpaceObject) {
        spaceObjects.add(spaceObject)
        if (spaceObject is Collidable) colliders.add(spaceObject)
    }

    fun clear() {
        scoreKeeper.clear()
        deferredActions.clear()
        spaceObjects.clear()
        colliders.clear()
    }

    fun remove(spaceObject: SpaceObject) {
        deferredActions.remove(spaceObject)
        spaceObjects.remove(spaceObject)
        if (spaceObject is Collidable) colliders.remove(spaceObject)
    }

Seems we can remove all that now. Tests are green. Commit: colliders collection removed.

OK, fine.

But now we have the curious situation that everything under SpaceObject is a Collidable except for DeferredAction.

We’d like to unwind that somehow. Let’s review the interfaces:

interface Collider {
    val position: Point
    val killRadius: Double
    fun interact(asteroid: Asteroid, trans: Transaction)
    fun interact(missile: Missile, trans: Transaction)
    fun interact(saucer: Saucer, trans: Transaction)
    fun interact(ship: Ship, trans: Transaction)
}

interface Collidable {
    val position: Point
    val killRadius: Double
    fun interactWith(other: Collidable, trans: Transaction)
    val collisionStrategy: Collider
}

interface SpaceObject {
    fun draw(drawer: Drawer)
    fun update(deltaTime: Double, trans: Transaction)
}

Now the fact is that DeferredAction isn’t really a SpaceObject at heart. It does have update, but its draw is empty:

class DeferredAction(
    val delay: Double,
    val cond: () -> Boolean,
    initialTransaction: Transaction,
    private val action: (Transaction) -> Unit
) : SpaceObject {
    constructor(delay: Double, initialTransaction: Transaction, action: (Transaction) -> Unit):
            this(delay, { true }, initialTransaction, action)
    private var elapsedTime = 0.0

    init {
        elapsedTime = 0.0
        initialTransaction.add(this)
    }

    override fun draw(drawer: Drawer) {}

    override fun update(deltaTime: Double, trans: Transaction) {
        elapsedTime += deltaTime
        if (elapsedTime >= delay && cond() ) {
            action(trans)
            trans.remove(this)
        }
    }
}

And it gets special handling in Transaction and SpaceObjectCollection.

class Transaction
    fun add(deferredAction: DeferredAction) {
        deferredActionAdds.add(deferredAction)
    }

    fun add(spaceObject: SpaceObject) { spaceObjects.add(spaceObject) }

    fun remove(deferredAction: DeferredAction) {
        deferredActionRemoves.add(deferredAction)
    }

    fun remove(spaceObject: SpaceObject) {
        removes.add(spaceObject)
    }

class SpaceObjectCollection
    fun add(deferredAction: DeferredAction) {
        deferredActions.add(deferredAction)
    }

    fun add(spaceObject: SpaceObject) {
        spaceObjects.add(spaceObject)
    }

    fun remove(deferredAction: DeferredAction) {
        deferredActions.remove(deferredAction)
    }

    fun remove(spaceObject: SpaceObject) {
        deferredActions.remove(spaceObject)
        spaceObjects.remove(spaceObject)
    }

It almost seems as if we could make deferredAction not be a SpaceObject at all and it should still work. Let me try that. Everything runs green. Commit: DeferredAction is no longer a SpaceObject.

Now let’s see about combining Collidable and SpaceObject interfaces into SpaceObject.

interface SpaceObject {
    val position: Point
    val killRadius: Double
    fun interactWith(other: Collidable, trans: Transaction)
    val collisionStrategy: Collider
    fun draw(drawer: Drawer)
    fun update(deltaTime: Double, trans: Transaction)
}

That runs green. Can I remove all references to Collidable now? There are over 20 of them. I start by changing the signature of interactWith:

    fun interactWith(other: SpaceObject, trans: Transaction)

That proceeds without demur. Test. Something isn’t happy with that change.

I’m getting other objections. Think I have to go through and change all the references. IDEA’s not smart enough to figure out what I’m up to. Changing the pairs operation has the tests running:

    fun pairsToCheck(): List<Pair<SpaceObject, SpaceObject>> {
        val pairs = mutableListOf<Pair<SpaceObject, SpaceObject>>()
        spaceObjects.indices.forEach { i ->
            spaceObjects.indices.minus(0..i).forEach { j ->
                pairs.add(spaceObjects[i] to spaceObjects[j])
            }
        }
        return pairs
    }

But I want to root out all the Collidables. I do a safe delete and fix all the references to Collidable to SpaceObject and remove all the implementors. Safe delete is happy. Tests … are green. Game works. Commit: remove Collidable interface entirely.

Let’s sum up. I think we’ve done a somewhat good thing.

Summary

We’ve removed a complete type definition from the game, Collidable, subsuming what it intends into SpaceObject, which now looks like this:

interface SpaceObject {
    val position: Point
    val killRadius: Double
    fun interactWith(other: SpaceObject, trans: Transaction)
    val collisionStrategy: Collider
    fun draw(drawer: Drawer)
    fun update(deltaTime: Double, trans: Transaction)
}

Only Asteroid, Missile, Saucer, Ship, and Splat implement this interface and four of the five are perfectly happy doing so. Splat doesn’t reallly need interactWith or collisionStrategy, but implementing them as empty gives us simplifications in SpaceObjectCollection. In particular, SpaceObjectCollection was able to remove one of its three sub-collections and external support for that collection.

We still have need for the virtual collections asteroids, missiles, saucers, and ships, although most of the use for them is in tests. We do ask questions about whether the ship or saucer is present, and whether missiles are present, inside the GameCycle. I think we’ll take a look at slimming down those requirements in upcoming days. It would be pleasant not to need those collections outside of tests, and perhaps some private methods on SpaceObjectCollection, such as the shipIsMissing and saucerIsPresent functions.

We have increased the complexity of Splat a bit, but it’s all obviously no-ops:

class Splat
    override fun interactWith(other: SpaceObject, trans: Transaction) = Unit
    override fun interact(asteroid: Asteroid, trans: Transaction) = Unit
    override fun interact(missile: Missile, trans: Transaction) = Unit
    override fun interact(saucer: Saucer, trans: Transaction) = Unit
    override fun interact(ship: Ship, trans: Transaction) = Unit

With that addition we bought a simplification in SpaceObjectCollection, removed an entire interface, and simplified the defintion of all the SpaceObjects. Along the way, we removed DeferredAction from the SpaceObjec hierarchy entirely.

I think this was a net win. A small win, but a win. We’ll see what happens next!