Bevy systems are often quoted as being “simple rust functions”. This magic is achieved via lots and lots of type magic. Basically, implementing traits over functions.
Rust has no variadics, so this is achieved via macros that generate separate impl code for functions with a number of supported arguments (in bevy up to 15). And then each of the arguments also implements traits.
A system is usually written as a normal function (or lambda), which is
automatically converted into a System
. Here is an example of doing it
manually using IntoSystem
on our function:
fn sys(query: Query<&A>) {
for a in &query {
println!("{a:?}");
}
}
let mut system = IntoSystem::into_system(sys);
The system can then be called on a particular World
. Resources, Commands,
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 World
runs your system it automatically figures out the right way to
call your function given your specific parameters by turning your Fn
into a
System
.
let mut world = World::new();
world.spawn(A);
system.initialize(&mut world);
system.run((), &mut world);
Or we can set our system to run with a Schedule
on many World
’s:
let mut schedule = Schedule::default();
schedule.add_systems(system);;
schedule.run(world);
System functions can have parameters, through which one can query and mutate Bevy ECS state.
Bevy uses function parameter types to determine data that needs to be sent to the system. It also uses data access information to determine which systems run in parallel.
We can see how it does so by manually creating our SystemState
:
// Work directly on the `World`
let mut world = World::new();
world.init_resource::<Events<MyEvent>>();
// Construct a `SystemState` struct, passing in a tuple of `SystemParam`
// as if you were writing an ordinary system.
let mut system_state: SystemState<(
EventWriter<MyEvent>,
Option<ResMut<MyResource>>,
Query<&MyComponent>,
)> = SystemState::new(&mut world);
// Use system_state.get_mut(&mut world) and unpack your system parameters into variables!
// system_state.get(&world) provides read-only versions of your system parameters instead.
let (event_writer, maybe_resource, query) = system_state.get_mut(&mut world);
// If you are using [`Commands`], you can choose when you want to apply them to the world.
// You need to manually call `.apply(world)` on the [`SystemState`] to apply them.
We can conveniently add systems to our app definition:
App::new()
.add_systems(Update, (
(attack, defend).in_set(Combat).before(check_health)
check_health,
(handle_death, respawn).after(check_health)
));
Instead of a nested tuple with before
and after
we can use chain
to
flatten our structure and run everything in the defined order:
App::new()
.add_systems(
Update,
(
(attack, defend).in_set(Combat)
check_health,
(handle_death, respawn)
).chain();
)
Before 0.11, there was
6 ways
to initialize our systems which caused a lot of
confusion. But its since been condensed into the nice ergonomics of a single
add_systems
call.
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.
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.
Systems are added to a Schedule
which are then run and data is fetched from
the World
automatically according to the system’s SystemParam
’s.
Ordering can be controlled with:
- The core sets like
Update
,PostUpdate
,FixedUpdate
, etc.. - by calling the
.before(this_system)
or.after(that_system)
methods when adding them to your schedule - by adding them to a
SystemSet
, and then using.configure_set(ThisSet.before(ThatSet))
syntax to configure many systems at once - through the use of
.add_systems(Update, (system_a, system_b, system_c).chain())
- by calling
.in_schedule
- by calling
.on_startup
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 the World
and 's
for data stored in a
parameter’s state.
Some common SystemParam
are:
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 or components |
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 |
A ParamSet
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...
}
}
You can create your own system parameters:
// 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<'w, 's> PlayerCounter<'w, 's> {
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);
}
System state
They can be stateful by using the Local<T>
as a type for an argument.
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!();
}
Combining systems
Higher order systems can even be composed of many other systems using
the pipe
method:
Should be used in combination with ParamSet
to avoid SystemParam
collisions.
.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),
)
)