Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ecs/ecs_guide.rs
6592 views
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
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 event, which
162
// tells our App to quit. Check out the "event.rs" example if you want to learn more about using
163
// events.
164
fn 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)
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
}
367
368