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.