GitHub Repo

A change improves many objects. Summary follows.

I’ve noticed a few things that seemed off to me. There were a few objects that maintain elapsed time, and they all had their own code lines to do it, in update. There were a few other update overrides just returning an empty list. These amount to duplication, and I was raised to eliminate duplication.

I tried fiddling with the interface ISpaceObject, but you can’t have a concrete value like elapsedTime in an interface. So I changed ISpaceObject to an abstract class for people to inherit from:

abstract class ISpaceObject {
    var elapsedTime = 0.0
    open val lifetime
        get() = Double.MAX_VALUE
    open val score: Int
        get() = 0

    fun tick(deltaTime: Double): List<ISpaceObject> {
        elapsedTime += deltaTime
        return update(deltaTime)
    }

    // defaulted, sometimes overridden
    open fun update(deltaTime: Double): List<ISpaceObject> { return emptyList() }

    open fun beginInteraction() {}
    open fun interactWith(other: ISpaceObject): List<ISpaceObject> { return emptyList() }
    open fun interactWithOther(other: ISpaceObject): List<ISpaceObject>{ return emptyList() }
    open fun finishInteraction(): Transaction = Transaction()

    open fun draw(drawer: Drawer) {}
    open fun finalize(): List<ISpaceObject> { return emptyList() }
}

In that class, I have a true var elapsedTime and a function to tick it and call update. Everything else is about the same as in the interface version, except that I’ve now defaulted all the methods and allowed them to be overridden as needed.

The idea is that each object will now have to implement only the methods it really needs, rather than being forced to do all of them.

Hill is going to object to this: he doesn’t like to have anything concrete in a superclass, if I understand him correctly. He likes to inherit from pure interfaces, I guess. And he could be right.

Note that I’ve added a method, tick that ticks the clock and then calls update. That necessitated this change to the game cycle:

    fun cycle(drawer: Drawer, seconds: Double) {
        val deltaTime = seconds - lastTime
        lastTime = seconds
        tick(deltaTime)
        beginInteractions()
        processInteractions()
        finishInteractions()
        draw(drawer)
    }

    fun tick(deltaTime: Double) {
        knownObjects.addAll(addsFromUpdates)
        addsFromUpdates.clear()
        knownObjects.forEach { addsFromUpdates.addAll(it.tick(deltaTime)) }
    }

Because of that change, I had to change a lot of tests to make them call tick instead of update. Perhaps I should have done something different with the naming. I prefer having it this way, because we do want classes overriding update as needed, but we don’t want them overriding tick because the plan now is that all objects have an ever-increasing clock elapsedTime of their very own.

The impact on the individual space objects is significant. Almost all of them were able to remove update, if not more. For example, ShipMaker, perhaps still our most complicated object, eliminated update entirely:

class ShipMaker(val ship: SolidObject) : ISpaceObject() {
    var safeToEmerge = true
    var asteroidTally = 0

    override fun beginInteraction() {
        safeToEmerge = true
        asteroidTally = 0
    }

    override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
        if (other is SolidObject && other.isAsteroid) asteroidTally += 1
        if (tooClose(other)) safeToEmerge = false
        return emptyList()
    }

    override fun interactWithOther(other: ISpaceObject): List<ISpaceObject> = interactWith(other)

    private fun tooClose(other:ISpaceObject): Boolean {
        return if (other !is SolidObject) false
        else (ship.position.distanceTo(other.position) < U.SAFE_SHIP_DISTANCE)
    }

    override fun finishInteraction(): Transaction {
        return if (elapsedTime > U.MAKER_DELAY && safeToEmerge) {
            replaceTheShip()
        } else {
            Transaction()
        }
    }

    private fun replaceTheShip(): Transaction {
        return Transaction().also {
            it.add(ship)
            it.add(ShipChecker(ship))
            it.remove(this)
            it.accumulate(Transaction.hyperspaceEmergence(ship,asteroidTally))
        }
    }
}

The individual changes were not interesting, just rather tedious. As we revise objects, you’ll see that they’re a bit simpler, mostly just because they no longer need to keep track of time on their own, or to implement a useless update. (We could probably have removed update with a judicious default in the interface, but I wanted everyone to have a clock of their own. Making it standard seems less confusing than having some have it and some not. And there was always the chance that someone (?) would implement timing differently. We probably wouldn’t like that.)

I see two things still to think about. First, I think it may be possible to arrange interactsWith and interactsWithOther so as to allow one or the other to default in most cases. That would save us another method. We’re using the two methods to ensure that all the special objects (the nonSolid ones) receive an interaction with all the SolidObjects, so that they can do their special magic. That can be done with the interactWithOther double dispatch, but we might find another way to do it, perhaps even by selecting which of the pair we’re considering should be primary.

The other thing is the pattern I wrote about this morning where a number of objects use the interactions in a similar fashion:

  • beginInteracton sets up some values
  • interactWith updates them
  • finishInteraction uses the values to create or destroy something

Maybe there’s some kind of pluggable strategy approach to take to doing those. So far, I haven’t thought of it.

Anyway, the new class has reduced duplication and removed some complexity in the day-to-day objects. That’s what superclasses and such are for, in my view. We’ll see what Hill has to say.

Details are in the repo, if you want to review them. See you next time!