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