Skip to content

Commit 2297753

Browse files
authored
Observer run conditions (#22602)
# Objective Allow observers to use run conditions, enabling conditional execution based on world state - the same pattern that systems use with .run_if(). Fixes #14195 Fixes #21442 ## Solution Add a run_if() method to observer systems via the ObserverSystemExt trait. Conditions are stored in Observer and checked before execution in the runner. Key implementation details: - ObserverWithCondition<E,B,M,S> wrapper preserves event type info for compile-time EntityEvent enforcement on entity observers - Conditions are ReadOnlySystem (enforced by SystemCondition trait), matching system run conditions - Multiple conditions chain with AND semantics, short-circuiting on first false It's a bit more involved than mentioned [here](#21442 (comment)) since we need the full system‑style API: chained run_ifs, entity observers with compile‑time EntityEvent checks, and use with add_observer/observe. That forces a typed wrapper + marker types + IntoObserver/IntoEntityObserver, and conditions must be initialized at spawn time (so the hook has to take/init/restore). The runner also needs a safety‑documented precheck before running the observer. ## Testing - Added 7 new tests covering: - Condition preventing/allowing execution - Multiple conditions (all true / one false) - Entity observers with conditions - Resource-based conditions - Builder pattern on Observer::new() - All existing tests pass (cargo test -p bevy_ecs) - Updated observers.rs example with Space key toggle --- ## Showcase ```rust #[derive(Resource)] struct GameActive(bool); // Global observer - only runs when game is active app.add_observer( on_damage.run_if(|state: Res<GameActive>| state.0) ); // Chained conditions (AND semantics) app.add_observer( on_damage .run_if(|state: Res<GameActive>| state.0) .run_if(|player: Query<&Health, With<Player>>| player.single().is_ok()) ); // Entity observer commands.spawn(Enemy).observe( on_hit.run_if(|game: Res<GameActive>| game.0) ); // Builder pattern world.spawn( Observer::new(on_event) .with_entity(target) .run_if(some_condition) ); ``` <details> <summary>Example from observers.rs</summary> ```rust #[derive(Resource, Default)] struct ExplosionsEnabled(bool); fn toggle_explosions(mut enabled: ResMut<ExplosionsEnabled>, input: Res<ButtonInput<KeyCode>>) { if input.just_pressed(KeyCode::Space) { enabled.0 = !enabled.0; info!("Explosions {}", if enabled.0 { "enabled" } else { "disabled" }); } } fn setup(app: &mut App) { app.add_observer( explode_mine.run_if(|enabled: Res<ExplosionsEnabled>| enabled.0) ); } ``` </details>
1 parent 4cc92a0 commit 2297753

File tree

11 files changed

+558
-63
lines changed

11 files changed

+558
-63
lines changed

crates/bevy_app/src/app.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ pub use bevy_derive::AppLabel;
1111
use bevy_ecs::{
1212
component::RequiredComponentsError,
1313
error::{DefaultErrorHandler, ErrorHandler},
14-
event::Event,
1514
intern::Interned,
1615
message::{message_update_system, MessageCursor},
16+
observer::IntoObserver,
1717
prelude::*,
1818
schedule::{
1919
InternedSystemSet, ScheduleBuildSettings, ScheduleCleanupPolicy, ScheduleError,
2020
ScheduleLabel,
2121
},
22-
system::{IntoObserverSystem, ScheduleSystem, SystemId, SystemInput},
22+
system::{ScheduleSystem, SystemId, SystemInput},
2323
};
2424
use bevy_platform::collections::HashMap;
2525
use core::{fmt::Debug, num::NonZero, panic::AssertUnwindSafe};
@@ -1392,10 +1392,7 @@ impl App {
13921392
/// }
13931393
/// });
13941394
/// ```
1395-
pub fn add_observer<E: Event, B: Bundle, M>(
1396-
&mut self,
1397-
observer: impl IntoObserverSystem<E, B, M>,
1398-
) -> &mut Self {
1395+
pub fn add_observer<M>(&mut self, observer: impl IntoObserver<M>) -> &mut Self {
13991396
self.world_mut().add_observer(observer);
14001397
self
14011398
}

crates/bevy_ecs/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ pub mod prelude {
8080
Message, MessageMutator, MessageReader, MessageWriter, Messages, PopulatedMessageReader,
8181
},
8282
name::{Name, NameOrEntity},
83-
observer::{Observer, On},
83+
observer::{Observer, ObserverSystemExt, On},
8484
query::{Added, Allow, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without},
8585
related,
8686
relationship::RelationshipTarget,
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//! Run conditions for observers.
2+
//!
3+
//! This module provides the types needed to add run conditions to observers,
4+
//! allowing them to conditionally execute based on world state.
5+
6+
use alloc::{boxed::Box, vec::Vec};
7+
use core::marker::PhantomData;
8+
9+
use crate::{
10+
bundle::Bundle,
11+
event::Event,
12+
schedule::{BoxedCondition, SystemCondition},
13+
system::{IntoObserverSystem, IntoSystem},
14+
world::{unsafe_world_cell::UnsafeWorldCell, World},
15+
};
16+
17+
/// Stores a boxed condition system for an observer.
18+
pub(crate) struct ObserverCondition {
19+
condition: BoxedCondition,
20+
}
21+
22+
impl ObserverCondition {
23+
pub(crate) fn new<M>(condition: impl SystemCondition<M>) -> Self {
24+
Self {
25+
condition: Box::new(IntoSystem::into_system(condition)),
26+
}
27+
}
28+
29+
pub(crate) fn from_boxed(condition: BoxedCondition) -> Self {
30+
Self { condition }
31+
}
32+
33+
pub(crate) fn initialize(&mut self, world: &mut World) {
34+
self.condition.initialize(world);
35+
}
36+
37+
/// # Safety
38+
/// - The condition must be initialized.
39+
/// - The world cell must have valid access for the condition's read-only parameters.
40+
pub(crate) unsafe fn check(&mut self, world: UnsafeWorldCell) -> bool {
41+
// SAFETY: Caller ensures world is valid and condition is initialized.
42+
// Conditions are read-only systems, so they won't cause aliasing issues.
43+
unsafe { self.condition.run_unsafe((), world) }.unwrap_or(false)
44+
}
45+
}
46+
47+
#[doc(hidden)]
48+
pub struct ObserverWithConditionMarker;
49+
50+
/// An observer system with run conditions that preserves event type information.
51+
///
52+
/// This type is returned by [`ObserverSystemExt::run_if`](super::ObserverSystemExt::run_if)
53+
/// and allows `entity.observe(system.run_if(cond))` to work with compile-time
54+
/// verification that the event implements [`EntityEvent`](crate::event::EntityEvent).
55+
pub struct ObserverWithCondition<E: Event, B: Bundle, M, S: IntoObserverSystem<E, B, M>> {
56+
pub(crate) system: S,
57+
pub(crate) conditions: Vec<BoxedCondition>,
58+
pub(crate) _marker: PhantomData<fn() -> (E, B, M)>,
59+
}
60+
61+
impl<E: Event, B: Bundle, M, S: IntoObserverSystem<E, B, M>> ObserverWithCondition<E, B, M, S> {
62+
/// Adds another run condition to this observer.
63+
///
64+
/// All conditions must return `true` for the observer to run (AND semantics).
65+
///
66+
/// **Note:** Chained `.run_if()` calls do **not** short-circuit — all conditions
67+
/// run every time to maintain correct change detection ticks. If you need
68+
/// short-circuit behavior, use `.run_if(a.and(b))`, but be aware this may cause
69+
/// stale `Changed<T>` detection if the second condition is frequently skipped.
70+
///
71+
/// # Example
72+
///
73+
/// ```
74+
/// # use bevy_ecs::prelude::*;
75+
/// # #[derive(Event)]
76+
/// # struct MyEvent;
77+
/// # #[derive(Resource)]
78+
/// # struct CondA(bool);
79+
/// # #[derive(Resource)]
80+
/// # struct CondB(bool);
81+
/// # fn on_event(_: On<MyEvent>) {}
82+
/// # let mut world = World::new();
83+
/// # world.insert_resource(CondA(true));
84+
/// # world.insert_resource(CondB(true));
85+
/// world.add_observer(
86+
/// on_event
87+
/// .run_if(|a: Res<CondA>| a.0)
88+
/// .run_if(|b: Res<CondB>| b.0)
89+
/// );
90+
/// ```
91+
pub fn run_if<C, CM>(mut self, condition: C) -> Self
92+
where
93+
C: SystemCondition<CM>,
94+
{
95+
self.conditions
96+
.push(Box::new(IntoSystem::into_system(condition)));
97+
self
98+
}
99+
100+
pub(crate) fn take_conditions(self) -> (S, Vec<ObserverCondition>) {
101+
let conditions = self
102+
.conditions
103+
.into_iter()
104+
.map(ObserverCondition::from_boxed)
105+
.collect();
106+
(self.system, conditions)
107+
}
108+
}

crates/bevy_ecs/src/observer/distributed_storage.rs

Lines changed: 138 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@
1111
1212
use core::any::Any;
1313

14+
use core::marker::PhantomData;
15+
1416
use crate::{
1517
component::{ComponentCloneBehavior, ComponentId, Mutable, StorageType},
16-
entity::Entity,
1718
error::{ErrorContext, ErrorHandler},
18-
event::{Event, EventKey},
19+
event::EventKey,
1920
lifecycle::{ComponentHook, HookContext},
20-
observer::{observer_system_runner, ObserverRunner},
21+
observer::{
22+
condition::{ObserverCondition, ObserverWithCondition, ObserverWithConditionMarker},
23+
observer_system_runner, ObserverRunner,
24+
},
2125
prelude::*,
2226
system::{IntoObserverSystem, ObserverSystem},
2327
world::DeferredWorld,
@@ -208,6 +212,7 @@ pub struct Observer {
208212
pub(crate) last_trigger_id: u32,
209213
pub(crate) despawned_watched_entities: u32,
210214
pub(crate) runner: ObserverRunner,
215+
pub(crate) conditions: Vec<ObserverCondition>,
211216
}
212217

213218
impl Observer {
@@ -234,6 +239,7 @@ impl Observer {
234239
runner: observer_system_runner::<E, B, I::System>,
235240
despawned_watched_entities: 0,
236241
last_trigger_id: 0,
242+
conditions: Vec::new(),
237243
}
238244
}
239245

@@ -246,21 +252,32 @@ impl Observer {
246252
let default_error_handler = world.default_error_handler();
247253
world.commands().queue(move |world: &mut World| {
248254
let entity = hook_context.entity;
249-
if let Some(mut observe) = world.get_mut::<Observer>(entity) {
255+
let mut conditions = {
256+
let Some(mut observe) = world.get_mut::<Observer>(entity) else {
257+
return;
258+
};
250259
if observe.descriptor.event_keys.is_empty() {
251260
return;
252261
}
253262
if observe.error_handler.is_none() {
254263
observe.error_handler = Some(default_error_handler);
255264
}
256-
world.register_observer(entity);
265+
core::mem::take(&mut observe.conditions)
266+
};
267+
for condition in &mut conditions {
268+
condition.initialize(world);
257269
}
270+
if let Some(mut observe) = world.get_mut::<Observer>(entity) {
271+
observe.conditions = conditions;
272+
}
273+
world.register_observer(entity);
258274
});
259275
},
260276
error_handler: None,
261277
runner,
262278
despawned_watched_entities: 0,
263279
last_trigger_id: 0,
280+
conditions: Vec::new(),
264281
}
265282
}
266283

@@ -326,6 +343,15 @@ impl Observer {
326343
self
327344
}
328345

346+
/// Adds a run condition to this observer.
347+
///
348+
/// The observer will only run if all conditions return `true` (AND semantics).
349+
/// Multiple conditions can be added by chaining `run_if` calls.
350+
pub fn run_if<M>(mut self, condition: impl SystemCondition<M>) -> Self {
351+
self.conditions.push(ObserverCondition::new(condition));
352+
self
353+
}
354+
329355
/// Returns the [`ObserverDescriptor`] for this [`Observer`].
330356
pub fn descriptor(&self) -> &ObserverDescriptor {
331357
&self.descriptor
@@ -435,18 +461,38 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
435461
let event_key = world.register_event_key::<E>();
436462
let components = B::component_ids(&mut world.components_registrator());
437463

438-
if let Some(mut observer) = world.get_mut::<Observer>(entity) {
464+
let system_ptr: *mut dyn ObserverSystem<E, B> = {
465+
let Some(mut observer) = world.get_mut::<Observer>(entity) else {
466+
return;
467+
};
439468
observer.descriptor.event_keys.push(event_key);
440469
observer.descriptor.components.extend(components);
441470

442471
let system: &mut dyn Any = observer.system.as_mut();
443-
let system: *mut dyn ObserverSystem<E, B> = system.downcast_mut::<S>().unwrap();
444-
// SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias
445-
unsafe {
446-
(*system).initialize(world);
447-
}
448-
world.register_observer(entity);
472+
system.downcast_mut::<S>().unwrap() as *mut dyn ObserverSystem<E, B>
473+
};
474+
475+
// SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias
476+
unsafe {
477+
(*system_ptr).initialize(world);
478+
}
479+
480+
let mut conditions = {
481+
let Some(mut observer) = world.get_mut::<Observer>(entity) else {
482+
return;
483+
};
484+
core::mem::take(&mut observer.conditions)
485+
};
486+
487+
for condition in &mut conditions {
488+
condition.initialize(world);
449489
}
490+
491+
if let Some(mut observer) = world.get_mut::<Observer>(entity) {
492+
observer.conditions = conditions;
493+
}
494+
495+
world.register_observer(entity);
450496
});
451497
}
452498

@@ -510,3 +556,83 @@ impl<T: Any + System> AnyNamedSystem for T {
510556
self.name()
511557
}
512558
}
559+
560+
/// Trait for types that can be converted into an [`Observer`].
561+
pub trait IntoObserver<Marker>: Send + 'static {
562+
/// Converts this type into an [`Observer`].
563+
fn into_observer(self) -> Observer;
564+
}
565+
566+
impl IntoObserver<()> for Observer {
567+
fn into_observer(self) -> Observer {
568+
self
569+
}
570+
}
571+
572+
impl<E: Event, B: Bundle, M, T: IntoObserverSystem<E, B, M>> IntoObserver<(E, B, M)> for T {
573+
fn into_observer(self) -> Observer {
574+
Observer::new(self)
575+
}
576+
}
577+
578+
impl<E: Event, B: Bundle, M: 'static, S: IntoObserverSystem<E, B, M>>
579+
IntoObserver<ObserverWithConditionMarker> for ObserverWithCondition<E, B, M, S>
580+
{
581+
fn into_observer(self) -> Observer {
582+
let (system, conditions) = self.take_conditions();
583+
let mut observer = Observer::new(system);
584+
observer.conditions = conditions;
585+
observer
586+
}
587+
}
588+
589+
/// Trait for types that can be converted into an entity-targeting [`Observer`].
590+
///
591+
/// This trait enforces that the event type implements [`EntityEvent`].
592+
#[diagnostic::on_unimplemented(
593+
message = "`{Self}` cannot be used as an entity observer",
594+
note = "entity observers require the event type to implement `EntityEvent`"
595+
)]
596+
pub trait IntoEntityObserver<Marker>: Send + 'static {
597+
/// Converts this type into an [`Observer`] that watches the given entity.
598+
fn into_observer_for_entity(self, entity: Entity) -> Observer;
599+
}
600+
601+
impl<E: EntityEvent, B: Bundle, M, T: IntoObserverSystem<E, B, M>> IntoEntityObserver<(E, B, M)>
602+
for T
603+
{
604+
fn into_observer_for_entity(self, entity: Entity) -> Observer {
605+
Observer::new(self).with_entity(entity)
606+
}
607+
}
608+
609+
impl<E: EntityEvent, B: Bundle, M: 'static, S: IntoObserverSystem<E, B, M>>
610+
IntoEntityObserver<ObserverWithConditionMarker> for ObserverWithCondition<E, B, M, S>
611+
{
612+
fn into_observer_for_entity(self, entity: Entity) -> Observer {
613+
let (system, conditions) = self.take_conditions();
614+
let mut observer = Observer::new(system);
615+
observer.conditions = conditions;
616+
observer.with_entity(entity)
617+
}
618+
}
619+
620+
/// Extension trait for adding run conditions to observer systems.
621+
pub trait ObserverSystemExt<E: Event, B: Bundle, M>: IntoObserverSystem<E, B, M> + Sized {
622+
/// Adds a run condition to this observer system.
623+
///
624+
/// The observer will only run if the condition returns `true`.
625+
/// Multiple conditions can be chained (AND semantics).
626+
fn run_if<C, CM>(self, condition: C) -> ObserverWithCondition<E, B, M, Self>
627+
where
628+
C: SystemCondition<CM>,
629+
{
630+
ObserverWithCondition {
631+
system: self,
632+
conditions: alloc::vec![Box::new(IntoSystem::into_system(condition))],
633+
_marker: PhantomData,
634+
}
635+
}
636+
}
637+
638+
impl<E: Event, B: Bundle, M, T: IntoObserverSystem<E, B, M>> ObserverSystemExt<E, B, M> for T {}

0 commit comments

Comments
 (0)