logo
Published on

Making a Game Engine in Rust 03: ECS

Authors

I know it’s been some time since the last post, but I’ve been busy working on the ECS (Entity-Component-System). It took a while to get right, but I’ve finally finished a basic, yet usable, implementation that will be crucial for future features in the game engine.

What is ECS?

ECS, or Entity-Component-System, is a design pattern used primarily in game development. It separates the data (components) from the behavior (systems) and applies it to the entities within the game world. This pattern allows for a more flexible, scalable, and efficient way to manage game objects and their interactions.

In an ECS:

  • Entities are the objects in your game (e.g., players, enemies, projectiles).
  • Components are the data associated with entities (e.g., position, velocity, health).
  • Systems operate on entities with specific components, defining the game’s behavior (e.g., movement system updates positions, collision system checks for overlaps).

ECS is particularly beneficial because it allows for better cache efficiency and more modular code. You can add, remove, or modify components and systems without affecting other parts of the game, making the engine more maintainable.

How It’s Implemented

To implement ECS, I created a struct called World, which acts as the main structure that the end-user interacts with. The World struct contains three primary components:

  • Resources: A struct that stores all the resources used in the game.
  • Entities: A struct that manages all the entities and their associated components.
  • Systems: A struct that holds all the systems responsible for game logic.

The World struct provides methods that simplify interactions with these underlying components, making it easier for the user to work with the ECS framework.

How It Works

One of the main challenges in ECS is allowing the user to access resources across different parts of the game. If we used simple references, we’d run into issues where multiple references require mutability, which Rust’s safety guarantees won’t allow.

To solve this, I utilized smart pointers—specifically, Rc and RefCell. These allow for interior mutability, meaning that while multiple references can exist, only one can be mutable at any given time. This approach ensures safety while allowing different parts of the program to modify and read shared data like the game’s score or state.

Tip: If you’re familiar with Rust, you might know about Arc, a multithreaded version of Rc. However, since I’m not focusing on multithreading right now, Arc isn’t necessary—at least for the time being.

Resources

The resources implementation is fairly simple. It’s just a struct with a HashMap that stores all the game’s resources, with the key being the type ID of the resource. I’ve added some methods to make resource retrieval easier, so working with resources is straightforward and efficient.

Entities

The entity system is a bit more complex. Essentially, entities are stored in a large table where each row represents a different component, and each column corresponds to an entity.

Here’s a simple example:

0123
Location(2,3)(0,0)(182, 12)(800, 800)
Speed(1, 0)(-2, 1)
Health10057

Not all entities have all components, so the challenge is efficiently determining which components an entity has without wasting time.

To address this, I implemented bitmasks. Each component type is assigned a unique bitmask, which is bit-shifted according to its index. For example:

  • Location: 00000001
  • Speed: 00000010
  • Health: 00000100

Each entity then has a map that stores a combination of these bitmasks, representing which components it possesses. This approach uses a bit more memory but significantly improves the efficiency of querying components.

For example, based on the table above, the bitmask for the first entity would be 00000111, for the second entity 00000001, and for the third 00000101.

Systems

The last part of the ECS is the systems. Systems work similarly to entities but are much simpler. Each system operates on specific components, and it includes at least one method that gets called to update the game state based on the components it interacts with.

What’s Next?

So that’s what I’ve been working on lately—getting the ECS up and running. It’s a crucial part of the game engine and will serve as the foundation for many other features. In the next post, I’ll probably talk about the renderer, but I might dive into the math library first. We’ll see how it goes.

Stay tuned, and thanks for following along!