GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

OK, you talked me into it. Let’s see about doing the System part of Entity - Component - System, just to see what it’s like. Plastic Sliders??

Motivation

The primary reason for the E-C-S design structure is performance, at least as I understand it. And it’s a odd kind of performance at that. Because a computer’s memory is far slower than its processors, when the code jumps around in memory, the program is slowed down substantially. Now in most cases, we probably just don’t care. It’s as fast as it is, if it needs to be faster we’ll buy another shard sort of thing. But gaming systems run on given hardware, and some of that hardware is a version or three out of date, and the program still has to fun fast enough to produce 24 frames a second, or 40, or 50, or whatever our bosses say.

So the E-C-S model, instead of bouncing around among various objects and their collaborators, embeds common key operations in Components that have all the data for some kind of behavior, and they put the code in a System that loops over all the Components of a given kind and processes them all in a batch. The result is that the code gets cached and runs faster.

Now it seems to me to be a silly kind of computer that only caches one chunk, and that can’t cache a bunch of bytes from here and a bunch from there, but when I last built a computer it was made out of plastic sliders with rubber tubing on them, so perhaps I’m not au courant with today’s hardware. Anyway, the E-C-S thing is how they do it.

So I think I’ll try it, to see what it’s like. I’m here to experience what various kinds of code are like and to share with you, after all, so why not?

Analysis

I think we should consider doing these things:

  1. Combine the ActionTimer and IdleTimer into one Timer that can go both ways;
  2. Use our new Timers in all of Missile, Ship, and Saucer;
  3. Arrange that our Timers are saved in a separate collection rather than with the SpaceObjects as they are now;
  4. In the Game cycle, process all the timers in a batch.

By #1, Combine, I mean to eliminate the duplication in the two timers:

class ActionTimer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val action: (ActionTimer) -> Unit
): Component {
    var time = delayTime
    fun update(deltaTime: Double) {
        if ( ! entity.active ) return
        time -= deltaTime
        if ( time <= 0.0) {
            action(this)
            time = delayTime
        }
    }
}

class IdleTimer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val action: (IdleTimer) -> Unit
) : Component {
    var time = delayTime

    fun update(deltaTime: Double) {
        if (entity.active) return
        time -= deltaTime
        if (time <= 0.0) {
            action(this)
            time = delayTime
        }
    }
}

If my eyes don’t deceive me, these two objects vary by exactly three characters, space-bang-space. We can make this more obvious, perhaps, with this refactoring:

class ActionTimer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val action: (ActionTimer) -> Unit
): Component {
    var time = delayTime
    fun update(deltaTime: Double) {
        if ( entity.active == true ) { // <--
            time -= deltaTime
            if (time <= 0.0) {
                action(this)
                time = delayTime
            }
        }
    }
}

class IdleTimer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val action: (IdleTimer) -> Unit
) : Component {
    var time = delayTime

    fun update(deltaTime: Double) {
        if (entity.active == false) { // <--
            time -= deltaTime
            if (time <= 0.0) {
                action(this)
                time = delayTime
            }
        }
    }
}

Tests are green, but we’re not doing production, we’re spiking some ideas. We won’t commit until we’re more sure. Let’s go a step further:

class ActionTimer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val action: (ActionTimer) -> Unit
    val processWhenActive: Boolean = true
): Component {
    var time = delayTime
    fun update(deltaTime: Double) {
        if ( entity.active == processWhenActive ) {
            time -= deltaTime
            if (time <= 0.0) {
                action(this)
                time = delayTime
            }
        }
    }
}

class IdleTimer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val action: (IdleTimer) -> Unit
    val processWhenActive: Boolean = false
) : Component {
    var time = delayTime

    fun update(deltaTime: Double) {
        if (entity.active == processWhenActive) {
            time -= deltaTime
            if (time <= 0.0) {
                action(this)
                time = delayTime
            }
        }
    }
}

Now they’re identical except for the setting of the defaulted construction parameter.

Push: Random Thought

As implemented, these timers subtract deltaTime from time until if goes zero. That means that each object is updated on every tick. That means that if it’s in cache, it’ll have to be flushed back to memory. If we care about memory access, that would be bad.

If the time member was an elapsed time, in the future, then we’d only have to update the time when the object is initialized, at creation time or when it triggers or is reset. That would reduce memory writes substantially. We don’t have elapsed time to hand, but our System, when we write it, could maintain it, or the Game could.

Let’s keep that in mind as a pretend optimization that we’ll do as part of getting a sense of how these System things might be done.

Pop: Random Thought

Clearly we can make do with just one kind of timer if we give it a third parameter as above. Let’s evolve toward that. Right now, none of the tests will compile, because they don’t provide the new processWhenActive parameter and therefore cannot provide the action. I’ll change them all as appropriate, referring each to whichever class it uses now. Like this, but there are lots of them:

    @Test
    fun `action timer triggers and resets on time going negative`() {
        val entity = newAsteroid()
        entity.active = true
        executed = false
        val timerTime = 1.0
        val timer = ActionTimer(entity, timerTime, true) { executed = true}
        timer.update(1.1)
        assertThat(executed)
            .describedAs("action should be taken")
            .isEqualTo(true)
        assertThat(timer.time)
            .describedAs("should be reset")
            .isEqualTo(timerTime)
    }

I had to move the processWhenActive flag up in the parameter list, so that action can be last. We’ll see that shortly.

I’ve done the ActionTimer. Now I’ll do the Idle ones and change the name while I’m at it. I’m going this with some delightfully clever multi-cursor editing.

In the case of IdleTimer, I changed them all to reference ActionTimer but with a false parameter:

    @Test
    fun `idle timer takes action on inactive entity when time elapsed`() {
        val entity = newAsteroid()
        entity.active = false
        executed = false
        val timerTime = 1.0
        val timer = ActionTimer(entity, timerTime, false) { executed = true}
        timer.update(1.0)
        assertThat(executed)
            .describedAs("action should be taken")
            .isEqualTo(true)
    }

Tests should run. They do. Should be able to remove IdleTimer class. Should still be green. Yes. Rename ActionTimer? Let’s just call it Timer. We have PluggableTimer but we’re going to get rid of it, I think. No, we have something called Timer. It’s used to time out missiles. Let’s rename that to OldTimer and call this nice new one Timer.

Done. Is PluggableTimer even used other than in an experimental test? No. Remove the test. Remove the class. Test. Green. Commit: Rename Timer to OldTimer, ActionTimer to Timer. Preparing for use of Timer only.

I think the next step will be to convert Missile to use the new timer, then do ship, then saucer.

We’ll be working in update, I think, which looks like this:

private fun updateEverything(
    spaceObjects: Array<SpaceObject>,
    deltaTime: Double,
    width: Int,
    height: Int
) {
    for (spaceObject in spaceObjects) {
        for (component in spaceObject.components) update(component, deltaTime)
        if (spaceObject.type == SpaceObjectType.SHIP) applyControls(spaceObject, deltaTime)
        move(spaceObject, width, height, deltaTime)
    }
}

fun update(component: Component, deltaTime: Double) {
    when (component) {
        is OldTimer -> {
            updateTimer(component, deltaTime)
        }
        is SaucerTimer -> {
            updateSaucerTimer(component, deltaTime)
        }
    }
}

(And so on, further details don’t matter for now.) We’ll change Missile and then add an is Timer to update. Here’s newMissile:

fun newMissile(): SpaceObject {
    return SpaceObject(SpaceObjectType.MISSILE, 0.0, 0.0, 0.0, 0.0, 0.0, false)
        .also { addComponent(it, OldTimer(it, U.MissileTime)) }
}

I run into some syntax problems and finally get this:

fun newMissile(): SpaceObject {
    return SpaceObject(SpaceObjectType.MISSILE, 0.0, 0.0, 0.0, 0.0, 0.0, false)
        .also { spaceObject ->
            val missileTimer = Timer(spaceObject, U.MissileTime, true) 
            	{ timer-> deactivate(timer.entity)}
            addComponent(spaceObject, missileTimer)
        }
}

Now a missile test is failing. Makes sense, they’re not getting timed out. We add this:

fun update(component: Component, deltaTime: Double) {
    when (component) {
        is OldTimer -> {
            updateOldTimer(component, deltaTime)
        }
        is SaucerTimer -> {
            updateSaucerTimer(component, deltaTime)
        }
        is Timer -> {
            component.update(deltaTime)
        }
    }
}

I think this ought to pass the tests and time out missiles in the game. The tests pass, a big load off my mind. Game allows only four missiles at a time, and they do time out. We are good.

Commit: Convert Missile to use new Timer.

Now there should be no valid uses of OldTimer. Maybe some tests. No tests, they’re all indirect, but I do find this, AGAIN:

fun deactivate(entity: SpaceObject) {
    entity.active = false
    for (component in entity.components) {
        when (component) {
            is OldTimer -> {
                component.time = component.startTime
            }
            is SaucerTimer -> {
                component.time = U.SaucerDelay
            }
        }
    }
    if (entity.type == SpaceObjectType.SHIP) shipGoneFor = 0.0
}

This is our old nemesis, remembering to reset our timers on deactivation. Fortunately, my search found this. I change it thus:

fun deactivate(entity: SpaceObject) {
    entity.active = false
    for (component in entity.components) {
        when (component) {
            is Timer -> {
                component.time = component.delayTime
            }
            is SaucerTimer -> {
                component.time = U.SaucerDelay
            }
        }
    }
    if (entity.type == SpaceObjectType.SHIP) shipGoneFor = 0.0
}

Now I can remove OldTimer. Test. Green. Commit: Remove OldTimer class. Ensure Timer reset in deactivate.

Ensure probably wasn’t the best term. We don’t have a test to be sure that we reset the timer. Hm, but we could have.

    @Test
    fun `timer resets on deactivate`() {
        val missile = newMissile()
        val timer = missile.components.find { it is Timer }!! as Timer
        val originalTime = timer.time
        update(timer, 0.5)
        assertThat(timer.time).describedAs("didn't tick down").isEqualTo(originalTime - 0.5, within(0.01))
        deactivate(missile)
        assertThat(timer.time).describedAs("didn't reset").isEqualTo(originalTime)
    }

Test fails saying didn’t tick down. Ah. The missile isn’t active!

    @Test
    fun `timer resets on deactivate`() {
        val missile = newMissile()
        missile.active = true
        val timer = missile.components.find { it is Timer }!! as Timer
        val originalTime = timer.time
        update(timer, 0.5)
        assertThat(timer.time).describedAs("didn't tick down").isEqualTo(originalTime - 0.5, within(0.01))
        deactivate(missile)
        assertThat(timer.time).describedAs("didn't reset").isEqualTo(originalTime)
    }

Green. Commit: test to ensure deactivate resets Timer object.

Writing that test makes me feel all righteous and good about myself again. Let’s reflect. We may have done enough for the morning session.

Reflection

We refactored our experimental IdleTimer and ActionTimer down to a single class that takes a new Boolean parameter to allow the one class to work either on active objects or inactive ones. It goes both ways. We applied it to the Missile, allowing us to remove the old Timer class, which we had renamed to OldTimer so that our new shiny one can be named Timer.

Ah. So far, we’re calling the update method on Timer during update:

fun update(component: Component, deltaTime: Double) {
    when (component) {
        is SaucerTimer -> {
            updateSaucerTimer(component, deltaTime)
        }
        is Timer -> {
            component.update(deltaTime)
        }
    }
}

That’s not in the spirit of E-C-S. It also violates our “rule” against methods. We should move the update code inside the update, like this:

fun update(component: Component, deltaTime: Double) {
    when (component) {
        is SaucerTimer -> {
            updateSaucerTimer(component, deltaTime)
        }
        is Timer -> {
            with(component) {
                if (entity.active == processWhenActive) {
                    time -= deltaTime
                    if (time <= 0.0) {
                        action(this)
                        time = delayTime
                    }
                }
            }
        }
    }
}

I think this ought to still work. It does. But are there still calls to Timer’s update method? There are a dozen, only in the tests. I think we can likely change them all from timer.update(deltaTime) to update(timer, deltaTime), passing them through our update function above.

I’ll break all those tests by removing the contents of the update method so that as I change the tests to the new format I can see it working. Sure enough, 8 tests fail (some of them do more than one update).

Yes, that works. Remove update method. Green. Commit: Remove update method from Timer, done in update function.

class Timer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val processWhenActive: Boolean = true,
    val action: (Timer) -> Unit
): Component {
    var time = delayTime
}

OK, that’ll do for the morning. I’ve been at it for an hour and three-quarters.

Summary

We have a new kind of Timer and have put it into play. The process of getting to the Timer was quite iterative. We built a PluggableTimer that accepted an action, then built two timers, one for active and one for inactive, then refactored to make them nearly identical (more nearly identical), parameterized the one, removed the other, and installed the new Timer.

There was no trouble or hassle anywhere along the way. The biggest change was when we removed the update method of Timer, to be more consistent with E-C-S design, which wasn’t even a goal until this morning.

Where do we stand with respect to E-C-S? There are these issues:

  • We don’t have the Components all together in typed tables;
  • We are iterating over all the component types together, and should do them one type at a time.

Both of these issues should be straightforward to resolve. We’ll cause our addComponent function to break the components into tables by type (and by then I think there will only be one type anyway, at least so far), and then we can readily change the looping to do each table separately.

And then we’ll have a tiny E-C-S with an actual System, a Timing System. Whee!

Stay tuned, Bruce! And all of you: see you next time!