From 0287fa8e311c4a09056492c87569f70b9f6cf8f4 Mon Sep 17 00:00:00 2001 From: Ash Blue Date: Mon, 3 Jun 2019 12:20:39 -0600 Subject: [PATCH] feat(behavior-tree): calling `Reset()` now triggers End on active nodes Previously calling reset on a BehaviorTree did not call `End()` because it was not performant. Nodes that return continue on a tree are now registered as active and removed after returning continue. Allowing them to be discovered at the top level by the BehaviorTree without traversing the entire system. --- .../Runtime/BehaviorTree/BehaviorTree.cs | 31 +++- .../Runtime/Decorators/DecoratorBase.cs | 2 +- .../Runtime/TaskParents/TaskParentBase.cs | 2 +- .../FluidBehaviorTree/Runtime/Tasks/ITask.cs | 2 +- .../Runtime/Tasks/TaskBase.cs | 9 +- .../Editor/BehaviorTrees/BehaviorTreeTest.cs | 46 ++++++ .../Tests/Editor/Tasks/TaskTest.cs | 132 +++++++++++++----- 7 files changed, 181 insertions(+), 43 deletions(-) diff --git a/Assets/FluidBehaviorTree/Runtime/BehaviorTree/BehaviorTree.cs b/Assets/FluidBehaviorTree/Runtime/BehaviorTree/BehaviorTree.cs index b184b16e..b1e75ffa 100644 --- a/Assets/FluidBehaviorTree/Runtime/BehaviorTree/BehaviorTree.cs +++ b/Assets/FluidBehaviorTree/Runtime/BehaviorTree/BehaviorTree.cs @@ -1,14 +1,24 @@ -using CleverCrow.Fluid.BTs.TaskParents; +using System.Collections.Generic; +using CleverCrow.Fluid.BTs.TaskParents; using CleverCrow.Fluid.BTs.Tasks; using UnityEngine; namespace CleverCrow.Fluid.BTs.Trees { - public class BehaviorTree { + public interface IBehaviorTree { + int TickCount { get; } + + void AddActiveTask (ITask task); + void RemoveActiveTask (ITask task); + } + + public class BehaviorTree : IBehaviorTree { private readonly GameObject _owner; + private readonly List _tasks = new List(); public int TickCount { get; private set; } - + public TaskRoot Root { get; } = new TaskRoot(); + public IReadOnlyList ActiveTasks => _tasks; public BehaviorTree (GameObject owner) { _owner = owner; @@ -18,13 +28,18 @@ public BehaviorTree (GameObject owner) { public TaskStatus Tick () { var status = Root.Update(); if (status != TaskStatus.Continue) { - TickCount++; + Reset(); } return status; } public void Reset () { + foreach (var task in _tasks) { + task.End(); + } + + _tasks.Clear(); TickCount++; } @@ -54,5 +69,13 @@ private void SyncNodes (ITaskParent taskParent) { } } } + + public void AddActiveTask (ITask task) { + _tasks.Add(task); + } + + public void RemoveActiveTask (ITask task) { + _tasks.Remove(task); + } } } \ No newline at end of file diff --git a/Assets/FluidBehaviorTree/Runtime/Decorators/DecoratorBase.cs b/Assets/FluidBehaviorTree/Runtime/Decorators/DecoratorBase.cs index 5e314b0e..81e4d4c7 100644 --- a/Assets/FluidBehaviorTree/Runtime/Decorators/DecoratorBase.cs +++ b/Assets/FluidBehaviorTree/Runtime/Decorators/DecoratorBase.cs @@ -13,7 +13,7 @@ public abstract class DecoratorBase : ITaskParent { public bool Enabled { get; set; } = true; public GameObject Owner { get; set; } - public BehaviorTree ParentTree { get; set; } + public IBehaviorTree ParentTree { get; set; } public TaskStatus LastStatus { get; private set; } public ITask Child => Children.Count > 0 ? Children[0] : null; diff --git a/Assets/FluidBehaviorTree/Runtime/TaskParents/TaskParentBase.cs b/Assets/FluidBehaviorTree/Runtime/TaskParents/TaskParentBase.cs index b1d1fd9d..4aef9d10 100644 --- a/Assets/FluidBehaviorTree/Runtime/TaskParents/TaskParentBase.cs +++ b/Assets/FluidBehaviorTree/Runtime/TaskParents/TaskParentBase.cs @@ -5,7 +5,7 @@ namespace CleverCrow.Fluid.BTs.TaskParents { public abstract class TaskParentBase : ITaskParent { - public BehaviorTree ParentTree { get; set; } + public IBehaviorTree ParentTree { get; set; } public TaskStatus LastStatus { get; private set; } public string Name { get; set; } diff --git a/Assets/FluidBehaviorTree/Runtime/Tasks/ITask.cs b/Assets/FluidBehaviorTree/Runtime/Tasks/ITask.cs index 35f62855..edc58145 100644 --- a/Assets/FluidBehaviorTree/Runtime/Tasks/ITask.cs +++ b/Assets/FluidBehaviorTree/Runtime/Tasks/ITask.cs @@ -22,7 +22,7 @@ public interface ITask { /// /// Tree this node belongs to /// - BehaviorTree ParentTree { get; set; } + IBehaviorTree ParentTree { get; set; } /// /// Last status returned by Update diff --git a/Assets/FluidBehaviorTree/Runtime/Tasks/TaskBase.cs b/Assets/FluidBehaviorTree/Runtime/Tasks/TaskBase.cs index e96be725..2e1c71c9 100644 --- a/Assets/FluidBehaviorTree/Runtime/Tasks/TaskBase.cs +++ b/Assets/FluidBehaviorTree/Runtime/Tasks/TaskBase.cs @@ -7,11 +7,12 @@ public abstract class TaskBase : ITask { private bool _start; private bool _exit; private int _lastTickCount; + private bool _active; public string Name { get; set; } public bool Enabled { get; set; } = true; public GameObject Owner { get; set; } - public BehaviorTree ParentTree { get; set; } + public IBehaviorTree ParentTree { get; set; } public TaskStatus LastStatus { get; private set; } @@ -32,9 +33,12 @@ public TaskStatus Update () { var status = GetUpdate(); LastStatus = status; - // Soft reset since the node has completed if (status != TaskStatus.Continue) { + if (_active) ParentTree?.RemoveActiveTask(this); Exit(); + } else if (!_active) { + ParentTree?.AddActiveTask(this); + _active = true; } return status; @@ -56,6 +60,7 @@ private void UpdateTicks () { /// Reset the node to be re-used /// public void Reset () { + _active = false; _start = false; _exit = false; } diff --git a/Assets/FluidBehaviorTree/Tests/Editor/BehaviorTrees/BehaviorTreeTest.cs b/Assets/FluidBehaviorTree/Tests/Editor/BehaviorTrees/BehaviorTreeTest.cs index 0d0a5375..6481d625 100644 --- a/Assets/FluidBehaviorTree/Tests/Editor/BehaviorTrees/BehaviorTreeTest.cs +++ b/Assets/FluidBehaviorTree/Tests/Editor/BehaviorTrees/BehaviorTreeTest.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using System.Linq; using CleverCrow.Fluid.BTs.TaskParents.Composites; using CleverCrow.Fluid.BTs.Tasks; +using CleverCrow.Fluid.BTs.Testing; using NSubstitute; using NUnit.Framework; using UnityEngine; @@ -121,6 +123,27 @@ public void It_should_increase_the_tick_count () { Assert.AreEqual(1, _tree.TickCount); } + + [Test] + public void It_should_call_End_on_active_nodes () { + var task = A.TaskStub().Build(); + + _tree.AddActiveTask(task); + _tree.Reset(); + + task.Received(1).End(); + } + + [Test] + public void It_should_not_call_End_on_active_nodes_twice_if_called_again () { + var task = A.TaskStub().Build(); + + _tree.AddActiveTask(task); + _tree.Reset(); + _tree.Reset(); + + task.Received(1).End(); + } } public class TickMethod : BehaviorTreeTest { @@ -218,5 +241,28 @@ public void Does_not_run_reset_on_2nd_action_if_1st_fails () { } } } + + public class AddActiveTaskMethod : BehaviorTreeTest { + [Test] + public void It_should_add_an_active_task () { + var task = A.TaskStub().Build(); + + _tree.AddActiveTask(task); + + Assert.IsTrue(_tree.ActiveTasks.Contains(task)); + } + } + + public class RemoveActiveTaskMethod : BehaviorTreeTest { + [Test] + public void It_should_add_an_active_task () { + var task = A.TaskStub().Build(); + + _tree.AddActiveTask(task); + _tree.RemoveActiveTask(task); + + Assert.IsFalse(_tree.ActiveTasks.Contains(task)); + } + } } } \ No newline at end of file diff --git a/Assets/FluidBehaviorTree/Tests/Editor/Tasks/TaskTest.cs b/Assets/FluidBehaviorTree/Tests/Editor/Tasks/TaskTest.cs index fb2131cf..aca1db1b 100644 --- a/Assets/FluidBehaviorTree/Tests/Editor/Tasks/TaskTest.cs +++ b/Assets/FluidBehaviorTree/Tests/Editor/Tasks/TaskTest.cs @@ -1,6 +1,7 @@ using CleverCrow.Fluid.BTs.Tasks; using CleverCrow.Fluid.BTs.Tasks.Actions; using CleverCrow.Fluid.BTs.Trees; +using NSubstitute; using NUnit.Framework; using UnityEngine; @@ -30,18 +31,81 @@ protected override void OnExit () { } public class UpdateMethod { - TaskExample node; + TaskExample _node; [SetUp] public void SetUpNode () { - node = new TaskExample(); + _node = new TaskExample(); + } + + public class RegisteringContinueNodes : UpdateMethod { + [Test] + public void It_should_call_BehaviorTree_AddActiveTask_on_continue () { + var tree = Substitute.For(); + _node.status = TaskStatus.Continue; + _node.ParentTree = tree; + + _node.Update(); + + tree.Received(1).AddActiveTask(_node); + } + + [Test] + public void It_should_not_call_BehaviorTree_AddActiveTask_on_continue_twice () { + var tree = Substitute.For(); + _node.status = TaskStatus.Continue; + _node.ParentTree = tree; + + _node.Update(); + _node.Update(); + + tree.Received(1).AddActiveTask(_node); + } + + [Test] + public void It_should_call_BehaviorTree_AddActiveTask_again_after_Reset () { + var tree = Substitute.For(); + _node.status = TaskStatus.Continue; + _node.ParentTree = tree; + + _node.Update(); + _node.Reset(); + _node.Update(); + + tree.Received(2).AddActiveTask(_node); + } + + [Test] + public void It_should_call_BehaviorTree_RemoveActiveTask_after_returning_continue () { + var tree = Substitute.For(); + _node.status = TaskStatus.Continue; + _node.ParentTree = tree; + + _node.Update(); + + _node.status = TaskStatus.Success; + _node.Update(); + + tree.Received(1).RemoveActiveTask(_node); + } + + [Test] + public void It_should_not_call_BehaviorTree_AddActiveTask_if_continue_was_not_returned () { + var tree = Substitute.For(); + _node.status = TaskStatus.Success; + _node.ParentTree = tree; + + _node.Update(); + + tree.Received(0).RemoveActiveTask(_node); + } } public class TreeTickCountChange : UpdateMethod { private GameObject _go; [SetUp] - public void BeforEach () { + public void BeforeEach () { _go = new GameObject(); } @@ -53,106 +117,106 @@ public void AfterEach () { [Test] public void Retriggers_start_if_tick_count_changes () { var tree = new BehaviorTree(_go); - tree.AddNode(tree.Root, node); + tree.AddNode(tree.Root, _node); tree.Tick(); tree.Tick(); - Assert.AreEqual(2, node.StartCount); + Assert.AreEqual(2, _node.StartCount); } [Test] public void Does_not_retrigger_start_if_tick_count_stays_the_same () { - node.status = TaskStatus.Continue; + _node.status = TaskStatus.Continue; var tree = new BehaviorTree(_go); - tree.AddNode(tree.Root, node); + tree.AddNode(tree.Root, _node); tree.Tick(); tree.Tick(); - Assert.AreEqual(1, node.StartCount); + Assert.AreEqual(1, _node.StartCount); } } public class StartEvent : UpdateMethod { [Test] public void Trigger_on_1st_run () { - node.Update(); + _node.Update(); - Assert.AreEqual(1, node.StartCount); + Assert.AreEqual(1, _node.StartCount); } [Test] public void Do_not_trigger_on_2nd_run () { - node.Update(); - node.Update(); + _node.Update(); + _node.Update(); - Assert.AreEqual(1, node.StartCount); + Assert.AreEqual(1, _node.StartCount); } [Test] public void Triggers_on_reset () { - node.status = TaskStatus.Continue; + _node.status = TaskStatus.Continue; - node.Update(); - node.Reset(); - node.Update(); + _node.Update(); + _node.Reset(); + _node.Update(); - Assert.AreEqual(2, node.StartCount); + Assert.AreEqual(2, _node.StartCount); } } public class InitEvent : UpdateMethod { [SetUp] public void TriggerUpdate () { - node.Update(); + _node.Update(); } [Test] public void Triggers_on_1st_update () { - Assert.AreEqual(1, node.InitCount); + Assert.AreEqual(1, _node.InitCount); } [Test] public void Does_not_trigger_on_2nd_update () { - node.Update(); + _node.Update(); - Assert.AreEqual(1, node.InitCount); + Assert.AreEqual(1, _node.InitCount); } [Test] public void Does_not_trigger_on_reset () { - node.Reset(); - node.Update(); + _node.Reset(); + _node.Update(); - Assert.AreEqual(1, node.InitCount); + Assert.AreEqual(1, _node.InitCount); } } public class ExitEvent : UpdateMethod { [Test] public void Does_not_trigger_on_continue () { - node.status = TaskStatus.Continue; - node.Update(); + _node.status = TaskStatus.Continue; + _node.Update(); - Assert.AreEqual(0, node.ExitCount); + Assert.AreEqual(0, _node.ExitCount); } [Test] public void Triggers_on_success () { - node.status = TaskStatus.Success; - node.Update(); + _node.status = TaskStatus.Success; + _node.Update(); - Assert.AreEqual(1, node.ExitCount); + Assert.AreEqual(1, _node.ExitCount); } [Test] public void Triggers_on_failure () { - node.status = TaskStatus.Failure; - node.Update(); + _node.status = TaskStatus.Failure; + _node.Update(); - Assert.AreEqual(1, node.ExitCount); + Assert.AreEqual(1, _node.ExitCount); } } }