Skip to content
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

Pattern: Behaviors #16

Open
abesto opened this issue Dec 9, 2023 · 0 comments
Open

Pattern: Behaviors #16

abesto opened this issue Dec 9, 2023 · 0 comments

Comments

@abesto
Copy link

abesto commented Dec 9, 2023

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(),
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant