
# Objective Currently, the observer API looks like this: ```rust app.add_observer(|trigger: Trigger<Explode>| { info!("Entity {} exploded!", trigger.target()); }); ``` Future plans for observers also include "multi-event observers" with a trigger that looks like this (see [Cart's example](https://github.com/bevyengine/bevy/issues/14649#issuecomment-2960402508)): ```rust trigger: Trigger<( OnAdd<Pressed>, OnRemove<Pressed>, OnAdd<InteractionDisabled>, OnRemove<InteractionDisabled>, OnInsert<Hovered>, )>, ``` In scenarios like this, there is a lot of repetition of `On`. These are expected to be very high-traffic APIs especially in UI contexts, so ergonomics and readability are critical. By renaming `Trigger` to `On`, we can make these APIs read more cleanly and get rid of the repetition: ```rust app.add_observer(|trigger: On<Explode>| { info!("Entity {} exploded!", trigger.target()); }); ``` ```rust trigger: On<( Add<Pressed>, Remove<Pressed>, Add<InteractionDisabled>, Remove<InteractionDisabled>, Insert<Hovered>, )>, ``` Names like `On<Add<Pressed>>` emphasize the actual event listener nature more than `Trigger<OnAdd<Pressed>>`, and look cleaner. This *also* frees up the `Trigger` name if we want to use it for the observer event type, splitting them out from buffered events (bikeshedding this is out of scope for this PR though). For prior art: [`bevy_eventlistener`](https://github.com/aevyrie/bevy_eventlistener) used [`On`](https://docs.rs/bevy_eventlistener/latest/bevy_eventlistener/event_listener/struct.On.html) for its event listener type. Though in our case, the observer is the event listener, and `On` is just a type containing information about the triggered event. ## Solution Steal from `bevy_event_listener` by @aevyrie and use `On`. - Rename `Trigger` to `On` - Rename `OnAdd` to `Add` - Rename `OnInsert` to `Insert` - Rename `OnReplace` to `Replace` - Rename `OnRemove` to `Remove` - Rename `OnDespawn` to `Despawn` ## Discussion ### Naming Conflicts?? Using a name like `Add` might initially feel like a very bad idea, since it risks conflict with `core::ops::Add`. However, I don't expect this to be a big problem in practice. - You rarely need to actually implement the `Add` trait, especially in modules that would use the Bevy ECS. - In the rare cases where you *do* get a conflict, it is very easy to fix by just disambiguating, for example using `ops::Add`. - The `Add` event is a struct while the `Add` trait is a trait (duh), so the compiler error should be very obvious. For the record, renaming `OnAdd` to `Add`, I got exactly *zero* errors or conflicts within Bevy itself. But this is of course not entirely representative of actual projects *using* Bevy. You might then wonder, why not use `Added`? This would conflict with the `Added` query filter, so it wouldn't work. Additionally, the current naming convention for observer events does not use past tense. ### Documentation This does make documentation slightly more awkward when referring to `On` or its methods. Previous docs often referred to `Trigger::target` or "sends a `Trigger`" (which is... a bit strange anyway), which would now be `On::target` and "sends an observer `Event`". You can see the diff in this PR to see some of the effects. I think it should be fine though, we may just need to reword more documentation to read better.
141 lines
4.9 KiB
Rust
141 lines
4.9 KiB
Rust
//! Example `no_std` compatible Bevy library.
|
|
|
|
// The first step to a `no_std` library is to add this annotation:
|
|
|
|
#![no_std]
|
|
|
|
// This does 2 things to your crate:
|
|
// 1. It prevents automatically linking the `std` crate with yours.
|
|
// 2. It switches to `core::prelude` instead of `std::prelude` for what is implicitly
|
|
// imported in all modules in your crate.
|
|
|
|
// It is common to want to use `std` when it's available, and fall-back to an alternative
|
|
// implementation which may make compromises for the sake of compatibility.
|
|
// To do this, you can conditionally re-include the standard library:
|
|
|
|
#[cfg(feature = "std")]
|
|
extern crate std;
|
|
|
|
// This still uses the `core` prelude, so items such as `std::println` aren't implicitly included
|
|
// in all your modules, but it does make them available to import.
|
|
|
|
// Because Bevy requires access to an allocator anyway, you are free to include `alloc` regardless
|
|
// of what features are enabled.
|
|
// This gives you access to `Vec`, `String`, `Box`, and many other allocation primitives.
|
|
|
|
extern crate alloc;
|
|
|
|
// Here's our first example of using something from `core` instead of `std`.
|
|
// Since `std` re-exports `core` items, they are the same type just with a different name.
|
|
// This means any 3rd party code written for `std::time::Duration` will work identically for
|
|
// `core::time::Duration`.
|
|
|
|
use core::time::Duration;
|
|
|
|
// With the above boilerplate out of the way, everything below should look very familiar to those
|
|
// who have worked with Bevy before.
|
|
|
|
use bevy::prelude::*;
|
|
|
|
// While this example doesn't need it, a lot of fundamental types which are exclusively in `std`
|
|
// have alternatives in `bevy::platform`.
|
|
// If you find yourself needing a `HashMap`, `RwLock`, or `Instant`, check there first!
|
|
|
|
#[expect(unused_imports, reason = "demonstrating some available items")]
|
|
use bevy::platform::{
|
|
collections::{HashMap, HashSet},
|
|
hash::DefaultHasher,
|
|
sync::{
|
|
atomic::{AtomicBool, AtomicUsize},
|
|
Arc, Barrier, LazyLock, Mutex, Once, OnceLock, RwLock, Weak,
|
|
},
|
|
time::Instant,
|
|
};
|
|
|
|
// Note that `bevy::platform::sync::Arc` exists, despite `alloc::sync::Arc` being available.
|
|
// The reason is not every platform has full support for atomic operations, so `Arc`, `AtomicBool`,
|
|
// etc. aren't always available.
|
|
// You can test for their inclusion with `#[cfg(target_has_atomic = "ptr")]` and other related flags.
|
|
// You can get a more cross-platform alternative from `portable-atomic`, but Bevy handles this for you!
|
|
// Simply use `bevy::platform::sync` instead of `core::sync` and `alloc::sync` when possible,
|
|
// and Bevy will handle selecting the fallback from `portable-atomic` when it is required.
|
|
|
|
/// Plugin for working with delayed components.
|
|
///
|
|
/// You can delay the insertion of a component by using [`insert_delayed`](EntityCommandsExt::insert_delayed).
|
|
pub struct DelayedComponentPlugin;
|
|
|
|
impl Plugin for DelayedComponentPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.register_type::<DelayedComponentTimer>()
|
|
.add_systems(Update, tick_timers);
|
|
}
|
|
}
|
|
|
|
/// Extension trait providing [`insert_delayed`](EntityCommandsExt::insert_delayed).
|
|
pub trait EntityCommandsExt {
|
|
/// Insert the provided [`Bundle`] `B` with a provided `delay`.
|
|
fn insert_delayed<B: Bundle>(&mut self, bundle: B, delay: Duration) -> &mut Self;
|
|
}
|
|
|
|
impl EntityCommandsExt for EntityCommands<'_> {
|
|
fn insert_delayed<B: Bundle>(&mut self, bundle: B, delay: Duration) -> &mut Self {
|
|
self.insert((
|
|
DelayedComponentTimer(Timer::new(delay, TimerMode::Once)),
|
|
DelayedComponent(bundle),
|
|
))
|
|
.observe(unwrap::<B>)
|
|
}
|
|
}
|
|
|
|
impl EntityCommandsExt for EntityWorldMut<'_> {
|
|
fn insert_delayed<B: Bundle>(&mut self, bundle: B, delay: Duration) -> &mut Self {
|
|
self.insert((
|
|
DelayedComponentTimer(Timer::new(delay, TimerMode::Once)),
|
|
DelayedComponent(bundle),
|
|
))
|
|
.observe(unwrap::<B>)
|
|
}
|
|
}
|
|
|
|
#[derive(Component, Deref, DerefMut, Reflect, Debug)]
|
|
#[reflect(Component)]
|
|
struct DelayedComponentTimer(Timer);
|
|
|
|
#[derive(Component)]
|
|
#[component(immutable)]
|
|
struct DelayedComponent<B: Bundle>(B);
|
|
|
|
#[derive(Event)]
|
|
struct Unwrap;
|
|
|
|
fn tick_timers(
|
|
mut commands: Commands,
|
|
mut query: Query<(Entity, &mut DelayedComponentTimer)>,
|
|
time: Res<Time>,
|
|
) {
|
|
for (entity, mut timer) in &mut query {
|
|
timer.tick(time.delta());
|
|
|
|
if timer.just_finished() {
|
|
commands
|
|
.entity(entity)
|
|
.remove::<DelayedComponentTimer>()
|
|
.trigger(Unwrap);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unwrap<B: Bundle>(trigger: On<Unwrap>, world: &mut World) {
|
|
if let Some(mut target) = trigger
|
|
.target()
|
|
.and_then(|target| world.get_entity_mut(target).ok())
|
|
{
|
|
if let Some(DelayedComponent(bundle)) = target.take::<DelayedComponent<B>>() {
|
|
target.insert(bundle);
|
|
}
|
|
}
|
|
|
|
world.despawn(trigger.observer());
|
|
}
|