forked from bevyengine/bevy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ecs_guide.rs
339 lines (310 loc) · 14 KB
/
ecs_guide.rs
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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
use bevy::{
app::{AppExit, ScheduleRunnerPlugin, ScheduleRunnerSettings},
ecs::schedule::ReportExecutionOrderAmbiguities,
prelude::*,
utils::Duration,
};
use rand::random;
/// 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.
//
// 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
struct Player {
name: String,
}
// Each player also has a score. This component holds on to that score
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(Default)]
struct GameState {
current_round: usize,
total_players: usize,
winning_player: Option<String>,
}
// This resource provides rules for our "game".
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 query.iter_mut() {
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.iter() {
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!("{} won the game!", player);
app_exit_events.send(AppExit);
} else if game_state.current_round == game_rules.max_rounds {
println!("Ran out of rounds. Nobody wins!");
app_exit_events.send(AppExit);
}
println!();
}
// 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_startup_system(startup_system)
// Normal: app.add_system(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 a "thread local system".
// These run on the main app thread (hence the name "thread local")
// WARNING: These will block all parallel execution of other systems until they finish, so they should generally be avoided if you
// care about performance
#[allow(dead_code)]
fn thread_local_system(world: &mut World) {
// this does the same thing as "new_player_system"
let total_players = world.get_resource_mut::<GameState>().unwrap().total_players;
let should_add_player = {
let game_rules = world.get_resource::<GameRules>().unwrap();
let add_new_player = random::<bool>();
add_new_player && total_players < game_rules.max_players
};
// Randomly add a new player
if should_add_player {
world.spawn().insert_bundle((
Player {
name: format!("Player {}", total_players),
},
Score { value: 0 },
));
let mut game_state = world.get_resource_mut::<GameState>().unwrap();
game_state.total_players += 1;
}
}
// Sometimes systems need their own unique "local" state. Bevy's ECS provides Local<T> resources for this case.
// Local<T> resources are unique to their system and are automatically initialized on your behalf (if they don't already exist).
// If you have a system's id, you can also access local resources directly in the Resources collection using `Resources::get_local()`.
// In general you should only need this feature in the following cases:
// 1. You have multiple instances of the same system and they each need their own unique state
// 2. You already have a global version of a resource that you don't want to overwrite for your current system
// 3. You are too lazy to register the system's resource as a global resource
#[derive(Default)]
struct State {
counter: usize,
}
// NOTE: this doesn't do anything relevant to our game, it is just here for illustrative purposes
#[allow(dead_code)]
fn local_state_system(mut state: Local<State>, query: Query<(&Player, &Score)>) {
for (player, score) in query.iter() {
println!("processed: {} {}", player.name, score.value);
}
println!("this system ran {} times", state.counter);
state.counter += 1;
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)]
enum MyStage {
BeforeRound,
AfterRound,
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
enum MyLabels {
ScoreCheck,
}
// 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::build()
// Resources can be added to our app like this
.insert_resource(State { counter: 0 })
// Some systems are configured by adding their settings as a resource
.insert_resource(ScheduleRunnerSettings::run_loop(Duration::from_secs(5)))
// 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 (configured above).
.add_plugin(ScheduleRunnerPlugin::default())
// Resources that implement the Default or FromResources trait can be added like this:
.init_resource::<GameState>()
// Startup systems run exactly once BEFORE all other systems. These are generally used for
// app initialization code (ex: adding entities and resources)
.add_startup_system(startup_system.system())
// my_system calls converts normal rust functions into ECS systems:
.add_system(print_message_system.system())
//
// SYSTEM EXECUTION ORDER
//
// Each system belongs to a `Stage`, which controls the execution strategy and broad order of the systems within each tick.
// Startup stages (which startup systems are registered in) will always complete before ordinary stages begin,
// and every system in a stage must complete before the next stage advances.
// Once every stage has concluded, the main loop is complete and begins again.
//
// By default, all systems 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.
//
// Rather than splitting each of your systems into separate stages, you should force an explicit ordering between them
// by giving the relevant systems a label with `.label`, then 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.
//
// Doing that will, in just about all cases, lead to better performance compared to
// splitting systems between stages, because it gives the scheduling algorithm more
// opportunities to run systems in parallel.
// Stages are still necessary, however: end of a stage is a hard sync point
// (meaning, no systems are running) where `Commands` issued by systems are processed.
// This is required because commands can perform operations that are incompatible with
// having systems in flight, such as spawning or deleting entities,
// adding or removing resources, etc.
//
// add_system(system) adds systems to the UPDATE stage by default
// However we can manually specify the stage if we want to. The following is equivalent to add_system(score_system)
.add_system_to_stage(CoreStage::Update, score_system.system())
// We can also create new stages. Here is what our games stage order will look like:
// "before_round": new_player_system, new_round_system
// "update": print_message_system, score_system
// "after_round": score_check_system, game_over_system
.add_stage_before(
CoreStage::Update,
MyStage::BeforeRound,
SystemStage::parallel(),
)
.add_stage_after(
CoreStage::Update,
MyStage::AfterRound,
SystemStage::parallel(),
)
.add_system_to_stage(MyStage::BeforeRound, new_round_system.system())
.add_system_to_stage(MyStage::BeforeRound, new_player_system.system())
// We can ensure that game_over system runs after score_check_system using explicit ordering constraints
// First, we label the system we want to refer to using `.label`
// Then, we use either `.before` or `.after` to describe the order we want the relationship
.add_system_to_stage(
MyStage::AfterRound,
score_check_system.system().label(MyLabels::ScoreCheck),
)
.add_system_to_stage(
MyStage::AfterRound,
game_over_system.system().after(MyLabels::ScoreCheck),
)
// We can check our systems for execution order ambiguities by examining the output produced in the console
// by adding the following Resource to our App :)
// Be aware that not everything reported by this checker is a potential problem, you'll have to make
// that judgement yourself.
.insert_resource(ReportExecutionOrderAmbiguities)
// This call to run() starts the app we just built!
.run();
}