Bevy Systems
Systems are where we trigger side effects that change our game's state.
In Bevy, systems are simple rust functions with one rule: They must have all their parameters implement SystemParam.
Bevy uses some type magic to figure out how to provide these system parameters to our systems without us having to manually pass them in. This technique is a form of dependency injection.
A rust function (or lambda) will be automatically converted to a System via the IntoSystem trait which Bevy calls when you register a system to your App.
// Behold, a system!
fn hello_world() {
println!("Hello, world!");
}
fn turn_into_system() {
// An illustration of how Bevy turns your function into a system
let mut system = IntoSystem::into_system(hello_world);
}
These systems can then be scheduled to run in our game loop.
Scheduling systems
A Schedule is a graph of systems, their dependencies and other schedules.
When systems are added to our App they are added to a particular Schedule. These schedules contain the rules of when each system should run over the course of each frame.
Each frame of our application Bevy runs systems in our schedules, but not every schedule runs every frame.
By default, Bevy is trying to schedule all systems that don't need mutable access to the same data to run in parallel. This is all in an effort to speed up our game.
To schedule a system we call add_systems and specify the ScheduleLabel and the system(s) we want to run:
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, hello_world);
}
Each call to add_systems can also take a tuple of systems:
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, (defend, attack));
}
Or, if we need fine grain control over ordering, we can use Bevy's built-in methods like before and after:
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, (defend, attack.after(defend)));
}
Or we can use chain to run a group of systems one after another:
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// Same outcome as the previous example, but more concise.
.add_systems(Update, (defend, attack).chain());
}
One thing to note is that having parameters and then using them conditionally won't block parallel execution:
fn system_a(mut commands: Commands) {
if random_bool() {
commands.spawn_empty();
}
}
fn system_b(mut commands: Commands) {
if random_bool() {
commands.spawn_empty();
}
}
There is a chance these two systems run in parallel. The overhead of the mutable borrow depends on whether or not we call it.
Fallibility
If your system returns a Result then Bevy considers it to be failable. This will cause Bevy to handle your error through a fallback error handler.
fn failable_system() -> Result<()> {
println!("Running failable system...");
Ok(())
}
Every error carries a Severity that tells Bevy how it should react. Errors with no explicit severity default to Severity::Panic, and the default fallback handler match_severity uses that severity to decide whether to panic, log, or ignore the error.
We can replace the fallback error handler for our whole app:
use bevy::ecs::error::warn;
App::new()
.set_error_handler(warn)
.add_plugins(DefaultPlugins)
.run();
This can only be called once, and panics if it is called again. Alternatively we can insert the FallbackErrorHandler resource into a World directly:
use bevy::ecs::error::{warn, FallbackErrorHandler};
world.insert_resource(FallbackErrorHandler(warn));
There are a bunch of built-in error-handlers under the bevy::ecs::error module:
match_severity: the default handler, defers to the error'sSeveritypanic: panics with the system errorerror: logs the system error at the error levelwarn: logs the system error at the warn levelinfo: logs the system error at the info leveldebug: logs the system error at the debug leveltrace: logs the system error at the trace levelignore: ignores the system error
Individual errors can be annotated with a Severity so that the default match_severity handler reacts accordingly:
use bevy::ecs::error::{ResultSeverityExt, Severity};
fn parse() -> Result<usize> {
let value: usize = "not a number".parse().with_severity(Severity::Warning)?;
Ok(value)
}
Fallible system parameters
Even if you do not return a Result from your system, there are certain parameters that your system can fail during a SystemParam::get_param call which will cause the system to be skipped or trigger the fallback error handler.
| System parameter | Behavior |
|---|---|
Res<R>, ResMut<R> | Resource has to exist, and the fallback error handler will be called if it doesn't. |
Single<D, F> | There must be exactly one matching entity, but the system will be silently skipped otherwise. |
Option<Single<D, F>> | There must be zero or one matching entity. The system will be silently skipped if there are more. |
Populated<D, F> | There must be at least one matching entity, but the system will be silently skipped otherwise. |
For example if we were to query for a component with Single that does not yet exist:
fn find_friends(friend: Single<&Ally>) {
// Uh oh, we don't have any friends yet.
}
The find_friends system would be skipped because the system parameter did not validate.
If we wanted the system to always run and decide ourselves what to do in each case then you can wrap Single in an Option:
fn find_friends_or_not(friend: Option<Single<&Ally>>) {
if let Some(ally) = friend {
info!("Yay we have a friend");
}
}
In this case the system would always run.
Any system parameter can be made optional like this, not just Single. Some parameters, like Res, will call the fallback error handler when they fail.
If we would rather skip the system entirely in that case we can wrap the parameter in If:
fn count_friends(friends: If<Res<Friends>>) {
// The system is skipped instead of erroring if the resource is missing.
info!("We have {} friends", friends.0);
}
The count_friends system only runs when the Friends resource exists, and is silently skipped otherwise.
Ordering
By default systems run in parallel with each other and their order is non-deterministic.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, (defend, attack));
}
In this example it is not possible to tell whether defend is going to run after, before or in parallel with attack.
Normal systems cannot safely access the World instance directly because it would block everyone else. Our World contains all of our components, so mutating arbitrary parts of it in parallel is not thread safe.
To control the order of our systems Bevy lets us set up our own system sets. System sets let you treat a group of systems as a single thing. Each system you define can be a part of any other system set.
Bevy gives us a convenient syntax to make small sets of systems with before, after, and chain.
Before and after
We can schedule systems to run before and after each other.
fn run_with_before_and_after() {
App::new().add_plugins(DefaultPlugins).add_systems(
Update,
(defend.before(end_turn), attack.after(defend), end_turn),
);
}
Something to be aware of is that each system must be added separately to the schedule. Even though we wrote defend.before(end_turn) it does not mean that Bevy will add end_turn. We have to also add that system ourselves.
Chaining
It's often more convenient to use chaining which removes the need to ensure you add all the systems separately:
fn run_with_chain() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, (defend, attack, end_turn).chain());
}
This will run each system in the set one after another in the order we define them. So this will run defend then attack then end_turn.
Custom system sets
We can make our own system sets by deriving them with the SystemSet macro:
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
struct EconomySet;
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
struct PhysicsSet;
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
enum InputSet {
Touch,
Mouse,
Gamepad,
}
Then we can order these larger sets at the top level of our app:
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(
Update,
(defend, attack, end_turn).chain().in_set(EconomySet),
)
.configure_sets(
Update,
(
EconomySet,
PhysicsSet,
InputSet::Touch,
InputSet::Mouse,
InputSet::Gamepad,
)
.chain(),
);
}
System params and param sets
System params will automatically fetch data from a World. They can only take parameters that implement SystemParam.
SystemParam structs have two lifetimes:
'wfor data stored in theWorld'sfor data stored in a parameter's state
Some common SystemParam are:
| System parameter | Description |
|---|---|
Res | A reference to a resource |
ResMut | A mutable reference to a resource |
Local | A local system variable that persists between invocations of the system |
Deferred | A param that stores a buffer which gets applied to a World during an apply_deferred call |
NonSend/NonSendMut | A shared or mutable borrow of a non Send resource, systems taking these are forced onto the main thread to avoid sending these resources between threads |
SystemChangeTick | Reads the previous and current change ticks through last_run() and this_run(), which can be used to check the time the system has been run at. |
Query | A query for resources, components or entities |
Commands | The main interface for scheduling commands to run |
MessageReader | An interface for reading messages of a particular type |
PopulatedMessageReader | Like MessageReader, but skips the system when there are no new messages |
MessageWriter | An interface for writing messages of a particular type |
MessageMutator | An interface for reading and mutating messages of a particular type in place |
&World | A reference to the current World |
&Archetypes | Metadata about archetypes |
&Bundles | Metadata about bundles |
&Components | Metadata about components |
&Entities | Metadata about entities |
RemovedComponents<T> | Yields entities that had a component of type T removed |
Unfortunately, Rust does not yet have variadics, so functions are limited to a maximum number of up to 16 function arguments.
A notable system parameter is the ParamSet which is a collection of potentially conflicting SystemParam's.
It allows systems to safely access and interact with up to 8 mutually exclusive params. For example: two queries that reference the same mutable data or a message reader and writer of the same type.
We can access the params of a ParamSet with p0, p1, etc according to the order they were defined in the type. A ParamSet can take any SystemParam.
ParamSet can be used when mutably accessing the same component twice in one system:
// This will panic at runtime when the system gets initialized.
fn bad_system(
mut enemies: Query<&mut Health, With<Enemy>>,
mut allies: Query<&mut Health, With<Ally>>,
) {
// ...
}
Instead ParamSet leverages the borrow checker to ensure that only one of the contained parameters are accessed at a given time.
fn good_system(
mut set: ParamSet<(
Query<&mut Health, With<Enemy>>,
Query<&mut Health, With<Ally>>,
)>,
) {
// This will access the first `SystemParam`.
for mut health in set.p0().iter_mut() {
// Do your fancy stuff here...
}
// The second `SystemParam`.
// This would fail to compile if the previous parameter was still borrowed.
for mut health in set.p1().iter_mut() {
// Do even fancier stuff here...
}
}
Custom system parameters
We can create our own system parameters by deriving the trait on a struct. The only thing we have to be careful of is the two lifetimes mentioned earlier.
// The [`SystemParam`] struct can contain any types that can also be included in
// a system function signature.
//
// In this example, it includes a query and a mutable resource.
#[derive(SystemParam)]
struct PlayerCounter<'w, 's> {
players: Query<'w, 's, &'static Player>,
count: ResMut<'w, PlayerCount>,
}
impl PlayerCounter<'_, '_> {
fn count(&mut self) {
self.count.0 = self.players.iter().len();
}
}
// The [`SystemParam`] can be used directly in a system argument.
fn count_players(mut counter: PlayerCounter) {
counter.count();
println!("{} players in the game", counter.count.0);
}
This can be very useful for reducing the number of parameters you need to pass your systems and to get around the 16 parameter limit due to the way Bevy uses macros to turn functions into system parameters.
System state
Normally your systems are stateless, but they can be locally stateful by using the Local<T> system parameter:
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!();
}
The local counter variable will keep its state between invocations of the function.
Combining systems
Higher order systems can be composed of many other systems using the pipe method. This function will take the output of the system and pass it as input to the next system.
The next system can accept that output through a first parameter of type In<T>, where T is the previous system's return type. If the previous system returns (), the next system does not need an input parameter at all.
This can be used in combination with ParamSet to avoid SystemParam collisions.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, (parse_message_system.pipe(handler_system)));
}
Exclusive systems
Usually we would schedule our commands with the Commands system parameter so they can be executed later at a sync point, in a system that has exclusive access to our World.
However, if we use the &mut World system parameter then we can manipulate the world directly. For example we can spawn entities exactly when the system runs, instead of scheduling commands to run later:
fn spawn_immediately(world: &mut World) {
world.spawn(Player);
}
Systems with this parameter are called exclusive systems.
The downside of exclusive systems is that they need exclusive access to the entire World, so they cannot run in parallel with any other system. Otherwise they work exactly the same as your normal systems.
Removing systems from a set
Normally the only way to prevent a system from running is to use a run condition. This has the overhead of running that condition every loop.
An alternative is to remove the systems from the schedule. This triggers an expensive, one-time schedule rebuild, but removes the per-frame overhead entirely.
fn plugin(app: &mut App) {
app.add_systems(Update, (system_a, (system_b, system_c).in_set(MySet)));
// Could also be done through a schedule
//
// schedule.remove_systems_in_set(my_system,
// ScheduleCleanupPolicy::RemoveSystemsOnly);
app
.remove_systems_in_set(
Update,
MySet,
ScheduleCleanupPolicy::RemoveSetAndSystems,
)
.unwrap();
}