mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
eda118d033
There is general consensus that our terminology for Events, "entity events", Observers, and BufferedEvents needs clarity. Additionally, many of us also agree that the current Observer system would benefit from additional static-ness: currently it is assumed that you can use events in pretty much any context, and they all go through the exact same code path. Alice put forth a proposal to [Overhaul Observers](https://hackmd.io/@bevy/rk4S92hmlg), and we have already partially implemented it for 0.17. I think it does a great job of outlining many of the issues at play, and it solves them reasonably well. But I _also_ think the proposed solution isn't yet ideal. Given that it is already partially implemented for 0.17, it is a breaking change, _and_ given that we have already broken the Observer API a number of times, I think we need to sort this out before the next release. This is a big changeset, but it is _largely_ just a reframing of what is already there. I haven't fundamentally changed the behaviors. I've just refined and constrained in a way that allows us to do what we are currently doing in a clearer, simpler, and more performant way. First, I'll give some quick notes on Alice's proposal (which you all should read if you haven't yet!): ### Notes on Alice's Proposal - I like the move toward a more static API - I think we've gone too far down the "separate terminology" path. The proposal introduces a zoo of apis, terms, and "subterms". I think we need to simplify our concepts and names to make this all easier to talk about and use in practice. - BroadcastEvent feels like the wrong name. EntityEvent is also "broadcast" in the exact same way - BufferedEvent is a completely different system than EntityEvent and BroadcastEvent. This muddles concepts too much. It needs its own standalone, single-word concept name. - "Universal observers": I think this should be fully context driven, rather than needing encoding in the API. - I agree we can't get rid of buffered events, and that merging them with "broadcast events" isn't helpful - I'm not quite sure how we'd make the proposed PropagateEvent subtrait work transparently. This can't be "layered on top" as a trait. It needs to be baked in at more fundamental level. * I don't like `app.add_broadcast_observers()`, `app.add_universal_observers()`, `Observer::entity_observer`, `Observer::broadcast`, etc. The `On` event should statically determine whether an observer is an "entity observer" or a "broadcast" Observer. This would already be encoded in the type system and is therefore something we can do on the developer's behalf. Likewise, any observer being registered at a top level is inherently _not_ a specific entity observer. All of these variants serve to make users guess and poke around in a way that is unnecessary. I want simple one word concept names, single constructors, etc. ### Proposed Principals - Static-ness: - Events should only be usable in the context they were defined to be used. - When triggered, Observers should *only* have access to fields and behaviors that are relevant: - Dont return Option or PLACEHOLDER: the field or function shouldn't exist - Entity events that don't support propagation shouldn't expose that functionality - Don't do unnecessary work at runtime - Event triggers shouldn't branch through every potential event code path - Don't clone potentially large lists of event context unnecessarily (Ex: we currently clone the component list for every observer invocation) - Minimize codegen - Don't recompile things redundantly. - Don't compile unnecessary code paths. - Clear and Simple - Minimize the number of concept names floating around, and lock each concept down heavily to a specific context - I'm convinced at this point that "buffered events" and "observer events" sharing concept names is wrong. We need two clean and clear terms, and I'm willing to give "buffered events" a slightly worse name if it means "observer events" can be nicer. - Don't throw the concept name "Event" out ... it is a very good name. Instead, constrain it to one specific thing. - Minimize our API surface - Events contain all context, including what used to be the "target". This lets people define the "target" name that makes the most sense for the context, and lets the documentation fully describe the context of that "target". ### Concepts - **Event** (the thing you "observe") - Rationale: "Event" is the clear choice for this concept. An "event" feels like something that happens in real time. "Event observers" are things that observe events when they occur (are triggered). Additionally, this is the concept that "propagates", and "event propagation" is a term people understand. - **Trigger**: (the verb that "causes" events to happen for targets). Events are Triggered. This can include additional context/ data that is passed to observers / informs the trigger behavior. Events have _exactly_ one Trigger. If you want a different trigger behavior, define a new event. This makes the system more static, more predictable, and easier to understand and document. `world.trigger_ref_with` makes it possible to pass in mutable reference to your own Trigger data, making it possible to customize the input trigger data and read out the final trigger data. - **Observer** (the thing that "observes" events): An event's `Trigger` determines which observers will run. - **Event Types**: You can build any "type" of event. The concept of a "target" has been removed. Instead, define a `Trigger` that expects a specific kind of event (ex: `E: EntityEvent`). - **EntityEvent** We add a new `EntityEvent` trait, which defines an `event.entity()` accessor. This is used by the `Trigger` impls : `EntityTrigger`, `PropagateEntityTrigger`, and `EntityComponentsTrigger`. - **Message** (the buffered thing you "read" and "write") - `Message` is a solid metaphor for what this is ... it is data that is written and then at some later point read by someone / something else. I expect existing consumers of "buffered events" to lament this name change, as "event" feels nicer. But having a separate name is within everyone's best interest. - **MessageReader** (the thing that reads messages) - **MessageWriter** (the thing that writes messages) ### The Changes - `Event` trait changes - Event is now used exclusively by Observers - Added `Event::Trigger`, which defines what trigger implementation this event will use - Added the `Trigger` trait - All of the shared / hard-coded observer trigger logic has been broken out into individual context-specific Trigger traits. - "Trigger Targets" have been removed. - Instead, Events, in combination with their Trigger impl, decide how they will be triggered. In general, this means that Events now include their "targets" as fields on the event. - APIs like `trigger_targets` have been replaced by `trigger`, which can now be used for any `Event` - `EntityEvent` trait changes - Propagation config has been removed from the `EntityEvent` trait. It now lives on the `Trigger` trait (specifically the `PropagateEntityTrigger` trait). - `EntityEvent` now provides `entity / entity_mut` accessors for the Event it is implemented for - `EntityEvent` defaults to having no propagation (uses the simpler `EntityTrigger`) - `#[entity_event(propagate)]` enables the "default" propagation logic (uses ChildOf). The existing `#[entity_event(traversal = X)]` has been renamed to `#[entity_event(propagate = X)` - Deriving `EntityEvent` requires either a single `MyEvent(Entity)`, the `entity` field name (`MyEvent { entity: Entity}`), or `MyEvent { #[event_entity] custom: Entity }` - Animation event changes - Animation events now have their own `AnimationEvent` trait, which sets the `AnimationEventTrigger`. This allows developers to pass in events that _dont_ include the Entity field (as this is set by the system). The custom trigger also opens the doors to cheaply passing in additional animation system context, accessible through `On` - `EntityComponentsTrigger` - The built in Add/Remove/etc lifecycle events now use the `EntityComponentsTrigger`, which passes in the components as additional state. This _significantly_ cuts down on clones, as it does a borrow rather than cloning the list into _each_ observer execution. - Each event now has an `entity` field. - Style changes - Prefer the event name for variables: `explode: On<Explode>` not `event: On<Explode>` - Prefer using the direct field name for the entity on entity events, rather than `event.entity()`. This allows us to use more specific names where appropriate, provides better / more contextual docs, and coaches developers to think of `On<MyEvent>` _as_ the event itself. Take a look at the changes to the examples and the built-in events to see what this looks like in practice. ### Downsides - Moving the "target" into the event adds some new constraints: - Triggering the same event for multiple entities requires multiple trigger calls. For "expensive" events (ex: lots of data attached to the event), this will be more awkward. Your options become: - Create multiple instances of the event, cloning the expensive data - Use `trigger_ref`, and mutate the event on each call to change the target. - Move the "expensive" shared data into the Trigger, and use `trigger_ref_with`` - We could build a new EntityEvent method that abstracts over the "event mutation" behavior and provides something like the old `trigger_target` behavior. - Use a different `EntityTargetTrigger` (not currently provided by bevy, but we could), which brings back the old behavior. This would be used with `trigger_with` to replicate the old pattern: `world.trigger_with(MyEvent, [e1, e2].into())` (or we could make the `into()` implicit) - Bubbling the event involves mutating the event to set the entity. This means that `trigger_ref` will result in the event's `EntityEvent::entity()` being the final bubbled entity instead of the initial entity. - Some APIs (trivially) benefit from the "target entity" being separate from the event. Specifically, this new API requires changes to the "Animation Event" system in AnimationPlayer. I think this is actually a good change set, as it allows us to: - Cheaply expose more animation state as part of a new AnimationEventTrigger impl - Move that "implict" entity target provided by the AnimationPlayer into the AnimationEventTrigger - Encode the "animation event trigger-ness" of the event into the type itself (by requiring `#[event(trigger = AnimationEventTrigger)]`) - By not implementing Default for AnimationEventTrigger, we can block animation events from being fired manually by the user. ### Draft TODO - [x] Fill in documentation and update existing docs - [ ] Benchmark: I expect this impl to be significantly faster. There might also be tangible binary size improvements, as I've removed a lot of redundant codegen. - [x] Update release notes and migration guides ### Next Steps - The `BufferedEvent -> Message` rename was not included to keep the size down. Fixes #19648 --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
163 lines
5.8 KiB
Rust
163 lines
5.8 KiB
Rust
//! Demonstrates picking for sprites and sprite atlases.
|
|
//! By default, the sprite picking backend considers a sprite only when a pointer is over an opaque pixel.
|
|
|
|
use bevy::{prelude::*, sprite::Anchor};
|
|
use std::fmt::Debug;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
|
|
.add_systems(Startup, (setup, setup_atlas))
|
|
.add_systems(Update, (move_sprite, animate_sprite))
|
|
.run();
|
|
}
|
|
|
|
fn move_sprite(
|
|
time: Res<Time>,
|
|
mut sprite: Query<&mut Transform, (Without<Sprite>, With<Children>)>,
|
|
) {
|
|
let t = time.elapsed_secs() * 0.1;
|
|
for mut transform in &mut sprite {
|
|
let new = Vec2 {
|
|
x: 50.0 * ops::sin(t),
|
|
y: 50.0 * ops::sin(t * 2.0),
|
|
};
|
|
transform.translation.x = new.x;
|
|
transform.translation.y = new.y;
|
|
}
|
|
}
|
|
|
|
/// Set up a scene that tests all sprite anchor types.
|
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
commands.spawn(Camera2d);
|
|
|
|
let len = 128.0;
|
|
let sprite_size = Vec2::splat(len / 2.0);
|
|
|
|
commands
|
|
.spawn((Transform::default(), Visibility::default()))
|
|
.with_children(|commands| {
|
|
for (anchor_index, anchor) in [
|
|
Anchor::TOP_LEFT,
|
|
Anchor::TOP_CENTER,
|
|
Anchor::TOP_RIGHT,
|
|
Anchor::CENTER_LEFT,
|
|
Anchor::CENTER,
|
|
Anchor::CENTER_RIGHT,
|
|
Anchor::BOTTOM_LEFT,
|
|
Anchor::BOTTOM_CENTER,
|
|
Anchor::BOTTOM_RIGHT,
|
|
]
|
|
.iter()
|
|
.enumerate()
|
|
{
|
|
let i = (anchor_index % 3) as f32;
|
|
let j = (anchor_index / 3) as f32;
|
|
|
|
// Spawn black square behind sprite to show anchor point
|
|
commands
|
|
.spawn((
|
|
Sprite::from_color(Color::BLACK, sprite_size),
|
|
Transform::from_xyz(i * len - len, j * len - len, -1.0),
|
|
Pickable::default(),
|
|
))
|
|
.observe(recolor_on::<Pointer<Over>>(Color::srgb(0.0, 1.0, 1.0)))
|
|
.observe(recolor_on::<Pointer<Out>>(Color::BLACK))
|
|
.observe(recolor_on::<Pointer<Press>>(Color::srgb(1.0, 1.0, 0.0)))
|
|
.observe(recolor_on::<Pointer<Release>>(Color::srgb(0.0, 1.0, 1.0)));
|
|
|
|
commands
|
|
.spawn((
|
|
Sprite {
|
|
image: asset_server.load("branding/bevy_bird_dark.png"),
|
|
custom_size: Some(sprite_size),
|
|
color: Color::srgb(1.0, 0.0, 0.0),
|
|
..default()
|
|
},
|
|
anchor.to_owned(),
|
|
// 3x3 grid of anchor examples by changing transform
|
|
Transform::from_xyz(i * len - len, j * len - len, 0.0)
|
|
.with_scale(Vec3::splat(1.0 + (i - 1.0) * 0.2))
|
|
.with_rotation(Quat::from_rotation_z((j - 1.0) * 0.2)),
|
|
Pickable::default(),
|
|
))
|
|
.observe(recolor_on::<Pointer<Over>>(Color::srgb(0.0, 1.0, 0.0)))
|
|
.observe(recolor_on::<Pointer<Out>>(Color::srgb(1.0, 0.0, 0.0)))
|
|
.observe(recolor_on::<Pointer<Press>>(Color::srgb(0.0, 0.0, 1.0)))
|
|
.observe(recolor_on::<Pointer<Release>>(Color::srgb(0.0, 1.0, 0.0)));
|
|
}
|
|
});
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct AnimationIndices {
|
|
first: usize,
|
|
last: usize,
|
|
}
|
|
|
|
#[derive(Component, Deref, DerefMut)]
|
|
struct AnimationTimer(Timer);
|
|
|
|
fn animate_sprite(
|
|
time: Res<Time>,
|
|
mut query: Query<(&AnimationIndices, &mut AnimationTimer, &mut Sprite)>,
|
|
) {
|
|
for (indices, mut timer, mut sprite) in &mut query {
|
|
let Some(texture_atlas) = &mut sprite.texture_atlas else {
|
|
continue;
|
|
};
|
|
|
|
timer.tick(time.delta());
|
|
|
|
if timer.just_finished() {
|
|
texture_atlas.index = if texture_atlas.index == indices.last {
|
|
indices.first
|
|
} else {
|
|
texture_atlas.index + 1
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
fn setup_atlas(
|
|
mut commands: Commands,
|
|
asset_server: Res<AssetServer>,
|
|
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
|
|
) {
|
|
let texture_handle = asset_server.load("textures/rpg/chars/gabe/gabe-idle-run.png");
|
|
let layout = TextureAtlasLayout::from_grid(UVec2::new(24, 24), 7, 1, None, None);
|
|
let texture_atlas_layout_handle = texture_atlas_layouts.add(layout);
|
|
// Use only the subset of sprites in the sheet that make up the run animation
|
|
let animation_indices = AnimationIndices { first: 1, last: 6 };
|
|
commands
|
|
.spawn((
|
|
Sprite::from_atlas_image(
|
|
texture_handle,
|
|
TextureAtlas {
|
|
layout: texture_atlas_layout_handle,
|
|
index: animation_indices.first,
|
|
},
|
|
),
|
|
Transform::from_xyz(300.0, 0.0, 0.0).with_scale(Vec3::splat(6.0)),
|
|
animation_indices,
|
|
AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
|
Pickable::default(),
|
|
))
|
|
.observe(recolor_on::<Pointer<Over>>(Color::srgb(0.0, 1.0, 1.0)))
|
|
.observe(recolor_on::<Pointer<Out>>(Color::srgb(1.0, 1.0, 1.0)))
|
|
.observe(recolor_on::<Pointer<Press>>(Color::srgb(1.0, 1.0, 0.0)))
|
|
.observe(recolor_on::<Pointer<Release>>(Color::srgb(0.0, 1.0, 1.0)));
|
|
}
|
|
|
|
// An observer that changes the target entity's color.
|
|
fn recolor_on<E: EntityEvent + Debug + Clone + Reflect>(
|
|
color: Color,
|
|
) -> impl Fn(On<E>, Query<&mut Sprite>) {
|
|
move |ev, mut sprites| {
|
|
let Ok(mut sprite) = sprites.get_mut(ev.event_target()) else {
|
|
return;
|
|
};
|
|
sprite.color = color;
|
|
}
|
|
}
|