ecs_guide/
ecs_guide.rs

1//! This is a guided introduction to Bevy's "Entity Component System" (ECS)
2//! All Bevy app logic is built using the ECS pattern, so definitely pay attention!
3//!
4//! Why ECS?
5//! * Data oriented: Functionality is driven by data
6//! * Clean Architecture: Loose coupling of functionality / prevents deeply nested inheritance
7//! * High Performance: Massively parallel and cache friendly
8//!
9//! ECS Definitions:
10//!
11//! Component: just a normal Rust data type. generally scoped to a single piece of functionality
12//!     Examples: position, velocity, health, color, name
13//!
14//! Entity: a collection of components with a unique id
15//!     Examples: Entity1 { Name("Alice"), Position(0, 0) },
16//!               Entity2 { Name("Bill"), Position(10, 5) }
17//!
18//! Resource: a shared global piece of data
19//!     Examples: asset storage, events, system state
20//!
21//! System: runs logic on entities, components, and resources
22//!     Examples: move system, damage system
23//!
24//! Now that you know a little bit about ECS, lets look at some Bevy code!
25//! We will now make a simple "game" to illustrate what Bevy's ECS looks like in practice.
26
27use bevy::{
28    app::{AppExit, ScheduleRunnerPlugin},
29    prelude::*,
30};
31use core::time::Duration;
32use rand::random;
33use std::fmt;
34
35// COMPONENTS: Pieces of functionality we add to entities. These are just normal Rust data types
36//
37
38// Our game will have a number of "players". Each player has a name that identifies them
39#[derive(Component)]
40struct Player {
41    name: String,
42}
43
44// Each player also has a score. This component holds on to that score
45#[derive(Component)]
46struct Score {
47    value: usize,
48}
49
50// Enums can also be used as components.
51// This component tracks how many consecutive rounds a player has/hasn't scored in.
52#[derive(Component)]
53enum PlayerStreak {
54    Hot(usize),
55    None,
56    Cold(usize),
57}
58
59impl fmt::Display for PlayerStreak {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            PlayerStreak::Hot(n) => write!(f, "{n} round hot streak"),
63            PlayerStreak::None => write!(f, "0 round streak"),
64            PlayerStreak::Cold(n) => write!(f, "{n} round cold streak"),
65        }
66    }
67}
68
69// RESOURCES: "Global" state accessible by systems. These are also just normal Rust data types!
70//
71
72// This resource holds information about the game:
73#[derive(Resource, Default)]
74struct GameState {
75    current_round: usize,
76    total_players: usize,
77    winning_player: Option<String>,
78}
79
80// This resource provides rules for our "game".
81#[derive(Resource)]
82struct GameRules {
83    winning_score: usize,
84    max_rounds: usize,
85    max_players: usize,
86}
87
88// SYSTEMS: Logic that runs on entities, components, and resources. These generally run once each
89// time the app updates.
90//
91
92// This is the simplest type of system. It just prints "This game is fun!" on each run:
93fn print_message_system() {
94    println!("This game is fun!");
95}
96
97// Systems can also read and modify resources. This system starts a new "round" on each update:
98// NOTE: "mut" denotes that the resource is "mutable"
99// Res<GameRules> is read-only. ResMut<GameState> can modify the resource
100fn new_round_system(game_rules: Res<GameRules>, mut game_state: ResMut<GameState>) {
101    game_state.current_round += 1;
102    println!(
103        "Begin round {} of {}",
104        game_state.current_round, game_rules.max_rounds
105    );
106}
107
108// This system updates the score for each entity with the `Player`, `Score` and `PlayerStreak` components.
109fn score_system(mut query: Query<(&Player, &mut Score, &mut PlayerStreak)>) {
110    for (player, mut score, mut streak) in &mut query {
111        let scored_a_point = random::<bool>();
112        if scored_a_point {
113            // Accessing components immutably is done via a regular reference - `player`
114            // has type `&Player`.
115            //
116            // Accessing components mutably is performed via type `Mut<T>` - `score`
117            // has type `Mut<Score>` and `streak` has type `Mut<PlayerStreak>`.
118            //
119            // `Mut<T>` implements `Deref<T>`, so struct fields can be updated using
120            // standard field update syntax ...
121            score.value += 1;
122            // ... and matching against enums requires dereferencing them
123            *streak = match *streak {
124                PlayerStreak::Hot(n) => PlayerStreak::Hot(n + 1),
125                PlayerStreak::Cold(_) | PlayerStreak::None => PlayerStreak::Hot(1),
126            };
127            println!(
128                "{} scored a point! Their score is: {} ({})",
129                player.name, score.value, *streak
130            );
131        } else {
132            *streak = match *streak {
133                PlayerStreak::Hot(_) | PlayerStreak::None => PlayerStreak::Cold(1),
134                PlayerStreak::Cold(n) => PlayerStreak::Cold(n + 1),
135            };
136
137            println!(
138                "{} did not score a point! Their score is: {} ({})",
139                player.name, score.value, *streak
140            );
141        }
142    }
143
144    // this game isn't very fun is it :)
145}
146
147// This system runs on all entities with the `Player` and `Score` components, but it also
148// accesses the `GameRules` resource to determine if a player has won.
149fn score_check_system(
150    game_rules: Res<GameRules>,
151    mut game_state: ResMut<GameState>,
152    query: Query<(&Player, &Score)>,
153) {
154    for (player, score) in &query {
155        if score.value == game_rules.winning_score {
156            game_state.winning_player = Some(player.name.clone());
157        }
158    }
159}
160
161// This system ends the game if we meet the right conditions. This fires an AppExit event, which
162// tells our App to quit. Check out the "event.rs" example if you want to learn more about using
163// events.
164fn game_over_system(
165    game_rules: Res<GameRules>,
166    game_state: Res<GameState>,
167    mut app_exit_events: EventWriter<AppExit>,
168) {
169    if let Some(ref player) = game_state.winning_player {
170        println!("{player} won the game!");
171        app_exit_events.write(AppExit::Success);
172    } else if game_state.current_round == game_rules.max_rounds {
173        println!("Ran out of rounds. Nobody wins!");
174        app_exit_events.write(AppExit::Success);
175    }
176}
177
178// This is a "startup" system that runs exactly once when the app starts up. Startup systems are
179// generally used to create the initial "state" of our game. The only thing that distinguishes a
180// "startup" system from a "normal" system is how it is registered:
181//      Startup: app.add_systems(Startup, startup_system)
182//      Normal:  app.add_systems(Update, normal_system)
183fn startup_system(mut commands: Commands, mut game_state: ResMut<GameState>) {
184    // Create our game rules resource
185    commands.insert_resource(GameRules {
186        max_rounds: 10,
187        winning_score: 4,
188        max_players: 4,
189    });
190
191    // Add some players to our world. Players start with a score of 0 ... we want our game to be
192    // fair!
193    commands.spawn_batch(vec![
194        (
195            Player {
196                name: "Alice".to_string(),
197            },
198            Score { value: 0 },
199            PlayerStreak::None,
200        ),
201        (
202            Player {
203                name: "Bob".to_string(),
204            },
205            Score { value: 0 },
206            PlayerStreak::None,
207        ),
208    ]);
209
210    // set the total players to "2"
211    game_state.total_players = 2;
212}
213
214// This system uses a command buffer to (potentially) add a new player to our game on each
215// iteration. Normal systems cannot safely access the World instance directly because they run in
216// parallel. Our World contains all of our components, so mutating arbitrary parts of it in parallel
217// is not thread safe. Command buffers give us the ability to queue up changes to our World without
218// directly accessing it
219fn new_player_system(
220    mut commands: Commands,
221    game_rules: Res<GameRules>,
222    mut game_state: ResMut<GameState>,
223) {
224    // Randomly add a new player
225    let add_new_player = random::<bool>();
226    if add_new_player && game_state.total_players < game_rules.max_players {
227        game_state.total_players += 1;
228        commands.spawn((
229            Player {
230                name: format!("Player {}", game_state.total_players),
231            },
232            Score { value: 0 },
233            PlayerStreak::None,
234        ));
235
236        println!("Player {} joined the game!", game_state.total_players);
237    }
238}
239
240// If you really need full, immediate read/write access to the world or resources, you can use an
241// "exclusive system".
242// WARNING: These will block all parallel execution of other systems until they finish, so they
243// should generally be avoided if you want to maximize parallelism.
244fn exclusive_player_system(world: &mut World) {
245    // this does the same thing as "new_player_system"
246    let total_players = world.resource_mut::<GameState>().total_players;
247    let should_add_player = {
248        let game_rules = world.resource::<GameRules>();
249        let add_new_player = random::<bool>();
250        add_new_player && total_players < game_rules.max_players
251    };
252    // Randomly add a new player
253    if should_add_player {
254        println!("Player {} has joined the game!", total_players + 1);
255        world.spawn((
256            Player {
257                name: format!("Player {}", total_players + 1),
258            },
259            Score { value: 0 },
260            PlayerStreak::None,
261        ));
262
263        let mut game_state = world.resource_mut::<GameState>();
264        game_state.total_players += 1;
265    }
266}
267
268// Sometimes systems need to be stateful. Bevy's ECS provides the `Local` system parameter
269// for this case. A `Local<T>` refers to a value of type `T` that is owned by the system.
270// This value is automatically initialized using `T`'s `FromWorld`* implementation upon the system's initialization.
271// In this system's `Local` (`counter`), `T` is `u32`.
272// Therefore, on the first turn, `counter` has a value of 0.
273//
274// *: `FromWorld` is a trait which creates a value using the contents of the `World`.
275// For any type which is `Default`, like `u32` in this example, `FromWorld` creates the default value.
276fn print_at_end_round(mut counter: Local<u32>) {
277    *counter += 1;
278    println!("In set 'Last' for the {}th time", *counter);
279    // Print an empty line between rounds
280    println!();
281}
282
283/// A group of related system sets, used for controlling the order of systems. Systems can be
284/// added to any number of sets.
285#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
286enum MySet {
287    BeforeRound,
288    Round,
289    AfterRound,
290}
291
292// Our Bevy app's entry point
293fn main() {
294    // Bevy apps are created using the builder pattern. We use the builder to add systems,
295    // resources, and plugins to our app
296    App::new()
297        // Resources that implement the Default or FromWorld trait can be added like this:
298        .init_resource::<GameState>()
299        // Plugins are just a grouped set of app builder calls (just like we're doing here).
300        // We could easily turn our game into a plugin, but you can check out the plugin example for
301        // that :) The plugin below runs our app's "system schedule" once every 5 seconds.
302        .add_plugins(ScheduleRunnerPlugin::run_loop(Duration::from_secs(5)))
303        // `Startup` systems run exactly once BEFORE all other systems. These are generally used for
304        // app initialization code (ex: adding entities and resources)
305        .add_systems(Startup, startup_system)
306        // `Update` systems run once every update. These are generally used for "real-time app logic"
307        .add_systems(Update, print_message_system)
308        // SYSTEM EXECUTION ORDER
309        //
310        // Each system belongs to a `Schedule`, which controls the execution strategy and broad order
311        // of the systems within each tick. The `Startup` schedule holds
312        // startup systems, which are run a single time before `Update` runs. `Update` runs once per app update,
313        // which is generally one "frame" or one "tick".
314        //
315        // By default, all systems in a `Schedule` run in parallel, except when they require mutable access to a
316        // piece of data. This is efficient, but sometimes order matters.
317        // For example, we want our "game over" system to execute after all other systems to ensure
318        // we don't accidentally run the game for an extra round.
319        //
320        // You can force an explicit ordering between systems using the `.before` or `.after` methods.
321        // Systems will not be scheduled until all of the systems that they have an "ordering dependency" on have
322        // completed.
323        // There are other schedules, such as `Last` which runs at the very end of each run.
324        .add_systems(Last, print_at_end_round)
325        // We can also create new system sets, and order them relative to other system sets.
326        // Here is what our games execution order will look like:
327        // "before_round": new_player_system, new_round_system
328        // "round": print_message_system, score_system
329        // "after_round": score_check_system, game_over_system
330        .configure_sets(
331            Update,
332            // chain() will ensure sets run in the order they are listed
333            (MySet::BeforeRound, MySet::Round, MySet::AfterRound).chain(),
334        )
335        // The add_systems function is powerful. You can define complex system configurations with ease!
336        .add_systems(
337            Update,
338            (
339                // These `BeforeRound` systems will run before `Round` systems, thanks to the chained set configuration
340                (
341                    // You can also chain systems! new_round_system will run first, followed by new_player_system
342                    (new_round_system, new_player_system).chain(),
343                    exclusive_player_system,
344                )
345                    // All of the systems in the tuple above will be added to this set
346                    .in_set(MySet::BeforeRound),
347                // This `Round` system will run after the `BeforeRound` systems thanks to the chained set configuration
348                score_system.in_set(MySet::Round),
349                // These `AfterRound` systems will run after the `Round` systems thanks to the chained set configuration
350                (
351                    score_check_system,
352                    // In addition to chain(), you can also use `before(system)` and `after(system)`. This also works
353                    // with sets!
354                    game_over_system.after(score_check_system),
355                )
356                    .in_set(MySet::AfterRound),
357            ),
358        )
359        // This call to run() starts the app we just built!
360        .run();
361}