-
-
Notifications
You must be signed in to change notification settings - Fork 66
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
Comments
Here's a version 2 that's a little cleaner and compiles on bevy mainuse 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 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. |
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
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)
The text was updated successfully, but these errors were encountered: