//! Create a simple behavior tree implementation
mod composite;
use bevy::prelude::*;
pub use composite::Sequence;
use std::{collections::HashMap, sync::Arc};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum NodeResult {
/// The node is still running
///
/// This contains the node to be ticked
Running(BehaviorArc),
/// The node succeeded
Success,
/// The node failed
Failure,
}
/// A wrapper around a behavior node reference that
/// implements pointer equality behavior
#[derive(Clone, Debug)]
pub struct BehaviorArc(Arc<dyn BehaviorNode>);
impl std::cmp::PartialEq for BehaviorArc {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl std::cmp::Eq for BehaviorArc {}
impl std::hash::Hash for BehaviorArc {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(self.0.as_ref(), state)
}
}
impl<T: BehaviorNode + 'static> From<T> for BehaviorArc {
fn from(value: T) -> Self {
Self(Arc::new(value))
}
}
// This is our main "behavior tree" trait.
// all nodes implement this trait.
pub trait BehaviorNode: std::fmt::Debug {
fn tick(&self, blackboard: &mut Blackboard, commands: &mut Commands) -> NodeResult;
fn arc(self) -> Arc<dyn BehaviorNode>
where
Self: Sized + 'static,
{
Arc::new(self)
}
}
// blackboard is how the behavior tree sees/shares information
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum BlackboardValue {
Integer(i64),
Float(f64),
Point(Vec2),
Vector(Vec2),
Boolean(bool),
// We want Copy, and strings are probably unneccessary
// String(Arc<str>),
}
// TODO If this ever librarifies, make this use mint
pub trait BlackboardValueVector {
fn point(self) -> Vec2;
fn vector(self) -> Vec2;
}
impl BlackboardValueVector for Vec2 {
fn point(self) -> Vec2 {
self
}
fn vector(self) -> Vec2 {
self
}
}
impl BlackboardValue {
fn into_point<T: BlackboardValueVector>(point: T) -> Self {
Self::Point(point.point())
}
fn into_vector<T: BlackboardValueVector>(vector: T) -> Self {
Self::Vector(vector.vector())
}
}
impl From<bool> for BlackboardValue {
fn from(value: bool) -> Self {
Self::Boolean(value)
}
}
macro_rules! bv_from {
(integer $type:ty) => {
impl From<$type> for BlackboardValue {
fn from(value: $type) -> Self {
Self::Integer(i64::from(value))
}
}
};
(float $type:ty) => {
impl From<$type> for BlackboardValue {
fn from(value: $type) -> Self {
Self::Float(f64::from(value))
}
}
};
}
bv_from!(integer u8);
bv_from!(integer u16);
bv_from!(integer u32);
bv_from!(integer i8);
bv_from!(integer i16);
bv_from!(integer i32);
bv_from!(float f32);
bv_from!(float f64);
#[derive(Debug, Default)]
pub struct Blackboard(HashMap<Box<str>, BlackboardValue>);
impl Blackboard {
pub fn set<S: AsRef<str>>(&mut self, key: S, value: BlackboardValue) {
self.0.insert(Box::from(key.as_ref()), value);
}
pub fn get<S: AsRef<str>>(&self, key: S) -> Option<&BlackboardValue> {
self.0.get(key.as_ref())
}
pub fn get_mut<S: AsRef<str>>(&mut self, key: S) -> Option<&mut BlackboardValue> {
self.0.get_mut(key.as_ref())
}
}
#[derive(Debug)]
/// Takes care of executing a behavior tree
pub struct BehaviorRunner {
tree: Arc<dyn BehaviorNode>,
current_tick: Option<BehaviorArc>,
blackboard: Blackboard,
}
impl BehaviorRunner {
pub fn new(tree: Arc<dyn BehaviorNode>) -> Self {
Self {
tree,
current_tick: None,
blackboard: default(),
}
}
pub fn is_running(&self) -> bool {
self.current_tick.is_some()
}
pub fn blackboard(&self) -> &Blackboard {
&self.blackboard
}
pub fn blackboard_mut(&mut self) -> &mut Blackboard {
&mut self.blackboard
}
fn tick_node(&mut self, node: &dyn BehaviorNode, commands: &mut Commands) -> Option<bool> {
match node.tick(&mut self.blackboard, commands) {
NodeResult::Running(nbp) => {
self.current_tick = Some(nbp);
None
}
NodeResult::Success => Some(true),
NodeResult::Failure => Some(false),
}
}
// returns None -> still running
// return Some(p) -> p true success, p false failure
pub fn proceed(&mut self, commands: &mut Commands) -> Option<bool> {
if let Some(bp) = self.current_tick.take() {
self.tick_node(bp.0.as_ref(), commands)
} else {
let node = self.tree.clone();
self.tick_node(node.as_ref(), commands)
}
}
}
#[cfg(test)]
mod tests {
use assert2::check;
use bevy::ecs::{
system::{CommandQueue, Commands},
world::World,
};
use bevy::prelude::*;
use super::{BehaviorNode, BehaviorRunner, Blackboard, BlackboardValue, NodeResult, Sequence};
fn create_testing_state() -> (CommandQueue, World) {
(CommandQueue::default(), World::new())
}
#[derive(Debug)]
struct MoveTo {
part: f32,
goal: Vec2,
}
impl BehaviorNode for MoveTo {
fn tick(&self, blackboard: &mut Blackboard, _commands: &mut Commands) -> NodeResult {
let Some(BlackboardValue::Point(position)) = blackboard.get_mut("position") else {
return NodeResult::Failure;
};
let movement = (self.goal - *position) * self.part.recip();
*position += movement;
const ERROR: f32 = 0.001;
if (self.goal - *position).length() < ERROR {
NodeResult::Success
} else {
NodeResult::Running(
Self {
part: self.part,
goal: self.goal,
}
.into(),
)
}
}
}
#[test]
fn test_seequence() {
let (mut cq, world) = create_testing_state();
let mut commands = Commands::new(&mut cq, &world);
let tree1 = MoveTo {
part: 5.0,
goal: Vec2::ZERO,
}
.arc();
let tree2 = [
MoveTo {
part: 5.0,
goal: Vec2::ZERO,
}
.arc(),
MoveTo {
part: 5.0,
goal: Vec2::splat(5.0),
}
.arc(),
]
.into_iter()
.collect::<Sequence>()
.arc();
// With no info, the node should fail
{
let mut runner1 = BehaviorRunner::new(tree1.clone());
check!(!runner1.is_running());
check!(runner1.proceed(&mut commands) == Some(false));
let mut runner2 = BehaviorRunner::new(tree2.clone());
check!(!runner2.is_running());
check!(runner2.proceed(&mut commands) == Some(false));
}
// Set some position
let starting_position = Vec2::X * 5.0;
// If a position is set, we will execute the action
{
let mut runner1 = BehaviorRunner::new(tree1.clone());
runner1
.blackboard_mut()
.set("position", BlackboardValue::into_point(starting_position));
check!(runner1.proceed(&mut commands) == None);
while let Some(BlackboardValue::Point(_)) = runner1.blackboard().get("position") {
if let Some(res) = runner1.proceed(&mut commands) {
// we should succeed
check!(res);
break;
}
}
let mut runner2 = BehaviorRunner::new(tree2.clone());
runner2
.blackboard_mut()
.set("position", BlackboardValue::into_point(starting_position));
while let Some(BlackboardValue::Point(pos)) = runner2.blackboard().get("position") {
eprintln!("position = {pos:?}\nrunner = {runner2:?}");
if let Some(res) = runner2.proceed(&mut commands) {
// we should succeed
check!(res);
break;
}
}
}
}
}