โ€ข

newsletter

Entity Component System to make game dev fun again

Game dev is littered with OOP and mutability. But games are complex, and this situation only leads to madness. But there is a solution: ECS. Let me show you how it works.


Sandro Maglione

Sandro Maglione

Software development

A new cool (simple and powerful) kid joined my (game) programming toolbox: Entity Component System (ECS).

It's not "new" (it's been around since 1998), but only now it landed on my purview as a game dev.

And it's here to stay ๐Ÿ™Œ

Ah, and remember when I discussed FP in game development last week? Well, you may not actually need it ๐Ÿ‘‡


Problems with classical game dev (that you may not know)

Here is some code I have written for a game in Dart:

class Npc extends SpriteComponent
    // ๐Ÿ‘‡ Some behaviors from somewhere else
    with
        HasGameRef,
        CollisionCallbacks,
        FlameBlocReader<GameStateCubit, GameState> {
  
  // ๐Ÿ‘‡ Data all stored inside the entity
  Vector2 _direction = Vector2(0, 1);
  double speed = 200.0;
  double stength;

  bool isChanging = false;
  PlayerStatus playerStatus;
  final int id;

  Npc(Vector2 position, {required this.id, required this.playerStatus})
      : stength = Random().nextDouble() {
    this.position = position;
    _direction = Vector2.random() * (Random().nextBool() ? 1 : -1);
  }

  @override
  void update(double dt) {
    super.update(dt);

    // ๐Ÿ‘‡ Something "is changing" and mutates the game state
    if (!isChanging) {
      position.add(_direction * speed * dt);
    }
  }

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);

    // ๐Ÿ‘‡ A bunch of checks for collisions all over the place
    if (bloc.state.isEnded == null) {
      if (other is Wall) {
        _direction.invert();
      } else if (other is Npc &&
          !other.isChanging &&
          other.playerStatus != playerStatus) {
        if (stength > other.stength) {
          other.changeStatus(playerStatus, false);
        } else {
          changeStatus(other.playerStatus, false);
        }
      }
    }
  }
}

This is your classic (broken) game dev workflow:

  • A class that represents your game entity
  • The class extends a bunch of behaviors from other classes (who knows what)
  • The class encapsulates all the data and logic for that entity
  • A series of (imperative) checks read the game state and decide what to do, often by mutating something in the world (even outside the entity i.e. changeStatus)
  • Each entity cares about its own behaviors, sharing features with inheritance

Read this all again. It's a recipe for disaster ๐Ÿซ 

This goes deep into OOP and mutability madness, quickly. And we are talking about a game here.

Games are complex. They aim to recreate slices of reality. Reality is a mess. And OOP doesn't help.

I define that workflow as "classic" since most game engines are based on it. You cannot escape it if you sneak your head into making a game.

FP rescued software development, what about game dev?

FP restores your sanity when you work with software (outside of games).

Patterns like composability, immutability, dependency injection, pure functions. These are all checks to help you scale (and local reasoning).

But the game dev world has been mostly untouched by FP. A game is a mutable thing, and having to keep everything pure and never changing brings more pain than it's worth.

Furthermore, game engines are mutable, all of them, mostly for performance and "usability" reasons.

Wrap it with FP it's just indiscreet ๐Ÿ’๐Ÿผโ€โ™‚๏ธ

Entity Component System to the rescue

But there is another path forward. It integrates with all game engines, it's simple, and brings composability back to game dev.

It's called Entity Component System (ECS) ๐Ÿ™Œ

Separate data from logic

The major pain point of OOP game dev is that logic and data are sandwiched together. It means that everything can change anything, data runs free and unrestricted.

In ECS data is all inside components, and logic is all inside systems.

This simple pattern brings back the ability to compose logic and control mutations:

  • Define components that encapsulate data
  • Define systems that act on a subset of components
// ๐Ÿ‘‡ Query all entities with position and velocity components attached
const moving = query({ position: Position, velocity: Velocity });

export const MovementSystem: SystemUpdate<GameEventMap> = ({
  world,
  deltaTime,
}) => {
  // ๐Ÿ‘‡ The system acts on everything that matches the query (all components required)
  moving(world).forEach(({ position, velocity }) => {
    position.x += velocity.vx * deltaTime;
    position.y += velocity.vy * deltaTime;
  });
};

Entities are just numbers

Where is your player here? Enemies? NPCs?

The mental model is flipped. Entities become containers of behaviors.

A "Player" is something that has a position, velocity, and input (all components).

These components define the concept of a "Player", and systems are responsible for making them do stuff.

You see composition here:

  • Logic is all inside systems, and you can reason about them individually
  • Entities only care about their components, what will happen to them is abstracted away
// `playerId` is just a number
const playerId = createEntity();

// A "player" is a container of components
// It has a position, velocity, sprite, etc.
addComponent(
  playerId,
  Velocity.idle,
  initialPosition,
  new Player(),
  new Sprite({ sprite: playerSprite }),
  new Collider({ body: playerBody })
);

I then published a package: @typeonce/ecs

I went deep into the rabbit hole, and emerged with a new package: @typeonce/ecs.

@typeonce/ecs makes ECS in TypeScript structured and type-safe. I am iterating on the API by building multiple games with it.

It's working great, bringing the excitement back to the game dev world.

Resources

Until I publish a more complete guide (๐Ÿ‘€), here are some resources for you:

You can also ask AI about it. Since the pattern has been around for 20 years, AI seems particularly knowledgeable about it ๐Ÿค“


I already see how these patterns can be combined with everything happening on the web to build something magical ๐Ÿช„

You can follow the (undocumented yet) progress on Github.

Something is coming out of this, stay tuned ๐Ÿ‘€

See you next ๐Ÿ‘‹

Start here.

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.