notes

Log | Files | Refs | README

entity_component_system.md (10631B)


      1 # Entity Component System (ECS)
      2 
      3 > **Common use cases:** Game engines, simulations, UI frameworks, any system
      4 > with many objects sharing overlapping data
      5 
      6 ---
      7 
      8 ## What is ECS?
      9 
     10 The **Entity Component System** is a composition-over-inheritance architectural
     11 pattern that separates _identity_ (entities), _data_ (components), and _logic_
     12 (systems). Instead of a class hierarchy where a `Player` extends `Character`,
     13 you build objects by attaching plain data structs to a bare ID.
     14 
     15 | Concept       | Role                                      | Rust analogy                                  |
     16 | ------------- | ----------------------------------------- | --------------------------------------------- |
     17 | **Entity**    | A unique ID — nothing more                | `u64` or a newtype wrapper                    |
     18 | **Component** | Plain data attached to an entity          | A `struct` with `#[derive(Component)]`        |
     19 | **System**    | Logic that queries and mutates components | A function that receives queries as arguments |
     20 
     21 ---
     22 
     23 ## Simple Example from Scratch
     24 
     25 A minimal ECS without any external crate to understand the mechanics:
     26 
     27 ```rust
     28 use std::collections::HashMap;
     29 
     30 type Entity = u64;
     31 
     32 // Components are plain structs
     33 #[derive(Debug)]
     34 struct Position { x: f32, y: f32 }
     35 
     36 #[derive(Debug)]
     37 struct Velocity { dx: f32, dy: f32 }
     38 
     39 // World holds component storage — one HashMap per component type
     40 #[derive(Default)]
     41 struct World {
     42     next_entity: Entity,
     43     positions:  HashMap<Entity, Position>,
     44     velocities: HashMap<Entity, Velocity>,
     45 }
     46 
     47 impl World {
     48     fn spawn(&mut self) -> Entity {
     49         let id = self.next_entity;
     50         self.next_entity += 1;
     51         id
     52     }
     53 }
     54 
     55 // A system is just a plain function
     56 fn movement_system(world: &mut World) {
     57     for (entity, vel) in &world.velocities {
     58         if let Some(pos) = world.positions.get_mut(entity) {
     59             pos.x += vel.dx;
     60             pos.y += vel.dy;
     61         }
     62     }
     63 }
     64 
     65 fn main() {
     66     let mut world = World::default();
     67 
     68     let player = world.spawn();
     69     world.positions.insert(player,  Position  { x: 0.0, y: 0.0 });
     70     world.velocities.insert(player, Velocity  { dx: 1.0, dy: 0.5 });
     71 
     72     let static_obstacle = world.spawn();
     73     world.positions.insert(static_obstacle, Position { x: 10.0, y: 10.0 });
     74     // No Velocity — this entity won't move
     75 
     76     movement_system(&mut world);
     77 
     78     println!("{:?}", world.positions[&player]); // Position { x: 1.0, y: 0.5 }
     79 }
     80 ```
     81 
     82 Key insight: `static_obstacle` was never touched by `movement_system` because it
     83 has no `Velocity`. Systems only operate on the _intersection_ of components they
     84 care about — no `if entity_is_static` checks required.
     85 
     86 ---
     87 
     88 ## Growing the Design
     89 
     90 Once the naive HashMap approach gets unwieldy you need:
     91 
     92 - **Archetypes** — group entities by their exact component set for better cache
     93   locality (how Bevy's world works)
     94 - **Sparse sets** — fast add/remove at the cost of iteration speed (used for
     95   rarely-changed components)
     96 - **System scheduling** — run systems in parallel when they don't share mutable
     97   access; `bevy_ecs` does this automatically via Rust's borrow rules
     98 
     99 ```rust
    100 // A slightly more ergonomic API pattern using a builder
    101 struct EntityBuilder<'w> {
    102     world: &'w mut World,
    103     id: Entity,
    104 }
    105 
    106 impl<'w> EntityBuilder<'w> {
    107     fn with_position(self, x: f32, y: f32) -> Self {
    108         self.world.positions.insert(self.id, Position { x, y });
    109         self
    110     }
    111     fn with_velocity(self, dx: f32, dy: f32) -> Self {
    112         self.world.velocities.insert(self.id, Velocity { dx, dy });
    113         self
    114     }
    115     fn build(self) -> Entity { self.id }
    116 }
    117 ```
    118 
    119 ---
    120 
    121 ## Bevy as a Production ECS
    122 
    123 [Bevy](https://bevyengine.org/) is the most prominent real-world ECS in Rust and
    124 a great study in ergonomic API design. Its ECS lives in the standalone
    125 [`bevy_ecs`](https://crates.io/crates/bevy_ecs) crate — you can use it without
    126 the rest of the game engine.
    127 
    128 ```rust
    129 use bevy::prelude::*;
    130 
    131 // Components — plain structs that derive `Component`
    132 #[derive(Component)]
    133 struct Position { x: f32, y: f32 }
    134 
    135 #[derive(Component)]
    136 struct Velocity { dx: f32, dy: f32 }
    137 
    138 // Systems are just functions; Bevy injects queries via its scheduler
    139 fn movement_system(mut query: Query<(&mut Position, &Velocity)>) {
    140     for (mut pos, vel) in &mut query {
    141         pos.x += vel.dx;
    142         pos.y += vel.dy;
    143     }
    144 }
    145 
    146 fn spawn_player(mut commands: Commands) {
    147     commands.spawn((
    148         Position { x: 0.0, y: 0.0 },
    149         Velocity { dx: 1.0, dy: 0.5 },
    150     ));
    151 }
    152 
    153 fn main() {
    154     App::new()
    155         .add_plugins(MinimalPlugins)
    156         .add_systems(Startup, spawn_player)
    157         .add_systems(Update, movement_system)
    158         .run();
    159 }
    160 ```
    161 
    162 Notice how `movement_system` never knows about entities at all — it just
    163 expresses _"give me every entity that has both a mutable Position and an
    164 immutable Velocity"_. Bevy's scheduler can then safely parallelise any two
    165 systems whose query sets don't conflict.
    166 
    167 ### Why Bevy's ECS is a Masterclass in Ergonomic Rust
    168 
    169 - **Function-parameter injection** via `SystemParam` traits — adding a
    170   `Res<Time>` parameter to a system just works
    171 - **`Commands` for deferred mutations** — you can't borrow the world mutably
    172   while iterating it, so commands queue up spawns/despawns to run between
    173   systems
    174 - **`With` / `Without` / `Or` filters** — express complex queries in the type
    175   system with no runtime cost
    176 - **Observers and triggers** (added in Bevy 0.14) — reactive event-driven logic
    177   built on top of the ECS
    178 
    179 ---
    180 
    181 ## Why ECS Fits Rust So Well
    182 
    183 Traditional OOP patterns with shared mutable object graphs fight the borrow
    184 checker constantly. ECS sidesteps this by:
    185 
    186 1. **Separating data from logic** — systems take fine-grained borrows, so two
    187    systems can run in parallel as long as they don't both need `&mut` on the
    188    same component type
    189 2. **Cache-friendly storage** — components of the same type are stored
    190    contiguously, turning what would be pointer-chasing in OOP into sequential
    191    memory reads
    192 3. **Composition without `dyn Trait`** — you don't need dynamic dispatch;
    193    queries are resolved at compile time via associated types and const generics
    194 
    195 ---
    196 
    197 ## Resources & Further Reading
    198 
    199 ### Talks
    200 
    201 - 🎥 **Chris Biscardi — "Bevy: A Case Study in Ergonomic Rust"**
    202   (RustConf 2024)\
    203   Deep dive into the API design tricks Bevy uses — applicable far beyond games.\
    204    205   [https://www.youtube.com/watch?v=CnoDOc6ML0Y](https://www.youtube.com/watch?v=CnoDOc6ML0Y)
    206 
    207 - 🎥 **Alice Cecile — "Architecting Bevy"** (interview, 2024)\
    208   Alice is a core Bevy contributor and foundation member. This talk covers ECS
    209   architecture decisions and long-term open-source project management.\
    210    211   [https://www.youtube.com/watch?v=PND2Wpy6U-E](https://www.youtube.com/watch?v=PND2Wpy6U-E)
    212 
    213 ### Video Series
    214 
    215 - 📺 **Brooks Patton — "Improve Your Rust Skills by Making an ECS Library"**
    216   (YouTube playlist, 2021)\
    217   Builds an ECS from scratch in Rust. Covers `TypeId`, `HashMap`, generics,
    218   `Copy`/`Clone`, interior mutability, and modules — great for understanding the
    219   internals.\
    220    221   [https://www.youtube.com/playlist?list=PLrmY5pVcnuE_SQSzGPWUJrf9Yo-YNeBYs](https://www.youtube.com/playlist?list=PLrmY5pVcnuE_SQSzGPWUJrf9Yo-YNeBYs)\
    222   → Code:
    223   [https://github.com/brooks-builds/improve_skills_by_building_ecs_library_in_rust](https://github.com/brooks-builds/improve_skills_by_building_ecs_library_in_rust)
    224 
    225 ### People to Follow
    226 
    227 | Person                              | Known for                                                                                                                                                  | Link                                                             |
    228 | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
    229 | **Alice Cecile** (`alice-i-cecile`) | Bevy core contributor, ECS architect, RFC author                                                                                                           | [github.com/alice-i-cecile](https://github.com/alice-i-cecile)   |
    230 | **Chris Biscardi**                  | Bevy educator, ergonomic Rust APIs, Rust Adventure                                                                                                         | [youtube/@chrisbiscardi](https://www.youtube.com/@chrisbiscardi) |
    231 | **Brooks Patton** (`brookzerker`)   | ECS from scratch series, Rust std-lib deep dives                                                                                                           | [youtube/@brookzerker](https://www.youtube.com/@brookzerker)     |
    232 | **Jon Gjengset**                    | "Crust of Rust" — intermediate Rust internals (not ECS-specific, but indispensable for understanding the mechanics ECS relies on like interior mutability) | [youtube/@jongjengset](https://www.youtube.com/@jongjengset)     |
    233 | **Alice Ryhl**                      | Tokio maintainer, async Rust expert — not ECS-specific, but her writing on structured concurrency complements ECS scheduling design                        | [ryhl.io](https://ryhl.io)                                       |
    234 
    235 ### Crates
    236 
    237 | Crate                                           | Notes                                                      |
    238 | ----------------------------------------------- | ---------------------------------------------------------- |
    239 | [`bevy_ecs`](https://crates.io/crates/bevy_ecs) | Production-grade, standalone; the reference implementation |
    240 | [`hecs`](https://crates.io/crates/hecs)         | Minimal, low-level; great for embedding                    |
    241 | [`specs`](https://crates.io/crates/specs)       | Older, more explicit; parallel systems via Rayon           |
    242 | [`shipyard`](https://crates.io/crates/shipyard) | Sparse-set ECS; fast add/remove                            |
    243 | [`flecs`](https://crates.io/crates/flecs_ecs)   | Rust bindings for the C flecs library; extremely mature    |
    244 
    245 ---
    246 
    247 ## When _Not_ to Use ECS
    248 
    249 ECS adds indirection and query overhead. Avoid it when:
    250 
    251 - You have **fewer than ~100 objects** that rarely change structure
    252 - Your logic is **purely sequential and single-threaded** with no parallelism
    253   benefit
    254 - You're building a **CRUD app or domain model** — here, plain structs and trait
    255   objects are cleaner
    256 
    257 A good rule of thumb: reach for ECS when you find yourself writing
    258 `if player.has_component::<Health>()` — that conditional is ECS trying to break
    259 out.