From 411ef98814779d75dc7cb3d244246eda154d3ef9 Mon Sep 17 00:00:00 2001 From: kaphula Date: Tue, 14 Nov 2023 13:45:53 +0200 Subject: [PATCH 1/7] Implement RepeatSequence behavior. --- bonsai/src/behavior.rs | 10 +++ bonsai/src/state.rs | 57 ++++++++++++++++ bonsai/src/visualizer.rs | 13 ++++ examples/Cargo.toml | 4 ++ examples/src/simple_npc_ai/main.rs | 101 +++++++++++++++++++++++++++++ 5 files changed, 185 insertions(+) create mode 100644 examples/src/simple_npc_ai/main.rs diff --git a/bonsai/src/behavior.rs b/bonsai/src/behavior.rs index a3d4939..21adf46 100644 --- a/bonsai/src/behavior.rs +++ b/bonsai/src/behavior.rs @@ -44,6 +44,16 @@ pub enum Behavior { /// Fails if the conditional behavior fails, /// or if any behavior in the loop body fails. While(Box>, Vec>), + + /// Runs a sequence on repeat as long as a conditional behavior + /// that precedes the sequence is running. + /// Conditional behavior is **only** checked before the sequence runs and + /// not during the sequence. + /// + /// Succeeds if the conditional behavior succeeds. + /// Fails if the conditional behavior fails, + /// or if any behavior in the sequence fails. + RepeatSequence(Box>, Vec>), /// Runs all behaviors in parallel until all succeeded. /// /// Succeeds if all behaviors succeed. diff --git a/bonsai/src/state.rs b/bonsai/src/state.rs index 57a772a..230e3ac 100644 --- a/bonsai/src/state.rs +++ b/bonsai/src/state.rs @@ -51,6 +51,8 @@ pub enum State { SequenceState(Vec>, usize, Box>), /// Keeps track of a `While` behavior. WhileState(Box>, Vec>, usize, Box>), + /// Keeps track of a `While` behavior. + RepeatSequenceState(Box>, Vec>, usize, Box>), /// Keeps track of a `WhenAll` behavior. WhenAllState(Vec>>), /// Keeps track of a `WhenAny` behavior. @@ -93,6 +95,10 @@ impl State { Behavior::WhenAll(all) => State::WhenAllState(all.into_iter().map(|ev| Some(State::new(ev))).collect()), Behavior::WhenAny(any) => State::WhenAnyState(any.into_iter().map(|ev| Some(State::new(ev))).collect()), Behavior::After(after_all) => State::AfterState(0, after_all.into_iter().map(State::new).collect()), + Behavior::RepeatSequence(ev, rep) => { + let state = State::new(rep[0].clone()); + State::RepeatSequenceState(Box::new(State::new(*ev)), rep, 0, Box::new(state)) + } } } @@ -285,6 +291,57 @@ impl State { RUNNING } } + (_, &mut RepeatSequenceState(ref mut ev_cursor, ref rep, ref mut i, ref mut cursor)) => { + + let cur = cursor; + let mut remaining_dt = upd.unwrap_or(0.0); + let mut remaining_e; + loop { + + // Only check the condition when the sequence starts. + if *i == 0 { + // If the event terminates, stop. + match ev_cursor.tick(e, f) { + (Running, _) => {} + x => return x, + }; + } + + + match cur.tick( + match upd { + Some(_) => { + remaining_e = UpdateEvent::from_dt(remaining_dt, e).unwrap(); + &remaining_e + } + _ => e, + }, + f, + ) { + (Failure, x) => return (Failure, x), + (Running, _) => break, + (Success, new_dt) => { + remaining_dt = match upd { + // Change update event with remaining delta time. + Some(_) => new_dt, + // Other events are 'consumed' and not passed to next. + _ => return RUNNING, + } + } + }; + *i += 1; + // If end of repeated events, + // start over from the first one. + if *i >= rep.len() { + *i = 0; + } + // Create a new cursor for next event. + // Use the same pointer to avoid allocation. + **cur = State::new(rep[*i].clone()); + } + RUNNING + } + // WaitForeverState, WaitState _ => RUNNING, } diff --git a/bonsai/src/visualizer.rs b/bonsai/src/visualizer.rs index 05d611f..ac2e758 100644 --- a/bonsai/src/visualizer.rs +++ b/bonsai/src/visualizer.rs @@ -14,6 +14,7 @@ pub(crate) enum NodeType { Select, If, Sequence, + RepeatSequence, While, WhenAll, WhenAny, @@ -87,6 +88,18 @@ impl BT { let right = Sequence(seq); self.dfs_recursive(right, node_id) } + Behavior::RepeatSequence(ev, seq) => { + let node_id = self.graph.add_node(NodeType::RepeatSequence); + self.graph.add_edge(parent_node, node_id, 1); + + // left + let left = *ev; + self.dfs_recursive(left, node_id); + + // right + let right = Sequence(seq); + self.dfs_recursive(right, node_id) + } Behavior::WhenAll(all) => { let node_id = self.graph.add_node(NodeType::WhenAll); self.graph.add_edge(parent_node, node_id, 1); diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 7ed7b1e..8e96e85 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -40,3 +40,7 @@ path = "src/boids/main.rs" [[bin]] name = "graphviz" path = "src/graphviz/main.rs" + +[[bin]] +name = "simple_npc_ai" +path = "src/simple_npc_ai/main.rs" diff --git a/examples/src/simple_npc_ai/main.rs b/examples/src/simple_npc_ai/main.rs new file mode 100644 index 0000000..bd475bb --- /dev/null +++ b/examples/src/simple_npc_ai/main.rs @@ -0,0 +1,101 @@ +use std::collections::HashMap; + +use bonsai_bt::{Behavior::Action, BT, Event, Failure, Running, Status, Success, UpdateArgs}; +use bonsai_bt::Behavior::RepeatSequence; + +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq)] +pub enum EnemyNPC { + Run, + Jump, + Shoot, + HasActionPointsLeft, +} + +fn game_tick(bt: &mut BT, state: &mut EnemyNPCState) -> Status { + + let e: Event = UpdateArgs { dt: 0.0 }.into(); + + #[rustfmt::skip] + let status = bt.state.tick(&e,&mut |args: bonsai_bt::ActionArgs| { + match *args.action { + EnemyNPC::Run => { + state.perform_action("run"); + return (Success, 0.0) + }, + EnemyNPC::HasActionPointsLeft => { + print!("Is NPC tired... "); + if state.action_points == 0 { + println!("yes!"); + return (Failure, 0.0); + } + else { + println!("no! Action points: {}", state.action_points ); + return(Running, 0.0) + } + } + EnemyNPC::Jump => { + state.perform_action("jump"); + return(Success, 0.0) + } + EnemyNPC::Shoot => { + state.perform_action("shoot"); + return(Success, 0.0) + } + } + }); + + // return status: + status.0 +} + +struct EnemyNPCState { + pub action_points: usize, + pub max_action_points: usize +} +impl EnemyNPCState { + fn consume_action_point(&mut self) { + self.action_points = self.action_points.checked_sub(1).unwrap_or(0); + } + fn rest(&mut self) { + self.action_points = self.max_action_points; + } + + fn perform_action(&mut self, action: &str) { + if self.action_points > 0 { + self.consume_action_point(); + println!("Performing action: {}. Action points: {}", action, self.action_points); + } + else { + println!("Cannot perform action: {}. Not enough action points", action); + } + } +} + +fn main() { + // define blackboard (even though we're not using it) + let blackboard: HashMap<(), ()> = HashMap::new(); + + let npc_ai = RepeatSequence( + Box::new(Action(EnemyNPC::HasActionPointsLeft)), + vec![ + Action(EnemyNPC::Run), + Action(EnemyNPC::Jump), + Action(EnemyNPC::Shoot) + ], + + ); + let mut bt = BT::new(npc_ai, blackboard); + + let mut npc_state = EnemyNPCState { + action_points: 10, + max_action_points: 10, + }; + + loop { + match game_tick(&mut bt, &mut npc_state) { + Success => {} + Failure => { break;} + Running => {} + } + } +} \ No newline at end of file From d0a99f9b89b2c4d749099d4ac4280c4519154b25 Mon Sep 17 00:00:00 2001 From: kaphula Date: Tue, 14 Nov 2023 13:48:43 +0200 Subject: [PATCH 2/7] Run fmt. --- bonsai/src/state.rs | 3 --- examples/src/simple_npc_ai/main.rs | 21 ++++++++------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/bonsai/src/state.rs b/bonsai/src/state.rs index 230e3ac..e432cbf 100644 --- a/bonsai/src/state.rs +++ b/bonsai/src/state.rs @@ -292,12 +292,10 @@ impl State { } } (_, &mut RepeatSequenceState(ref mut ev_cursor, ref rep, ref mut i, ref mut cursor)) => { - let cur = cursor; let mut remaining_dt = upd.unwrap_or(0.0); let mut remaining_e; loop { - // Only check the condition when the sequence starts. if *i == 0 { // If the event terminates, stop. @@ -307,7 +305,6 @@ impl State { }; } - match cur.tick( match upd { Some(_) => { diff --git a/examples/src/simple_npc_ai/main.rs b/examples/src/simple_npc_ai/main.rs index bd475bb..a4fffce 100644 --- a/examples/src/simple_npc_ai/main.rs +++ b/examples/src/simple_npc_ai/main.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use bonsai_bt::{Behavior::Action, BT, Event, Failure, Running, Status, Success, UpdateArgs}; use bonsai_bt::Behavior::RepeatSequence; +use bonsai_bt::{Behavior::Action, Event, Failure, Running, Status, Success, UpdateArgs, BT}; #[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq)] pub enum EnemyNPC { @@ -12,7 +12,6 @@ pub enum EnemyNPC { } fn game_tick(bt: &mut BT, state: &mut EnemyNPCState) -> Status { - let e: Event = UpdateArgs { dt: 0.0 }.into(); #[rustfmt::skip] @@ -50,7 +49,7 @@ fn game_tick(bt: &mut BT, state: &mut EnemyNPCState) -> Status struct EnemyNPCState { pub action_points: usize, - pub max_action_points: usize + pub max_action_points: usize, } impl EnemyNPCState { fn consume_action_point(&mut self) { @@ -64,8 +63,7 @@ impl EnemyNPCState { if self.action_points > 0 { self.consume_action_point(); println!("Performing action: {}. Action points: {}", action, self.action_points); - } - else { + } else { println!("Cannot perform action: {}. Not enough action points", action); } } @@ -77,12 +75,7 @@ fn main() { let npc_ai = RepeatSequence( Box::new(Action(EnemyNPC::HasActionPointsLeft)), - vec![ - Action(EnemyNPC::Run), - Action(EnemyNPC::Jump), - Action(EnemyNPC::Shoot) - ], - + vec![Action(EnemyNPC::Run), Action(EnemyNPC::Jump), Action(EnemyNPC::Shoot)], ); let mut bt = BT::new(npc_ai, blackboard); @@ -94,8 +87,10 @@ fn main() { loop { match game_tick(&mut bt, &mut npc_state) { Success => {} - Failure => { break;} + Failure => { + break; + } Running => {} } } -} \ No newline at end of file +} From d1a75b37c4b57d0813462b76580ac024174245e3 Mon Sep 17 00:00:00 2001 From: kaphula Date: Tue, 21 Nov 2023 11:50:54 +0200 Subject: [PATCH 3/7] Update RepeatSequence example program. --- examples/src/simple_npc_ai/main.rs | 115 ++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 18 deletions(-) diff --git a/examples/src/simple_npc_ai/main.rs b/examples/src/simple_npc_ai/main.rs index a4fffce..da44c59 100644 --- a/examples/src/simple_npc_ai/main.rs +++ b/examples/src/simple_npc_ai/main.rs @@ -1,14 +1,16 @@ use std::collections::HashMap; use bonsai_bt::Behavior::RepeatSequence; -use bonsai_bt::{Behavior::Action, Event, Failure, Running, Status, Success, UpdateArgs, BT}; +use bonsai_bt::{Behavior::Action, Event, Failure, Running, Status, Success, UpdateArgs, BT, While, Sequence}; #[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq)] pub enum EnemyNPC { Run, - Jump, Shoot, HasActionPointsLeft, + Rest, + Die, + IsDead, } fn game_tick(bt: &mut BT, state: &mut EnemyNPCState) -> Status { @@ -22,24 +24,36 @@ fn game_tick(bt: &mut BT, state: &mut EnemyNPCState) -> Status return (Success, 0.0) }, EnemyNPC::HasActionPointsLeft => { - print!("Is NPC tired... "); if state.action_points == 0 { - println!("yes!"); - return (Failure, 0.0); + println!("NPC does not have actions points left... "); + return (Success, 0.0); } else { - println!("no! Action points: {}", state.action_points ); + println!("NPC has action points: {}", state.action_points ); return(Running, 0.0) } } - EnemyNPC::Jump => { - state.perform_action("jump"); - return(Success, 0.0) - } EnemyNPC::Shoot => { state.perform_action("shoot"); return(Success, 0.0) } + EnemyNPC::Rest => { + if state.fully_rested() { + return (Success, 0.0) + } + state.rest(); + return (Running, 0.0) + } + EnemyNPC::Die => { + state.die(); + return (Success, 0.0); + } + EnemyNPC::IsDead => { + if state.is_alive() { + return (Running, 0.0); + } + return (Success, 0.0); + } } }); @@ -50,13 +64,31 @@ fn game_tick(bt: &mut BT, state: &mut EnemyNPCState) -> Status struct EnemyNPCState { pub action_points: usize, pub max_action_points: usize, + pub alive: bool, } impl EnemyNPCState { fn consume_action_point(&mut self) { self.action_points = self.action_points.checked_sub(1).unwrap_or(0); } fn rest(&mut self) { - self.action_points = self.max_action_points; + self.action_points = (self.action_points + 1).min(self.max_action_points); + println!("Rested for a while... Action points: {}", self.action_points); + } + fn die(&mut self) { + println!("NPC died..."); + self.alive = false + } + fn is_alive(&self) -> bool { + if self.alive { + println!("NPC is alive..."); + } + else { + println!("NPC is dead..."); + } + self.alive + } + fn fully_rested(&self) -> bool { + self.action_points == self.max_action_points } fn perform_action(&mut self, action: &str) { @@ -64,29 +96,76 @@ impl EnemyNPCState { self.consume_action_point(); println!("Performing action: {}. Action points: {}", action, self.action_points); } else { - println!("Cannot perform action: {}. Not enough action points", action); + println!("Cannot perform action: {}. Not enough action points.", action); } } } +/// Demonstrates a usage of [RepeatSequence] behavior with +/// a simple NPC simulation. +/// +/// The NPC AI first enters a higher [RepeatSequence] that +/// checks if the NPC is dead, then it succeeds to inner [RepeatSequence] +/// where the NPC performs actions until it is determined that +/// no action points are left to consume. Then the AI control flow returns +/// to the previous higher sequence where the executions continues and the NPC rests +/// and regains its actions points. After that the NPC is killed and it is once again +/// checked if the NPC is alive. Then the program quits. +/// +/// Timeline of execution in more detail: +/// +/// 1. check if the NPC is dead (no) +/// 2. execute "run and shoot" subprogram +/// 3. check if action points are available (yes) +/// 4. run +/// 5. shoot +/// 6. check if action points are available (yes) +/// 7. run +/// 8. shoot (notice that we don't have action points +/// here but we try anyway and move on the sequence) +/// 9. check if action points are available (no) +/// 10. exit the subprogram +/// 11. rest and regain action points +/// (this action returns [Running] until fully rested +/// so control flow is returned to main loop) +/// 12. kill the NPC +/// 13. check if the NPC is dead (yes) +/// 14. quit +/// +/// +/// +/// fn main() { // define blackboard (even though we're not using it) let blackboard: HashMap<(), ()> = HashMap::new(); - let npc_ai = RepeatSequence( + let run_and_shoot_ai = RepeatSequence( Box::new(Action(EnemyNPC::HasActionPointsLeft)), - vec![Action(EnemyNPC::Run), Action(EnemyNPC::Jump), Action(EnemyNPC::Shoot)], + vec![Action(EnemyNPC::Run), Action(EnemyNPC::Shoot)], ); - let mut bt = BT::new(npc_ai, blackboard); + let top_ai = RepeatSequence( + Box::new(Action(EnemyNPC::IsDead)), + vec![run_and_shoot_ai.clone(), Action(EnemyNPC::Rest), Action(EnemyNPC::Die)], + ); + let mut bt = BT::new(top_ai, blackboard); + + let print_graph = false; + if print_graph { + println!("{}", bt.get_graphviz()); + } + let max_actions = 3; let mut npc_state = EnemyNPCState { - action_points: 10, - max_action_points: 10, + action_points: max_actions, + max_action_points: max_actions, + alive: true, }; + loop { + println!("reached main loop..."); match game_tick(&mut bt, &mut npc_state) { - Success => {} + Success | Failure => { break; } From ba5eb4f3619031e10c56113ba4e75ce374fceeb2 Mon Sep 17 00:00:00 2001 From: kaphula Date: Tue, 21 Nov 2023 15:47:18 +0200 Subject: [PATCH 4/7] Update NPC example, update readmes, fix typos, add tests for RepeatSequence. --- README.md | 1 + bonsai/src/behavior.rs | 5 ++ bonsai/src/state.rs | 4 +- bonsai/tests/behavior_tests.rs | 91 +++++++++++++++++++++++++++++++++- examples/README.md | 9 ++++ 5 files changed, 107 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8e85ca9..abfc72f 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ A Behavior Tree forms a tree structure where each node represents a process. Whe For example, if you have a state `A` and a state `B`: - Move from state `A` to state `B` if `A` succeeds: `Sequence([A, B])` +- Move from state `A` to sequence of states `[B]` if `A` is running. If all states in the sequence `[B]` succeed in order, check if `A` is still running and repeat. Stop if `A` succeeds or any of the states fail: `RepeatSequence(A, [B])` - Try `A` first and then try `B` if `A` fails: `Select([A, B])` - If `condition` succeedes do `A`, else do `B` : `If(condition, A, B)` - If `A` succeeds, return failure (and vice-versa): `Invert(A)` diff --git a/bonsai/src/behavior.rs b/bonsai/src/behavior.rs index 21adf46..fac959f 100644 --- a/bonsai/src/behavior.rs +++ b/bonsai/src/behavior.rs @@ -53,6 +53,11 @@ pub enum Behavior { /// Succeeds if the conditional behavior succeeds. /// Fails if the conditional behavior fails, /// or if any behavior in the sequence fails. + /// + /// # Panics + /// + /// Panics if the given behavior sequence is empty. + /// RepeatSequence(Box>, Vec>), /// Runs all behaviors in parallel until all succeeded. /// diff --git a/bonsai/src/state.rs b/bonsai/src/state.rs index e432cbf..eda9667 100644 --- a/bonsai/src/state.rs +++ b/bonsai/src/state.rs @@ -51,7 +51,7 @@ pub enum State { SequenceState(Vec>, usize, Box>), /// Keeps track of a `While` behavior. WhileState(Box>, Vec>, usize, Box>), - /// Keeps track of a `While` behavior. + /// Keeps track of a `RepeatSequence` behavior. RepeatSequenceState(Box>, Vec>, usize, Box>), /// Keeps track of a `WhenAll` behavior. WhenAllState(Vec>>), @@ -96,7 +96,7 @@ impl State { Behavior::WhenAny(any) => State::WhenAnyState(any.into_iter().map(|ev| Some(State::new(ev))).collect()), Behavior::After(after_all) => State::AfterState(0, after_all.into_iter().map(State::new).collect()), Behavior::RepeatSequence(ev, rep) => { - let state = State::new(rep[0].clone()); + let state = State::new(rep.get(0).expect("RepeatSequence must have at least one behavior!").clone()); State::RepeatSequenceState(Box::new(State::new(*ev)), rep, 0, Box::new(state)) } } diff --git a/bonsai/tests/behavior_tests.rs b/bonsai/tests/behavior_tests.rs index ccd810d..af3cd6c 100644 --- a/bonsai/tests/behavior_tests.rs +++ b/bonsai/tests/behavior_tests.rs @@ -1,4 +1,4 @@ -use crate::behavior_tests::TestActions::{Dec, Inc, LessThan}; +use crate::behavior_tests::TestActions::{Dec, Inc, LessThan, LessThanRunningSuccess}; use bonsai_bt::{ Action, Behavior::{After, AlwaysSucceed, If, Invert, Select}, @@ -6,6 +6,7 @@ use bonsai_bt::{ Status::Running, Success, UpdateArgs, Wait, WaitForever, WhenAll, While, }; +use bonsai_bt::Behavior::RepeatSequence; /// Some test actions. #[derive(Clone, Debug)] @@ -16,6 +17,8 @@ enum TestActions { Dec, ///, Check if less than LessThan(i32), + /// Check if less than and return [Running]. If more or equal return [Success]. + LessThanRunningSuccess(i32) } // A test state machine that can increment and decrement. @@ -41,6 +44,17 @@ fn tick(mut acc: i32, dt: f64, state: &mut State) -> (i32, bonsai_b (Failure, args.dt) } } + TestActions::LessThanRunningSuccess(v) => { + println!("inside LessThanRunningSuccess with acc: {}", acc); + if acc < v { + println!("success {}<{}", acc, v); + (Running, args.dt) + } else { + println!("failure {}>={}", acc, v); + (Success, args.dt) + } + + } }); println!("status: {:?} dt: {}", s, t); @@ -59,6 +73,7 @@ fn tick_with_ref(acc: &mut i32, dt: f64, state: &mut State) { *acc -= 1; (Success, args.dt) } + TestActions::LessThanRunningSuccess(_) | LessThan(_) => todo!(), }); } @@ -410,3 +425,77 @@ fn test_after_all_succeed_out_of_order() { assert_eq!(s, Failure); assert_eq!(dt, 0.0); } + + +#[test] +fn test_repeat_sequence() { + { + let a: i32 = 0; + let after = RepeatSequence(Box::new(Action(LessThanRunningSuccess(5))), + vec![Action(Inc)]); + + let mut state = State::new(after); + + let (a, s, dt) = tick(a, 0.0, &mut state); + + assert_eq!(a, 5); + assert_eq!(s, Success); + assert_eq!(dt, 0.0); + + let (a, s, dt) = tick(a, 0.0, &mut state); + + assert_eq!(a, 5); + assert_eq!(s, Success); + assert_eq!(dt, 0.0); + } +} + +#[test] +fn test_repeat_sequence_fail() { + { + let a: i32 = 4; + let after = RepeatSequence(Box::new(Action(LessThanRunningSuccess(5))), + vec![Action(Dec), Action(LessThan(0))]); + let mut state = State::new(after); + let (a, s, dt) = tick(a, 0.0, &mut state); + + assert_eq!(a, 3); + assert_eq!(s, Failure); + assert_eq!(dt, 0.0); + } +} + +#[test] +fn test_repeat_sequence_timed() { + let a: i32 = 0; + let time_step = 0.1; + let steps = 5; + let after = RepeatSequence(Box::new(Action(LessThanRunningSuccess(steps))), + vec![Wait(time_step), Action(Inc)]); + let mut state = State::new(after); + + // increment 3 times + let (a, s, dt) = tick(a, time_step * 3.0, &mut state); + assert_eq!(dt, 0.0); + assert_eq!(a, 3); + assert_eq!(s, Running); + + + let (a, s, dt) = tick(a, 100.0, &mut state); + assert_eq!(dt, 100.0); + assert_eq!(a, 5); + assert_eq!(s, Success); +} + + +#[test] +#[should_panic] +fn test_repeat_sequence_empty() { + let a: i32 = 1; + let after = RepeatSequence( Box::new(Action(LessThanRunningSuccess(0))), + vec![]); + + // panics because no behaviors... + let mut state = State::new(after); +} + diff --git a/examples/README.md b/examples/README.md index f5958fa..66af43f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,6 +2,15 @@ `cargo build --package examples` +## Game NPC AI console application + +Demonstrates use of a behavior tree in minimal and easy to follow console application setting where a fictional +non-playing game character +updates its AI state. Run and inspect this example if you want to get a quick introduction on how behavior tree can be +used in an application. + +`cargo run --bin simple_npc_ai` + ## Boids flocking Constructing boids flocking behavior by copying the same behavior tree across many agents. From d74e77a18cdd41a9f1b1886dbf0226cc2cefef21 Mon Sep 17 00:00:00 2001 From: kaphula Date: Tue, 21 Nov 2023 15:52:48 +0200 Subject: [PATCH 5/7] Change panic message. --- bonsai/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonsai/src/state.rs b/bonsai/src/state.rs index eda9667..2e573a0 100644 --- a/bonsai/src/state.rs +++ b/bonsai/src/state.rs @@ -96,7 +96,7 @@ impl State { Behavior::WhenAny(any) => State::WhenAnyState(any.into_iter().map(|ev| Some(State::new(ev))).collect()), Behavior::After(after_all) => State::AfterState(0, after_all.into_iter().map(State::new).collect()), Behavior::RepeatSequence(ev, rep) => { - let state = State::new(rep.get(0).expect("RepeatSequence must have at least one behavior!").clone()); + let state = State::new(rep.get(0).expect("RepeatSequence's sequence of behaviors to run cannot be empty!").clone()); State::RepeatSequenceState(Box::new(State::new(*ev)), rep, 0, Box::new(state)) } } From bacbb30e496950b957e76e079adfc2db08dd82ec Mon Sep 17 00:00:00 2001 From: kaphula Date: Sat, 25 Nov 2023 19:13:14 +0200 Subject: [PATCH 6/7] Fix pre-commit errors. --- bonsai/src/state.rs | 6 +++++- bonsai/tests/behavior_tests.rs | 34 ++++++++++++------------------ examples/src/boids/main.rs | 6 +++--- examples/src/simple_npc_ai/main.rs | 25 ++++++++++------------ 4 files changed, 33 insertions(+), 38 deletions(-) diff --git a/bonsai/src/state.rs b/bonsai/src/state.rs index 2e573a0..6ec2267 100644 --- a/bonsai/src/state.rs +++ b/bonsai/src/state.rs @@ -96,7 +96,11 @@ impl State { Behavior::WhenAny(any) => State::WhenAnyState(any.into_iter().map(|ev| Some(State::new(ev))).collect()), Behavior::After(after_all) => State::AfterState(0, after_all.into_iter().map(State::new).collect()), Behavior::RepeatSequence(ev, rep) => { - let state = State::new(rep.get(0).expect("RepeatSequence's sequence of behaviors to run cannot be empty!").clone()); + let state = State::new( + rep.get(0) + .expect("RepeatSequence's sequence of behaviors to run cannot be empty!") + .clone(), + ); State::RepeatSequenceState(Box::new(State::new(*ev)), rep, 0, Box::new(state)) } } diff --git a/bonsai/tests/behavior_tests.rs b/bonsai/tests/behavior_tests.rs index af3cd6c..1cb6a16 100644 --- a/bonsai/tests/behavior_tests.rs +++ b/bonsai/tests/behavior_tests.rs @@ -1,4 +1,5 @@ use crate::behavior_tests::TestActions::{Dec, Inc, LessThan, LessThanRunningSuccess}; +use bonsai_bt::Behavior::RepeatSequence; use bonsai_bt::{ Action, Behavior::{After, AlwaysSucceed, If, Invert, Select}, @@ -6,7 +7,6 @@ use bonsai_bt::{ Status::Running, Success, UpdateArgs, Wait, WaitForever, WhenAll, While, }; -use bonsai_bt::Behavior::RepeatSequence; /// Some test actions. #[derive(Clone, Debug)] @@ -18,7 +18,7 @@ enum TestActions { ///, Check if less than LessThan(i32), /// Check if less than and return [Running]. If more or equal return [Success]. - LessThanRunningSuccess(i32) + LessThanRunningSuccess(i32), } // A test state machine that can increment and decrement. @@ -53,7 +53,6 @@ fn tick(mut acc: i32, dt: f64, state: &mut State) -> (i32, bonsai_b println!("failure {}>={}", acc, v); (Success, args.dt) } - } }); println!("status: {:?} dt: {}", s, t); @@ -73,8 +72,7 @@ fn tick_with_ref(acc: &mut i32, dt: f64, state: &mut State) { *acc -= 1; (Success, args.dt) } - TestActions::LessThanRunningSuccess(_) | - LessThan(_) => todo!(), + TestActions::LessThanRunningSuccess(_) | LessThan(_) => todo!(), }); } @@ -426,13 +424,11 @@ fn test_after_all_succeed_out_of_order() { assert_eq!(dt, 0.0); } - #[test] fn test_repeat_sequence() { { let a: i32 = 0; - let after = RepeatSequence(Box::new(Action(LessThanRunningSuccess(5))), - vec![Action(Inc)]); + let after = RepeatSequence(Box::new(Action(LessThanRunningSuccess(5))), vec![Action(Inc)]); let mut state = State::new(after); @@ -454,8 +450,10 @@ fn test_repeat_sequence() { fn test_repeat_sequence_fail() { { let a: i32 = 4; - let after = RepeatSequence(Box::new(Action(LessThanRunningSuccess(5))), - vec![Action(Dec), Action(LessThan(0))]); + let after = RepeatSequence( + Box::new(Action(LessThanRunningSuccess(5))), + vec![Action(Dec), Action(LessThan(0))], + ); let mut state = State::new(after); let (a, s, dt) = tick(a, 0.0, &mut state); @@ -470,8 +468,10 @@ fn test_repeat_sequence_timed() { let a: i32 = 0; let time_step = 0.1; let steps = 5; - let after = RepeatSequence(Box::new(Action(LessThanRunningSuccess(steps))), - vec![Wait(time_step), Action(Inc)]); + let after = RepeatSequence( + Box::new(Action(LessThanRunningSuccess(steps))), + vec![Wait(time_step), Action(Inc)], + ); let mut state = State::new(after); // increment 3 times @@ -480,22 +480,16 @@ fn test_repeat_sequence_timed() { assert_eq!(a, 3); assert_eq!(s, Running); - let (a, s, dt) = tick(a, 100.0, &mut state); assert_eq!(dt, 100.0); assert_eq!(a, 5); assert_eq!(s, Success); } - #[test] #[should_panic] fn test_repeat_sequence_empty() { - let a: i32 = 1; - let after = RepeatSequence( Box::new(Action(LessThanRunningSuccess(0))), - vec![]); - + let after = RepeatSequence(Box::new(Action(LessThanRunningSuccess(0))), vec![]); // panics because no behaviors... - let mut state = State::new(after); + let _state = State::new(after); } - diff --git a/examples/src/boids/main.rs b/examples/src/boids/main.rs index 670e2b4..95a30ad 100644 --- a/examples/src/boids/main.rs +++ b/examples/src/boids/main.rs @@ -84,7 +84,7 @@ impl event::EventHandler for GameState { for i in 0..(self.boids).len() { let boids_vec = self.boids.to_vec(); - let mut b = &mut self.boids[i]; + let b = &mut self.boids[i]; game_tick(self.dt.as_secs_f32(), input::mouse::position(ctx), b, boids_vec); //Convert new velocity to postion change @@ -113,8 +113,8 @@ impl event::EventHandler for GameState { }); let text_pos = glam::vec2( - (WIDTH - menu_text.width(ctx) as f32) / 2.0, - (HEIGHT - menu_text.height(ctx) as f32) / 2.0, + (WIDTH - menu_text.width(ctx)) / 2.0, + (HEIGHT - menu_text.height(ctx)) / 2.0, ); graphics::draw(ctx, &menu_text, graphics::DrawParam::default().dest(text_pos))?; diff --git a/examples/src/simple_npc_ai/main.rs b/examples/src/simple_npc_ai/main.rs index da44c59..9189ac8 100644 --- a/examples/src/simple_npc_ai/main.rs +++ b/examples/src/simple_npc_ai/main.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use bonsai_bt::Behavior::RepeatSequence; -use bonsai_bt::{Behavior::Action, Event, Failure, Running, Status, Success, UpdateArgs, BT, While, Sequence}; +use bonsai_bt::{Behavior::Action, Event, Failure, Running, Status, Success, UpdateArgs, BT}; #[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq)] pub enum EnemyNPC { @@ -21,38 +21,38 @@ fn game_tick(bt: &mut BT, state: &mut EnemyNPCState) -> Status match *args.action { EnemyNPC::Run => { state.perform_action("run"); - return (Success, 0.0) + (Success, 0.0) }, EnemyNPC::HasActionPointsLeft => { if state.action_points == 0 { println!("NPC does not have actions points left... "); - return (Success, 0.0); + (Success, 0.0) } else { println!("NPC has action points: {}", state.action_points ); - return(Running, 0.0) + (Running, 0.0) } } EnemyNPC::Shoot => { state.perform_action("shoot"); - return(Success, 0.0) + (Success, 0.0) } EnemyNPC::Rest => { if state.fully_rested() { return (Success, 0.0) } state.rest(); - return (Running, 0.0) + (Running, 0.0) } EnemyNPC::Die => { state.die(); - return (Success, 0.0); + (Success, 0.0) } EnemyNPC::IsDead => { if state.is_alive() { return (Running, 0.0); } - return (Success, 0.0); + (Success, 0.0) } } }); @@ -68,7 +68,7 @@ struct EnemyNPCState { } impl EnemyNPCState { fn consume_action_point(&mut self) { - self.action_points = self.action_points.checked_sub(1).unwrap_or(0); + self.action_points = self.action_points.saturating_sub(1); } fn rest(&mut self) { self.action_points = (self.action_points + 1).min(self.max_action_points); @@ -81,8 +81,7 @@ impl EnemyNPCState { fn is_alive(&self) -> bool { if self.alive { println!("NPC is alive..."); - } - else { + } else { println!("NPC is dead..."); } self.alive @@ -161,12 +160,10 @@ fn main() { alive: true, }; - loop { println!("reached main loop..."); match game_tick(&mut bt, &mut npc_state) { - Success | - Failure => { + Success | Failure => { break; } Running => {} From fe0a648c0169a6e21e72633e4605cee71bc5a0e3 Mon Sep 17 00:00:00 2001 From: kaphula Date: Sat, 25 Nov 2023 19:14:14 +0200 Subject: [PATCH 7/7] Update to version 0.5.0. --- bonsai/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonsai/Cargo.toml b/bonsai/Cargo.toml index 892c576..a2247ff 100644 --- a/bonsai/Cargo.toml +++ b/bonsai/Cargo.toml @@ -12,7 +12,7 @@ name = "bonsai-bt" readme = "../README.md" repository = "https://github.com/sollimann/bonsai.git" rust-version = "1.60.0" -version = "0.4.9" +version = "0.5.0" [lib] name = "bonsai_bt"