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


We have three versions for a reason: comparison of their designs. Let’s do a bit of that today.

There are as many ways to start understanding code as there are lines of code, and perhaps more. If we were tossed into a team maintaining one of these programs, there would probably be other team members, and in a team functioning to my liking, they’d be doing pair programming, and the newbie (me) would get to sit with various team members and work in whatever area they were modifying.

I’d ask questions and they’d tell me things, and yet our focus would probably be mostly on the various individual issues that we needed to work on. We might never get around to some aspects of the program, if they were never worked on.

WWhen I try to learn about a program from scratch, without a guide, I tend to browse through the code, looking for things that I recognize, trying to build up a picture of what’s going on. I might start at the beginning … but I might not. I recall, for example, a bug in a FORTRAN compiler that we were using Lo! these many years ago. We had listings of the compiler, and I scanned through them, on those huge sheets of printer paper that we use to use, until I found symbols relating to the bug. I followed my nose from there, mentally tracing through the assembly code, and actually found the bug, which we duly reported to IBM. It was one of the highlights of my learning career, and yes, it was about six decades in the past now.

So there are many ways. Today, we’ll start near the beginning, looking at the main program. We’ll start on the “flat” version, the one we’ve been working on most recently. (i.e. Yesterday.)

Flat Version

In the main, we see this:

fun main() = application {
    configure {
        title = "Asteroids"
        width = U.ScreenWidth
        height = U.ScreenHeight
    }

    program {
//        val image = loadImage("data/images/pm5544.png")
        val font = loadFont("data/fonts/default.otf", 64.0)
        createGame(U.SaucerMissileCount, U.ShipMissileCount, U.AsteroidCount)
        startGame(width, height)
        var lastTime = 0.0
        var deltaTime = 0.0
        keyboard.keyDown.listen {
            when (it.name) {
                "d" -> {Controls.left = true}
                "f" -> {Controls.right = true}
                "j" -> {Controls.accelerate = true}
                "k" -> {Controls.fire = true}
                "space" -> {Controls.hyperspace = true}
//                "q" -> { insertQuarter()}
            }
        }
        keyboard.keyUp.listen {
            when (it.name) {
                "d" -> {Controls.left = false}
                "f" -> {Controls.right = false}
                "j" -> {Controls.accelerate = false}
                "k" -> {Controls.fire = false}
                "space" -> {
                    Controls.hyperspace = false
                }
            }
        }

        extend {
            drawer.fill = ColorRGBa.WHITE
            drawer.stroke = ColorRGBa.RED
            deltaTime = seconds - lastTime
            lastTime = seconds
            gameCycle(SpaceObjects,width,height,drawer, deltaTime)
        }
    }
}

We’ll assume that we know enough OPENRNDR to understand this program/extend, where the program part sets up the “static” part of things and the extend part runs a zillion times per second. We know that seconds will be the elapsed run time, and drawer will be an instance of Drawer, for drawing on the screen.

We quickly sort out that the program bit calls createGame and startGame, and that the extend bit calls gameCycle repeatedly.

Would we start with creating and starting, or would we start with the cycle? I would be inclined to start with the cycle, figuring that the creation and starting are probably mostly simple boilerplate, the usual create objects and init things blah blah.

So gameCycle:

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)
    checkIfAsteroidsNeeded(deltaTime)
}

Hm, this isn’t bad. We update all the space objects. Don’t know what those are, but presumably the ships and saucers and asteroids. We draw everything. Makes sense: generally you want to update everything and then draw, because otherwise causality kind of drifts across the objects. We’d probably prefer them to all be on the same time tick when we draw them.

Then we check collisions, sure that’ll be missiles killing asteroids and such. Then we draw the score, sure, makes sense, and then we check if asteroids are needed. That’s probably the thing that gives us a new wave.

If we were working in the code, we’d probably rename that method, like this:

) {
    updateEverything(spaceObjects, deltaTime, width, height)
    drawEverything(spaceObjects, drawer, deltaTime)
    checkCollisions()
    drawScore(drawer)
    checkIfNewAsteroidWaveNeeded(deltaTime)
}

Since that’s a machine refactoring, we’d commit the code.

Centralized Version

But we are here to compare these versions. Let’s look at the “Centralized” version in the same area and see what we find.

The main looks similar with a slightly different setup for the controls, which we ignore, and a simpler extend:

    extend {
        drawer.fontMap = font
        game.cycle(seconds, drawer)
    }

This version uses objects, so there’s a game. A look up above tells us that it’s an instance of Game, but we’ll more likely just use IDEA’s Command+B to find game.cycle:

    fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
        val deltaTime = elapsedSeconds - lastTime
        lastTime = elapsedSeconds
        cycler.cycle(deltaTime, drawer)
    }

Huh. Well, there’s the elapsed time delta time thing. The other program does that in main. And I guess there’s a cycler and it cycles. Hovering over it tells me it’s a GameCycler and another Command+B gives me this:

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

OK, this starts to make sense. tick isn’t a very useful name. What does it do?

    private fun tick(deltaTime: Double) {
        updateTimersFirst(deltaTime)
        thenUpdateSpaceObjects(deltaTime)
    }

    private fun updateTimersFirst(deltaTime: Double) {
        with (knownObjects) {
            performWithTransaction { trans ->
                deferredActions().forEach { it.update(deltaTime, trans) }
            }
        }
    }

    private fun thenUpdateSpaceObjects(deltaTime: Double) {
        with (knownObjects) {
            performWithTransaction { trans ->
                spaceObjects().forEach { it.update(deltaTime, trans) }
            }
        }
    }

Tick is updating the objects, first Timers, whatever they are, and then the objects. Reading the two subordinate functions, we see that there is a trans action thing to look into, and that there are deferredActions and spaceObjects collections to update. The deferredActions must be the timers referred to.

We are inclined to rename tick to updateEverything to match the other program, but we’re not here to make them the same, we’re just here to understand.

Before we reflect, let’s check out the “Decentralized” version as well.

Decentralized Version

We quickly find game.cycle, which looks like this:

    fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
        val deltaTime = elapsedSeconds - lastTime
        lastTime = elapsedSeconds
        tick(deltaTime)
        beginInteractions()
        processInteractions()
        finishInteractions()
        drawer?.let {draw(drawer)}
    }

Hm, similar structure but here we don’t see to have a GameCycler. Let’s have a look at tick:

    fun tick(deltaTime: Double) {
        val trans = Transaction()
        knownObjects.forEach { it.update(deltaTime, trans) }
        knownObjects.applyChanges(trans)
    }

So here we create a transaction and then apply its changes. If I had to guess, I’d assume that performWithTransaction does the same thing more neatly. If I wanted to look, I could verify that, and I’d be right.

I might wonder why we don’t update any timers, since the centralized version does, but it’s early days.

Let’s reflect.

Reflection

We have, I think, roughly the same level of understanding of these three programs. We know that they initialize, they cycle, and they update objects, interact (or collide) them, and draw them.

We might wonder why the one game says “collide” and these two speak of “interact”, but that’s a pretty subtle distinction and we might wave it off as just what they called it. I happen to know that the more general name “interactions” is a clue that the objects in these two versions are interacting in ways other than collision, but aside from a desire to foreshadow discoveries, I don’t think we’d draw any big conclusions.

More notable is that these latter two versions use “transactions” and the first one does not. If curiosity got the best of us, we’d look to see what was up. Checking implementors of update would give us a quick hint. In decentralized, looking for implementors of update, we might be quite interested by this display of objects:

long list of implementors

What are all those objects??? We saw “deferred action” in Centralized. But SaucerMaker? ShipChecker? ShipMaker? Those are interesting. But nothing pops out about transactions, which is what we were interested in.

In Missile, we find this update:

    override fun update(deltaTime: Double, trans: Transaction) {
        timeOut.execute(trans)
        position = (position + velocity * deltaTime).cap()
    }

Ah, he’s at least using the transaction. What is timeOut.execute?

class OneShot(private val delay: Double, private val action: (Transaction)->Unit) {
    var deferred: DeferredAction? = null
    fun execute(trans: Transaction) {
        deferred = deferred ?: DeferredAction(delay, trans) {
            deferred = null
            action(it)
        }
    }

    fun cancel(trans: Transaction) {
        deferred?.let { trans.remove(it); deferred = null }
    }
}

OK, that’s weird and I’m daunted. Because we’re pairing as we explore, we’re a bit more brave than we might be, and at least here we see a remove on the transaction, removing the deferred, which is a DeferredAction or null.

What is the action? We don’t know and we’re not sure we have time to find out.

Let’s look at the centralized version to see if the transaction stuff is any more obvious.

Again, it is the updates that are using the transaction:

    private fun thenUpdateSpaceObjects(deltaTime: Double) {
        with (knownObjects) {
            performWithTransaction { trans ->
                spaceObjects().forEach { it.update(deltaTime, trans) }
            }
        }
    }

We look at implementors:

smaller list of implementors, all making sense

OK, these are all at least objects that we imagine we could understand. We quickly check a couple of update implementations and find this in Missile:

    override fun update(deltaTime: Double, trans: Transaction) {
        timeOut.execute(trans)
        position = (position + velocity * deltaTime).cap()
    }

OK, he’s going to check his timeout, whatever that is. We find it quickly:

    private val timeOut = OneShot(U.MISSILE_LIFETIME) {
        it.remove(this)
        it.add(Splat(this))
    }

Another one-shot, but we see now that when the timeOut executes, it probably removes this, the missile, and adds a Splat(this). Because we’ve played the game we know that in this version, when a missile times out it leaves a little spray on the screen.

So, we might guess, if we hadn’t already, that the games are maintaining a collection of things, space objects, and can add and remove them. Remove the missile, add the splat. Makes a kind of sense.

Still Reflecting

Note that as we talk about these programs, we continue to look at them and to explore. When we have questions we speculate but then try to firm up our speculation by exploring the code.

Note also that we seem to have had to read a lot more code in the Centralized and Decentralized versions. We pretty much stopped with gameCycle in the flat version and figured, right, it updates, draws, checks collisions, checks to see if it needs a new wave, great.

In the other two more object-oriented versions, we found that things were similar. In Centralized, down in a GameCycler object. We tick (update), interact, check for waves and ships. Yeah, OK.

The Decentralized version also looks similar, without a GameCycler.

The transaction caught our eye, and so we drilled down a bit deeper and it looks those two programs are maintaining a collection (or two?) of objects and can add and remove those objects. We don’t know this yet, but the Flat version has a fixed collection of objects and it marks them active or inactive, rather than add and remove them. It doesn’t have a transaction because it doesn’t need one.

Assessment

Which of these is easier to understand? It’s too soon to say but it sure looks as if the Flat version is pretty simple. There you are, right at the top in gameCycle, and you’re already updating, drawing, checking collisions. Very straightforward.

In the other two, what’s going on has been harder to discover, at least here at the beginning. We do find an inherent complexity difference, the variable collections of De- and Centralized, vs the fixed collection of Flat. But if we’re wise—and we are—we’ll quickly just assume that the space object collection is always up to date in the transaction versions, and not think about the transactions until we have to. And remember, we still don’t know how the Flat version manages creating and killing asteroids and such. In the other two versions we can feel pretty sure that when asteroids are needed, we’ll add them and when they die we’ll remove them.

We have had to drill deeper into the OO versions, and we took more time, but we actually have more information. It’s not always information we were looking for, but we actually know more about those versions. Our “knowledge” of the Flat version is superficial. It’s also pretty likely to be mostly right, but a big part of our comfort with it is because we have intuition about what those names mean. We don’t really know how it works.

That said, we were faced with a lot of complexity in the two OO versions. We browsed multiple classes and were exposed to a daunting number of class names in the Decentralized version.

Dealing with an OO program is, I think, different from dealing with a more procedural one like our Flat version. In the procedural case, we trace through the functions, drilling down or skipping over as we like, and once in a while we encounter some data that we have to check out. That will happen in a day or so, as we continue this comparison.

In the object-oriented case, we also trace through the functions, but we’re also tracing through different objects at the same time. It’s “Missile” and “update”, not just “updateMissile”. We need to keep more mental balls in our mental air at the same time, and we need to know when to drop one. For example, once we’re in Missile.update, we may well be able to forget about missiles and think about updates. But we’re never sure … because if there’s an update for missile, there’s probably one for asteroid and ship and … what are all those makers and checkers???

Remember that FORTRAN compiler bug that I found? I had to trek through a listing that was about six inches of stacked printout until, somehow, I found whatever it was. In the Flat version, if we want to know how some specific thing comes about, we won’t have much of a clue. In the object-oriented versions, we have the classes to help us. If it has to do with missiles hitting asteroids, we can look in asteroid and in missile and have a good chance of finding what we want. In the flat version, we’ll have to drill top down, or scan pages of code.

In a small program, drilling top down is probably faster. In a large program, the separate classes provide us with quick entry in to the details. At least, they can provide that, if our objects are well designed, nicely separated into chunks that make sense to the reader.

Object-oriented programming provides more ways of expressing and classifying things, which helps us exponentially as the program size increases. But it is also inherently more complicated. In the flat version we basically have structures and functions. In an object-oriented version we have structures with associated functions. The breakout is more granular … and there is more to think about.

Conclusion?

I have no real conclusion yet, and you’ll want to draw your own. I can tell you that even in a tiny program like Asteroids, I like having individual classes for things like Asteroid and Missile, and I like having the ability to build a separate GameCycler class to handle, well, game cycling, In the two object-oriented versions of Asteroid, each version has around 25 or 30 different classes. Those classes group common ideas and behavior for me, and I can find my way around quickly.

And that is an acquired skill, one that I was not born with, and that I’ve built up over years. I think that an object-oriented program offers more ways to grasp it and examine it, and that those ways help, once we find them, and can confuse us, until we get used to them.

At least that’s what I think today. What will I think next time? We’ll find out, but probably not tomorrow. I think tomorrow I’m going to get my eyes dilated. If so, no article.

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.