mirror of
https://github.com/bevyengine/bevy
synced 2024-11-26 06:30:19 +00:00
ed2b8e0f35
# Objective Add basic bubbling to observers, modeled off `bevy_eventlistener`. ## Solution - Introduce a new `Traversal` trait for components which point to other entities. - Provide a default `TraverseNone: Traversal` component which cannot be constructed. - Implement `Traversal` for `Parent`. - The `Event` trait now has an associated `Traversal` which defaults to `TraverseNone`. - Added a field `bubbling: &mut bool` to `Trigger` which can be used to instruct the runner to bubble the event to the entity specified by the event's traversal type. - Added an associated constant `SHOULD_BUBBLE` to `Event` which configures the default bubbling state. - Added logic to wire this all up correctly. Introducing the new associated information directly on `Event` (instead of a new `BubblingEvent` trait) lets us dispatch both bubbling and non-bubbling events through the same api. ## Testing I have added several unit tests to cover the common bugs I identified during development. Running the unit tests should be enough to validate correctness. The changes effect unsafe portions of the code, but should not change any of the safety assertions. ## Changelog Observers can now bubble up the entity hierarchy! To create a bubbling event, change your `Derive(Event)` to something like the following: ```rust #[derive(Component)] struct MyEvent; impl Event for MyEvent { type Traverse = Parent; // This event will propagate up from child to parent. const AUTO_PROPAGATE: bool = true; // This event will propagate by default. } ``` You can dispatch a bubbling event using the normal `world.trigger_targets(MyEvent, entity)`. Halting an event mid-bubble can be done using `trigger.propagate(false)`. Events with `AUTO_PROPAGATE = false` will not propagate by default, but you can enable it using `trigger.propagate(true)`. If there are multiple observers attached to a target, they will all be triggered by bubbling. They all share a bubbling state, which can be accessed mutably using `trigger.propagation_mut()` (`trigger.propagate` is just sugar for this). You can choose to implement `Traversal` for your own types, if you want to bubble along a different structure than provided by `bevy_hierarchy`. Implementers must be careful never to produce loops, because this will cause bevy to hang. ## Migration Guide + Manual implementations of `Event` should add associated type `Traverse = TraverseNone` and associated constant `AUTO_PROPAGATE = false`; + `Trigger::new` has new field `propagation: &mut Propagation` which provides the bubbling state. + `ObserverRunner` now takes the same `&mut Propagation` as a final parameter. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Torstein Grindvik <52322338+torsteingrindvik@users.noreply.github.com> Co-authored-by: Carter Anderson <mcanders1@gmail.com>
125 lines
4.7 KiB
Rust
125 lines
4.7 KiB
Rust
//! Demonstrates how to propagate events through the hierarchy with observers.
|
|
|
|
use std::time::Duration;
|
|
|
|
use bevy::{log::LogPlugin, prelude::*, time::common_conditions::on_timer};
|
|
use rand::{seq::IteratorRandom, thread_rng, Rng};
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins((MinimalPlugins, LogPlugin::default()))
|
|
.add_systems(Startup, setup)
|
|
.add_systems(
|
|
Update,
|
|
attack_armor.run_if(on_timer(Duration::from_millis(200))),
|
|
)
|
|
// Add a global observer that will emit a line whenever an attack hits an entity.
|
|
.observe(attack_hits)
|
|
.run();
|
|
}
|
|
|
|
// In this example, we spawn a goblin wearing different pieces of armor. Each piece of armor
|
|
// is represented as a child entity, with an `Armor` component.
|
|
//
|
|
// We're going to model how attack damage can be partially blocked by the goblin's armor using
|
|
// event bubbling. Our events will target the armor, and if the armor isn't strong enough to block
|
|
// the attack it will continue up and hit the goblin.
|
|
fn setup(mut commands: Commands) {
|
|
commands
|
|
.spawn((Name::new("Goblin"), HitPoints(50)))
|
|
.observe(take_damage)
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn((Name::new("Helmet"), Armor(5)))
|
|
.observe(block_attack);
|
|
parent
|
|
.spawn((Name::new("Socks"), Armor(10)))
|
|
.observe(block_attack);
|
|
parent
|
|
.spawn((Name::new("Shirt"), Armor(15)))
|
|
.observe(block_attack);
|
|
});
|
|
}
|
|
|
|
// This event represents an attack we want to "bubble" up from the armor to the goblin.
|
|
#[derive(Clone, Component)]
|
|
struct Attack {
|
|
damage: u16,
|
|
}
|
|
|
|
// We enable propagation by implementing `Event` manually (rather than using a derive) and specifying
|
|
// two important pieces of information:
|
|
impl Event for Attack {
|
|
// 1. Which component we want to propagate along. In this case, we want to "bubble" (meaning propagate
|
|
// from child to parent) so we use the `Parent` component for propagation. The component supplied
|
|
// must implement the `Traversal` trait.
|
|
type Traversal = Parent;
|
|
// 2. We can also choose whether or not this event will propagate by default when triggered. If this is
|
|
// false, it will only propagate following a call to `Trigger::propagate(true)`.
|
|
const AUTO_PROPAGATE: bool = true;
|
|
}
|
|
|
|
/// An entity that can take damage.
|
|
#[derive(Component, Deref, DerefMut)]
|
|
struct HitPoints(u16);
|
|
|
|
/// For damage to reach the wearer, it must exceed the armor.
|
|
#[derive(Component, Deref)]
|
|
struct Armor(u16);
|
|
|
|
/// A normal bevy system that attacks a piece of the goblin's armor on a timer.
|
|
fn attack_armor(entities: Query<Entity, With<Armor>>, mut commands: Commands) {
|
|
let mut rng = rand::thread_rng();
|
|
if let Some(target) = entities.iter().choose(&mut rng) {
|
|
let damage = thread_rng().gen_range(1..20);
|
|
commands.trigger_targets(Attack { damage }, target);
|
|
info!("⚔️ Attack for {} damage", damage);
|
|
}
|
|
}
|
|
|
|
fn attack_hits(trigger: Trigger<Attack>, name: Query<&Name>) {
|
|
if let Ok(name) = name.get(trigger.entity()) {
|
|
info!("Attack hit {}", name);
|
|
}
|
|
}
|
|
|
|
/// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage.
|
|
fn block_attack(mut trigger: Trigger<Attack>, armor: Query<(&Armor, &Name)>) {
|
|
let (armor, name) = armor.get(trigger.entity()).unwrap();
|
|
let attack = trigger.event_mut();
|
|
let damage = attack.damage.saturating_sub(**armor);
|
|
if damage > 0 {
|
|
info!("🩸 {} damage passed through {}", damage, name);
|
|
// The attack isn't stopped by the armor. We reduce the damage of the attack, and allow
|
|
// it to continue on to the goblin.
|
|
attack.damage = damage;
|
|
} else {
|
|
info!("🛡️ {} damage blocked by {}", attack.damage, name);
|
|
// Armor stopped the attack, the event stops here.
|
|
trigger.propagate(false);
|
|
info!("(propagation halted early)\n");
|
|
}
|
|
}
|
|
|
|
/// A callback on the armor wearer, triggered when a piece of armor is not able to block an attack,
|
|
/// or the wearer is attacked directly.
|
|
fn take_damage(
|
|
trigger: Trigger<Attack>,
|
|
mut hp: Query<(&mut HitPoints, &Name)>,
|
|
mut commands: Commands,
|
|
mut app_exit: EventWriter<bevy::app::AppExit>,
|
|
) {
|
|
let attack = trigger.event();
|
|
let (mut hp, name) = hp.get_mut(trigger.entity()).unwrap();
|
|
**hp = hp.saturating_sub(attack.damage);
|
|
|
|
if **hp > 0 {
|
|
info!("{} has {:.1} HP", name, hp.0);
|
|
} else {
|
|
warn!("💀 {} has died a gruesome death", name);
|
|
commands.entity(trigger.entity()).despawn_recursive();
|
|
app_exit.send(bevy::app::AppExit::Success);
|
|
}
|
|
|
|
info!("(propagation reached root)\n");
|
|
}
|