Skip to content

Observers that can observe multiple different event types #14649

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ItsDoot opened this issue Aug 8, 2024 · 6 comments · May be fixed by #14919
Open

Observers that can observe multiple different event types #14649

ItsDoot opened this issue Aug 8, 2024 · 6 comments · May be fixed by #14919
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! D-Unsafe Touches with unsafe code in some way S-Ready-For-Implementation This issue is ready for an implementation PR. Go for it!

Comments

@ItsDoot
Copy link
Contributor

ItsDoot commented Aug 8, 2024

What problem does this solve or what need does it fill?

Being able to trigger the same observer with multiple different event types would be useful for code reuse and centralizing related logic. This is different from enum Event types, which don't allow you to listen to individual variants for separate observers.

What solution would you like?

#[derive(Event)]
struct FooEvent { foo: i32 }
#[derive(Event)]
struct BarEvent { bar: bool }

// I imagine `Or<(A, B)>` would deref into some `enum Or2<A, B> { A(A), B(B) }`
// `Or<(A, B, C)>` would deref into some `enum Or3<A, B, C> { A(A), B(B), C(C) }`
// etc
// Also, I imagine we'll need a separate `world.observe_any_of()`-style method to correctly handle the internal details
world.observe(|trigger: Trigger<Or<(FooEvent, BarEvent)>>| {
    match trigger.event() {
        Or2::A(FooEvent { foo }) => { /* do something with foo... */ },
        Or2::B(BarEvent { bar }) => { /* do something with bar... */ },
    }
});

What alternative(s) have you considered?

Use an enum:

#[derive(Event)]
enum MyEvent {
    Foo { foo: i32 },
    Bar { bar: bool },
}

world.observe(|trigger: Trigger<MyEvent>| {
    match trigger.event() {
        MyEvent::Foo { foo } => { /* ... */ },
        MyEvent::Bar { bar } => { /* ... */ },
    }
});

However, that prevents us from listening to only 1 of the event types, or a subset of them.

Additional context

This was mentioned on discord as currently possible, albeit with unsafe APIs, so we should introduce a safe wrapper:

Diddykonga — Today at 8:41 PM
Can observers 'observe' multiple events? if not, then the query would need to be split from the observer at some point.
James 🦃 — Today at 9:02 PM
Observers can absolutely observe multiple events
(unsafely)
nth — Today at 9:03 PM
What does the trigger return for the event?
James 🦃 — Today at 9:04 PM
You have to use unsafe APIs
So you can make the trigger return any type and you just have to promise it's safe
The observer for a query should set the ObserverRunner manually to not pay for system overhead
Diddykonga — Today at 9:06 PM
Right since you cant carry that information via generics, unsafe is the only way (variadics when?)
James 🦃 — Today at 9:06 PM
And the ObserverRunner API just gets a pointer and deferred world
(similar to a hook)
doot — Today at 9:13 PM
I wonder if you could wrap that safely in a Trigger<Either<A, B>> type api
actually probably something like a Trigger<Or<(A, B, C, ...)>>

@ItsDoot ItsDoot added C-Feature A new feature, making something new possible S-Needs-Triage This issue needs to be labelled labels Aug 8, 2024
@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events and removed S-Needs-Triage This issue needs to be labelled labels Aug 8, 2024
@alice-i-cecile alice-i-cecile moved this to Active: engine observers and hooks in Alice's Work Planning Aug 8, 2024
@ItsDoot ItsDoot added the D-Unsafe Touches with unsafe code in some way label Aug 8, 2024
@cart
Copy link
Member

cart commented Aug 8, 2024

Rather than implement a new "query style" API, we could also consider:

#[derive(Event)]
struct Foo { foo: i32 }
#[derive(Event)]
struct Bar { bar: bool }


#[derive(Event)]
enum Combined {
    Foo(Foo),
    Bar(Bar),
}

// Pretty sure this type elision works out
world
  .observe(trigger_map(Combined::Foo))
  .observe(trigger_map(Combined::Bar))

world.observe(|trigger: Trigger<Combined>| {
    match trigger.event() {
        MyEvent::Foo { foo } => { /* ... */ },
        MyEvent::Bar { bar } => { /* ... */ },
    }
});

@cart
Copy link
Member

cart commented Aug 8, 2024

Notably, this would require essentially no changes. Just an implementation of the trigger_map observer system, which would watch for the wrapped type and trigger the Combined enum type.

@cart
Copy link
Member

cart commented Aug 8, 2024

Just put together a PR for consideration, given how simple this was to implement.

@cart
Copy link
Member

cart commented Aug 9, 2024

Before we go too deep down this rabbit hole, can we discuss actual use cases? It would be good to come up with some real-world scenarios to illustrate why this is necessary.

@Azorlogh
Copy link
Contributor

Here is a real-world use case that came up for me:
I have Highlight component whose value is derived from the presences of two marker components Hovered and Selected. It will only need to change if either Has<Hovered> or Has<Selected> changes. I currently have that logic attached to 4 observers: Trigger<OnAdd, Hovered>, Trigger<OnRemove, Hovered>, Trigger<OnAdd, Selected>, Trigger<OnRemove, Selected>, but the logic inside them is the same.
With #14664, I would be able to create a combined trigger HoverOrSelectionChanged to write it as a single observer.

If I understand correctly, #14674 would not solve this particular case since I couldn't listen to two components at once?

@maniwani
Copy link
Contributor

maniwani commented Aug 27, 2024

This is just an observation, but if the eventual plan is to use observers to keep query caches updated, those observers would know when archetypes start/stop matching and could share those events as triggers for other observers.

In the use case described in the previous comment, a hypothetical observer keeping some Query<(With<Hovered>, With<Selected>)> in sync with the world would be in an ideal position to report the relevant changes.

You might point out that ambiguity between many same/similar queries would be an issue, but that issue would simply disappear if both components and queries become entities (and Entity replaces ComponentId).

We can see that asking if entity e has T is equivalent to asking if e matches Query<With<T>>. Similarly, e beginning or ceasing to match Query<With<T>> coincides with adding or removing T (true for bundles as well).

If components and queries are both entities, you could actually start thinking of queries as "inferred components" (more commonly known as rules), i.e. started matching Q and stopped matching Q can be treated as added "component" Q and removed "component" Q events for observers to subscribe to, where Q is a specific entity (avoiding ambiguity).

flecs has a pattern like this it calls "monitors" and an interesting way people use it is to subscribe to queries that express "invalid" bundles. That way, they can see if entities are getting into bad states.

@BenjaminBrienen BenjaminBrienen added D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Ready-For-Implementation This issue is ready for an implementation PR. Go for it! labels Jan 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! D-Unsafe Touches with unsafe code in some way S-Ready-For-Implementation This issue is ready for an implementation PR. Go for it!
Projects
None yet
6 participants