GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

Kotlin’s object could help me clean up the globals a bit. Shall we allow it? Also: more Cat Scripture.

And it came to pass on the morning of the fourth day that Lo! the cat did sit upon the Alter of Sacrifice. The people bowed and did ask whether the car wanted food and the cat spake “Now!” and the people did provide. Again the white fish of the ocean and the star-kissed tuna of the sea did give their lives for that of the cat, and the cat ate of them and was satisfied.

And the people, while trusting not in the cat’s mercy, for the cat had none, went about their day.

Are Objects OK?

The game’s controls presently set a number of Boolean globals:

var controls_left: Boolean = false
var controls_right: Boolean = false
var controls_accelerate: Boolean = false
var controls_fire: Boolean = false
var controls_hyperspace: Boolean = false

We could instead create an object like this:

object Controls {
    var left: Boolean = false
    var right: Boolean = false
    var accelerate: Boolean = false
    var fire: Boolean = false
    var hyperspace: Boolean = false
}

Then, code like this …

    if (controls_left) spaceObject.angle -= 250.0 * deltaTime
    if (controls_right) spaceObject.angle += 250.0 * deltaTime

… could become this:

    if (Controls.left) spaceObject.angle -= 250.0 * deltaTime
    if (Controls.right) spaceObject.angle += 250.0 * deltaTime

That would be equally clear and would vastly reduce the game’s global name space. We could even create the Controls object locally, probably in main, and pass it to the game as a parameter.

The question in my mind is whether doing this is consistent with the purpose of this exercise, which is to build the game just with top-level functions rather than object methods. I think it is. The object in this case, since it has no methods, doesn’t break the rules and is just another way of building and populating a data structure.

We’ll do it, at least the first part, creating the single global and hooking the main and game to it. A bit of multi-cursor editing and the job is done. Test the game. Works fine. Commit: convert to Controls object.

So that’s nice. We have more globals that we could deal with, and we have a vast array of magic numbers that we could package up.

const val Width = 1024
const val Height = 1024
lateinit var spaceObjects: Array<SpaceObject>
lateinit var Ship: SpaceObject

These are two different kinds of things. The first two are just literal constants defining, in this case, the screen width and height, while the latter two are actually game-play objects. I think it’s time to move the literals into an object and begin to move more magic values over into that object. We’ll call it U for Universe, because we’ll be typing it a lot.

object U {
    const val Width = 1024
    const val Height = 1024
}

There are just a few references to those two, and I’ll fix them right up.

fun main() = application {
    configure {
        title = "Asteroids"
        width = U.Width
        height = U.Height
    }
fun startGame() {
    Ship.active = true
    Ship.x = U.Width/2 + 0.0
    Ship.y = U.Height/2 + 0.0
}

What would be better than this would be to pass width and height to startGame, so let’s do that.

fun startGame(width: Int, height: Int) {
    Ship.active = true
    Ship.x = width/2.0
    Ship.y = height/2.0
}

That’s a bit less silly. Sorry about the 0.0 stuff, I had been unaware of toDouble. Vincible ignorance, because now I do know.

    program {
        val font = loadFont("data/fonts/default.otf", 64.0)
        createGame(6,26)
        startGame(width, height)

What we see in these two calls is that we haven’t stabilized the game creation and setup. We see more evidence of this instability in the extend that cycles the game:

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

A Code Smell?

When we find ourselves passing in an increasingly random list of parameters, we might start suspecting that there should be a single object or structure containing some or all of the information being passed. Of course, if we had a real Game object, it might have saved width and height when it was created, so that it wouldn’t need them again. We can, of course, at least have a game struct / object without methods, and fill in some of that info. I think we’ll do that in due time: there are changes coming here and we’ll have occasion to do better when we know more.

Test. There’s one test that checks the position of the ship on start, and I had to add width and height values to that. In fact, I think I’ll improve it this way:

    @Test
    fun `start game makes ship active`() {
        createGame(6, 26)
        assertThat(Ship.active).isEqualTo(false)
        startGame(500, 600)
        assertThat(Ship.active).isEqualTo(true)
        assertThat(Ship.x).isEqualTo(250.0)
        assertThat(Ship.y).isEqualTo(300.0)
    }

I had it originally using U.Width and U.Height. Then it occurred to me that a conceivable error would be to set the ship’s starting coordinates to 512,512 without regard to the actual width and height. Now we can be pretty sure it’s being computed. We could, of course, be even more sure by trying two more values but belt and suspenders is enough, we don’t need to staple our pants to our hips.

Let’s take a quick scan for magic numbers that we can move to U.

        createGame(U.MissileCount,U.AsteroidCount)
        ...
fun fireMissile() {
    Controls.fire = false
    val missile: SpaceObject = availableShipMissile() ?: return
    val offset = Vector2(U.MissileOffset, 0.0).rotate(Ship.angle)
    missile.x = offset.x + Ship.x
    missile.y = offset.y + Ship.y
    val velocity = Vector2(U.MissileSpeed, 0.0).rotate(Ship.angle)
    missile.dx = velocity.x + Ship.dx
    missile.dy = velocity.y + Ship.dy
    missile.timer = U.MissileTime
    missile.active = true
}

Those are supported by this:

object U {
    const val Width = 1024
    const val Height = 1024
    const val MissileCount = 6
    const val AsteroidCount = 26
    const val MissileOffset = 50.0
    const val SpeedOfLight = 500.0
    const val MissileSpeed = SpeedOfLight/3.0
    const val MissileTime = 3.0
}

I think that’s much nicer. I find one more, in applyControls:

        val deltaV = Vector2(U.ShipDeltaV, 0.0).rotate(spaceObject.angle) * deltaTime

Now let’s alphabetize those constants as best we can.

object U {
    const val AsteroidCount = 26
    const val Height = 1024
    const val MissileCount = 6
    const val MissileOffset = 50.0
    const val MissileTime = 3.0
    const val ShipDeltaV = 120.0
    const val SpeedOfLight = 500.0
    const val SpeedOfMissile = SpeedOfLight/3.0
    const val Width = 1024
}

I renamed MissileSpeed to SpeedOfMissile so that it could be alphabetized and still come after SpeedOfLight, so that it’ll compile.

We’re mostly keeping the name of the thing first. Let’s do this:

object U {
    const val AsteroidCount = 26
    const val LightSpeed = 500.0
    const val MissileCount = 6
    const val MissileOffset = 50.0
    const val MissileSpeed = LightSpeed/3.0
    const val MissileTime = 3.0
    const val ScreenHeight = 1024
    const val ScreenWidth = 1024
    const val ShipDeltaV = 120.0
}

Did you know that IDEA has a Sort Lines command? Neither did I but I looked and there it was. And renaming is trivially easy, since you just Shift-F6 and type, and IDEA renames everything everywhere. Sweet.

Test, because trust but verify. Green. Game works, of course. Commit: Move magic values to U.

One more thing. Rather than have negative time mean no timer, let’s let the timer be null.

data class SpaceObject(
    val type: SpaceObjectType,
    var x: Double,
    var y: Double,
    var dx: Double,
    var dy: Double,
    var angle: Double = 0.0,
    var active: Boolean = true,
) {
    var timer: Double? = null
}

Then we can do this:

fun tickTimer(spaceObject: SpaceObject, deltaTime: Double) {
    if (spaceObject.timer != null) {
        spaceObject.timer = spaceObject.timer!! - deltaTime
        if (spaceObject.timer!! <= 0.0) {
            spaceObject.timer = null
            spaceObject.active = false
        }
    }
}

Why am I doing this? Well, Dirk Groot and Bruce Onder are talking with me about ECS, Entity-Component-Services design and one way of representing such a thing would be that if an object doesn’t support a component, it would have a null pointer to it. (Other ways include having a list of the components you support as part of the object, and having separate tables of components with pointers or IDs for the owner objects.)

Dirk and Bruce both think this design is tending toward ECS, and while I feel that full-on ECS is too much for this simple game, it seemed that it might be fun to try it. It actually added a line of code to do it this way, but that’s OK, it makes a bit more sense this way, I think, since it doesn’t have that mysterious “well, if it’s negative” vibe to it.

We might, in a future moment, treat controls like this, either by adding a controls? member to SpaceObject, or perhaps even by adding a list of components, since then there would be a few different combinations. It might be amusing and I’m sure that Dirk and Bruce would advise me.

For now, we’re green and the game works, so commit: space object timer can be intentionally null meaning no timer.

That’s enough for today, I think. Wait, one more thing:

fun tickTimer(spaceObject: SpaceObject, deltaTime: Double) {
    with(spaceObject) {
        if (timer != null) {
            timer = timer!! - deltaTime
            if (timer!! <= 0.0) {
                timer = null
                active = false
            }
        }
    }
}

That’s a bit less busy, but there sure are a lot of brackets there. I suspect there’s an even more clever way to do this but this is better.

Note to self
Is this code asking for a small object containing the timer instead of just the Double? It might be.

Let’s sum up.

Summary

We began, as required by our creed, with a sacrifice of seafood to the cat. Then, fortified by the cat’s grudging acceptance, we moved our controls into a little object, and having gained momentum, created another object to centralize our constants. In a series of small steps we removed quite a few magic numbers from the game and arranged them nicely in the U object.

Then, we experimented with letting the timer member of a SpaceObject be null, signifying that the object has no timer, just to get a sense of whether we’d like that style. It wasn’t horrible and is inspiring me to try the List<Any> idea that Dirk offered. I’m starting to think that even in this small program it might be useful. I have some vague concerns, but it’s probably worth trying.

Mini-ECS here we come? Maybe. Tune in next time!