GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo
See footnote:1


By The Way
I think I’m about done with Kotlin. I like it, and I like IDEA. I do not like gradle, which as far as I can figure out was placed on this earth to torment us so that the rigors of The Other Place seem a welcome relief when we shuffle off this mortal coil. I think IDEA and Kotlin are super products, and I’m done with them.

For my next trick, I think I’ll pick up PyCharm and do some fun things with Python. I believe that I’ll start … with Asteroids, because I’m fresh on the problem and the general issues of implementation, which will give me a reasonably-sized problem to work on while learning Python’s language and style.

But for now …

Further Comparison

Today I want to try to think about the decentralized version of this program, why I like it so much, despite that it is clearly harder to understand.

The Three Ways

In Kotlin, I’ve just built Asteroids in three different ways. I brought each version to approximately the same level of features, close enough that I feel that I can compare the designs reasonably fairly. In the reverse order that I did them, they are:

Old School: Flat
The final version, called “flat”, is quite procedural. The main structuring was done with plain top-level functions, working on data structures that have no methods. I did allow myself to use a few built-in types with their methods, but didn’t create new types for the very distinct objects in the game, asteroids, missiles, ships, and so on.

This version processes the space objects with code that checks their type field in order to decide the details of processing them, and it uses the trick of pulling out sub-collections of certain types to process them in a consistent way. In a typical assembly-language version of the program, it would loop over all of them and skip the ones it didn’t want to process. In the “flat” version it makes the sub-collection and iterates it, which is arguably the more modern style.

Finally, in this version, I tried out the Entity Component System style of design a little bit. That’s not entirely consistent with the desire to build it in the old school style, but I wanted to try ECS. My conclusion, boiled down, is that ECS might be so much more efficient as to justify its use, but that it does not make for a more clear design than a more Object-Oriented style.

Object-Oriented: Centralized
This version was done because my Zoom colleagues found the first “decentralized” version hard to understand, so I was motivated to compare that version to a more conventional OO version. In this version, the various objects, Asteroid, Ship, and so on, are unique classes. The differences in their behavior (in the game) are reflected in different implementations of methods, such as for dealing with the fact that they have collided with other objects.

This implementation breaks out collision behavior into Strategy objects, which has the advantage of breaking that logic away from other matters such as moving and drawing, and of course it keeps the collision behavior fairly nicely packaged all together. Collision itself is done using a double dispatch, where the interaction of two objects starts with a standard call interactWith, which each object implements by calling a typed function interact, and with every object implementing all the possibilities, interact(asteroid, interact(missile, and so on. I’m looking forward to doing Asteroids again in Python, just to see what I do in that duck-typing language.

This “centralized” version is easier to understand than the one we’ll talk about next, which I call “decentralized”. The primary reason is that more of the logic is built into the game loop, while that’s not the case in what comes next.

Object-Oriented: Decentralized
This version has almost no logic at the level of the game and its cycle. Essentially everything is managed by objects that appear to the game to be just things in space. The game part of the code literally does not know (much of) anything about Asteroids. It doesn’t know there are ships, it doesn’t know how many there are. It doesn’t know there are asteroids, and doesn’t know that when they are all gone it should create new ones.

Instead, there are small objects in space—in the mix, I like to say—that manage matters like creating ships and asteroids. For example, there is an object ShipChecker. All it ever does is look into space and see if there is a ship. If there is not, ShipChecker creates a ShipMaker object and destroys itself. ShipMaker, when it is created, waits a discreet interval, creates a Ship, creates a new ShipChecker, and destroys itself. Now there’s a ship and a checker again. All’s right with the world, until the next time the ship is destroyed.

Similarly, there is a WaveMaker. It looks into space to see if there are any asteroids left. If there are not, it makes some. There’s a SaucerMaker, a ScoreKeeper, whatever little objects were needed to make the game work. All the game logic is embedded in these interacting objects.

It’s easy to see how it might be hard to figure out how the game works: a different set of checkers and makers could make an entirely different game. They could surely make Spacewar, and I’d bet they could make Space Invaders, and the main game code wouldn’t need much if any changing.

And yet, once you get the idea, it’s very easy to change the details of the game, and even easy to add new things. Do you want an asteroid that adjusts its path to chase the ship? Code it up, create a ChaserMaker, and voila! there it is. The game part of the code is oblivious to these things.

Now as much as I love this version, I must grant that it can be hard to think about how the interactions work, and since they weren’t all done the same way, I have to review the code before working on them. But then … I always have to review the code, don’t I? At a wild guess, if you have a fully-fledged ECS system, it’s easy to add some new kind of thing, because you just have to code up a new component and wire it in to the objects that need it. And yet, I suspect that an ECS system is harder to understand as a whole, because of all the little components. I think my decentralized version is similar. It’s easy to add to it, and harder to grasp as a whole.

I could be wrong, but for sure, I am fond of this version. Kill your darlings? Hell, no, I won’t.

Some Code

Let’s look at the same thing, three ways. Because I mentioned it above, let’s look at how the three versions manage creating a new wave of asteroids.

Flat

fun gameCycle(
    spaceObjects: Array<SpaceObject>,
    width: Int,
    height: Int,
    drawer: Drawer,
    deltaTime: Double
) {
    updateEverything(spaceObjects, deltaTime, width, height)
    drawEverything(spaceObjects, drawer, deltaTime)
    checkCollisions()
    drawScore(drawer)
    checkIfNewAsteroidWaveNeeded(deltaTime)
}

fun checkIfNewAsteroidWaveNeeded(deltaTime: Double) {
    if (activeAsteroids(SpaceObjects).isEmpty()) {
        AsteroidsGoneFor += deltaTime
        if (AsteroidsGoneFor > U.AsteroidWaveDelay) {
            AsteroidsGoneFor = 0.0
            activateAsteroids(nextWaveSize(currentWaveSize))
        }
    }
}

Here we count the asteroids and if there aren’t any, we tick up a timer and if the timer has elapsed we reset the timer and activate some asteroids.

Pretty straightforward, I’d say.

Centralized

    private val waveOneShot = OneShot(4.0) { makeWave(it) }

    fun cycle(deltaTime: Double, drawer: Drawer?= null) {
        tick(deltaTime)
        beforeInteractions()
        processInteractions()
        U.AsteroidTally = knownObjects.asteroidCount()
        createNewWaveIfNeeded()
        createSaucerIfNeeded()
        createShipIfNeeded()
        drawer?.let { draw(drawer) }
    }

    private fun createNewWaveIfNeeded() {
        if ( U.AsteroidTally == 0 ) {
            knownObjects.performWithTransaction { waveOneShot.execute(it) }
        }
    }

    fun makeWave(it: Transaction) {
        for (i in 1..howMany()) {
            it.add(Asteroid(U.randomEdgePoint()))
        }
    }

    fun howMany(): Int {
        return numberOfAsteroidsToCreate.also {
            numberOfAsteroidsToCreate += 2
            if (numberOfAsteroidsToCreate > 11) numberOfAsteroidsToCreate = 11
        }
    }

Here we have a well-known variable, U.AsteroidTally, because a number of objects want to know how many asteroids there are. A matter of efficiency and perhaps not a good idea, but there you are.

We check the tally and if there are no asteroids, execute the waveOneShot, which will make a new wave after 4 seconds. There’s a bit more detail about the OneShot in the description of the decentralized version below.

Decentralized

class WaveMaker(var numberToCreate: Int = 4): ISpaceObject, InteractingSpaceObject {
    private val oneShot = OneShot(4.0) { makeWave(it) }
    private var asteroidsMissing = true

    override fun update(deltaTime: Double, trans: Transaction) {}
    override fun callOther(other: InteractingSpaceObject, trans: Transaction) = Unit

    override val subscriptions = Subscriptions (
        beforeInteractions = { asteroidsMissing = true},
        interactWithAsteroid = { _, _ -> asteroidsMissing = false },
        afterInteractions = { if (asteroidsMissing) oneShot.execute(it) }
    )

    fun howMany(): Int {
        return numberToCreate.also {
            numberToCreate += 2
            if (numberToCreate > 11) numberToCreate = 11
        }
    }

    fun makeWave(it: Transaction) {
        for (i in 1..howMany()) {
            it.add(Asteroid(U.randomEdgePoint()))
        }
    }
}

Here’s the flow for WaveMaker:

  1. Before interactions, set asteroidsMissing to true
  2. During interactions, if you see an asteroid, set the flag false
  3. After interaction, if the flag is still set, execute your OneShot.

The OneShot object’s execute can be called repeatedly and it only does its action once, so after the OneShot’s 4 seconds elapse, it executes makeWave, which makes the wave. The OneShot resets and won’t be executed again until there are again no asteroids.

I think that’s nice. YMMV.

Summary (for now)

It seems to me that these three implementations are pretty close to the same in size and complexity. The differences in complexity are more about the choice to use a one-shot object, which the flat version can’t do because … no objects. And once you know what a one-shot does, it’s pretty clear and the code to create the waves is essentially the same in all versions. It’s all a matter of how the condition is detected and how the action is triggered.

The flat version’s decision is the easiest to view, I think: “Are there no asteroids, and has the timer run out” is pretty much right there in the code. With the OneShot, the timer is inside, and the fact that the OneShot ignores redundant triggering is perhaps obscure. At first reading you’d see this code:

afterInteractions = { if (asteroidsMissing) oneShot.execute(it) }

And you might think “but wait, it’s going to do that 240 times before the four seconds runs out”, and yes, it is, but the OneShot is going to ignore 239 of them. So you have to learn a bit, and once you do, you’re probably OK but there’s that initial WHA??? that hits you.

Any OO program tends to have some of those and procedural programs often have their own hidden features, but in Asteroids, things are so simple that the flat version can’t hide much … except that there’s no real breakout of functions, so you sort of have to consider all of them, or trace through the code to find things.

The decentralized version takes the OO style to extremes, because there’s really no way to trace through the code. All the objects are quite independent so unless you somehow stumble on the well-hidden notion that the WaveMaker makes the waves … see, it’s not that hard, really. But you do have to have a grasp of the notion of independent objects interacting, and that’s not really visible in the code.

I Wonder …

I wonder how that aspect could be made easier to see. I’ve tried to explain it over zoom and in text quite a few times, and my really mostly quite smart colleagues haven’t really grasped the idea and certainly haven’t see how truly nifty it is. Perhaps they aren’t as smart as I give them credit for … or, could it be that independent objects interacting is inherently harder to grasp than carefully choreographed objects?

It could be. I think the decentralized version offers an additional obstacle to understanding compared to the other two. I prefer the design for its flexibility, and freely grant that it’s more than is needed for something as simple as Asteroids.

But what if you wanted a button to press to select Asteroids, Spacewar, or SpaceInvaders … the decentralized version might be more interesting then. Perhaps.

YM totally MV. If you care to, let me know what you think. It won’t hurt my feelings if you disagree thoughtfully. I’m here to help you think, not to tell you what to think.

See you next time!


  1. The repos up at the top are three different designs, essentially the same program, done so that we can compare them. Unfortunately you may have to read around 300 articles to get the entire picture. I apologize for my prolixity.