use bevy::{
ecs::{intern::Interned, schedule::ScheduleLabel},
prelude::*,
};
pub mod conditional;
pub mod converter;
pub mod node;
pub mod parallel;
pub mod sequential;
pub mod task;
#[cfg(test)]
mod tester_util;
use node::{Node, NodeStatus};
pub mod prelude {
#[cfg(feature = "serde")]
pub use crate::BehaviorTreeSource;
pub use crate::{
BehaviorTree, BehaviorTreePlugin, BehaviorTreeRoot, BehaviorTreeSystemSet, Freeze,
TreeStatus, conditional::prelude::*, converter::prelude::*, node::prelude::*,
parallel::prelude::*, sequential::prelude::*, task::prelude::*,
};
}
pub struct BehaviorTreePlugin {
schedule: Interned<dyn ScheduleLabel>,
}
impl BehaviorTreePlugin {
pub fn in_schedule(mut self, schedule: impl ScheduleLabel) -> Self {
self.schedule = schedule.intern();
self
}
}
impl Default for BehaviorTreePlugin {
fn default() -> Self {
Self {
schedule: PostUpdate.intern(),
}
}
}
impl Plugin for BehaviorTreePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<Assets<BehaviorTreeRoot>>().add_systems(
self.schedule,
(update).in_set(BehaviorTreeSystemSet::Update),
);
#[cfg(feature = "serde")]
{
app.init_asset::<BehaviorTreeRoot>()
.add_systems(PreUpdate, load_from_source);
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, SystemSet)]
pub enum BehaviorTreeSystemSet {
Update,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Asset, TypePath, Debug)]
pub struct BehaviorTreeRoot {
node: Box<dyn Node>,
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize, Component, Clone, Debug)]
pub struct BehaviorTreeSource {
pub path: String,
}
#[derive(Component, Clone, Debug)]
#[require(TreeStatus)]
pub struct BehaviorTree {
root: Handle<BehaviorTreeRoot>,
}
impl BehaviorTree {
pub fn new(root: Handle<BehaviorTreeRoot>) -> Self {
Self { root }
}
pub fn from_node<N: Node>(node: N, asset_server: &mut Assets<BehaviorTreeRoot>) -> Self {
let handle = asset_server.add(BehaviorTreeRoot {
node: Box::new(node),
});
Self { root: handle }
}
pub fn begin(&self, world: &mut World, entity: Entity) -> NodeStatus {
world.resource_scope(|world, assets: Mut<Assets<BehaviorTreeRoot>>| {
assets
.get(&self.root)
.map(|root| root.node.as_ref().begin(world, entity))
.unwrap_or(NodeStatus::Beginning)
})
}
pub fn resume(
&self,
world: &mut World,
entity: Entity,
state: Box<dyn node::NodeState>,
) -> NodeStatus {
world.resource_scope(|world, assets: Mut<Assets<BehaviorTreeRoot>>| {
match assets.get(&self.root) {
None => NodeStatus::Pending(state),
Some(root) => root.node.as_ref().resume(world, entity, state),
}
})
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Component, Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Freeze;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Component, Debug)]
pub struct TreeStatus(NodeStatus);
impl Default for TreeStatus {
fn default() -> Self {
Self(NodeStatus::Beginning)
}
}
fn update(
world: &mut World,
query: &mut QueryState<(Entity, &BehaviorTree), (With<TreeStatus>, Without<Freeze>)>,
) {
let targets = query
.iter(world)
.map(|(entity, tree)| (entity, tree.clone()))
.collect::<Vec<_>>();
targets.into_iter().for_each(|(entity, tree)| {
if let Some(TreeStatus(status)) = world.entity_mut(entity).take::<TreeStatus>() {
let new_status = match status {
NodeStatus::Beginning => tree.begin(world, entity),
NodeStatus::Pending(state) => tree.resume(world, entity, state),
NodeStatus::Complete(_) => status,
};
world.entity_mut(entity).insert(TreeStatus(new_status));
}
});
}
#[cfg(feature = "serde")]
pub fn load_from_source(
mut commands: Commands,
asset_server: Res<AssetServer>,
query: Query<(Entity, &BehaviorTreeSource), (Added<BehaviorTreeSource>, Without<BehaviorTree>)>,
) {
for (entity, source) in query.iter() {
let handle = asset_server.load(&source.path);
commands
.entity(entity)
.insert(BehaviorTree { root: handle });
}
}
#[cfg(test)]
mod tests {
use crate::{node::NodeStatus, tester_util::prelude::*};
#[test]
fn test_tree_end_with_result() {
let mut app = App::new();
app.add_plugins((TesterPlugin, BehaviorTreePlugin::default()));
let task = TesterTask0::new(1, NodeResult::Success);
let tree = BehaviorTree::from_node(
task,
&mut app.world_mut().resource_mut::<Assets<BehaviorTreeRoot>>(),
);
let entity = app.world_mut().spawn(tree).id();
app.update();
app.update();
let status = app.world().get::<TreeStatus>(entity);
assert!(
status.is_some(),
"BehaviorTree should have result on the end."
);
assert!(
if let Some(TreeStatus(NodeStatus::Complete(result))) = status {
result == &NodeResult::Success
} else {
false
},
"BehaviorTree should have result that match with the result of the root."
);
}
#[test]
fn test_freeze() {
let mut app = App::new();
app.add_plugins((TesterPlugin, BehaviorTreePlugin::default()));
let task = TesterTask0::new(2, NodeResult::Success);
let tree = BehaviorTree::from_node(
task,
&mut app.world_mut().resource_mut::<Assets<BehaviorTreeRoot>>(),
);
let entity = app.world_mut().spawn(tree).id();
app.update();
app.world_mut().entity_mut(entity).insert(Freeze);
app.update(); app.update(); app.update(); app.world_mut().entity_mut(entity).remove::<Freeze>();
app.update(); let expected = TestLog {
log: vec![
TestLogEntry {
task_id: 0,
updated_count: 0,
frame: 1,
},
TestLogEntry {
task_id: 0,
updated_count: 1,
frame: 2,
},
TestLogEntry {
task_id: 0,
updated_count: 2,
frame: 3,
},
TestLogEntry {
task_id: 0,
updated_count: 3,
frame: 4,
},
],
};
let found = app.world().get_resource::<TestLog>().unwrap();
assert!(
found == &expected,
"Task should not proceed while freeze. found: {:?}",
found
);
}
#[cfg(feature = "serde")]
#[test]
fn test_save_and_load_roundtrip() {
use std::fs;
use std::path::Path;
let task = TesterTask0::new(1, NodeResult::Success);
let root = BehaviorTreeRoot {
node: Box::new(task),
};
let test_asset_dir = Path::new("target").join("test_assets");
fs::create_dir_all(&test_asset_dir).expect("Failed to create test directory");
let file_name = "roundtrip_tree.ron";
let file_path = test_asset_dir.join(file_name);
let ron = ron::to_string(&root).expect("Failed to serialize test asset");
fs::write(&file_path, ron).expect("Failed to write test asset");
let mut app = App::new();
app.add_plugins((TesterPlugin, BehaviorTreePlugin::default()));
let source = BehaviorTreeSource {
path: file_name.to_string(),
};
let entity = app.world_mut().spawn(source).id();
for _ in 0..50 {
app.update();
}
app.world_mut()
.resource_scope(|world, asset_server: Mut<AssetServer>| {
if let Some(tree) = world.query::<&BehaviorTree>().iter(world).next() {
let load_state = asset_server.get_load_state(&tree.root);
println!("LoadState = {:?}", load_state);
}
});
let status = app.world().get::<TreeStatus>(entity);
assert!(
status.is_some(),
"BehaviorTree should have result on the end."
);
assert!(
if let Some(TreeStatus(NodeStatus::Complete(result))) = status {
println!("Found TreeStatus with result: {:?}", result);
result == &NodeResult::Success
} else {
if let Some(TreeStatus(s)) = status {
match s {
NodeStatus::Beginning => println!("TreeStatus is still Beginning"),
NodeStatus::Pending(_) => println!("TreeStatus is still Pending"),
NodeStatus::Complete(r) => {
println!("TreeStatus is Complete with result: {:?}", r)
}
}
}
false
},
"BehaviorTree should have result that match with the result of the root."
);
}
}