Skip to content

Commit

Permalink
feat(thinker): Add support for scheduling one-off Actions on a Thinker (
Browse files Browse the repository at this point in the history
#57)

Fixes: #30
  • Loading branch information
zkat committed Sep 25, 2022
1 parent 849ab34 commit 382d201
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 9 deletions.
87 changes: 87 additions & 0 deletions examples/one_off.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use bevy::log::LogSettings;
use bevy::prelude::*;
use bevy::utils::tracing::{debug, trace};
use big_brain::prelude::*;

#[derive(Clone, Component, Debug)]
struct OneOff;

fn one_off_action_system(mut query: Query<(&mut ActionState, &ActionSpan), With<OneOff>>) {
for (mut state, span) in &mut query {
let _guard = span.span().enter();
match *state {
ActionState::Requested => {
debug!("One-off action!");
*state = ActionState::Success;
}
ActionState::Cancelled => {
debug!("One-off action was cancelled. Considering this a failure.");
*state = ActionState::Failure;
}
_ => {}
}
}
}

pub fn init_entities(mut cmd: Commands) {
// You at least need to have a Thinker in order to schedule one-off
// actions. It's not a general-purpose task scheduler.
cmd.spawn().insert(Thirst::new(75.0, 2.0)).insert(
Thinker::build()
.label("My Thinker")
.picker(FirstToScore { threshold: 0.8 }),
);
}

#[derive(Component, Debug)]
pub struct Thirst {
pub per_second: f32,
pub thirst: f32,
}

impl Thirst {
pub fn new(thirst: f32, per_second: f32) -> Self {
Self { thirst, per_second }
}
}

pub fn thirst_system(
time: Res<Time>,
mut thirsts: Query<(Entity, &mut Thirst)>,
// We need to get to the Thinker. That takes a couple of steps.
has_thinkers: Query<&HasThinker>,
mut thinkers: Query<(&mut Thinker, &ActionSpan)>,
) {
for (actor, mut thirst) in &mut thirsts {
thirst.thirst += thirst.per_second * (time.delta().as_micros() as f32 / 1_000_000.0);
if thirst.thirst >= 100.0 {
let thinker_ent = has_thinkers.get(actor).unwrap().entity();
let (mut thinker, span) = thinkers.get_mut(thinker_ent).unwrap();
let _guard = span.span().enter();
debug!("Scheduling one-off action");
thinker.schedule_action(OneOff);
thirst.thirst = 0.0;
}
trace!("Thirst: {}", thirst.thirst);
}
}

fn main() {
// Once all that's done, we just add our systems and off we go!
App::new()
.insert_resource(LogSettings {
// Use `RUST_LOG=big_brain=trace,thirst=trace cargo run --example
// one_off --features=trace` to see extra tracing output.
filter: "big_brain=debug,one_off=debug".to_string(),
..Default::default()
})
.add_plugins(DefaultPlugins)
.add_plugin(BigBrainPlugin)
.add_startup_system(init_entities)
.add_system(thirst_system)
// Big Brain has specific stages for Scorers and Actions. If
// determinism matters a lot to you, you should add your action and
// scorer systems to these stages.
.add_system_to_stage(BigBrainStage::Actions, one_off_action_system)
.run();
}
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ pub mod prelude {
AllOrNothing, FixedScore, ProductOfScorers, Score, ScorerBuilder, SumOfScorers,
WinningScorer,
};
pub use thinker::{Action, ActionSpan, Actor, Scorer, ScorerSpan, Thinker, ThinkerBuilder};
pub use thinker::{
Action, ActionSpan, Actor, HasThinker, Scorer, ScorerSpan, Thinker, ThinkerBuilder,
};
}

use bevy::prelude::*;
Expand Down
72 changes: 64 additions & 8 deletions src/thinker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Thinkers are the "brain" of an entity. You attach Scorers to it, and the Thinker picks the right Action to run based on the resulting Scores.
*/

use std::sync::Arc;
use std::{collections::VecDeque, sync::Arc};

use bevy::{
prelude::*,
Expand Down Expand Up @@ -118,6 +118,7 @@ pub struct Thinker {
choices: Vec<Choice>,
current_action: Option<(Action, ActionBuilderWrapper)>,
span: Span,
scheduled_actions: VecDeque<ActionBuilderWrapper>,
}

impl Thinker {
Expand All @@ -127,6 +128,11 @@ impl Thinker {
pub fn build() -> ThinkerBuilder {
ThinkerBuilder::new()
}

pub fn schedule_action(&mut self, action: impl ActionBuilder + 'static) {
self.scheduled_actions
.push_back(ActionBuilderWrapper::new(Arc::new(action)));
}
}

/**
Expand Down Expand Up @@ -189,10 +195,6 @@ impl ThinkerBuilder {
}

impl ActionBuilder for ThinkerBuilder {
fn label(&self) -> Option<&str> {
self.label.as_deref()
}

fn build(&self, cmd: &mut Commands, action_ent: Entity, actor: Entity) {
let span = span!(
Level::DEBUG,
Expand All @@ -214,14 +216,19 @@ impl ActionBuilder for ThinkerBuilder {
.picker
.clone()
.expect("ThinkerBuilder must have a Picker"),
choices,
otherwise: self.otherwise.clone(),
choices,
current_action: None,
span,
scheduled_actions: VecDeque::new(),
})
.insert(Name::new("Thinker"))
.insert(ActionState::Requested);
}

fn label(&self) -> Option<&str> {
self.label.as_deref()
}
}

pub fn thinker_component_attach_system(
Expand Down Expand Up @@ -260,6 +267,12 @@ pub fn actor_gone_cleanup(
#[derive(Component, Debug)]
pub struct HasThinker(Entity);

impl HasThinker {
pub fn entity(&self) -> Entity {
self.0
}
}

pub struct ThinkerIterations {
index: usize,
max_duration: Duration,
Expand Down Expand Up @@ -364,6 +377,14 @@ pub fn thinker_system(
&scorer_spans,
true,
);
} else if should_schedule_action(&mut thinker, &mut action_states) {
debug!("Spawning scheduled action.");
let action = thinker
.scheduled_actions
.pop_front()
.expect("we literally just checked if it was there.");
let new_action = actions::spawn_action(action.1.as_ref(), &mut cmd, *actor);
thinker.current_action = Some((Action(new_action), action.clone()));
} else if let Some(default_action_ent) = &thinker.otherwise {
// Otherwise, let's just execute the default one! (if it's there)
let default_action_ent = default_action_ent.clone();
Expand All @@ -380,11 +401,11 @@ pub fn thinker_system(
);
} else {
#[cfg(feature = "trace")]
trace!("No action was picked. No `otherwise` clause. Thinker sitting quietly for now.");
trace!("No action was picked. No `otherwise` clause. No scheduled actions. Thinker sitting quietly for now.");
if let Some((action_ent, _)) = &thinker.current_action {
let action_span = action_spans.get(action_ent.0).expect("Where is it?");
let _guard = action_span.span.enter();
let curr_action_state = action_states.get_mut(action_ent.0).expect("Couldn't find a component corresponding to the current action. This is definitely a bug.");
let mut curr_action_state = action_states.get_mut(action_ent.0).expect("Couldn't find a component corresponding to the current action. This is definitely a bug.");
let previous_done = matches!(
*curr_action_state,
ActionState::Success | ActionState::Failure
Expand All @@ -396,6 +417,8 @@ pub fn thinker_system(
// Despawn the action itself.
cmd.entity(action_ent.0).despawn_recursive();
thinker.current_action = None;
} else if *curr_action_state == ActionState::Init {
*curr_action_state = ActionState::Requested;
}
}
}
Expand All @@ -408,6 +431,39 @@ pub fn thinker_system(
iterations.index = 0;
}

fn should_schedule_action(
thinker: &mut Mut<Thinker>,
states: &mut Query<&mut ActionState>,
) -> bool {
#[cfg(feature = "trace")]
let thinker_span = thinker.span.clone();
#[cfg(feature = "trace")]
let _thinker_span_guard = thinker_span.enter();
if thinker.scheduled_actions.is_empty() {
#[cfg(feature = "trace")]
trace!("No scheduled actions. Not scheduling anything.");
false
} else if let Some((action_ent, _)) = &mut thinker.current_action {
let curr_action_state = states.get_mut(action_ent.0).expect("Couldn't find a component corresponding to the current action. This is definitely a bug.");
if matches!(
*curr_action_state,
ActionState::Success | ActionState::Failure
) {
#[cfg(feature = "trace")]
trace!("Current action is already done. Can schedule.");
true
} else {
#[cfg(feature = "trace")]
trace!("Current action is still executing. Not scheduling anything.");
false
}
} else {
#[cfg(feature = "trace")]
trace!("No current action actions. Can schedule.");
true
}
}

#[allow(clippy::too_many_arguments)]
fn exec_picked_action(
cmd: &mut Commands,
Expand Down

0 comments on commit 382d201

Please sign in to comment.