 ed2b8e0f35
			
		
	
	
		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>
		
	
			
		
			
				
	
	
		
			126 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			126 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");
 | |
| }
 |