Dylan Falconer

Join 500+ game programmers getting weekly tips.

Entity Systems - Two ECS Alternatives

I’m not making the case for or against ECS (Entity Component Systems) in this article.

I’ll assume you have done your research and decided against it.

So then, what is an Entity System and how do we build one?

Keeping things simple, we’ll put data required for all entities onto an Entity type.

This is often referred to as a “mega-struct”.

Entity :: struct {
	pos, vel:                   Vec2,
	collider_size:              Vec2,
	sprite_size, sprite_offset: Vec2,
	health, max_health:         int,
	flags:                      bit_set[Entity_Flags],
}

We can store a bunch of boolean values inside a bit_set.

Entity_Flags :: enum {
	Is_Dead,
	Is_Flipped,
	// etc...
}

We’ll store our entities in a simple dynamic array.

entities: [dynamic]Entity

Then, if you language allows it, I recommend creating a distinct type for your entity IDs.

Entity_Id :: distinct int

Why a distinct type?

Imagine we use a simple int.

Then imagine we also create another system that uses int as IDs.

other_system_id := other_system_create()
entity := entity_get(other_system_id)

Obviously that is contrived, but you can get into situations where you pass the wrong ID in because they are all int.

With distinct types, you’ll get a type error at compile time if you pass anything other than Entity_Id in.

Let’s build out the entity system procs:

entity_get :: proc(id: Entity_Id) -> ^Entity {
	if int(id) < len(entities) {
		return &entities[int(id)]
	}
	return nil
}

entity_create :: proc(entity: Entity) -> Entity_Id {
	for &e, i in entities {
		if .Is_Dead in e.flags {
			e = entity
			e.flags -= {.Is_Dead}
			return Entity_Id(i)
		}
	}

	index := len(entities)
	append(&entities, entity)

	return Entity_Id(index)
}

entity_kill :: proc(id: Entity_Id) -> bool {
	if int(id) < len(entities) {
		entities[int(id)].flags += {.Is_Dead}
		return true
	}
	return false
}

You should not keep Entity pointers across calls of entity_create. If the array is full and a reallocation is required, pointers will be invalidated.

These are almost the simplest versions I have used across various projects.

There may be a problem, though, depending on your game’s design.

For example:

To counter this issue, we can introduce the concept of a “generation” to the entities.

Entity :: struct {
	generation: int,
	// other fields remain unchanged
}

Now when you assign an Entity to follow another, you also include the generation.

Something like this, (just one way of many)

Follow :: struct {
	id: Entity_Id,
	generation: int,
}

following: map[Entity_Id]Follow

Then in your update code, you’d make sure the generation matches:

if follow, ok := following[id]; ok {
	other := entity_get(other_id)
	if other.generation == follow.generation {
		// follow code goes here
	} else {
		delete_key(id)
	}
}

The generation is set when an entity slot is reused:

entity_create :: proc(entity: Entity) -> Entity_Id {
	for &e, i in entities {
		if .Is_Dead in e.flags {
			gen := e.generation + 1 // Save generation before overwriting
			e = entity
			e.flags -= {.Is_Dead}
			e.generation = gen // Set to new generation
			return Entity_Id(i)
		}
	}

	index := len(entities)
	append(&entities, entity)

	return Entity_Id(index)
}

Here’s the rest of a very simple program I wrote that uses the first style:

entity_update :: proc(dt: f32) {
	for &e in entities {
		if .Is_Dead in e.flags do continue

		e.pos += e.vel * dt
	}
}

main :: proc() {
	id := entity_create({pos = 100, vel = 200})

	dt :: 1.0 / 60.0

	for i in 0 ..< 4 {
		entity_update(dt)
		fmt.println(entity_get(id).pos)
	}
}

It prints out:

[103.333336, 103.333336]
[106.66667, 106.66667]
[110.000008, 110.000008]
[113.33334, 113.33334]

Alright, that’s it!

Those are my two simple entity systems to get you going without having to create complex ECS queries.

If you enjoyed this post, please consider joining the mailing list. Thank you for reading!


← Articles

Join 500+ game programmers getting weekly tips.