docs/bevy/examples/ecs/ecs_guide.rs 14.5 K · 366 lines · raw · history

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, messages, 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
27 use bevy::{
28 app::{AppExit, ScheduleRunnerPlugin},
29 prelude::*,
30 };
31 use core::time::Duration;
32 use rand::random;
33 use 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)]
40 struct Player {
41 name: String,
42 }
43
44 // Each player also has a score. This component holds on to that score
45 #[derive(Component)]
46 struct 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)]
53 enum PlayerStreak {
54 Hot(usize),
55 None,
56 Cold(usize),
57 }
58
59 impl 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)]
74 struct 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)]
82 struct 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:
93 fn 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
100 fn 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.
109 fn 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.
149 fn 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 message, which
162 // tells our App to quit. Check out the "message.rs" example if you want to learn more about using
163 // messages.
164 fn game_over_system(
165 game_rules: Res<GameRules>,
166 game_state: Res<GameState>,
167 mut app_exit_writer: MessageWriter<AppExit>,
168 ) {
169 if let Some(ref player) = game_state.winning_player {
170 println!("{player} won the game!");
171 app_exit_writer.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_writer.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)
183 fn 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
219 fn 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.
244 fn 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.
276 fn 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)]
286 enum MySystems {
287 BeforeRound,
288 Round,
289 AfterRound,
290 }
291
292 // Our Bevy app's entry point
293 fn 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 (
334 MySystems::BeforeRound,
335 MySystems::Round,
336 MySystems::AfterRound,
337 )
338 .chain(),
339 )
340 // The add_systems function is powerful. You can define complex system configurations with ease!
341 .add_systems(
342 Update,
343 (
344 // These `BeforeRound` systems will run before `Round` systems, thanks to the chained set configuration
345 (
346 // You can also chain systems! new_round_system will run first, followed by new_player_system
347 (new_round_system, new_player_system).chain(),
348 exclusive_player_system,
349 )
350 // All of the systems in the tuple above will be added to this set
351 .in_set(MySystems::BeforeRound),
352 // This `Round` system will run after the `BeforeRound` systems thanks to the chained set configuration
353 score_system.in_set(MySystems::Round),
354 // These `AfterRound` systems will run after the `Round` systems thanks to the chained set configuration
355 (
356 score_check_system,
357 // In addition to chain(), you can also use `before(system)` and `after(system)`. This also works
358 // with sets!
359 game_over_system.after(score_check_system),
360 )
361 .in_set(MySystems::AfterRound),
362 ),
363 )
364 // This call to run() starts the app we just built!
365 .run();
366 }