Why is Entity Component System (ECS) so awesome for game development?
During game development, we sometimes want to consider new design patterns. In this case, we can try out the ECS, which has been used more than once in video games and game engines.
Overview 👀
Entity Component System (ECS) is a design pattern mostly used in video game development to represent game objects.
Let’s review the purpose of each ECS part:
- An entity represents a general-purpose object and usually, it consists of a unique id.
- A component is associated with an entity and holds the data needed for a particular aspect.
- A system acts on all entities with the required components.
The idea of ECS is entities are defined by the components and systems act globally over all entities with associated components.
As an example, the different game objects have health which is represented by the entity, a component holds data needed for it, and the system queries for entities that could cause object damage and does health calculations on the sets of components for each entity.
ECS uses composition over inheritance, which means every entity is defined by associated components but not by type hierarchy.
The behavior of an entity can be changed at runtime by systems that add, remove or modify components. This eliminates the ambiguity problems of deep and wide inheritance hierarchies.
Common ECS approaches are compatible with data-oriented designs. Data for all instances of a component are commonly stored together in physical memory, enabling efficient memory access for systems that operate over many entities.
Well-known companies who used ECS in their products:
- In 2007, Codemasters experimented with ECS design during “Operation Flashpoint: Dragon Rising” game development.
- In 2015, Apple Inc. introduced a GameplayKit framework that includes an implementation of ECS.
- In 2018, Unity released its megacity demo using ECS.
Bevy ECS 🦅
One of the modern versions of the game engine written on Rust that uses ECS is Bevy.
Bevy ECS is a custom implementation of the ECS pattern. But it has advantages over other ECS implementations written on Rust which often requires complex lifetimes, traits, builder patterns, or macros.
- An Entity is a simple type with a unique integer.
- A Component is a regular struct that derives from the Bevy
Component
trait. - A System is a normal Rust function.
The code example for the health of the game hero will look like this:
use bevy::{prelude::*};
/* Define health component */
#[derive(Component)]
struct Health(u32);
/* Define game hero component which will use health */
#[derive(Component)]
struct Hero;
/* Add game hero with health */
fn add_hero(mut commands: Commands) {
commands.spawn((Hero, Health(100)));
}
/* Define Health System which iterates over every Health component
for entities that also have a Hero component */
fn check_hero_health(mut query: Query<&mut Health, With<Hero>>) {
for health in query.iter_mut() {
println!("health: {}", health.0);
}
/* Dumb condition for health mutation */
if health.0 > 100 {
health.0 = health.0 - 1;
}
}
/* Register system in Bevy app */
fn main() {
App::new()
.add_startup_system(add_hero) /* Called only once */
.add_system(check_hero_health) /* Called on every update */
.run();
}
Notice that Health
is a separate component but not the property of Hero
. It’s because other game characters for instance enemies could have health
as well. So it makes sense to split data into small pieces to encourage code reuse.
As we can see, check_hero_health
the function uses Query
a helper that provides selective access to the component in the system. Also, there is With
a helper struct that filters entities with a specified component.
Another flexible aspect of Bevy, the system parameters are dynamic, and depending on what is passed the system function runs on that data.
The entity is not mentioned in the code, but Bevy creates it under the hood when spawns given components. If you need to change, modify or delete entities it’s possible to get it from a result that returns commands.spawn
or to query Entity
struct in the system.
fn start_up(mut commands: Commands) {
let entity_commands = commands.spawn((Hero, Health(100)));
...
}
/* or */
fn system(query: Query<Entity, With<Hero>>) {
for entity in query.iter() {
...
}
}
Also should be highlighter the following Bevy advantages 🔦:
- Free and open source.
- Supports both 2d and 3d rendering.
- Not tied to a specific graphics API.
- Currently supports Windows, macOS, Linux, and Web platforms. iOS and Android are on the way.
- Simple entry threshold — it's not required from the beginning to have deep knowledge of Rust.
Conclusion📍
To summarize, if you are looking for a new experience in game development then try out the ECS pattern.
Also, Bevy is a great ECS engine for that, especially if you’re a fan of Rust. Its simplicity, flexibility, and major platform support will definitely bring you pleasure in game development.
Personally, I’m using Bevy for a pet project game and am more than happy with it.