From 419c6ef62e4439dacd164083c45c8a18057efdb3 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Wed, 5 Feb 2020 13:18:01 +0100 Subject: [PATCH 01/16] added pause and resume functionality There is now a 'Pause'-Method and a 'Resume'-Method on every node. When Pause() is called on the behavior tree (the root), the current active tasks are stopped but without affecting the decorators and composites stop logic. When Resume() is called the previously stopped tasks are started again. Pause() and Resume() should't be called on other nodes but the root. Instead of changing the 'Root', 'Task' and 'Composite' class as suggested, I just changed the common parent class 'Container'. --- Scripts/Composite/Composite.cs | 2 -- Scripts/Container.cs | 42 +++++++++++++++++++++++++++++++++- Scripts/Decorator/Decorator.cs | 1 + Scripts/Node.cs | 16 ++++++++++--- Scripts/Task/Task.cs | 10 ++++++++ 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/Scripts/Composite/Composite.cs b/Scripts/Composite/Composite.cs index a00f936d..ae8ba554 100644 --- a/Scripts/Composite/Composite.cs +++ b/Scripts/Composite/Composite.cs @@ -4,8 +4,6 @@ namespace NPBehave { public abstract class Composite : Container { - protected Node[] Children; - public Composite(string name, Node[] children) : base(name) { this.Children = children; diff --git a/Scripts/Container.cs b/Scripts/Container.cs index 22f9efa4..ae593731 100644 --- a/Scripts/Container.cs +++ b/Scripts/Container.cs @@ -1,10 +1,15 @@ +using System.Collections.Generic; using UnityEngine.Assertions; namespace NPBehave { public abstract class Container : Node { + protected Node[] Children; + private readonly List pausedChildren = new List(); + private bool collapse = false; + public bool Collapse { get @@ -25,7 +30,42 @@ public void ChildStopped(Node child, bool succeeded) { // Assert.AreNotEqual(this.currentState, State.INACTIVE, "The Child " + child.Name + " of Container " + this.Name + " was stopped while the container was inactive. PATH: " + GetPath()); Assert.AreNotEqual(this.currentState, State.INACTIVE, "A Child of a Container was stopped while the container was inactive."); - this.DoChildStopped(child, succeeded); + + if (currentState != State.PAUSED) + { + this.DoChildStopped(child, succeeded); + } + } + + override public void Pause() + { + if (!IsActive) + return; + + currentState = State.PAUSED; + + foreach (Node child in Children) + { + if (child.IsActive) + { + child.Pause(); + this.pausedChildren.Add(child); + } + } + } + + override public void Resume() + { + if (currentState != State.PAUSED) + return; + + currentState = State.ACTIVE; + + foreach (Node child in pausedChildren) + { + child.Resume(); + } + this.pausedChildren.Clear(); } protected abstract void DoChildStopped(Node child, bool succeeded); diff --git a/Scripts/Decorator/Decorator.cs b/Scripts/Decorator/Decorator.cs index 4bb66c40..d2bfce00 100644 --- a/Scripts/Decorator/Decorator.cs +++ b/Scripts/Decorator/Decorator.cs @@ -8,6 +8,7 @@ public abstract class Decorator : Container public Decorator(string name, Node decoratee) : base(name) { this.Decoratee = decoratee; + Children = new[] {decoratee}; this.Decoratee.SetParent(this); } diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 83b440a6..fd464841 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -9,6 +9,7 @@ public enum State INACTIVE, ACTIVE, STOP_REQUESTED, + PAUSED, } protected State currentState = State.INACTIVE; @@ -138,6 +139,16 @@ public void Stop() #endif DoStop(); } + + public virtual void Pause() + { + + } + + public virtual void Resume() + { + + } protected virtual void DoStart() { @@ -148,8 +159,7 @@ protected virtual void DoStop() { } - - + /// THIS ABSOLUTLY HAS TO BE THE LAST CALL IN YOUR FUNCTION, NEVER MODIFY /// ANY STATE AFTER CALLING Stopped !!!! protected virtual void Stopped(bool success) @@ -198,7 +208,7 @@ protected virtual void DoParentCompositeStopped(Composite composite) override public string ToString() { - return !string.IsNullOrEmpty(Label) ? (this.Name + "{"+Label+"}") : this.Name; + return !string.IsNullOrEmpty(Label) ? (this.Name + "{" + Label + "}") : this.Name; } protected string GetPath() diff --git a/Scripts/Task/Task.cs b/Scripts/Task/Task.cs index c644f56c..84894184 100644 --- a/Scripts/Task/Task.cs +++ b/Scripts/Task/Task.cs @@ -5,5 +5,15 @@ public abstract class Task : Node public Task(string name) : base(name) { } + + public override void Pause() + { + Stop(); + } + + public override void Resume() + { + Start(); + } } } \ No newline at end of file From 4e305986e758fdfb7b0b88b53937745a57c2025f Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Wed, 5 Feb 2020 19:21:33 +0100 Subject: [PATCH 02/16] changed ObservingDecorator to stop observing on Pause() and start observing again on Resume() --- Scripts/Decorator/ObservingDecorator.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Scripts/Decorator/ObservingDecorator.cs b/Scripts/Decorator/ObservingDecorator.cs index 63962994..fa1001ad 100644 --- a/Scripts/Decorator/ObservingDecorator.cs +++ b/Scripts/Decorator/ObservingDecorator.cs @@ -41,6 +41,22 @@ override protected void DoStop() Decoratee.Stop(); } + public override void Pause() + { + base.Pause(); + if (currentState == State.PAUSED) + StopObserving(); + } + + public override void Resume() + { + if (currentState != State.PAUSED) + return; + + base.Resume(); + StartObserving(); + } + protected override void DoChildStopped(Node child, bool result) { Assert.AreNotEqual(this.CurrentState, State.INACTIVE); From 8cae75bbb5e0ae63451fae563b3c865e4a116d73 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Wed, 5 Feb 2020 19:31:22 +0100 Subject: [PATCH 03/16] service decorator stops now when paused Unregister Clock timers at Pause() and register them again at Resume() in Service decorator. --- Scripts/Decorator/Service.cs | 44 ++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/Scripts/Decorator/Service.cs b/Scripts/Decorator/Service.cs index 6f5bc0e9..7a854117 100644 --- a/Scripts/Decorator/Service.cs +++ b/Scripts/Decorator/Service.cs @@ -31,6 +31,39 @@ public Service(System.Action service, Node decoratee) : base("Service", decorate } protected override void DoStart() + { + startService(); + Decoratee.Start(); + } + + override protected void DoStop() + { + Decoratee.Stop(); + } + + protected override void DoChildStopped(Node child, bool result) + { + stopService(); + Stopped(result); + } + + public override void Pause() + { + base.Pause(); + if (currentState == State.PAUSED) + stopService(); + } + + public override void Resume() + { + if (currentState == State.PAUSED) + { + base.Resume(); + startService(); + } + } + + private void startService() { if (this.interval <= 0f) { @@ -46,15 +79,9 @@ protected override void DoStart() { InvokeServiceMethodWithRandomVariation(); } - Decoratee.Start(); } - override protected void DoStop() - { - Decoratee.Stop(); - } - - protected override void DoChildStopped(Node child, bool result) + private void stopService() { if (this.interval <= 0f) { @@ -68,9 +95,8 @@ protected override void DoChildStopped(Node child, bool result) { this.Clock.RemoveTimer(InvokeServiceMethodWithRandomVariation); } - Stopped(result); } - + private void InvokeServiceMethodWithRandomVariation() { serviceMethod(); From e6a7674e56fd30c6bd83c638f3b41c72d20ab84b Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Wed, 5 Feb 2020 19:32:56 +0100 Subject: [PATCH 04/16] WaitForCondition timer now stops on pause --- Scripts/Decorator/WaitForCondition.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Scripts/Decorator/WaitForCondition.cs b/Scripts/Decorator/WaitForCondition.cs index 0c4c2d8d..430c259a 100644 --- a/Scripts/Decorator/WaitForCondition.cs +++ b/Scripts/Decorator/WaitForCondition.cs @@ -28,6 +28,27 @@ public WaitForCondition(Func condition, Node decoratee) : base("WaitForCon } protected override void DoStart() + { + addTimerOrStartImmediately(); + } + + public override void Pause() + { + base.Pause(); + if (currentState == State.PAUSED) + Clock.RemoveTimer(checkCondition); + } + + public override void Resume() + { + if (currentState == State.PAUSED) + { + base.Resume(); + addTimerOrStartImmediately(); + } + } + + private void addTimerOrStartImmediately() { if (!condition.Invoke()) { From ff15f4d6d6c5972ebacd8ae8ccfde1dd6b26ecb3 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Wed, 5 Feb 2020 20:59:09 +0100 Subject: [PATCH 05/16] instead of checking if the container is active/paused and fail silently, asserting the right values Because Pause() is only cascaded on active nodes, we can assume that Pause() is only called on active node. The only exception is the first node on which Pause() is called, therefore there is an assert checking this condition. There is also an assert checking that Resume() is only called on a paused container. --- Scripts/Container.cs | 8 ++------ Scripts/Decorator/ObservingDecorator.cs | 6 +----- Scripts/Decorator/Service.cs | 10 +++------- Scripts/Decorator/WaitForCondition.cs | 10 +++------- 4 files changed, 9 insertions(+), 25 deletions(-) diff --git a/Scripts/Container.cs b/Scripts/Container.cs index ae593731..e1bd7237 100644 --- a/Scripts/Container.cs +++ b/Scripts/Container.cs @@ -39,9 +39,7 @@ public void ChildStopped(Node child, bool succeeded) override public void Pause() { - if (!IsActive) - return; - + Assert.AreEqual(this.currentState, State.ACTIVE, "Only an active container can be paused."); currentState = State.PAUSED; foreach (Node child in Children) @@ -56,9 +54,7 @@ override public void Pause() override public void Resume() { - if (currentState != State.PAUSED) - return; - + Assert.AreEqual(this.currentState, State.PAUSED, "Only a paused contained can be resumed."); currentState = State.ACTIVE; foreach (Node child in pausedChildren) diff --git a/Scripts/Decorator/ObservingDecorator.cs b/Scripts/Decorator/ObservingDecorator.cs index fa1001ad..c1d49128 100644 --- a/Scripts/Decorator/ObservingDecorator.cs +++ b/Scripts/Decorator/ObservingDecorator.cs @@ -44,15 +44,11 @@ override protected void DoStop() public override void Pause() { base.Pause(); - if (currentState == State.PAUSED) - StopObserving(); + StopObserving(); } public override void Resume() { - if (currentState != State.PAUSED) - return; - base.Resume(); StartObserving(); } diff --git a/Scripts/Decorator/Service.cs b/Scripts/Decorator/Service.cs index 7a854117..202f8a15 100644 --- a/Scripts/Decorator/Service.cs +++ b/Scripts/Decorator/Service.cs @@ -50,17 +50,13 @@ protected override void DoChildStopped(Node child, bool result) public override void Pause() { base.Pause(); - if (currentState == State.PAUSED) - stopService(); + stopService(); } public override void Resume() { - if (currentState == State.PAUSED) - { - base.Resume(); - startService(); - } + base.Resume(); + startService(); } private void startService() diff --git a/Scripts/Decorator/WaitForCondition.cs b/Scripts/Decorator/WaitForCondition.cs index 430c259a..7dfbcd99 100644 --- a/Scripts/Decorator/WaitForCondition.cs +++ b/Scripts/Decorator/WaitForCondition.cs @@ -35,17 +35,13 @@ protected override void DoStart() public override void Pause() { base.Pause(); - if (currentState == State.PAUSED) - Clock.RemoveTimer(checkCondition); + Clock.RemoveTimer(checkCondition); } public override void Resume() { - if (currentState == State.PAUSED) - { - base.Resume(); - addTimerOrStartImmediately(); - } + base.Resume(); + addTimerOrStartImmediately(); } private void addTimerOrStartImmediately() From c00c3d2ab7ca2c0c38e9e139644ed1ee6c622a49 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Thu, 6 Feb 2020 13:29:47 +0100 Subject: [PATCH 06/16] wrote test for pausing and resuming I have written five tests: 1.) pausing and resuming of simple behavior tree 2.) pausing and resuming of slighly more complex behavior tree 3.) checking if the behavior tree ignores the blackboard conditon when paused -> this fails, the ignoring works, but not the notifying after resuming 4.) service is inactive when paused 5.) ignore WaitForCondition when paused --- Editor/Tests/PauseResumeTest.cs | 251 ++++++++++++++++++++++++++++++++ Editor/Tests/_utils/MockTask.cs | 22 +++ 2 files changed, 273 insertions(+) create mode 100644 Editor/Tests/PauseResumeTest.cs create mode 100644 Editor/Tests/_utils/MockTask.cs diff --git a/Editor/Tests/PauseResumeTest.cs b/Editor/Tests/PauseResumeTest.cs new file mode 100644 index 00000000..e7ba9e45 --- /dev/null +++ b/Editor/Tests/PauseResumeTest.cs @@ -0,0 +1,251 @@ +using System.IO; +using NUnit.Framework; + +namespace NPBehave +{ + public class PauseResumeTest : Test + { + + [Test] + // tests pausing and resuming a very simple behavior tree + public void SimpleBehaviorTree() + { + // building a very simple behavior tree: two tasks in a selector + this.Timer = new Clock(); + this.Blackboard = new Blackboard(Timer); + + MockTask firstTask = new MockTask(false); + MockTask secondTask = new MockTask(false); + + Selector selector = new Selector(firstTask, secondTask); + TestRoot behaviorTree = new TestRoot(Blackboard, Timer, selector); + + // starting the tree + behaviorTree.Start(); + + // first task should be active and second inactive + Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + + // now pause the tree + behaviorTree.Pause(); + + // the previously active task should be stopped (inactive) now + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + + // and the containers above should be in pause mode + Assert.AreEqual(Node.State.PAUSED, behaviorTree.CurrentState); + Assert.AreEqual(Node.State.PAUSED, selector.CurrentState); + + // resume the tree again + behaviorTree.Resume(); + + // the first task should be active again and the second inactive + Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + + // also the containers above should be also active again + Assert.AreEqual(Node.State.ACTIVE, behaviorTree.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, selector.CurrentState); + + // stopping the first task and the first task should be inactive and the second active + firstTask.Stop(); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, secondTask.CurrentState); + } + + [Test] + // tests pausing and resuming a more complex behavior tree + public void SlightlyMoreComplexBehaviorTree() + { + // building a slighly more complex behavior tree + this.Timer = new Clock(); + this.Blackboard = new Blackboard(Timer); + + MockTask firstTask = new MockTask(false); + MockTask secondTask = new MockTask(false); + MockTask thirdTask = new MockTask(false); + + Selector bottomSelector = new Selector(secondTask, thirdTask); + Selector topSelector = new Selector(firstTask, bottomSelector); + TestRoot behaviorTree = new TestRoot(Blackboard, Timer, topSelector); + + // starting the tree + behaviorTree.Start(); + + // first task should be active + Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, thirdTask.CurrentState); + + // now pause the tree + behaviorTree.Pause(); + + // the previously active task should be stopped (inactive) now + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + + // only the containers that lead to the previously active task should be paused + Assert.AreEqual(Node.State.PAUSED, behaviorTree.CurrentState); + Assert.AreEqual(Node.State.PAUSED, topSelector.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, bottomSelector.CurrentState); + + // resume the tree again + behaviorTree.Resume(); + + // the first task should be active again + Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, thirdTask.CurrentState); + + // also the containers above should be also active again + Assert.AreEqual(Node.State.ACTIVE, behaviorTree.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, topSelector.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, bottomSelector.CurrentState); + + // stopping the first task and the second task should be active + firstTask.Stop(); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, secondTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, thirdTask.CurrentState); + + // pausing the tree and all task should be inactive and the container paused + behaviorTree.Pause(); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, thirdTask.CurrentState); + Assert.AreEqual(Node.State.PAUSED, behaviorTree.CurrentState); + Assert.AreEqual(Node.State.PAUSED, topSelector.CurrentState); + Assert.AreEqual(Node.State.PAUSED, bottomSelector.CurrentState); + + // resuming again and the second task should be active again and also the containers + behaviorTree.Resume(); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, secondTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, thirdTask.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, behaviorTree.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, topSelector.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, bottomSelector.CurrentState); + } + + [Test] + public void IgnoreBlackBoardConditionChangedWhenPaused() + { + // building a behavior tree with two task and the first has a condition + this.Timer = new Clock(); + this.Blackboard = new Blackboard(Timer); + + MockTask firstTask = new MockTask(false); + MockTask secondTask = new MockTask(false); + + BlackboardCondition firstCondition = new BlackboardCondition("first", Operator.IS_EQUAL, true, Stops.SELF, firstTask); + Selector selector = new Selector(firstCondition, secondTask); + TestRoot behaviorTree = new TestRoot(Blackboard, Timer, selector); + + Blackboard.Set("first", true); + + // start the tree + behaviorTree.Start(); + + // first task should be active + Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + + // now pause the tree + behaviorTree.Pause(); + + // blackboard condition should be paused now + Assert.AreEqual(Node.State.PAUSED, firstCondition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + + // when changing the condition nothing should happen because it is ignored in pause state + Blackboard.Set("first", false); + Timer.Update(0.1f); + Assert.AreEqual(Node.State.PAUSED, firstCondition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + + // TODO: this doesn't work here!!! The change isn't notified! + // when resuming the change should be notified and the second task should be active + behaviorTree.Resume(); + Timer.Update(0.1f); + Assert.AreEqual(Node.State.INACTIVE, firstCondition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, secondTask.CurrentState); + } + + [Test] + public void ServiceInactiveWhenPause() + { + // I am not really sure if the requirement is rigth that the service should be really inactive when paused. + // But if the blackboard condition was set only in a service, the problem above with the + // blackboard condition would be solved, because the blackboard wouldn't be updated in pause state + + // building a behavior tree with only one service and a task + this.Timer = new Clock(); + this.Blackboard = new Blackboard(Timer); + + MockTask task = new MockTask(false); + + int serviceRunCount = 0; + Service service = new Service(() => serviceRunCount++, task); + TestRoot behaviorTree = new TestRoot(Blackboard, Timer, service); + + behaviorTree.Start(); + + // the service run count should be one + Assert.AreEqual(1, serviceRunCount); + + // after update it should be two + Timer.Update(0.1f); + Assert.AreEqual(2, serviceRunCount); + + // pause the tree + behaviorTree.Pause(); + Timer.Update(0.1f); + // service method shouldn't be called. It should still be two. + Assert.AreEqual(2, serviceRunCount); + + // after resuming the service should be active again and the counter should be three + // because when resuming the method is called + behaviorTree.Resume(); + Assert.AreEqual(3, serviceRunCount); + } + + [Test] + public void IgnoreWaitForConditionWhenPause() + { + this.Timer = new Clock(); + this.Blackboard = new Blackboard(Timer); + + int condition = 0; + MockTask task = new MockTask(false); + WaitForCondition waitForCondition = new WaitForCondition(() => condition != 0, task); + + TestRoot behaviorTree = new TestRoot(Blackboard, Timer, waitForCondition); + + behaviorTree.Start(); + Assert.AreEqual(Node.State.ACTIVE, behaviorTree.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, waitForCondition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, task.CurrentState); + + // when pausing and the condition is met, it should be ignored + behaviorTree.Pause(); + condition = 1; + Timer.Update(0.1f); + Assert.AreEqual(Node.State.PAUSED, behaviorTree.CurrentState); + Assert.AreEqual(Node.State.PAUSED, waitForCondition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, task.CurrentState); + + // when resuming and the condition is met the task should be active + behaviorTree.Resume(); + Timer.Update(0.1f); + Assert.AreEqual(Node.State.ACTIVE, behaviorTree.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, waitForCondition.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, task.CurrentState); + } + } +} \ No newline at end of file diff --git a/Editor/Tests/_utils/MockTask.cs b/Editor/Tests/_utils/MockTask.cs new file mode 100644 index 00000000..8ade1a07 --- /dev/null +++ b/Editor/Tests/_utils/MockTask.cs @@ -0,0 +1,22 @@ +namespace NPBehave +{ + public class MockTask : Task + { + private bool suceedsOnExplicitStop; + + public MockTask(bool suceedsOnExplicitStop) : base("MockTask") + { + this.suceedsOnExplicitStop = suceedsOnExplicitStop; + } + + protected override void DoStop() + { + this.Stopped(suceedsOnExplicitStop); + } + + public void Finish(bool success) + { + this.Stopped(success); + } + } +} \ No newline at end of file From b9cb9b7bc1120b1b3310887d41a97a0dd12a9306 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Thu, 6 Feb 2020 20:28:55 +0100 Subject: [PATCH 07/16] Update README.md to include section about pausing and resuming the tree --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index c29db781..2a578cc8 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,23 @@ In case your Monster gets killed or you just destroy your GameObject, you should // ... ``` +## Pausing the Tree + +The tree can be paused with `Pause()`. When the tree is paused the current executing task is stopped +and the tree does not change its state anymore. +Then the tree can be resumed with `Resume()` again and the previously stopped task is restarted again. +This is useful for executing behavior tree independent logic while the tree is paused, +e.g. an enemy should be stunned after he got hurt. + +```csharp + public IEnumerator StunEnemy() + { + behaviorTree.Pause(); + yield return enemy.Stun(); + behaviorTree.Resume(); + } +``` + ## The Debugger You can use the `Debugger` component to debug the behavior trees at runtime in the inspector. From 47f81b8c21b9e9332b11b01c7dc32c0f91937e21 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Thu, 6 Feb 2020 20:32:37 +0100 Subject: [PATCH 08/16] Add some more (still failing) tests for pausing the behavior tree The problem of the tests failing is still that there is no notification after resuming that the blackboard condition has changed. --- Editor/Tests/PauseResumeTest.cs | 222 +++++++++++++++++++++++++------- 1 file changed, 172 insertions(+), 50 deletions(-) diff --git a/Editor/Tests/PauseResumeTest.cs b/Editor/Tests/PauseResumeTest.cs index e7ba9e45..2e3078df 100644 --- a/Editor/Tests/PauseResumeTest.cs +++ b/Editor/Tests/PauseResumeTest.cs @@ -5,7 +5,6 @@ namespace NPBehave { public class PauseResumeTest : Test { - [Test] // tests pausing and resuming a very simple behavior tree public void SimpleBehaviorTree() @@ -13,48 +12,48 @@ public void SimpleBehaviorTree() // building a very simple behavior tree: two tasks in a selector this.Timer = new Clock(); this.Blackboard = new Blackboard(Timer); - + MockTask firstTask = new MockTask(false); MockTask secondTask = new MockTask(false); - + Selector selector = new Selector(firstTask, secondTask); TestRoot behaviorTree = new TestRoot(Blackboard, Timer, selector); - + // starting the tree behaviorTree.Start(); - + // first task should be active and second inactive Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); - + // now pause the tree behaviorTree.Pause(); - + // the previously active task should be stopped (inactive) now Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); - + // and the containers above should be in pause mode Assert.AreEqual(Node.State.PAUSED, behaviorTree.CurrentState); Assert.AreEqual(Node.State.PAUSED, selector.CurrentState); - + // resume the tree again behaviorTree.Resume(); - + // the first task should be active again and the second inactive Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); - + // also the containers above should be also active again Assert.AreEqual(Node.State.ACTIVE, behaviorTree.CurrentState); Assert.AreEqual(Node.State.ACTIVE, selector.CurrentState); - + // stopping the first task and the first task should be inactive and the second active firstTask.Stop(); Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.ACTIVE, secondTask.CurrentState); } - + [Test] // tests pausing and resuming a more complex behavior tree public void SlightlyMoreComplexBehaviorTree() @@ -62,55 +61,55 @@ public void SlightlyMoreComplexBehaviorTree() // building a slighly more complex behavior tree this.Timer = new Clock(); this.Blackboard = new Blackboard(Timer); - + MockTask firstTask = new MockTask(false); MockTask secondTask = new MockTask(false); MockTask thirdTask = new MockTask(false); - + Selector bottomSelector = new Selector(secondTask, thirdTask); Selector topSelector = new Selector(firstTask, bottomSelector); TestRoot behaviorTree = new TestRoot(Blackboard, Timer, topSelector); - + // starting the tree behaviorTree.Start(); - + // first task should be active Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, thirdTask.CurrentState); - + // now pause the tree behaviorTree.Pause(); - + // the previously active task should be stopped (inactive) now Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); - + // only the containers that lead to the previously active task should be paused Assert.AreEqual(Node.State.PAUSED, behaviorTree.CurrentState); Assert.AreEqual(Node.State.PAUSED, topSelector.CurrentState); Assert.AreEqual(Node.State.INACTIVE, bottomSelector.CurrentState); - + // resume the tree again behaviorTree.Resume(); - + // the first task should be active again Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, thirdTask.CurrentState); - + // also the containers above should be also active again Assert.AreEqual(Node.State.ACTIVE, behaviorTree.CurrentState); Assert.AreEqual(Node.State.ACTIVE, topSelector.CurrentState); Assert.AreEqual(Node.State.INACTIVE, bottomSelector.CurrentState); - + // stopping the first task and the second task should be active firstTask.Stop(); Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.ACTIVE, secondTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, thirdTask.CurrentState); - + // pausing the tree and all task should be inactive and the container paused behaviorTree.Pause(); Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); @@ -119,7 +118,7 @@ public void SlightlyMoreComplexBehaviorTree() Assert.AreEqual(Node.State.PAUSED, behaviorTree.CurrentState); Assert.AreEqual(Node.State.PAUSED, topSelector.CurrentState); Assert.AreEqual(Node.State.PAUSED, bottomSelector.CurrentState); - + // resuming again and the second task should be active again and also the containers behaviorTree.Resume(); Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); @@ -129,45 +128,51 @@ public void SlightlyMoreComplexBehaviorTree() Assert.AreEqual(Node.State.ACTIVE, topSelector.CurrentState); Assert.AreEqual(Node.State.ACTIVE, bottomSelector.CurrentState); } - + [Test] - public void IgnoreBlackBoardConditionChangedWhenPaused() - { + // the behavior tree should ignore the blackboard condition (with self stop rule) when paused + public void IgnoreBlackBoardConditionWhenPausedSelf() + { // building a behavior tree with two task and the first has a condition this.Timer = new Clock(); this.Blackboard = new Blackboard(Timer); - + MockTask firstTask = new MockTask(false); MockTask secondTask = new MockTask(false); - BlackboardCondition firstCondition = new BlackboardCondition("first", Operator.IS_EQUAL, true, Stops.SELF, firstTask); + BlackboardCondition firstCondition = + new BlackboardCondition("first", Operator.IS_EQUAL, true, Stops.SELF, firstTask); Selector selector = new Selector(firstCondition, secondTask); TestRoot behaviorTree = new TestRoot(Blackboard, Timer, selector); - + Blackboard.Set("first", true); - + // start the tree behaviorTree.Start(); - + // first task should be active Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); - + // now pause the tree behaviorTree.Pause(); - - // blackboard condition should be paused now + + // blackboard condition should be paused now, the observers are unregistered + // unregister the observers is actually unnecessary because only active nodes can be stopped + // and restart is only called when there is at least one active child + // and in pause state is everything inactive + // but it leads to less unnecessary executed code Assert.AreEqual(Node.State.PAUSED, firstCondition.CurrentState); Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); - + // when changing the condition nothing should happen because it is ignored in pause state Blackboard.Set("first", false); Timer.Update(0.1f); Assert.AreEqual(Node.State.PAUSED, firstCondition.CurrentState); Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); - + // TODO: this doesn't work here!!! The change isn't notified! // when resuming the change should be notified and the second task should be active behaviorTree.Resume(); @@ -183,32 +188,32 @@ public void ServiceInactiveWhenPause() // I am not really sure if the requirement is rigth that the service should be really inactive when paused. // But if the blackboard condition was set only in a service, the problem above with the // blackboard condition would be solved, because the blackboard wouldn't be updated in pause state - + // building a behavior tree with only one service and a task this.Timer = new Clock(); this.Blackboard = new Blackboard(Timer); - + MockTask task = new MockTask(false); int serviceRunCount = 0; Service service = new Service(() => serviceRunCount++, task); TestRoot behaviorTree = new TestRoot(Blackboard, Timer, service); - + behaviorTree.Start(); - + // the service run count should be one Assert.AreEqual(1, serviceRunCount); - + // after update it should be two Timer.Update(0.1f); Assert.AreEqual(2, serviceRunCount); - + // pause the tree behaviorTree.Pause(); Timer.Update(0.1f); // service method shouldn't be called. It should still be two. Assert.AreEqual(2, serviceRunCount); - + // after resuming the service should be active again and the counter should be three // because when resuming the method is called behaviorTree.Resume(); @@ -224,14 +229,14 @@ public void IgnoreWaitForConditionWhenPause() int condition = 0; MockTask task = new MockTask(false); WaitForCondition waitForCondition = new WaitForCondition(() => condition != 0, task); - + TestRoot behaviorTree = new TestRoot(Blackboard, Timer, waitForCondition); - + behaviorTree.Start(); Assert.AreEqual(Node.State.ACTIVE, behaviorTree.CurrentState); Assert.AreEqual(Node.State.ACTIVE, waitForCondition.CurrentState); Assert.AreEqual(Node.State.INACTIVE, task.CurrentState); - + // when pausing and the condition is met, it should be ignored behaviorTree.Pause(); condition = 1; @@ -239,7 +244,7 @@ public void IgnoreWaitForConditionWhenPause() Assert.AreEqual(Node.State.PAUSED, behaviorTree.CurrentState); Assert.AreEqual(Node.State.PAUSED, waitForCondition.CurrentState); Assert.AreEqual(Node.State.INACTIVE, task.CurrentState); - + // when resuming and the condition is met the task should be active behaviorTree.Resume(); Timer.Update(0.1f); @@ -247,5 +252,122 @@ public void IgnoreWaitForConditionWhenPause() Assert.AreEqual(Node.State.ACTIVE, waitForCondition.CurrentState); Assert.AreEqual(Node.State.ACTIVE, task.CurrentState); } + + // Testing different composite types behavior when pausing and the blackboard condition with Stops.IMMEDIATE_RESTART is changed. + // The change of the condition should be ignored. + [Test] + public void IgnoreBlackboardConditionWhenPausedImmediateRestartSelector() + { + this.Timer = new Clock(); + this.Blackboard = new Blackboard(Timer); + + MockTask firstTask = new MockTask(false); + MockTask secondTask = new MockTask(false); + + BlackboardCondition condition = + new BlackboardCondition("first", Operator.IS_EQUAL, true, Stops.IMMEDIATE_RESTART, firstTask); + Selector selector = new Selector(condition, secondTask); + TestRoot behaviorTree = new TestRoot(Blackboard, Timer, selector); + + // set the condition for the first task to false so that the second task should be active + Blackboard.Set("first", false); + behaviorTree.Start(); + Assert.AreEqual(Node.State.INACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, secondTask.CurrentState); + + // when pausing the tree and setting the blackboard condition to true nothing should happens + behaviorTree.Pause(); + Blackboard.Set("first", true); + Timer.Update(0.1f); + Assert.AreEqual(Node.State.INACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + + // now resume the tree and the first task should be the active one because of the true blackboard condition + behaviorTree.Resume(); + Timer.Update(0.1f); + Assert.AreEqual(Node.State.ACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + } + + [Test] + public void IgnoreBlackboardConditionWhenPausedImmediateRestartSequence() + { + this.Timer = new Clock(); + this.Blackboard = new Blackboard(Timer); + + MockTask firstTask = new MockTask(true); + MockTask secondTask = new MockTask(true); + + BlackboardCondition condition = + new BlackboardCondition("first", Operator.IS_EQUAL, true, Stops.IMMEDIATE_RESTART, firstTask); + Sequence sequence = new Sequence(condition, secondTask); + TestRoot behaviorTree = new TestRoot(Blackboard, Timer, sequence); + + // set the condition for the first task to true so that the first task should be active + Blackboard.Set("first", true); + behaviorTree.Start(); + // update timer so the first task is active + Timer.Update(0.1f); + firstTask.Finish(true); + Blackboard.Set("first", false); + Assert.AreEqual(Node.State.INACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, secondTask.CurrentState); + + // when pausing the tree and setting the blackboard condition to true nothing should happens + behaviorTree.Pause(); + Blackboard.Set("first", true); + Timer.Update(0.1f); + Assert.AreEqual(Node.State.INACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + + // now resume the tree and the first task should be the active one because of the true blackboard condition + behaviorTree.Resume(); + Timer.Update(0.1f); + Assert.AreEqual(Node.State.ACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + } + + [Test] + public void IgnoreBlackboardConditionWhenPausedImmediateRestartParallel() + { + this.Timer = new Clock(); + this.Blackboard = new Blackboard(Timer); + + MockTask firstTask = new MockTask(false); + MockTask secondTask = new MockTask(false); + + BlackboardCondition condition = + new BlackboardCondition("first", Operator.IS_EQUAL, true, Stops.IMMEDIATE_RESTART, firstTask); + Parallel parallel = new Parallel(Parallel.Policy.ALL, Parallel.Policy.ALL, condition, secondTask); + TestRoot behaviorTree = new TestRoot(Blackboard, Timer, parallel); + + // set the condition for the first task to false so that the second task should be active + Blackboard.Set("first", false); + behaviorTree.Start(); + Assert.AreEqual(Node.State.INACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, secondTask.CurrentState); + + // when pausing the tree and setting the blackboard condition to true nothing should happens + behaviorTree.Pause(); + Blackboard.Set("first", true); + Timer.Update(0.1f); + Assert.AreEqual(Node.State.INACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + + // now resume the tree and the first task should be the active one because of the true blackboard condition + behaviorTree.Resume(); + Timer.Update(0.1f); + Assert.AreEqual(Node.State.ACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); + Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + } } } \ No newline at end of file From 3523ae53cf9f874efa473fa6f92b090dc27121cd Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Thu, 6 Feb 2020 20:47:35 +0100 Subject: [PATCH 09/16] Add check that lower priority nodes aren't stopped in pause state (also no restart) When a ObservingDecorator is Paused, the decorator stops observing. But there is also the case where another child in the parent composite is paused and the ObservingDecorator is inactive but still observes for changes to do his logic depending on the stops behavior. For this case there is now an additional check so that no evaluation for stopping and restarting happens in the pause state. --- Scripts/Decorator/ObservingDecorator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Scripts/Decorator/ObservingDecorator.cs b/Scripts/Decorator/ObservingDecorator.cs index c1d49128..4e059a16 100644 --- a/Scripts/Decorator/ObservingDecorator.cs +++ b/Scripts/Decorator/ObservingDecorator.cs @@ -78,6 +78,10 @@ override protected void DoParentCompositeStopped(Composite parentComposite) protected void Evaluate() { + if (ParentNode.CurrentState == State.PAUSED) + { + return; + } if (IsActive && !IsConditionMet()) { if (stopsOnChange == Stops.SELF || stopsOnChange == Stops.BOTH || stopsOnChange == Stops.IMMEDIATE_RESTART) From 84448e22e55676687fce8b615b65403ce8a23643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Ku=CC=88bler?= Date: Thu, 6 Feb 2020 22:39:01 +0100 Subject: [PATCH 10/16] Fix issue with BlackboardCondition not properly picking up changes on resume, made to the blackboard while the BT was paused --- Scripts/Decorator/BlackboardCondition.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Scripts/Decorator/BlackboardCondition.cs b/Scripts/Decorator/BlackboardCondition.cs index f3bad895..6c1937c0 100644 --- a/Scripts/Decorator/BlackboardCondition.cs +++ b/Scripts/Decorator/BlackboardCondition.cs @@ -47,6 +47,14 @@ public BlackboardCondition(string key, Operator op, Stops stopsOnChange, Node de this.stopsOnChange = stopsOnChange; } + public override void Resume() + { + base.Resume(); + if( this.CurrentState == State.ACTIVE ) + { + Evaluate(); + } + } override protected void StartObserving() { From 628397b10735172d3a541aeff6be4e49fd49a676 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Fri, 7 Feb 2020 12:30:11 +0100 Subject: [PATCH 11/16] Fix problems with blackboard condition not evaluated after resuming To fix the problem, I changed that an inactive blackboard condition within a paused composite is actually also paused, so that after resuming an evaluation happens. For this to happen I changed the Pause() method in Container so that Pause() is always called even if the container is active. With exceptions to the tasks, on them Pause() should only be called when there are active, so that they can be stopped. Then the child is only added to the pausedChildren when the state is paused or when it is a stopped task. In ObservingDecorator the method Pause() was then overwritten to always pause, no matter in which state in, but only propagate Pause() on the children when active. Because it's Paused it will be evaluated after Resume(). The only other tricky thing was the StopLowerPriorityChildrenForChild() method in Composite. This method searches for a lower priority active child and thus it's important when resuming that the lower priority nodes are resumed before the higher priority nodes (from right to left). So we need to resume from right to left. Therefore a Stack for the pausedChildren was used so that the nodes are resumed from right to left. --- Scripts/Container.cs | 30 ++++++++++++++-------- Scripts/Decorator/BlackboardCondition.cs | 11 +------- Scripts/Decorator/ObservingDecorator.cs | 32 +++++++++++++++++++++++- Scripts/Root.cs | 6 +++++ 4 files changed, 58 insertions(+), 21 deletions(-) diff --git a/Scripts/Container.cs b/Scripts/Container.cs index e1bd7237..0aac0ec1 100644 --- a/Scripts/Container.cs +++ b/Scripts/Container.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using UnityEngine.Assertions; namespace NPBehave @@ -6,8 +7,8 @@ namespace NPBehave public abstract class Container : Node { protected Node[] Children; - private readonly List pausedChildren = new List(); - + protected readonly Stack pausedChildren = new Stack(); + private bool collapse = false; public bool Collapse @@ -39,15 +40,26 @@ public void ChildStopped(Node child, bool succeeded) override public void Pause() { - Assert.AreEqual(this.currentState, State.ACTIVE, "Only an active container can be paused."); + if (!IsActive) + return; currentState = State.PAUSED; - foreach (Node child in Children) { - if (child.IsActive) + if (child is Task) + { + if (child.IsActive) + { + child.Pause(); + this.pausedChildren.Push(child); + } + } + else { child.Pause(); - this.pausedChildren.Add(child); + if (child.CurrentState == State.PAUSED) + { + this.pausedChildren.Push(child); + } } } } @@ -56,12 +68,10 @@ override public void Resume() { Assert.AreEqual(this.currentState, State.PAUSED, "Only a paused contained can be resumed."); currentState = State.ACTIVE; - - foreach (Node child in pausedChildren) + while (pausedChildren.Any()) { - child.Resume(); + pausedChildren.Pop().Resume(); } - this.pausedChildren.Clear(); } protected abstract void DoChildStopped(Node child, bool succeeded); diff --git a/Scripts/Decorator/BlackboardCondition.cs b/Scripts/Decorator/BlackboardCondition.cs index 6c1937c0..b74e0a43 100644 --- a/Scripts/Decorator/BlackboardCondition.cs +++ b/Scripts/Decorator/BlackboardCondition.cs @@ -46,16 +46,7 @@ public BlackboardCondition(string key, Operator op, Stops stopsOnChange, Node de this.key = key; this.stopsOnChange = stopsOnChange; } - - public override void Resume() - { - base.Resume(); - if( this.CurrentState == State.ACTIVE ) - { - Evaluate(); - } - } - + override protected void StartObserving() { this.RootNode.Blackboard.AddObserver(key, onValueChanged); diff --git a/Scripts/Decorator/ObservingDecorator.cs b/Scripts/Decorator/ObservingDecorator.cs index 4e059a16..15440086 100644 --- a/Scripts/Decorator/ObservingDecorator.cs +++ b/Scripts/Decorator/ObservingDecorator.cs @@ -8,6 +8,7 @@ public abstract class ObservingDecorator : Decorator { protected Stops stopsOnChange; private bool isObserving; + private State beforePauseState; public ObservingDecorator(string name, Stops stopsOnChange, Node decoratee) : base(name, decoratee) { @@ -43,13 +44,42 @@ override protected void DoStop() public override void Pause() { - base.Pause(); + beforePauseState = currentState; + currentState = State.PAUSED; + + // only propagate Pause() on children when it was active + if (beforePauseState != State.ACTIVE) + { + return; + } + + foreach (Node child in Children) + { + if (child is Task task) + { + if (child.IsActive) + { + task.Pause(); + this.pausedChildren.Push(child); + } + } + else + { + child.Pause(); + if (child.CurrentState == State.PAUSED) + { + this.pausedChildren.Pop().Resume(); + } + } + } StopObserving(); } public override void Resume() { base.Resume(); + currentState = beforePauseState; + Evaluate(); StartObserving(); } diff --git a/Scripts/Root.cs b/Scripts/Root.cs index 55acc1e1..61181efc 100644 --- a/Scripts/Root.cs +++ b/Scripts/Root.cs @@ -96,5 +96,11 @@ override protected void DoChildStopped(Node node, bool success) Stopped(success); } } + + public override void Pause() + { + Assert.AreEqual(this.currentState, State.ACTIVE, "Only an active tree can be paused."); + base.Pause(); + } } } From 7abf31e48097818abfed3ad36f786d8be4dee4bc Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Fri, 7 Feb 2020 12:31:50 +0100 Subject: [PATCH 12/16] Change test to requirement that ObservingDecorator is always paused in paused parent composite all test are passing now. --- Editor/Tests/PauseResumeTest.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Editor/Tests/PauseResumeTest.cs b/Editor/Tests/PauseResumeTest.cs index 2e3078df..a95d3cd9 100644 --- a/Editor/Tests/PauseResumeTest.cs +++ b/Editor/Tests/PauseResumeTest.cs @@ -158,10 +158,6 @@ public void IgnoreBlackBoardConditionWhenPausedSelf() behaviorTree.Pause(); // blackboard condition should be paused now, the observers are unregistered - // unregister the observers is actually unnecessary because only active nodes can be stopped - // and restart is only called when there is at least one active child - // and in pause state is everything inactive - // but it leads to less unnecessary executed code Assert.AreEqual(Node.State.PAUSED, firstCondition.CurrentState); Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); @@ -280,7 +276,7 @@ public void IgnoreBlackboardConditionWhenPausedImmediateRestartSelector() behaviorTree.Pause(); Blackboard.Set("first", true); Timer.Update(0.1f); - Assert.AreEqual(Node.State.INACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.PAUSED, condition.CurrentState); Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); @@ -321,7 +317,7 @@ public void IgnoreBlackboardConditionWhenPausedImmediateRestartSequence() behaviorTree.Pause(); Blackboard.Set("first", true); Timer.Update(0.1f); - Assert.AreEqual(Node.State.INACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.PAUSED, condition.CurrentState); Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); @@ -358,7 +354,7 @@ public void IgnoreBlackboardConditionWhenPausedImmediateRestartParallel() behaviorTree.Pause(); Blackboard.Set("first", true); Timer.Update(0.1f); - Assert.AreEqual(Node.State.INACTIVE, condition.CurrentState); + Assert.AreEqual(Node.State.PAUSED, condition.CurrentState); Assert.AreEqual(Node.State.INACTIVE, firstTask.CurrentState); Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); @@ -367,7 +363,7 @@ public void IgnoreBlackboardConditionWhenPausedImmediateRestartParallel() Timer.Update(0.1f); Assert.AreEqual(Node.State.ACTIVE, condition.CurrentState); Assert.AreEqual(Node.State.ACTIVE, firstTask.CurrentState); - Assert.AreEqual(Node.State.INACTIVE, secondTask.CurrentState); + Assert.AreEqual(Node.State.ACTIVE, secondTask.CurrentState); } } } \ No newline at end of file From 0ebe0565f53fece1ba3666733d7d2df5812c87c9 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Fri, 7 Feb 2020 12:44:59 +0100 Subject: [PATCH 13/16] Change order of restarting timers in Resume() When resuming the timer should be restarted before the node is activated again, because after activation the node could be immediatly stopped which would lead to inconsistent state. --- Scripts/Decorator/ObservingDecorator.cs | 2 +- Scripts/Decorator/Service.cs | 2 +- Scripts/Decorator/WaitForCondition.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Scripts/Decorator/ObservingDecorator.cs b/Scripts/Decorator/ObservingDecorator.cs index 15440086..be284a86 100644 --- a/Scripts/Decorator/ObservingDecorator.cs +++ b/Scripts/Decorator/ObservingDecorator.cs @@ -77,10 +77,10 @@ public override void Pause() public override void Resume() { + StartObserving(); base.Resume(); currentState = beforePauseState; Evaluate(); - StartObserving(); } protected override void DoChildStopped(Node child, bool result) diff --git a/Scripts/Decorator/Service.cs b/Scripts/Decorator/Service.cs index 202f8a15..2e1e5f9f 100644 --- a/Scripts/Decorator/Service.cs +++ b/Scripts/Decorator/Service.cs @@ -55,8 +55,8 @@ public override void Pause() public override void Resume() { - base.Resume(); startService(); + base.Resume(); } private void startService() diff --git a/Scripts/Decorator/WaitForCondition.cs b/Scripts/Decorator/WaitForCondition.cs index 7dfbcd99..7712177d 100644 --- a/Scripts/Decorator/WaitForCondition.cs +++ b/Scripts/Decorator/WaitForCondition.cs @@ -40,8 +40,8 @@ public override void Pause() public override void Resume() { - base.Resume(); addTimerOrStartImmediately(); + base.Resume(); } private void addTimerOrStartImmediately() From 83ba8a2cfe80e79d69995f31d7ba49765e0322d1 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Fri, 13 Mar 2020 10:18:35 +0100 Subject: [PATCH 14/16] Fix stupid mistake to resume child instead of adding it to the paused list --- Scripts/Container.cs | 2 +- Scripts/Decorator/ObservingDecorator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/Container.cs b/Scripts/Container.cs index 0aac0ec1..7ec10045 100644 --- a/Scripts/Container.cs +++ b/Scripts/Container.cs @@ -66,7 +66,7 @@ override public void Pause() override public void Resume() { - Assert.AreEqual(this.currentState, State.PAUSED, "Only a paused contained can be resumed."); + Assert.AreEqual(this.currentState, State.PAUSED, "Only a paused container can be resumed."); currentState = State.ACTIVE; while (pausedChildren.Any()) { diff --git a/Scripts/Decorator/ObservingDecorator.cs b/Scripts/Decorator/ObservingDecorator.cs index be284a86..d28736f7 100644 --- a/Scripts/Decorator/ObservingDecorator.cs +++ b/Scripts/Decorator/ObservingDecorator.cs @@ -68,7 +68,7 @@ public override void Pause() child.Pause(); if (child.CurrentState == State.PAUSED) { - this.pausedChildren.Pop().Resume(); + this.pausedChildren.Push(child); } } } From c3567de01bdb5057830630dde540749c787a9990 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Fri, 13 Mar 2020 10:20:16 +0100 Subject: [PATCH 15/16] Pause clock for 'TimeMax' decorator Else TimeMax stops the children which leads to an inconsistent state. --- Scripts/Decorator/TimeMax.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Scripts/Decorator/TimeMax.cs b/Scripts/Decorator/TimeMax.cs index 954cb6b1..bc8247e4 100644 --- a/Scripts/Decorator/TimeMax.cs +++ b/Scripts/Decorator/TimeMax.cs @@ -58,6 +58,16 @@ protected override void DoChildStopped(Node child, bool result) } } + public override void Pause() + { + Clock.RemoveTimer(TimeoutReached); + } + + public override void Resume() + { + Clock.AddTimer(limit, randomVariation, 0, TimeoutReached); + } + private void TimeoutReached() { if (!waitForChildButFailOnLimitReached) From cd2af2047553a9aa5e6d3533c99fd7aa835fcb92 Mon Sep 17 00:00:00 2001 From: Lukas Gobelet Date: Sat, 14 Mar 2020 17:16:50 +0100 Subject: [PATCH 16/16] Add calling base implementation of pause and resume in TimeMax decorator forgot to do this --- Scripts/Decorator/TimeMax.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Scripts/Decorator/TimeMax.cs b/Scripts/Decorator/TimeMax.cs index bc8247e4..a30c9565 100644 --- a/Scripts/Decorator/TimeMax.cs +++ b/Scripts/Decorator/TimeMax.cs @@ -60,11 +60,13 @@ protected override void DoChildStopped(Node child, bool result) public override void Pause() { + base.Pause(); Clock.RemoveTimer(TimeoutReached); } public override void Resume() { + base.Resume(); Clock.AddTimer(limit, randomVariation, 0, TimeoutReached); }