Module bevy_cobweb::react
source · Expand description
§Reactivity Primitives
Reactivity is built on system commands, system events, a core reactivity API, and a custom scheduling algorithm.
§System Commands
All reactors are SystemCommands
.
§Spawning Systems
Systems can be spawned as SystemCommands
with Commands::spawn_system_command
. System commands are similar to Bevy one-shot systems, however the actual system is wrapped in a closure that takes World
and a SystemCommandCleanup
as input. See Scheduling for more details.
Example:
let syscommand = commands.spawn_system_command(
|weebles: Res<Weebles>|
{
println!("there are {} weebles", weebles.num());
}
);
§Running System Commands
A SystemCommand
can be manually run by scheduling it as a Bevy Command
. Scheduling a system command will cause a reaction tree to run (see Scheduling).
commands.add(syscommand);
§System Events
You can send data directly to a system spawned as a SystemCommand
by sending it a system event.
For example, using the SystemEvent
system parameter to consume the event data:
let syscommand = commands.spawn_system_command(
|mut data: SystemEvent<Vec<u32>>|
{
let Some(data) = data.take() else { return; };
for val in data
{
println!("recieved {}", val);
}
}
);
commands.send_system_event(syscommand, vec![0, 18, 42]);
Sending a system event will cause a reaction tree to run (see Scheduling).
§Reactivity API
ECS reactivity is only implemented for ReactResource
resources and ReactComponent
components, which are accessed with ReactRes
/ReactResMut
system parameters and the React<C>
component wrapper (or Reactive<C>
/ReactiveMut<C>
system parameters) respectively.
We use ReactResource
/ReactComponent
instead of Bevy change detection in order to achieve precise, responsive, recursive reactions with an ergonomic API. When Bevy implements observers, we expect the ‘extra’ API layer to be reduced or eliminated.
A reactor will run in the first apply_deferred
after its reaction trigger is detected. If a reactor triggers other reactors, they will run immediately after the initial reactor in a telescoping fashion until the entire tree of reactions terminates. Recursive reactions are fully supported. For more details see Scheduling.
§Registering Reactors
Reactors are registered with ReactCommands
, which are obtained from Commands::react
. You must specify a ‘reaction trigger’:
fn setup(mut c: Commands)
{
c.react().on(resource_mutation::<A>(),
|a: ReactRes<A>|
{
//...
}
);
}
The available reaction triggers are:
resource_mutation<R: ReactResource>
insertion<C: ReactComponent>
mutation<C: ReactComponent>
removal<C: ReactComponent>
entity_insertion<C: ReactComponent>
entity_mutation<C: ReactComponent>
entity_removal<C: ReactComponent>
despawn
broadcast<E>
entity_event<E>
any_entity_event<E>
A reactor can be associated with multiple reaction triggers:
fn setup(mut c: Commands)
{
c.react().on((resource_mutation::<A>(), entity_insertion::<B>(entity)),
move |a: ReactRes<A>, q: Query<&React<B>>|
{
q.get(entity);
//...etc.
}
);
}
§Revoking Reactors
Reactors can be revoked with RevokeTokens
obtained on registration.
let token = c.react().on_revokable(resource_mutation::<A>(), || { todo!(); });
c.react().revoke(token);
§Trigger Type: Resource Mutation
Add a reactive resource to your app:
#[derive(ReactResource, Default)]
struct Counter(u32);
app.add_plugins(ReactPlugin)
.init_react_resource::<Counter>();
Mutate the resource:
fn increment(mut c: Commands, mut counter: ReactResMut<Counter>)
{
counter.get_mut(&mut c).0 += 1;
}
React to the resource mutation:
fn setup(mut c: Commands)
{
c.react().on(resource_mutation::<Counter>(),
|counter: ReactRes<Counter>|
{
println!("count: {}", counter.0);
}
);
}
§Trigger Type: Component Insertion/Mutation/Removal
A reactor can listen to component insertion/mutation/removal on any entity or a specific entity. In either case, the reactor can read which entity the event occurred on with the InsertionEvent
, MutationEvent
, and RemovalEvent
system parameters.
#[derive(ReactComponent)]
struct Health(u16);
fn setup(mut c: Commands)
{
// On any entity.
c.react().on(insertion::<Health>(),
|event: InsertionEvent<Health>, q: Query<&React<Health>>|
{
let Some(entity) = event.read() else { return; };
let health = q.get(entity).unwrap();
println!("new health: {}", health.0);
}
);
// On a specific entity.
let entity = c.spawn_empty().id();
c.react().on(entity_mutation::<Health>(entity),
|event: InsertionEvent<Health>, q: Query<&React<Health>>|
{
let Some(entity) = event.read() else { return; };
let health = q.get(entity).unwrap();
println!("updated health: {}", health.0);
}
);
// Trigger the insertion reactors.
c.react().insert(entity, Health(0u16));
}
fn add_health(mut c: Commands, mut q: Query<&mut React<Health>>)
{
for health in q.iter_mut()
{
health.get_mut(&mut c).0 += 10;
}
}
§Trigger Type: Despawns
React to a despawn, using the DespawnEvent
system parameter to read which entity was despawned:
c.react().on(despawn(entity),
|entity: DespawnEvent|
{
println!("entity despawned: {}", entity.read().unwrap());
}
);
§Trigger Type: Broadcast Events
Send a broadcast:
c.react().broadcast(0u32);
React to the event, using the BroadcastEvent
system parameter to access event data:
c.react().on(broadcast::<u32>(),
|event: BroadcastEvent<u32>|
{
if let Some(event) = event.read()
{
println!("broadcast: {}", event);
}
}
);
§Trigger Type: Entity Events
Entity events can be considered ‘scoped broadcasts’, sent only to systems listening to the target entity. If the target entity is despawned, then entity events targeting it will be dropped.
Send an entity event:
c.react().entity_event(entity, 0u32);
React to the event, using the EntityEvent
system parameter to access event data:
c.react().on(entity_event::<u32>(entity),
|event: EntityEvent<u32>|
{
if let Some((entity, event)) = event.read()
{
println!("entity: {:?}, event: {}", entity, event);
}
}
);
§One-off Reactors
If you only want a reactor to run once, use ReactCommands::once
:
let entity = c.spawn(Player);
c.react().once(broadcast::<ResetEverything>(),
move |world: &mut World|
{
world.despawn(entity);
}
);
§Reactor Cleanup
Reactors are stateful boxed Bevy systems, so it is useful to manage their memory use. We control reactor lifetimes with ReactorMode
, which has three settings. You can manually specify the mode using ReactCommands::with
.
ReactorMode::Persistent
: The reactor will never be cleaned up even if it has no triggers. This is the most efficient mode because there is no need to allocate a despawn counter or revoke token.- See
ReactCommands::on_persistent
, which returns aSystemCommand
.
- See
ReactorMode::Cleanup
: The reactor will be cleaned up if it has no triggers, including if it started withdespawn
triggers and all despawns have fired.- See
ReactCommands::on
.
- See
ReactorMode::Revokable
: The reactor will be cleaned up if it has no triggers, including if it starts withdespawn
triggers and all despawns have fired. Otherwise, you can revoke it manually with itsRevokeToken
.- See
ReactCommands::on_revokable
, which returns aRevokeToken
.
- See
§World Reactors
Special WorldReactors
can be registered with apps and accessed with the Reactor<T: WorldReactor>
system parameter. World reactors are similar to Bevy systems in that they live for the entire lifetime of an app.
The advantage of world reactors over normal reactors is you can easily add/remove triggers from them anywhere in your app. You can also easily run them manually from anywhere in your app. They also only need to be allocated once, as opposed to normal reactors that must be boxed every time you register one (and then their internal system state needs to be initialized).
Define a WorldReactor
:
#[derive(ReactComponent)]
struct A;
struct DemoReactor;
impl WorldReactor for DemoReactor
{
type StartingTriggers = InsertionTrigger<A>;
type Triggers = EntityMutationTrigger<A>;
fn reactor(self) -> SystemCommandCallback
{
SystemCommandCallback::new(
|insertion: InsertionEvent<A>, mutation: MutationEvent<A>|
{
if let Some(_) = insertion.read()
{
println!("A was inserted on an entity");
}
if let Some(_) = mutation.read()
{
println!("A was mutated on an entity");
}
}
)
}
}
Add the reactor to your app:
fn setup(app: &mut App)
{
app.add_reactor_with(DemoReactor, mutation::<A>());
}
Add a trigger to the reactor:
fn spawn_a(mut c: Commands, mut reactor: Reactor<DemoReactor>)
{
let entity = c.spawn_empty().id();
c.react().insert(entity, A);
reactor.add(&mut c, entity_mutation::<A>(entity));
}
§Entity World Reactors
Similar to WorldReactor
is EntityWorldReactor
, which is used for entity-specific reactors (entity component insertion/mutation/removal and entity events). For each entity that is tracked by the reactor, you can add EntityWorldReactor::Local
data that is readable/writable with EntityLocal
when that entity triggers a reaction.
Adding an entity to an entity world reactor will register that reactor to run whenever the triggers in EntityWorldReactor::Triggers
are activated on that entity. You don’t need to manually specify the triggers.
In the following example, we write the time to a reactive component every 500ms. The reactor picks this up and prints a message tailored to the reacting entity.
#[derive(ReactComponent, Eq, PartialEq)]
struct TimeRecorder(Duration);
struct TimeReactor;
impl EntityWorldReactor for TimeReactor
{
type Triggers = EntityMutation<TimeRecorder>;
type Local = String;
fn reactor() -> SystemCommandCallback
{
SystemCommandCallback::new(
|data: EntityLocal<TimeReactor>, components: Reactive<TimeRecorder>|
{
let (entity, data) = data.get();
let Some(component) = components.get(entity) else { return };
println!("Entity {:?} now has {:?}", data, component);
}
)
}
}
fn prep_entity(mut c: Commands, reactor: EntityReactor<TimeReactor>)
{
let entity = c.spawn(TimeRecorder(Duration::default()));
reactor.add(&mut c, entity, "ClockTracker");
}
fn update_entity(mut commands: Commands, time: Res<Time>, mut components: ReactiveMut<TimeRecorder>)
{
components.set_single_if_not_eq(&mut c, TimeRecorder(time.elapsed()));
}
struct ExamplePlugin;
impl Plugin for ExamplePlugin
{
fn build(&self, app: &mut App)
{
app.add_entity_reactor::<TimeReactor>()
.add_systems(Setup, prep_entity)
.add_systems(Update, update_entity.run_if(on_timer(Duration::from_millis(500))));
}
}
§Scheduling
In order to support recursive reactions and system events, bevy_cobweb
extends Bevy’s simple Commands
feature by adding additional command-like scheduling, resulting in a 4-tier structure. Processing all of those tiers requires a custom scheduling algorithm, which we discuss below.
§Commands
Conceptually, the four tiers are as follows:
- Inner-system commands (
Commands
): Single-system ECS mutations and system-specific deferred logic. - System commands (
SystemCommand
): Execution of a single system. OneSystemCommand
can schedule further system commands, which can be considered ‘extensions’ of their parent in a functional-programming sense. - System events (
EventCommand
): Sending data to a system which triggers it to run. System events scheduled by other system events are then considered follow-up actions, rather than extensions of the originating event. - Reactions (
ReactionCommand
): ECS mutations or reactive events that trigger a system to run. A single reaction may result in a single system running, a cascade of system commands, or a cascade of system commands followed by a series of system events. Reactions may also trigger other reactions, which will run after the previous reaction has fully resolved itself (after all system commands and events have been recursively processed).
Each tier expands in a telescoping fashion. When one Command
is done running, all commands queued by that Command
are immediately executed before any previous commands, and so on for the other tiers.
Telescoping Caveat
Reaction trees are often triggered within normal Bevy systems by ECS mutations/events/etc. These trees will therefore run at a specific point in the command queues of the normal Bevy systems that trigger them, rather than waiting until the end of the queue.
§Innovations
There are two important innovations that the bevy_cobweb
command-resolver algorithm introduces.
- Rearranged
apply_deferred
:- The problem: Any Bevy system can have internally deferred logic that is saved in system parameters. After a system runs, that deferred logic can be applied by calling
system.apply_deferred(&mut world)
. The problem with this is if the deferred logic includes triggers to run the same system again (e.g. because of reactivity), an error will occur because the system is currently in use. - The solution: To solve this,
bevy_cobweb
only usesapply_deferred
to apply the first command tier. Everything else is executed after the system has been returned to the world.
- The problem: Any Bevy system can have internally deferred logic that is saved in system parameters. After a system runs, that deferred logic can be applied by calling
- Injected cleanup: In
bevy_cobweb
you access reactive event data with theInsertionEvent
,MutationEvent
,RemovalEvent
,DespawnEvent
,BroadcastEvent
,EntityEvent
, andSystemEvent
system parameters. In order to properly set the underlying data of these parameters such that future system calls won’t accidentally have access to that data, our strategy is to insert the data to custom resources and entities immediately before runningSystemCommands
and then remove that data immediately after the system has run but before callingapply_deferred
. We do this with an injected cleanup callback in the system runner (SystemCommandCleanup
).
§Scheduler Algorithm
The scheduler has two pieces. Note that all systems in this context are custom one-shot systems stored on entities.
In order to rearrange apply_deferred
as described, all system commands, system events, and reactions are queued within internal CobwebCommandQueue
resources.
1. System command runner
At the lowest level is the system command runner, which executes a single scheduled system command. All Bevy Commands
and system commands created by the system that is run will be resolved here.
- Remove the target system command from the
World
.- If the system is missing, run the cleanup callback and return.
- Remove pre-existing pending system commands.
- Run the system command. Internally this does the following:
- Run the system on the world:
system.run((), world)
. - Invoke the cleanup callback.
- Apply deferred:
system.apply_deferred(world)
.
- Run the system on the world:
- Reinsert the system command into the
World
. - Take pending system commands and run them with this system runner. Doing this will automatically cause system command telescoping.
- Replace pre-existing pending system commands that were removed.
2. Reaction tree
Whenever a system command, system event, or reaction is scheduled, we schedule a normal Bevy Command
that launches a reaction tree. The reaction tree will early-out if a reaction tree is already being processed.
The reaction tree will fully execute all recursive system commands, system events, and reactions before returning. The algorithm is as follows:
- Set the reaction tree flag to prevent the tree from being recursively executed.
- Remove existing system events and reactions.
- Loop until there are no pending system commands, system events, or reactions.
- Loop until there are no pending system commands or system events.
- Loop until there are no pending system commands.
- Pop one system command from the queue and run it with the system runner. This will internally telescope.
- Remove pending system events and push them to the front of the system events queue.
- Pop one system event from the queue and run it.
- Loop until there are no pending system commands.
- Remove pending reactions and push them to the front of the reactions queue.
- Pop one reaction from the queue and run it.
- Loop until there are no pending system commands or system events.
- Unset the reaction tree flag now that everything has been processed.
Structs§
- Reaction trigger for any entity event of a given type.
- System parameter for reading broadcast event data.
- Reaction trigger for broadcast events.
- System parameter for reading entity despawn events in systems that react to those events.
- Reaction trigger for despawns.
- System parameter for reading entity event data.
- Reaction trigger for entity events.
- Reaction trigger for
ReactComponent
insertions on a specific entity. - System parameter for reading entity-specific data for
EntityWorldReactor
reactors. - Reaction trigger for
ReactComponent
mutations on a specific entity. - System parameter for accessing and updating an
EntityWorldReactor
. - Reaction trigger for
ReactComponent
removals from a specific entity. - System parameter for reading entity component insertion events in systems that react to those events.
- Reaction trigger for
ReactComponent
insertions on any entity. - System parameter for reading entity component mutation events in systems that react to those events.
- Reaction trigger for
ReactComponent
mutations on any entity. - Component wrapper that enables reacting to component mutations.
- Struct that drives reactivity.
- Prepares the react framework so that reactors may be registered with
ReactCommands
. - Immutable reader for reactive resources.
- Mutable wrapper for reactive resources.
- System parameter for accessing
React<T>
components immutably. - System parameter for accessing
React<T>
components mutably. - System parameter for accessing and updating a
WorldReactor
. - System parameter for reading entity component removal events in systems that react to those events.
- Reaction trigger for
ReactComponent
removals from any entity. - Reaction trigger for
ReactResource
mutations. - Token for revoking reactors.
- A system command.
- Owns a system command callback.
- Records a cleanup callback that can be injected into system commands for cleanup after the system command runs but before its
apply_deferred
is called. - System parameter for receiving system event data.
Enums§
- Handle for managing a reactor within the react backend.
- Setting for controlling how reactors are cleaned up.
Traits§
- Helper trait for
EntityTriggerBundle
. - Helper trait for registering reactors with
EntityWorldReactor
. - Trait for persistent reactors that are registered in the world.
- Extends
App
with reactivity helpers. - Extends
Commands
with reactivity helpers. - Tag trait for reactive components.
- Extends
EntityCommands
with reactivity helpers. - Extends the
World
API with reactive resource methods. - Tag trait for reactive resources.
- Extends
World
with reactivity helpers. - Helper trait for registering reactors with
ReactCommands
. - Helper trait for registering reactors with
ReactCommands
. - Trait for persistent reactors that are registered in the world.
Functions§
- Returns an
AnyEntityEventTrigger
reaction trigger. - Returns a
BroadcastTrigger
reaction trigger. - Returns a
DespawnTrigger
reaction trigger. - Returns an
EntityEventTrigger
reaction trigger. - Returns a
EntityInsertionTrigger
reaction trigger. - Returns a
EntityMutationTrigger
reaction trigger. - Returns a
EntityRemovalTrigger
reaction trigger. - Extracts reactor types from a
ReactionTriggerBundle
. - Returns a
InsertionTrigger
reaction trigger. - Returns a
MutationTrigger
reaction trigger. - Runs a reaction tree to completion.
- Returns a
RemovalTrigger
reaction trigger. - Returns a
ResourceMutationTrigger
reaction trigger. - Queues removal and despawn reactors.
- Spawns a ref-counted
SystemCommand
from a given raw system. - Spawns a ref-counted
SystemCommand
from a pre-defined callback. - Spawns a system as a
SystemCommand
. - Spawns a
SystemCommand
from a pre-defined callback.