GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

I may be moving toward Entity-Component-Systems design. Let’s find out. Further Cat Scripture.

And on the morning of the fifth day, the cat did demand sacrifice and the people did provide. The cat ate of the whitefish of the ocean and at of the tuna of the star-kissed sea, and found that it was good. The cat napped and the people did rejoice.

Ive been studying Entity-Component-Systems pages a bit and I’ll share what I’ve gleaned so far. I’m sure that I don’t have it entirely right, so correct me if it seems fun to do so.

ECS can be seen as an alternative to inheritance, and it is more flexible in that it allows for objects to have similar behavior along many dimensions, where inheritance tends to lock you in to certain combinations with other combinations being impossible or difficult. And that’s if you will tolerate implementation inheritance, which some folks in these parts deplore.

There are three notions in ECS, and you can probably guess what they are. An “Entity” is any thing in your program. In our case, they’re probably asteroids, ships, saucer, missiles. A “Component” is a bit of data relating to a particular kind of behavior that the thing might have. One of our objects might have a motion behavior, a timer behavior, a drawing behavior. The Component holds the data for that behavior, the position and velocity, or the timer value. And the “System” is the code that handles that kind of behavior.

The trick of the trade is that an entity can have any collection of components it needs to do what it does. In a game with more different kinds of behavior, this is more valuable than it may be here. A chest might have a trap component, a puzzle component, and a treasure component. An NPC cat might have wares and some conversation component. In our game, there aren’t many candidates for components … at least not as I see things right now.

But we do have at least one or two possibilities, so we’ll push forward and see what we see.

From what I’ve read, there are two basic approaches to ECS. One is to have each Entity hold a list of its Components, and when we update, that list is iterated and each element updated. Thus, each Entity just has the behaviors that it has.

A second approach seems to be to keep all the Components of a given kind together, and to process each component type as a batch, all the traps, then all the puzzles, and so on. The articles I’ve read often seem focused on performance and caching and this scheme seems to be aimed at performance. And of course I may not be grasping the point yet, having just scanned a few articles to get the idea.

What else do I “know”? Well, generally each Component has a pointer back to the Entity, and it’s easy to see that often some behavior will want to talk to the Entity. However you’re storing the Component, the Entity probably needs an addComponent method and possible a removal method as well.

That’s what I think I “know” about ECS. My plan is to try to build something that is similar to what I think I understand, and to see what happens.

Tentative Plan

I think I want to turn the timer into a Component. I also plan to make the controls into a Component, and possibly motion itself. We’ll see what we see when we see it.

So the SpaceObject, our Entity, will need a collection of Components and a way to add them. Dirk suggested List<Any> for the list, but I think I’ll be happier with a superclass or interface Component.

So let’s do an interface for Component. For now, I’ll leave it in the SpaceObject file.

interface Component { val entity: SpaceObject 

A Wild Problem Appears

I think that normally a Component would have an update method. Until now, this design has been working without methods or even much in the way of classes. Am I going to change the rules to do this? Let’s wait, but this is going to get nasty.

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
    var components: List<Component> = emptyList()
}

Let’s make a timer component. I think we’re going to consider that we’re spiking here and that we’ll probably roll this back after we learn what we should have done.

data class Timer(
	override val entity: SpaceObject, 
	var time: Double): Component

Now where we set the timer, let’s instead add in one of these things.

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
}

I’ll try this:

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
    addComponent(missile, Timer(missile, U.MissileTime))
    missile.active = true
}

We can see here that we really want to send addComponent to the missile but we gave up methods for Lent or something.

I’ll putaddComponent in the SpaceObject file, I guess. And I have to change the components to be mutable. Forgot.

    var components: MutableList<Component> = mutableListOf()
    ...
fun addComponent(entity: SpaceObject, component: Component){
    entity.components.add(component)
}

So far so good. Now to process the components. That’ll go in gameCycle:

fun gameCycle(
    spaceObjects: Array<SpaceObject>,
    width: Int,
    height: Int,
    drawer: Drawer,
    deltaTime: Double
) {
    for (spaceObject in spaceObjects) {
        if (spaceObject.type == SpaceObjectType.SHIP) {
            applyControls(spaceObject, deltaTime)
        }
        if (spaceObject.type == SpaceObjectType.MISSILE){
            tickTimer(spaceObject, deltaTime)
        }
        move(spaceObject, width, height, deltaTime)
    }
    for (spaceObject in spaceObjects) {
        if (spaceObject.active) draw(spaceObject, drawer)
    }
}

I guess we just do it right away:

    for (spaceObject in spaceObjects) {
        for (component in spaceObject.components) {
            update(component, deltaTime)
        }
        if (spaceObject.type == SpaceObjectType.SHIP) {
            applyControls(spaceObject, deltaTime)
        }

We really wish we could say component.update(deltaTime) and take advantage of the object dispatch. This no-methods rule is starting to seem really silly. I think part of the issue is due to trying to wedge ECS in.

I think this might be what I want:

fun update(component: Component, deltaTime: Double) {
    when (component) {
        is Timer -> {
            component.time -= deltaTime
            if (component.time <= 0.0) {
                component.entity.active = false
                removeComponent(component.entity, component)
            }
        }
    }
}

If we have a timer, we tick it down. If the time has expired, we set the entity to inactive and remove this component. I’ll have to implement that, of course …

fun removeComponent(entity: SpaceObject, component: Component) {
    entity.components.remove(component)
}

This might actually work. Let’s try the tests and the game. The fire four missiles test fails but I suspect it needs to call a different function. I’ll try it in the game.

Wow. The game explodes when the missile times out:


│  com.ronjeffries.flat.TemplateProgramKt.main(TemplateProgram.kt:-1)
├─ com.ronjeffries.flat.TemplateProgramKt.main(TemplateProgram.kt:7)
│  org.openrndr.ApplicationBuilderKt.application(ApplicationBuilder.kt:89)
│  org.openrndr.Application.run$openrndr_application(Application.kt:71)
│  org.openrndr.internal.gl3.ApplicationGLFWGL3.loop(ApplicationGLFWGL3.kt:885)
│  org.openrndr.internal.gl3.ApplicationGLFWGL3.drawFrame(ApplicationGLFWGL3.kt:966)
│  org.openrndr.ProgramImplementation.drawImpl(Program.kt:456)
│  org.openrndr.ProgramImplementation.extend.functionExtension.{ }.beforeDraw(Program.kt:312)
├─ com.ronjeffries.flat.TemplateProgram.main.{ :ApplicationBuilder }.{ :Program }.{ }.{ }(TemplateProgram.kt:43)
├─ com.ronjeffries.flat.TemplateProgram.main.{ :ApplicationBuilder }.{ :Program }.{ }.{ }(TemplateProgram.kt:48)
├─ com.ronjeffries.flat.GameKt.gameCycle(Game.kt:39)
├─ java.util.ArrayList.Itr.next(ArrayList.java:967)
├─ java.util.ArrayList.Itr.checkForComodification(ArrayList.java:1013)
│
↑ null (ConcurrentModificationException) 

Process finished with exit code 1

So that’s interesting.

Oh, I think I know what it is. I can’t remove a component while iterating the components list.

Bummer. How shall we handle that?

    for (spaceObject in spaceObjects) {
        removed = mutableListOf()
        for (component in spaceObject.components) {
            update(component, deltaTime)
        }
        removed.forEach { spaceObject.components.remove(it)}

fun removeComponent(entity: SpaceObject, component: Component) {
    removed.add(component)
}

I expect this to work in the game. It does. Is there some kind of iterable that you can remove from while iterating it? If not, we may need to invent it.

This is a bit horrible, because I have the global removed in Game, referenced both by Game and SpaceObject.

But it works. Now let’s see about the test.

    @Test
    fun `can fire four missiles`() {
        createGame(6, 26)
        assertThat(activeMissileCount()).isEqualTo(0)
        fireMissile()
        assertThat(activeMissileCount()).isEqualTo(1)
        fireMissile()
        assertThat(activeMissileCount()).isEqualTo(2)
        fireMissile()
        assertThat(activeMissileCount()).isEqualTo(3)
        fireMissile()
        assertThat(activeMissileCount()).isEqualTo(4)
        fireMissile()
        assertThat(activeMissileCount()).isEqualTo(4)
        val missile = spaceObjects.find { 
        	it.type == SpaceObjectType.MISSILE && 
        	it.active == true}
        tickTimer(missile!!, 3.1)
        assertThat(activeMissileCount()).describedAs("reactivating").isEqualTo(3)
    }

Right, that tickTimer is obsolete now. How about this:

    @Test
    fun `can fire four missiles`() {
        createGame(6, 26)
        assertThat(activeMissileCount()).isEqualTo(0)
        fireMissile()
        assertThat(activeMissileCount()).isEqualTo(1)
        fireMissile()
        assertThat(activeMissileCount()).isEqualTo(2)
        fireMissile()
        assertThat(activeMissileCount()).isEqualTo(3)
        fireMissile()
        assertThat(activeMissileCount()).isEqualTo(4)
        fireMissile()
        assertThat(activeMissileCount()).isEqualTo(4)
        val missile = spaceObjects.find { 
        	it.type == SpaceObjectType.MISSILE && 
        	it.active == true}
        missile!!.components.forEach {update(it, 3.1)}
        assertThat(activeMissileCount()).describedAs("reactivating").isEqualTo(3)
    }

We rip the components untimely from the missile and update them. Since it has a Timer, we update it. The Timer code reactivates the missile, QED. Let’s assess.

Reflection

Well, the good news is that we have a Component, Timer, and it works.

We should have used removeAll:

    for (spaceObject in spaceObjects) {
        removed = mutableListOf()
        for (component in spaceObject.components) {
            update(component, deltaTime)
        }
        spaceObject.components.removeAll(removed)

We have a rather nasty situation with the global removed that is referenced in both Game and SpaceObject. We could arrange that the entity contains its own removal list. And in any reasonable implementation, we’d have an update method on the entity that would deal with such things.

I’m concluding that the ECS style will really go much better with real objects, not just data structures.

Still, now that we have this much in place, future components should come down to defining their data and another branch in the when.

I think I’ll commit this. It works and the main objection is that one global collection. Commit: initial ECS component: Timer.

Oh, I can presumably remove the timer variable from the SpaceObject now. I can remove the tickTimer function, and this:

    if (spaceObject.type == SpaceObjectType.MISSILE){
        tickTimer(spaceObject, deltaTime)
    }

Test. Commit: remove old timer var and code in favor of Timer Component.

I think we’ll wrap. Let me sum up.

Summary

Even with this nearly trivial Component, I can see advantages. The code can be better isolated even without methods on the Component, all coming down to a single when, I expect. The Component has direct knowledge of its entity and can do what it needs to do. I am wondering whether to permit one component to remove others from the object, but so far we have no need of that.

I think I’m going to like this. And I think I’d like it better with methods on my objects. But it’ll be interesting to do it this way, at least for a while longer.

Thanks to Dirk and Bruce for coaxing me into trying this.

See you next time!