
# Objective Fixes a part of #14274. Bevy has an incredibly inconsistent naming convention for its system sets, both internally and across the ecosystem. <img alt="System sets in Bevy" src="https://github.com/user-attachments/assets/d16e2027-793f-4ba4-9cc9-e780b14a5a1b" width="450" /> *Names of public system set types in Bevy* Most Bevy types use a naming of `FooSystem` or just `Foo`, but there are also a few `FooSystems` and `FooSet` types. In ecosystem crates on the other hand, `FooSet` is perhaps the most commonly used name in general. Conventions being so wildly inconsistent can make it harder for users to pick names for their own types, to search for system sets on docs.rs, or to even discern which types *are* system sets. To reign in the inconsistency a bit and help unify the ecosystem, it would be good to establish a common recommended naming convention for system sets in Bevy itself, similar to how plugins are commonly suffixed with `Plugin` (ex: `TimePlugin`). By adopting a consistent naming convention in first-party Bevy, we can softly nudge ecosystem crates to follow suit (for types where it makes sense to do so). Choosing a naming convention is also relevant now, as the [`bevy_cli` recently adopted lints](https://github.com/TheBevyFlock/bevy_cli/pull/345) to enforce naming for plugins and system sets, and the recommended naming used for system sets is still a bit open. ## Which Name To Use? Now the contentious part: what naming convention should we actually adopt? This was discussed on the Bevy Discord at the end of last year, starting [here](<https://discord.com/channels/691052431525675048/692572690833473578/1310659954683936789>). `FooSet` and `FooSystems` were the clear favorites, with `FooSet` very narrowly winning an unofficial poll. However, it seems to me like the consensus was broadly moving towards `FooSystems` at the end and after the poll, with Cart ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311140204974706708)) and later Alice ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311092530732859533)) and also me being in favor of it. Let's do a quick pros and cons list! Of course these are just what I thought of, so take it with a grain of salt. `FooSet`: - Pro: Nice and short! - Pro: Used by many ecosystem crates. - Pro: The `Set` suffix comes directly from the trait name `SystemSet`. - Pro: Pairs nicely with existing APIs like `in_set` and `configure_sets`. - Con: `Set` by itself doesn't actually indicate that it's related to systems *at all*, apart from the implemented trait. A set of what? - Con: Is `FooSet` a set of `Foo`s or a system set related to `Foo`? Ex: `ContactSet`, `MeshSet`, `EnemySet`... `FooSystems`: - Pro: Very clearly indicates that the type represents a collection of systems. The actual core concept, system(s), is in the name. - Pro: Parallels nicely with `FooPlugins` for plugin groups. - Pro: Low risk of conflicts with other names or misunderstandings about what the type is. - Pro: In most cases, reads *very* nicely and clearly. Ex: `PhysicsSystems` and `AnimationSystems` as opposed to `PhysicsSet` and `AnimationSet`. - Pro: Easy to search for on docs.rs. - Con: Usually results in longer names. - Con: Not yet as widely used. Really the big problem with `FooSet` is that it doesn't actually describe what it is. It describes what *kind of thing* it is (a set of something), but not *what it is a set of*, unless you know the type or check its docs or implemented traits. `FooSystems` on the other hand is much more self-descriptive in this regard, at the cost of being a bit longer to type. Ultimately, in some ways it comes down to preference and how you think of system sets. Personally, I was originally in favor of `FooSet`, but have been increasingly on the side of `FooSystems`, especially after seeing what the new names would actually look like in Avian and now Bevy. I prefer it because it usually reads better, is much more clearly related to groups of systems than `FooSet`, and overall *feels* more correct and natural to me in the long term. For these reasons, and because Alice and Cart also seemed to share a preference for it when it was previously being discussed, I propose that we adopt a `FooSystems` naming convention where applicable. ## Solution Rename Bevy's system set types to use a consistent `FooSet` naming where applicable. - `AccessibilitySystem` → `AccessibilitySystems` - `GizmoRenderSystem` → `GizmoRenderSystems` - `PickSet` → `PickingSystems` - `RunFixedMainLoopSystem` → `RunFixedMainLoopSystems` - `TransformSystem` → `TransformSystems` - `RemoteSet` → `RemoteSystems` - `RenderSet` → `RenderSystems` - `SpriteSystem` → `SpriteSystems` - `StateTransitionSteps` → `StateTransitionSystems` - `RenderUiSystem` → `RenderUiSystems` - `UiSystem` → `UiSystems` - `Animation` → `AnimationSystems` - `AssetEvents` → `AssetEventSystems` - `TrackAssets` → `AssetTrackingSystems` - `UpdateGizmoMeshes` → `GizmoMeshSystems` - `InputSystem` → `InputSystems` - `InputFocusSet` → `InputFocusSystems` - `ExtractMaterialsSet` → `MaterialExtractionSystems` - `ExtractMeshesSet` → `MeshExtractionSystems` - `RumbleSystem` → `RumbleSystems` - `CameraUpdateSystem` → `CameraUpdateSystems` - `ExtractAssetsSet` → `AssetExtractionSystems` - `Update2dText` → `Text2dUpdateSystems` - `TimeSystem` → `TimeSystems` - `AudioPlaySet` → `AudioPlaybackSystems` - `SendEvents` → `EventSenderSystems` - `EventUpdates` → `EventUpdateSystems` A lot of the names got slightly longer, but they are also a lot more consistent, and in my opinion the majority of them read much better. For a few of the names I took the liberty of rewording things a bit; definitely open to any further naming improvements. There are still also cases where the `FooSystems` naming doesn't really make sense, and those I left alone. This primarily includes system sets like `Interned<dyn SystemSet>`, `EnterSchedules<S>`, `ExitSchedules<S>`, or `TransitionSchedules<S>`, where the type has some special purpose and semantics. ## Todo - [x] Should I keep all the old names as deprecated type aliases? I can do this, but to avoid wasting work I'd prefer to first reach consensus on whether these renames are even desired. - [x] Migration guide - [x] Release notes
367 lines
14 KiB
Rust
367 lines
14 KiB
Rust
//! 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::*,
|
|
};
|
|
use core::time::Duration;
|
|
use rand::random;
|
|
use std::fmt;
|
|
|
|
// 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,
|
|
}
|
|
|
|
// Enums can also be used as components.
|
|
// This component tracks how many consecutive rounds a player has/hasn't scored in.
|
|
#[derive(Component)]
|
|
enum PlayerStreak {
|
|
Hot(usize),
|
|
None,
|
|
Cold(usize),
|
|
}
|
|
|
|
impl fmt::Display for PlayerStreak {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
PlayerStreak::Hot(n) => write!(f, "{n} round hot streak"),
|
|
PlayerStreak::None => write!(f, "0 round streak"),
|
|
PlayerStreak::Cold(n) => write!(f, "{n} round cold streak"),
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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`, `Score` and `PlayerStreak` components.
|
|
fn score_system(mut query: Query<(&Player, &mut Score, &mut PlayerStreak)>) {
|
|
for (player, mut score, mut streak) in &mut query {
|
|
let scored_a_point = random::<bool>();
|
|
if scored_a_point {
|
|
// Accessing components immutably is done via a regular reference - `player`
|
|
// has type `&Player`.
|
|
//
|
|
// Accessing components mutably is performed via type `Mut<T>` - `score`
|
|
// has type `Mut<Score>` and `streak` has type `Mut<PlayerStreak>`.
|
|
//
|
|
// `Mut<T>` implements `Deref<T>`, so struct fields can be updated using
|
|
// standard field update syntax ...
|
|
score.value += 1;
|
|
// ... and matching against enums requires dereferencing them
|
|
*streak = match *streak {
|
|
PlayerStreak::Hot(n) => PlayerStreak::Hot(n + 1),
|
|
PlayerStreak::Cold(_) | PlayerStreak::None => PlayerStreak::Hot(1),
|
|
};
|
|
println!(
|
|
"{} scored a point! Their score is: {} ({})",
|
|
player.name, score.value, *streak
|
|
);
|
|
} else {
|
|
*streak = match *streak {
|
|
PlayerStreak::Hot(_) | PlayerStreak::None => PlayerStreak::Cold(1),
|
|
PlayerStreak::Cold(n) => PlayerStreak::Cold(n + 1),
|
|
};
|
|
|
|
println!(
|
|
"{} did not score a point! Their score is: {} ({})",
|
|
player.name, score.value, *streak
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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.write(AppExit::Success);
|
|
} else if game_state.current_round == game_rules.max_rounds {
|
|
println!("Ran out of rounds. Nobody wins!");
|
|
app_exit_events.write(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 },
|
|
PlayerStreak::None,
|
|
),
|
|
(
|
|
Player {
|
|
name: "Bob".to_string(),
|
|
},
|
|
Score { value: 0 },
|
|
PlayerStreak::None,
|
|
),
|
|
]);
|
|
|
|
// 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 },
|
|
PlayerStreak::None,
|
|
));
|
|
|
|
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.
|
|
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 },
|
|
PlayerStreak::None,
|
|
));
|
|
|
|
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.
|
|
// 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 MySystems {
|
|
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
|
|
(
|
|
MySystems::BeforeRound,
|
|
MySystems::Round,
|
|
MySystems::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(MySystems::BeforeRound),
|
|
// This `Round` system will run after the `BeforeRound` systems thanks to the chained set configuration
|
|
score_system.in_set(MySystems::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(MySystems::AfterRound),
|
|
),
|
|
)
|
|
// This call to run() starts the app we just built!
|
|
.run();
|
|
}
|