Merge ObserverState and Observer into single component (#18728)
# Objective - bevy removed `Observe` type parameters in #15151 ,it enables merging `Observer` and `ObserverState ` into a single component. with this consolidation ,we can improve efficiency while reducing boilerplate. ## Solution - remove `ObserverState `and merge it into `Observer` ## Testing 40%~60% performance win due to removal of redundant look up.  This also improves ergonomics when using dynamic observer ```rust // previously world.spawn(ObserverState { // SAFETY: we registered `event_a` above and it matches the type of EventA descriptor: unsafe { ObserverDescriptor::default().with_events(vec![event_a]) }, runner: |mut world, _trigger, _ptr, _propagate| { world.resource_mut::<Order>().observed("event_a"); }, ..Default::default() }); // now let observe = unsafe { Observer::with_dynamic_runner(|mut world, _trigger, _ptr, _propagate| { world.resource_mut::<Order>().observed("event_a"); }) .with_event(event_a) }; world.spawn(observe); ```
This commit is contained in:
parent
3442e2556d
commit
5ed8e0639a
@ -3,11 +3,12 @@ use crate::{
|
|||||||
Component, ComponentCloneBehavior, ComponentHook, HookContext, Mutable, StorageType,
|
Component, ComponentCloneBehavior, ComponentHook, HookContext, Mutable, StorageType,
|
||||||
},
|
},
|
||||||
entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent},
|
entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent},
|
||||||
observer::ObserverState,
|
|
||||||
world::World,
|
world::World,
|
||||||
};
|
};
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
|
use super::Observer;
|
||||||
|
|
||||||
/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to.
|
/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ObservedBy(pub(crate) Vec<Entity>);
|
pub struct ObservedBy(pub(crate) Vec<Entity>);
|
||||||
@ -27,7 +28,7 @@ impl Component for ObservedBy {
|
|||||||
let Ok(mut entity_mut) = world.get_entity_mut(e) else {
|
let Ok(mut entity_mut) = world.get_entity_mut(e) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(mut state) = entity_mut.get_mut::<ObserverState>() else {
|
let Some(mut state) = entity_mut.get_mut::<Observer>() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
state.despawned_watched_entities += 1;
|
state.despawned_watched_entities += 1;
|
||||||
@ -77,10 +78,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo
|
|||||||
.entity_mut(target)
|
.entity_mut(target)
|
||||||
.insert(ObservedBy(observed_by.clone()));
|
.insert(ObservedBy(observed_by.clone()));
|
||||||
|
|
||||||
for observer in &observed_by {
|
for observer_entity in observed_by.iter().copied() {
|
||||||
let mut observer_state = world
|
let mut observer_state = world
|
||||||
.get_mut::<ObserverState>(*observer)
|
.get_mut::<Observer>(observer_entity)
|
||||||
.expect("Source observer entity must have ObserverState");
|
.expect("Source observer entity must have Observer");
|
||||||
observer_state.descriptor.entities.push(target);
|
observer_state.descriptor.entities.push(target);
|
||||||
let event_types = observer_state.descriptor.events.clone();
|
let event_types = observer_state.descriptor.events.clone();
|
||||||
let components = observer_state.descriptor.components.clone();
|
let components = observer_state.descriptor.components.clone();
|
||||||
|
@ -315,13 +315,6 @@ impl ObserverDescriptor {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn merge(&mut self, descriptor: &ObserverDescriptor) {
|
|
||||||
self.events.extend(descriptor.events.iter().copied());
|
|
||||||
self.components
|
|
||||||
.extend(descriptor.components.iter().copied());
|
|
||||||
self.entities.extend(descriptor.entities.iter().copied());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the `events` that the observer is watching.
|
/// Returns the `events` that the observer is watching.
|
||||||
pub fn events(&self) -> &[ComponentId] {
|
pub fn events(&self) -> &[ComponentId] {
|
||||||
&self.events
|
&self.events
|
||||||
@ -728,11 +721,10 @@ impl World {
|
|||||||
pub(crate) fn register_observer(&mut self, observer_entity: Entity) {
|
pub(crate) fn register_observer(&mut self, observer_entity: Entity) {
|
||||||
// SAFETY: References do not alias.
|
// SAFETY: References do not alias.
|
||||||
let (observer_state, archetypes, observers) = unsafe {
|
let (observer_state, archetypes, observers) = unsafe {
|
||||||
let observer_state: *const ObserverState =
|
let observer_state: *const Observer = self.get::<Observer>(observer_entity).unwrap();
|
||||||
self.get::<ObserverState>(observer_entity).unwrap();
|
|
||||||
// Populate ObservedBy for each observed entity.
|
// Populate ObservedBy for each observed entity.
|
||||||
for watched_entity in &(*observer_state).descriptor.entities {
|
for watched_entity in (*observer_state).descriptor.entities.iter().copied() {
|
||||||
let mut entity_mut = self.entity_mut(*watched_entity);
|
let mut entity_mut = self.entity_mut(watched_entity);
|
||||||
let mut observed_by = entity_mut.entry::<ObservedBy>().or_default().into_mut();
|
let mut observed_by = entity_mut.entry::<ObservedBy>().or_default().into_mut();
|
||||||
observed_by.0.push(observer_entity);
|
observed_by.0.push(observer_entity);
|
||||||
}
|
}
|
||||||
@ -853,7 +845,7 @@ mod tests {
|
|||||||
use crate::component::ComponentId;
|
use crate::component::ComponentId;
|
||||||
use crate::{
|
use crate::{
|
||||||
change_detection::MaybeLocation,
|
change_detection::MaybeLocation,
|
||||||
observer::{Observer, ObserverDescriptor, ObserverState, OnReplace},
|
observer::{Observer, OnReplace},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
traversal::Traversal,
|
traversal::Traversal,
|
||||||
};
|
};
|
||||||
@ -1368,14 +1360,14 @@ mod tests {
|
|||||||
world.init_resource::<Order>();
|
world.init_resource::<Order>();
|
||||||
let event_a = OnRemove::register_component_id(&mut world);
|
let event_a = OnRemove::register_component_id(&mut world);
|
||||||
|
|
||||||
world.spawn(ObserverState {
|
// SAFETY: we registered `event_a` above and it matches the type of EventA
|
||||||
// SAFETY: we registered `event_a` above and it matches the type of EventA
|
let observe = unsafe {
|
||||||
descriptor: unsafe { ObserverDescriptor::default().with_events(vec![event_a]) },
|
Observer::with_dynamic_runner(|mut world, _trigger, _ptr, _propagate| {
|
||||||
runner: |mut world, _trigger, _ptr, _propagate| {
|
|
||||||
world.resource_mut::<Order>().observed("event_a");
|
world.resource_mut::<Order>().observed("event_a");
|
||||||
},
|
})
|
||||||
..Default::default()
|
.with_event(event_a)
|
||||||
});
|
};
|
||||||
|
world.spawn(observe);
|
||||||
|
|
||||||
world.commands().queue(move |world: &mut World| {
|
world.commands().queue(move |world: &mut World| {
|
||||||
// SAFETY: we registered `event_a` above and it matches the type of EventA
|
// SAFETY: we registered `event_a` above and it matches the type of EventA
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use alloc::{boxed::Box, vec, vec::Vec};
|
use alloc::{boxed::Box, vec};
|
||||||
use core::any::Any;
|
use core::any::Any;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -12,85 +12,6 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use bevy_ptr::PtrMut;
|
use bevy_ptr::PtrMut;
|
||||||
|
|
||||||
/// Contains [`Observer`] information. This defines how a given observer behaves. It is the
|
|
||||||
/// "source of truth" for a given observer entity's behavior.
|
|
||||||
pub struct ObserverState {
|
|
||||||
pub(crate) descriptor: ObserverDescriptor,
|
|
||||||
pub(crate) runner: ObserverRunner,
|
|
||||||
pub(crate) last_trigger_id: u32,
|
|
||||||
pub(crate) despawned_watched_entities: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ObserverState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
runner: |_, _, _, _| {},
|
|
||||||
last_trigger_id: 0,
|
|
||||||
despawned_watched_entities: 0,
|
|
||||||
descriptor: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObserverState {
|
|
||||||
/// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`]
|
|
||||||
/// is triggered.
|
|
||||||
pub fn with_event(mut self, event: ComponentId) -> Self {
|
|
||||||
self.descriptor.events.push(event);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Observe the given event list. This will cause the [`Observer`] to run whenever an event with any of the given [`ComponentId`]s
|
|
||||||
/// is triggered.
|
|
||||||
pub fn with_events(mut self, events: impl IntoIterator<Item = ComponentId>) -> Self {
|
|
||||||
self.descriptor.events.extend(events);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Observe the given [`Entity`] list. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
|
|
||||||
/// for any [`Entity`] target in the list.
|
|
||||||
pub fn with_entities(mut self, entities: impl IntoIterator<Item = Entity>) -> Self {
|
|
||||||
self.descriptor.entities.extend(entities);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Observe the given [`ComponentId`] list. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
|
|
||||||
/// for any [`ComponentId`] target in the list.
|
|
||||||
pub fn with_components(mut self, components: impl IntoIterator<Item = ComponentId>) -> Self {
|
|
||||||
self.descriptor.components.extend(components);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for ObserverState {
|
|
||||||
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
|
|
||||||
type Mutability = Mutable;
|
|
||||||
|
|
||||||
fn on_add() -> Option<ComponentHook> {
|
|
||||||
Some(|mut world, HookContext { entity, .. }| {
|
|
||||||
world.commands().queue(move |world: &mut World| {
|
|
||||||
world.register_observer(entity);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_remove() -> Option<ComponentHook> {
|
|
||||||
Some(|mut world, HookContext { entity, .. }| {
|
|
||||||
let descriptor = core::mem::take(
|
|
||||||
&mut world
|
|
||||||
.entity_mut(entity)
|
|
||||||
.get_mut::<ObserverState>()
|
|
||||||
.unwrap()
|
|
||||||
.as_mut()
|
|
||||||
.descriptor,
|
|
||||||
);
|
|
||||||
world.commands().queue(move |world: &mut World| {
|
|
||||||
world.unregister_observer(entity, descriptor);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type for function that is run when an observer is triggered.
|
/// Type for function that is run when an observer is triggered.
|
||||||
///
|
///
|
||||||
/// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component,
|
/// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component,
|
||||||
@ -264,16 +185,17 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate:
|
|||||||
/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities!
|
/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities!
|
||||||
///
|
///
|
||||||
/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`].
|
/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`].
|
||||||
///
|
|
||||||
/// When first added, [`Observer`] will also create an [`ObserverState`] component, which registers the observer with the [`World`] and
|
|
||||||
/// serves as the "source of truth" of the observer.
|
/// serves as the "source of truth" of the observer.
|
||||||
///
|
///
|
||||||
/// [`SystemParam`]: crate::system::SystemParam
|
/// [`SystemParam`]: crate::system::SystemParam
|
||||||
pub struct Observer {
|
pub struct Observer {
|
||||||
system: Box<dyn Any + Send + Sync + 'static>,
|
|
||||||
descriptor: ObserverDescriptor,
|
|
||||||
hook_on_add: ComponentHook,
|
hook_on_add: ComponentHook,
|
||||||
error_handler: Option<fn(BevyError, ErrorContext)>,
|
error_handler: Option<fn(BevyError, ErrorContext)>,
|
||||||
|
system: Box<dyn Any + Send + Sync + 'static>,
|
||||||
|
pub(crate) descriptor: ObserverDescriptor,
|
||||||
|
pub(crate) last_trigger_id: u32,
|
||||||
|
pub(crate) despawned_watched_entities: u32,
|
||||||
|
pub(crate) runner: ObserverRunner,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Observer {
|
impl Observer {
|
||||||
@ -298,6 +220,35 @@ impl Observer {
|
|||||||
descriptor: Default::default(),
|
descriptor: Default::default(),
|
||||||
hook_on_add: hook_on_add::<E, B, I::System>,
|
hook_on_add: hook_on_add::<E, B, I::System>,
|
||||||
error_handler: None,
|
error_handler: None,
|
||||||
|
runner: observer_system_runner::<E, B, I::System>,
|
||||||
|
despawned_watched_entities: 0,
|
||||||
|
last_trigger_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Observer`] with custom runner, this is mostly used for dynamic event observer
|
||||||
|
pub fn with_dynamic_runner(runner: ObserverRunner) -> Self {
|
||||||
|
Self {
|
||||||
|
system: Box::new(|| {}),
|
||||||
|
descriptor: Default::default(),
|
||||||
|
hook_on_add: |mut world, hook_context| {
|
||||||
|
world.commands().queue(move |world: &mut World| {
|
||||||
|
let entity = hook_context.entity;
|
||||||
|
if let Some(mut observe) = world.get_mut::<Observer>(entity) {
|
||||||
|
if observe.descriptor.events.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if observe.error_handler.is_none() {
|
||||||
|
observe.error_handler = Some(default_error_handler());
|
||||||
|
}
|
||||||
|
world.register_observer(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error_handler: None,
|
||||||
|
runner,
|
||||||
|
despawned_watched_entities: 0,
|
||||||
|
last_trigger_id: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,6 +309,21 @@ impl Component for Observer {
|
|||||||
hook(world, context);
|
hook(world, context);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
fn on_remove() -> Option<ComponentHook> {
|
||||||
|
Some(|mut world, HookContext { entity, .. }| {
|
||||||
|
let descriptor = core::mem::take(
|
||||||
|
&mut world
|
||||||
|
.entity_mut(entity)
|
||||||
|
.get_mut::<Self>()
|
||||||
|
.unwrap()
|
||||||
|
.as_mut()
|
||||||
|
.descriptor,
|
||||||
|
);
|
||||||
|
world.commands().queue(move |world: &mut World| {
|
||||||
|
world.unregister_observer(entity, descriptor);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
||||||
@ -373,12 +339,8 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
|||||||
.get_entity(observer_trigger.observer)
|
.get_entity(observer_trigger.observer)
|
||||||
.debug_checked_unwrap()
|
.debug_checked_unwrap()
|
||||||
};
|
};
|
||||||
// SAFETY: Observer was triggered so must have an `ObserverState`
|
// SAFETY: Observer was triggered so must have an `Observer`
|
||||||
let mut state = unsafe {
|
let mut state = unsafe { observer_cell.get_mut::<Observer>().debug_checked_unwrap() };
|
||||||
observer_cell
|
|
||||||
.get_mut::<ObserverState>()
|
|
||||||
.debug_checked_unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Move this check into the observer cache to avoid dynamic dispatch
|
// TODO: Move this check into the observer cache to avoid dynamic dispatch
|
||||||
let last_trigger = world.last_trigger_id();
|
let last_trigger = world.last_trigger_id();
|
||||||
@ -386,15 +348,8 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.last_trigger_id = last_trigger;
|
state.last_trigger_id = last_trigger;
|
||||||
|
|
||||||
// SAFETY: Observer was triggered so must have an `Observer` component.
|
// SAFETY: Observer was triggered so must have an `Observer` component.
|
||||||
let error_handler = unsafe {
|
let error_handler = unsafe { state.error_handler.debug_checked_unwrap() };
|
||||||
observer_cell
|
|
||||||
.get::<Observer>()
|
|
||||||
.debug_checked_unwrap()
|
|
||||||
.error_handler
|
|
||||||
.debug_checked_unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let trigger: Trigger<E, B> = Trigger::new(
|
let trigger: Trigger<E, B> = Trigger::new(
|
||||||
// SAFETY: Caller ensures `ptr` is castable to `&mut T`
|
// SAFETY: Caller ensures `ptr` is castable to `&mut T`
|
||||||
@ -402,12 +357,12 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
|||||||
propagate,
|
propagate,
|
||||||
observer_trigger,
|
observer_trigger,
|
||||||
);
|
);
|
||||||
|
|
||||||
// SAFETY:
|
// SAFETY:
|
||||||
// - observer was triggered so must have an `Observer` component.
|
// - observer was triggered so must have an `Observer` component.
|
||||||
// - observer cannot be dropped or mutated until after the system pointer is already dropped.
|
// - observer cannot be dropped or mutated until after the system pointer is already dropped.
|
||||||
let system: *mut dyn ObserverSystem<E, B> = unsafe {
|
let system: *mut dyn ObserverSystem<E, B> = unsafe {
|
||||||
let mut observe = observer_cell.get_mut::<Observer>().debug_checked_unwrap();
|
let system = state.system.downcast_mut::<S>().debug_checked_unwrap();
|
||||||
let system = observe.system.downcast_mut::<S>().unwrap();
|
|
||||||
&mut *system
|
&mut *system
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -461,48 +416,26 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
|||||||
) {
|
) {
|
||||||
world.commands().queue(move |world: &mut World| {
|
world.commands().queue(move |world: &mut World| {
|
||||||
let event_id = E::register_component_id(world);
|
let event_id = E::register_component_id(world);
|
||||||
let mut components = Vec::new();
|
let mut components = vec![];
|
||||||
B::component_ids(&mut world.components_registrator(), &mut |id| {
|
B::component_ids(&mut world.components_registrator(), &mut |id| {
|
||||||
components.push(id);
|
components.push(id);
|
||||||
});
|
});
|
||||||
let mut descriptor = ObserverDescriptor {
|
if let Some(mut observe) = world.get_mut::<Observer>(entity) {
|
||||||
events: vec![event_id],
|
observe.descriptor.events.push(event_id);
|
||||||
components,
|
observe.descriptor.components.extend(components);
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let error_handler = default_error_handler();
|
if observe.error_handler.is_none() {
|
||||||
|
observe.error_handler = Some(default_error_handler());
|
||||||
// Initialize System
|
|
||||||
let system: *mut dyn ObserverSystem<E, B> =
|
|
||||||
if let Some(mut observe) = world.get_mut::<Observer>(entity) {
|
|
||||||
descriptor.merge(&observe.descriptor);
|
|
||||||
if observe.error_handler.is_none() {
|
|
||||||
observe.error_handler = Some(error_handler);
|
|
||||||
}
|
|
||||||
let system = observe.system.downcast_mut::<S>().unwrap();
|
|
||||||
&mut *system
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
// SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias
|
|
||||||
unsafe {
|
|
||||||
(*system).initialize(world);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut entity = world.entity_mut(entity);
|
|
||||||
if let crate::world::Entry::Vacant(entry) = entity.entry::<ObserverState>() {
|
|
||||||
entry.insert(ObserverState {
|
|
||||||
descriptor,
|
|
||||||
runner: observer_system_runner::<E, B, S>,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
let system: *mut dyn ObserverSystem<E, B> = observe.system.downcast_mut::<S>().unwrap();
|
||||||
|
// SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias
|
||||||
|
unsafe {
|
||||||
|
(*system).initialize(world);
|
||||||
|
}
|
||||||
|
world.register_observer(entity);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: Integrate `ObserverState` component into `Observer`.
|
||||||
|
pull_requests: [18728]
|
||||||
|
---
|
||||||
|
|
||||||
|
`ObserverState` and `Observer` have been merged into a single component.
|
||||||
|
now you can use `Observer::with_dynamic_runner` to build custom Observe.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let observe = unsafe {
|
||||||
|
Observer::with_dynamic_runner(|mut world, trigger, ptr, propagate| {
|
||||||
|
// do something
|
||||||
|
})
|
||||||
|
.with_event(event_a)
|
||||||
|
};
|
||||||
|
world.spawn(observe);
|
||||||
|
```
|
Loading…
Reference in New Issue
Block a user