#How to handle heterogeneous behaviors for homogeneous components

1 messages · Page 1 of 1 (latest)

tired rapids
#

Hi everyone,

I'm looking for a better way to associate unique logic/behaviors with entities that share the same interaction trigger.

For example, i have different Sprites sharing a common interaction (e.g., "On<Pointer<Press>>"), but the actual resulting behavior varies significantly (e.g., opening a unique UI menu, triggering a specific script, etc.).

I want to avoid a centralized "god-system" with a massive match statement, as it violates the Open/Closed Principle and becomes a maintenance burden for data-driven content.

How to keep the UI interaction logic decoupled from the game logic?
Any advice or pointers to existing patterns would be greatly appreciated. Thanks!

scarlet tulip
#

I usually make the that On<Pointer<Press>> observer emit my own SomethingWasClick entity event. Then whatever drives your different behaviors, I assume different components, they have On<Add/Insert> hooks for themselves and they then add their own observers that drive their own logic. So On<Press> on entity -> Custom click event is dispatched -> All different behvaiors on the entity listen for it to do their own stuff.

It can be further extended with children. For example imagine that you have something that gives out rewards after completion. Each reward is a separate entity connected via custom relationship. Then when completion is triggered, you iterate over rewards in the relation and trigger completion event on each child(so the event contain information which reward is triggered and can execute rewarding logic - and each reward has it's own observer logic to execute).

That usually allows me to separate each 'logical' part into it's own observer but sometimes creates long chains of triggers. Good for ui, not so much for hot-looping.

noble gate
#

can just use a component for each behavior, then query for them on the target entity in several observers or schedule systems

noble gate
scarlet tulip
#

Just keeps it contained. Pointer<_> needs guard, which button pressed, what is the state of the game etc. So one observer does validation and then triggers 'yep clicked, everyone do your stuff'

tired rapids
#

Thanks for the insight, Arrekin! To make sure I fully grasp your pattern, is this a correct representation of your architecture in pseudocode?

#[derive(EntityEvent)]
pub struct SomethingWasClicked {
    pub entity: Entity,
}

app.observe(|click: On<Pointer<Press>>, mut commands: Commands| {
    if click.button != PointerButton::Primary { return; }
    commands.trigger(SomethingWasClicked { entity: click.entity });
});

app.world_mut().register_component_hooks::<BarracksBehavior>()
    .on_add(|mut world, entity, _| {
        world.commands().entity(entity).observe(|_: On<SomethingWasClicked>| {
            open_barracks_ui();
        });
    });

app.world_mut().register_component_hooks::<FactoryBehavior>()
    .on_add(|mut world, entity, _| {
        world.commands().entity(entity).observe(|_: On<SomethingWasClicked>| {
            open_factory_ui();
        });
    });

commands.spawn((..., BarracksBehavior));
commands.spawn((..., FactoryBehavior));
scarlet tulip
#

I do it more localised, something like this:

#[derive(EntityEvent)]
pub struct SomethingWasClicked {
    pub entity: Entity,
}

#[derive(Component)] struct SomeMarker;

fn some_system_spawing_some_marker(...) {
    commands.spawn(SomeMarker, Behavior1, Bahavior2).observe(
      on_pointer_press_observer_that_trigger_something_was_clicked
    );
}

#[derive(Compoent)] struct Behavior1;
impl Behavior1 {
  fn on_insert_add_observer(trigger: On<Insert, Behavior1>, ...) {
      commands.entity(trigger.entity).observe(your_observer_that_waits_for_something_was_clicked_and_does_its_own_thing);
  }
  // You can add on remove to deregister the observer in On<Remove> if you want
  // Impl block purely for structure
 }

// Behavior 2 the same, just registering different observer.

You can mix it in many ways. You can make child entities that either listen on themselves, or attach observers to parents. You can add behavior enums if you want it configurable(attach enum component then in On<insert> match and attach the right observer and then remove the config component -> that decouples your spawn points from knowing about all the different observers you have, instead exposing enum to select it. I'm using builder pattern where I spawn builder, builder in On<Add> removes itself while adding all the required components for real entity. I find it better than Bundles. And I usually use standard triggers/observers not hooks, so I can write my queries normally instead via World