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

Alt design pattern with In Out Systems #90

Open
stargazing-dino opened this issue Dec 31, 2023 · 1 comment
Open

Alt design pattern with In Out Systems #90

stargazing-dino opened this issue Dec 31, 2023 · 1 comment

Comments

@stargazing-dino
Copy link
Contributor

stargazing-dino commented Dec 31, 2023

Hiyo ! I was playing around with other possible patterns - mostly out of curiosity and as a learning experience and was recently reading a one shot systems related PR and thought that'd be cool. Anyways, here's how I'd think it'd make sense as a utility ai architecture

pub struct BrainPlugin;

impl Plugin for BrainPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, setup)
            .add_systems(Update, (decide_best_action, sleepiness_tick));
    }
}

#[derive(Component)]
pub struct Thinker;

#[derive(Component)]
pub struct Sleepiness(f32);

#[derive(Bundle)]
pub struct MyActorBundle {
    pub thinker: Thinker,
    pub considerations: Considerations,
    pub action_state: ActionState,
    pub sleepiness: Sleepiness,
}

#[derive(Component)]
pub struct Considerations {
    pub considerations: Vec<Consideration>,
}

#[derive(Component)]
pub enum ActionState {
    Idle,
    Executing,
    Done,
}

pub struct Consideration {
    pub name: String,
    pub scorer: SystemId<Entity, f32>,
    pub action: SystemId<ActionState, ActionState>,
}

fn my_sleep_scorer(In(entity): In<Entity>, sleepiness: Query<&Sleepiness>) -> f32 {
    let sleepiness = sleepiness.get(entity).unwrap();
    sleepiness.0 / 100.0
}

fn sleep(In(action_state): In<ActionState>) -> ActionState {
    todo!();
}

fn decide_best_action(world: &mut World) {
    let mut highest_consideration: Option<&Consideration> = None;
    let mut highest_score = 0.0;
    let mut query = world.query::<(Entity, &Considerations)>();

    for (actor_entity, considerations) in query.iter(world) {
        for consideration in &considerations.considerations {
            // This doesn't compile because of multiple borrows :(
            let Ok(score) = world.run_system_with_input(consideration.scorer, actor_entity) else {
                continue;
            };

            if score > highest_score {
                highest_consideration = Some(consideration);
                highest_score = score;
            }
        }
    }

    if let Some(consideration) = highest_consideration {
        let Ok(next) = world.run_system_with_input(consideration.action, ActionState::Idle) else {
            return;
        };

        todo!("set next action state");
    }
}

fn setup(world: &mut World) {
    let scorer = world.register_system(my_sleep_scorer);
    let action = world.register_system(sleep);

    world.spawn(MyActorBundle {
        thinker: Thinker,
        action_state: ActionState::Idle,
        considerations: Considerations {
            considerations: vec![Consideration {
                name: "Sleep".into(),
                scorer: scorer,
                action: action,
            }],
        },
        sleepiness: Sleepiness(0.0),
    });
}

fn sleepiness_tick(mut sleepiness: Query<&mut Sleepiness>) {
    for mut sleepiness in sleepiness.iter_mut() {
        sleepiness.0 += 1.0;
    }
}

I hope you find this interesting ! I thought so and just wanted to share so I could hear your opinion on it :D

I'm a big rust noob so this might be totally wrong/absurd

(also sorry if this was better suited as a discussion ! lol)

@stargazing-dino
Copy link
Contributor Author

Here's a version 2 that's a little cleaner and compiles on bevy main
use bevy_app::{App, Plugin, Startup, Update};
use bevy_ecs::{prelude::*, system::SystemId};
use bevy_time::Time;
use bevy_utils::hashbrown::HashMap;

pub struct BrainBoxPlugin;

impl Plugin for BrainBoxPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, setup)
            .add_systems(Update, (decide_best_action, sleepiness_tick));
    }
}

#[derive(Component)]
pub struct Thinker;

#[derive(Bundle)]
pub struct MyActorBundle {
    pub thinker: Thinker,
    pub considerations: Considerations,
    pub action_state: ActionState,
    pub sleepiness: Sleepiness,
}

#[derive(Component)]
pub struct Considerations {
    pub considerations: Vec<Consideration>,
    pub default_consideration: Option<Consideration>,
}

#[derive(Component, Clone, Copy)]
pub enum ActionState {
    Idle,
    Executing,
    Done,
}

pub struct Consideration {
    pub debug_name: String,
    pub scorer: SystemId<Entity, f32>,
    pub action: SystemId<Entity, ActionState>,
}

/// We need this in order to move out of the query
struct ConsiderationSystems {
    scorer: SystemId<Entity, f32>,
    pub action: SystemId<Entity, ActionState>,
}

fn decide_best_action(
    world: &mut World,
    mut previous_scores: Local<HashMap<Entity, f32>>,
    mut previous_action_states: Local<HashMap<Entity, ActionState>>,
) {
    let mut query = world.query::<(Entity, &Considerations)>();
    let mut entity_considerations = HashMap::<Entity, Vec<ConsiderationSystems>>::new();

    for (actor_entity, considerations) in query.iter(world) {
        for consideration in &considerations.considerations {
            entity_considerations
                .entry(actor_entity.clone())
                .or_insert_with(Vec::new)
                .push(ConsiderationSystems {
                    scorer: consideration.scorer.clone(),
                    action: consideration.action.clone(),
                });
        }
    }

    for (actor_entity, considerations) in entity_considerations {
        for systems in considerations {
            let Ok(score) = world.run_system_with_input(systems.scorer, actor_entity) else {
                continue;
            };
            let previous_score = previous_scores.get(&actor_entity).copied().unwrap_or(0.0);
            let previous_action_state = previous_action_states
                .get(&actor_entity)
                .copied()
                .unwrap_or(ActionState::Idle);
            let mut next_action_state = previous_action_state;

            if score > previous_score {
                next_action_state = world
                    .run_system_with_input(systems.action, actor_entity)
                    .unwrap();
            } else {
                // Run our default consideration
            }

            previous_scores.insert(actor_entity, score);
            previous_action_states.insert(actor_entity, next_action_state);
        }
    }
}

// ------------------------------
// User code

#[derive(Component)]
pub struct Sleepiness {
    pub level: f32,
    /// While resting, how much sleepiness is reduced per second
    pub per_second: f32,
    /// Until what level of sleepiness should we rest?
    pub until: f32,
}

fn sleepiness_tick(mut sleepiness: Query<&mut Sleepiness>) {
    for mut sleepiness in sleepiness.iter_mut() {
        sleepiness.level += 1.0;
    }
}

fn my_sleep_scorer(In(entity): In<Entity>, sleepiness: Query<&Sleepiness>) -> f32 {
    let sleepiness = sleepiness.get(entity).unwrap();
    sleepiness.level
}

fn sleep(
    In(entity): In<Entity>,
    time: Res<Time>,
    mut sleepiness: Query<&mut Sleepiness>,
) -> ActionState {
    let mut sleepiness = sleepiness.get_mut(entity).unwrap();
    if sleepiness.level >= sleepiness.until {
        ActionState::Done
    } else {
        sleepiness.level -= time.delta_seconds() * sleepiness.per_second;
        ActionState::Executing
    }
}

fn setup(world: &mut World) {
    // I'm sure we can register the systems ourselves rather than force that on the user.
    // They'd just provide us function pointers in that case
    let scorer = world.register_system(my_sleep_scorer);
    let action = world.register_system(sleep);

    world.spawn(MyActorBundle {
        thinker: Thinker,
        action_state: ActionState::Idle,
        considerations: Considerations {
            considerations: vec![Consideration {
                debug_name: "Sleep".into(),
                scorer: scorer,
                action: action,
            }],
            default_consideration: None,
        },
        sleepiness: Sleepiness {
            level: 0.0,
            per_second: 1.0,
            until: 80.0,
        },
    });
}

I also played around with a version that uses bevy-trait-query and I like the look of that one more than the vec of considerations but it's surprisingly difficult to get right. It also requires that the user registers each consideration type as a Consideration which is a bit odd.

Semantically it makes a lot of sense tho:

#[bevy_trait_query::queryable]
pub trait Consideration {
    fn score(&self, entity: Entity, world: &mut World) -> f32;

    fn action(&self, entity: Entity, world: &mut World) -> ActionState;
}

#[derive(Component)]
pub struct Sleep {
    pub sleepiness: f32,
}

impl Consideration for Sleep {
    fn score(&self, entity: Entity, world: &mut World) -> f32 {
        self.sleepiness
    }

    fn action(&self, entity: Entity, world: &mut World) -> ActionState {
        todo!();
    }
}

(look at that exclusive world access tho 😅 )

I'll post the full length version if I get it running.

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