Bevy Systems
Systems are rust functions that take special parameters that implement SystemParam
. We specify the system parameters we want and Bevy uses some type magic to figure out how to provide them.
Rust has no variadics, so this is achieved via macros that implement traits for functions with a number of supported arguments (in Bevy up to 16).
A rust function (or lambda) can be automatically converted to a System
via the IntoSystem
trait:
fn hello_world() {
println!("Hello, world!");
}
// This is usually done for you automatically,
// it is only here for illustration
let mut system = IntoSystem::into_system(hello_world);
Resources, commands, and queries, all describe ways of fetching data to and from the ECS. So your parameters are a declarative way of interacting with your game World
.
When your App
runs your systems, Bevy figures out the right way to call your function with your requested parameters.
All systems must return either an empty ()
or a Result
except for the special case of one-shot systems.
Scheduling systems
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.
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 schedule label 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 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)));
}
One thing to note is that just having parameters and 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.
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.
Ordering can be controlled by:
- Using core sets like
Update
,PostUpdate
,FixedUpdate
, etc.. - Calling the
.before(this_system)
or.after(that_system)
methods when adding them to your schedule - Adding them to a
SystemSet
, and then using.configure_set(ThisSet.before(ThatSet))
syntax to configure many systems at once - Through the use of chaining like
(system_a, system_b, system_c).chain()
System params and param sets
System params will automatically fetch data from a World
without interacting directly with the World
struct.
Systems take SystemParam
trait parameters. SystemParam
structs have two lifetimes:
'w
for data stored in theWorld
's
for 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_system_buffers call |
NonSend/NonSendMut | A shared 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 containing a last_run and this_run each holding a Tick 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 |
EventReader | An interface for reading events of a particular type |
EventWriter | An interface for writing events of a particular type |
&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 |
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 an event 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 paramters 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 a system and pass it as input to the next system.
As long as our next system has its first parameter as a type In<T>
it can be composed this way.
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),
data_pipe_system.pipe(info),
parse_message_system.pipe(debug),
warning_pipe_system.pipe(warn),
parse_error_message_system.pipe(error),
parse_message_system.pipe(ignore),
),
);
}
Exclusive systems
Usually we would schedule our commands with the Command
system parameter so they can be executed later in a system that has exclusive access to our World
.
However, if we use the World
system parameter then we can manipulate the state directly. For example we can run commands exactly when the system runs, instead of scheduling them 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 cannot be run in parallel if other systems also need to mutate the world. Otherwise they work exactly the same as your normal systems.