Learning from Game Dev: Entity, Component, Systems

All the game developers that I’ve spoken with (hobby or pro) tell me, “Making games is entirely different from making generic software.” While I agree with that statement, there are definitely nuggets to mine from the world of game development.

The pattern of using Entity Component Systems (ECS) is one such nugget. ECS is a method of processing game state in a flexible and extensible way. It has a heavy focus on Data-Oriented Design, composition over inheritance, and separation of data from logic.

The Problem

Games, more than many other software applications, track a lot of state. At any given time, there could be hundreds of enemies and thousands of bullets–each having its own state: position, velocity, animation frame, etc.

This introduces two concerns. First, how do we build the behavior we want into each of these objects? Secondly, how do we manage all this state? How do we store and access this data in a way that is manageable?

A Solution

Let’s see how ECS works and answers these questions:

Entity

Conceptually, an entity is any object in your game: a bullet, the player, a tank. An entity is the collection of its components and a unique identifier. In many implementations of ECS, entities only exist as an ID. There is no logic allowed here.

Component

Components hold raw data needed to support some desired behavior (e.g., position information is needed to draw the bullet on the screen).

System

Systems are where the logic of given behaviors live. For example, a Movement system might iterate over all the entities that have both a Position component and a Velocity component. The system would modify the position according to the velocity.

Let’s tee up the problem a little more and go back to Object-Oriented Programming 101. In our game, we want both Projectiles and Grenades. We’ll have them extend our GameObject class. This is fine until we want an RPG, which puts us into the dreaded “Diamond of Death”.

A diagram of a multiple inheritance problem with the labels "RPG, Grenade, Projectile, and Game Object"
Multiple inheritance problem

Using ECS with its focus on composition prevents this problem. We simply create Projectile and Grenade components and tie them to an entity ID. At this point, you can start to treat your components as rows in a relational database. An entity is merely a join across many component tables with the entity ID as the join field. A system will query for the components required to execute its logic on each entity.

A diagram of components as database tables—one box labelled "grenades," and the other "projectiles."
RPGSystem would simply query for the entities it needs:
SELECT p.*, g.* FROM projectiles p INNER JOIN grenades g ON g.eid = p.eid;

Systems now process just the relevant pieces of data in an efficient way. This solves the second problem of how to handle large amounts of objects while applying logic to each one. It makes new entities and components quick and easy to create and modify via data changes, and it makes working with and testing systems easy.

Takeaways for Non-Game Devs

I’ve been using some form of ECS for many years in my game development (see Gamebox, Killbox, Pixel Monster, Metroid Clone). Here are some of my top takeaways:

  • Composition over inheritance; inheritance has its place, but it can complicate testing and reasoning about your codebase.
  • State is hard; keep as many things stateless as possible. With the exception of some caching, systems are nice functions that iterate over their entities and contain no state.
  • Pushing state into raw searchable data when possible is a good thing.
  • Representing things in data lets you build interesting introspection tools.
  • Concepts can exist in your code without having a class named after them; entities don’t really exist, just their collection of components.

Branching out into unfamiliar paradigms is a great source of new ideas and patterns to write better software. The Entity Component System pattern may not directly apply to our day-to-day projects, but it pushes us out of our standard Object-Oriented model, and that’s a good thing.