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 | } |