GithubHelp home page GithubHelp logo

bevy_eventlistener's Introduction

Event listeners, bubbling, and callbacks for Bevy

CI crates.io docs.rs Bevy tracking

An implementation of event listeners and callbacks, allowing you to define behavior with components.

Overview

  • Define custom events that can target entities.
  • Add event listener components that run callbacks when the specified event reaches that entity.
  • Define callbacks as normal bevy systems.
  • Events bubble up entity hierarchies, allowing you to attach behavior to trees of entities. For example, you could put a single event listener on the root entity of a scene, that runs a callback if any child entity is clicked on. This works because events that target the child of a scene will bubble up the hierarchy until an event listener is found.

Example

Taken from the minimal example, here we have a goblin wearing a few pieces of armor. An Attack event can target any of these entities. If an Attack reaches a piece of armor, the armor will try to absorb the attack. Any damage it is not able to absorb will bubble to the goblin wearing the armor.

commands
    .spawn((
        Name::new("Goblin"),
        HitPoints(50),
        On::<Attack>::run(take_damage),
    ))
    .with_children(|parent| {
        parent.spawn((
            Name::new("Helmet"),
            Armor(5),
            On::<Attack>::run(block_attack),
        ));
        parent.spawn((
            Name::new("Socks"),
            Armor(10),
            On::<Attack>::run(block_attack),
        ));
    });

UI

This library is intended to be upstreamed to bevy for use in making interactive UI. However, as demonstrated above, event bubbling is applicable to any kind of event that needs to traverse an entity hierarchy. This follows the basic principles of ECS patterns: it works on any entity with the required components, not just UI.

This library was initially extracted from the 0.13 version of bevy_mod_picking, as it became obvious that this is a generically useful feature.

Performance

Using DOM data from the most complex websites I could find, the stress test example was built to help benchmark the performance of this implementation with a representative dataset. Using a DOM complexity:

  • Depth: 64 (how many levels of children for an entity at the root)
  • Total nodes: 12,800 (total number of entities spawned)
  • Listener density: 20% (what percent of entities have event listeners?)

image

The blue line can be read as "how long does it take all of these events to bubble up a hierarchy and trigger callbacks at ~20% of the 64 nodes as it traverses depth?". A graph is built for every event as an acceleration structure, which allows us to have linearly scaling performance.

The runtime cost of each event decreases as the total number of events increase, this is because graph construction is a fixed cost for each type of event. Adding more events simply amortizes that cost across more events. At 50 events the runtime cost is only ~500ns/event, and about 25us total. To reiterate, this is using an entity hierarchy similar to the most complex websites I could find.

Bevy version support

bevy bevy_eventlistener
0.14 0.8
0.13 0.7
0.12 0.6
0.11 0.5

License

All code in this repository is dual-licensed under either:

at your option. This means you can select the license you prefer.

Your contributions

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

bevy_eventlistener's People

Contributors

aevyrie avatar bonsairobo avatar jnhyatt avatar paul-hansen avatar tristancacqueray avatar vultix avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

bevy_eventlistener's Issues

Running entity systems in fixed update

Hello, is there a way, to run systems from On::run in fixed update?

My usecase is that i want to simulate physics for separate world assigned to separate entities, meaning each entity has a physics world and I want to process the input with bevy_eventlistener and then simulate the physics from that input in fixed update.

Thanks in advance.

Add `On::set_next_state`

This is a common simple pattern that uses a surprising amount of boilerplate. We could simplify this to a single method call.

Event sinking

We currently can traverse up the hierarchy via bubbles, which is great!
I think we can extend this by also allowing for sinking - letting an Event propagate down the tree from the calling node.

While this may prove worse in terms of performance (linearly going up the tree vs recursively going down children) for most situations, there are times when having a parent node and propagating events generated on the parent to all the children is a good design choice.

This feature would work like another trait on EntityEvents, : #[can_sink]

This would not block #[can_bubble], and if you define both then your event gets sent both up and down the tree.

Re-export `ListenerInput`

If I understand correctly, ListenerInput should probably be re-exported with other callbacks here:

pub use bevy_eventlistener_core::{
callbacks::{Listener, ListenerMut},
event_listener::{EntityEvent, On},
EventListenerPlugin,
};

as it's commonly used like in this bevy_mod_picking example:

struct DoSomethingComplex(Entity, f32);

impl From<ListenerInput<Pointer<Down>>> for DoSomethingComplex {
    fn from(event: ListenerInput<Pointer<Down>>) -> Self {
        DoSomethingComplex(event.target, event.hit.depth)
    }
}

calling `stop_propagation()` on immutable events

I was disappointed to discover this won't work:

On::<ScrollWheel>::listener_component_mut::<Scrolling>(move |ev, scrolling| {
    // (Do stuff)
    ev.stop_propagation(); // ERROR: 'ev' is immutable
}),

The problem is that listener_component_mut gives me an immutable listener, rather than a mutable one; most of the other convenience methods have the same limitation. So if I want to call stop_propagation() I have to use run().

I generally want to call stop_propagation() in most handlers, which means that the convenience methods hardly ever get used.

Run once

Consider run_once, where once the callback is executed, remove the On::<T> component (or whatever preferred alternatively to achieve a single call to callback)

Custom events sometimes get eaten

Here's a minimal example that illustrates the issue I discussed last week on the Discord. When I sent a custom event "Clicked" (not "Click"), the first event is sometimes not received:

  • The event is sent properly.
  • The event is always received by the global event reader.
  • The event is NOT received by the On handler, but only the first time.
  • Subsequent events are properly received by the On handler as expected.

The problem is intermittent - you may have to run the example multiple times in order to see it. For me, happens about 50% of the time, so it shouldn't take many runs to see it.

When it is not working, you'll see output like this:

--> Sending Clicked id='One' target=4v0
? Reading global clicked: id='One' target=4v0

And when it is working, you'll see output like this:

--> Sending Clicked id='One' target=4v0
<-- Received Clicked Button id='One' target=4v0
? Reading global clicked: id='One' target=4v0

main.rs.zip

Upgrade to `bevy` `0.11.0`

Hi there! I'm using this as a standalone crate in a project and looking to update Bevy to latest. I'm happy to help migrate the code, just wanted to open this issue to verify that you don't already have a branch out for this. Let me know and I'll get in a pull request ASAP.

Add the ability to have multiple callbacks in a single listener

This feature is necessary because there's currently no way to have more than one of a callback of a single type on an entity.

This is usually just a papercut, but it is required in bevy_mod_picking, in order to merge on_click / on_drag etc into a single callback.

In turn, we want to do that in order to handle the ordering of input events correctly„ using bevyengine/bevy#12100

Support targeting multiple entities

Some events intrinsically needs to targets multiple entities.

For example collision events will need to be able to report to multiple entities. You can currently simulate this by cloning the event (which can contain lots of data like a vector of manifolds) and sending them to each entity.

I think the ability to natively target multiple entities would help a lot for this kind of use-case.

Run if feature

Bevy systems have very helpful control flow extension methods called run_if.

Is there a way to use them with this library that is documented? I have tried a simple run_if and am getting a compiler error as shown below.

error[E0277]: the trait bound `NodeConfigs<std::boxed::Box<dyn bevy::prelude::System<In = (), Out = ()>>>: bevy::prelude::IntoSystem<(), (), _>` is not satisfied
  --> src/game/character.rs:99:17
   |
98 |             on_drag_start: On::<Pointer<DragStart>>::run(
   |                            ----------------------------- required by a bound introduced by this call
99 |                 drag_start_character_system.distributive_run_if(tool_active(ToolType::Arrow)),
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `bevy::prelude::IntoSystem<(), (), _>` is not implemented for `NodeConfigs<std::boxed::Box<dyn bevy::prelude::System<In = (), Out = ()>>>`
   |
note: required by a bound in `On::<E>::run`
  --> /Users/stevenbeshensky/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_eventlistener_core-0.6.1/src/event_listener.rs:41:39
   |
41 |     pub fn run<Marker>(callback: impl IntoSystem<(), (), Marker>) -> Self {
   |                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `On::<E>::run`

Add a default impl for `On`

This would be useful to construct bundle types with On components that have no effect.

I'm running into this while trying to construct ButtonBundle equivalents with On<Pointer<Click>> and On<Pointer<Hover>> fields.

Make On clone-able.

If On is clonable it means it can be dynamically shared with other entities. Which is very useful when building out a UI.

Pattern: Behaviors

This crate is awesome! It's solving exactly the problem that was driving me nuts when trying to represent intents & actions in a turn-based roguelike prototype I'm using to play around with bevy.

An architecture for representing behaviors emerged pretty directly from the way bevy_eventlistener is set up. I'm sharing it here because 1. maybe it's useful for someone 2. maybe it's generic enough to be supported with a bit of extra library code, either here or in a separate crate.

Goals

  • Minimal boilerplate and duplication
  • Intents (i.e. "I want to move here", "I want to skip this turn") should be EntityEvents
  • Behaviors triggered by these intents should be listener systems, and fully encapsulate all the logic

What it looks like

Prototype: abesto/treeffect@8bc7403 (this is a commit that moves from intents being components, and behaviors being regular systems)

Intent

Nothing interesting here, just so that the rest makes sense.

#[derive(Clone, Event, EntityEvent)]
pub struct MovementIntent {
    #[target]
    pub actor: Entity,
    pub vector: IVec2,
}

EntityEvents are registered in a plugin with some macro magic for further DRY:

// short for "event listener plugins"
macro_rules! elp {
    ($app:ident, $($events:ident),+ $(,)?) => {
        $($app.add_plugins(EventListenerPlugin::<$events>::default());)+
    };
}

pub struct EventsPlugin;

impl Plugin for EventsPlugin {
    fn build(&self, app: &mut App) {
        app.add_event::<took_turn::TookTurn>();
        elp!(app, MovementIntent, AttackIntent, WaitIntent);
    }
}

On reflection, elp should probably be replaced with an App extention trait with .add_intents or something more generic.

Side-track: that might actually be a meaningful separate feature request. app.add_entity_event::<WaitIntent>() is sensible. It still wouldn't be DRY enough, so I'd probably still have a similar wrapper macro.

Behavior machinery

A trait so we can hang logic somewhere, implemented for tuples (spoiler: think about bundles):

pub trait Behavior {
    fn behavior() -> Self;
}

#[impl_for_tuples(1, 16)]
impl Behavior for BehaviorIdentifier {
    fn behavior() -> Self {
        for_tuples!( ( #( (BehaviorIdentifier::behavior())  ),* ) )
    }
}

Simple Behavior

fn movement(
    mut query: Query<&mut Position, With<Active>>,
    map: Res<Map>,
    intent: Listener<MovementIntent>,
    mut ev_took_turn: EventWriter<TookTurn>,
) {
    let entity = intent.listener();
    let Ok(mut position) = query.get_mut(entity) else {
        return;
    };
    let new_position = map.iclamp(&(position.xy.as_ivec2() + intent.vector));
    if map.is_walkable(&new_position) {
        position.xy = new_position;
    }
    ev_took_turn.send(entity.into());
}

behavior!(movement);

I'm ignoring event propagation here, because I'm not using entity hierarchies. I'm probably picking the wrong one out of (Listener, Target) somewhere.

behavior! is a simple macro that, if I was an adult, I'd implement as a derive macro. It's coupled very tightly to my naming conventions, unsure how feasible this would be as library code. It generates code like this:

pub type MovementBehavior = On<MovementIntent>;

impl Behavior for MovementBehavior {
    fn behavior() -> Self {
        On::<MovementIntent>::run(movement)
    }
}

Composed Behavior

pub type ActorBehavior = (MovementBehavior, AttackBehavior, WaitBehavior);

I guess you could call this a behavior bundle.

Used in a Bundle

You can pretty much figure this out from the rest, but for the record:

type Behaviors = ActorBehavior;  // Could add more specific behaviors here

#[derive(Bundle)]
pub struct PlayerBundle {
    ...
    pub behaviors: Behaviors,
}

impl Default for PlayerBundle {
    fn default() -> Self {
        PlayerBundle {
             ...
            behaviors: Behaviors::behavior(),
        }
    }
}

Does not compile if bevy_reflect's "documentation" feature is enabled after updating to 0.5

I ran into this issue updating the editor for my game where I use the bevy_reflect "documentation" feature to show component documentation for easy access. It used to work with bevy 0.10 and in bevy 0.11 it is now failing with the below error, repeated for every type that impl_reflect_value is used on.

error[E0599]: no method named `with_docs` found for struct `type_info::ValueInfo` in the current scope
   --> /home/paul/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_reflect-0.11.3/src/impls/uuid.rs:6:1
    |
6   |   impl_reflect_value!(::bevy_utils::Uuid(
    |  _-
7   | |     Serialize,
8   | |     Deserialize,
9   | |     Default,
...   |
12  | |     Hash
13  | | ));
    | |  ^
    | |  |
    | |__method not found in `ValueInfo`
    |    in this macro invocation
    |
   ::: /home/paul/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_reflect-0.11.3/src/type_info.rs:177:1
    |
177 |   pub struct ValueInfo {
    |   -------------------- method `with_docs` not found for this struct
    |
   ::: /home/paul/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_reflect_derive-0.11.3/src/lib.rs:381:1
    |
381 |   pub fn impl_reflect_value(input: TokenStream) -> TokenStream {
    |   ------------------------------------------------------------ in this expansion of `impl_reflect_value!`

Minimal Reproduction

  1. Create a new project with this Cargo.toml

    [package]
    name = "bevy_docs_test"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    bevy_eventlistener = "0.5"
    
    bevy_reflect = {version="0.11", features=["documentation"]}
  2. Run cargo run

Changing bevy_reflect to version 0.11 and bevy_eventlistener to 0.4 makes it work again.
Removing the documentation feature from bevy_reflect also makes it work again.

Make `On::send_event` more flexible

    pub fn send_event<F: Event + From<ListenerInput<E>>>() -> Self {

This is the current signature of On::send_event.

However, I want to be able to quickly do things like "send an AppExit event when this button is clicked".

I propose three related methods:

  1. send_event: takes an event of type F and captures it in the closure.
  2. convert_and_send_event: current behavior: converts E to F and sends it.
  3. send_default_event: sends an event with the Default value.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.