Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pause and resume #30

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
369 changes: 369 additions & 0 deletions Editor/Tests/PauseResumeTest.cs

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions Editor/Tests/_utils/MockTask.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 0 additions & 2 deletions Scripts/Composite/Composite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
48 changes: 47 additions & 1 deletion Scripts/Container.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Assertions;

namespace NPBehave
{
public abstract class Container : Node
{
protected Node[] Children;
protected readonly Stack<Node> pausedChildren = new Stack<Node>();

private bool collapse = false;

public bool Collapse
{
get
Expand All @@ -25,7 +31,47 @@ 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 is Task)
{
if (child.IsActive)
{
child.Pause();
this.pausedChildren.Push(child);
}
}
else
{
child.Pause();
if (child.CurrentState == State.PAUSED)
{
this.pausedChildren.Push(child);
}
}
}
}

override public void Resume()
{
Assert.AreEqual(this.currentState, State.PAUSED, "Only a paused container can be resumed.");
currentState = State.ACTIVE;
while (pausedChildren.Any())
{
pausedChildren.Pop().Resume();
}
}

protected abstract void DoChildStopped(Node child, bool succeeded);
Expand Down
3 changes: 1 addition & 2 deletions Scripts/Decorator/BlackboardCondition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ public BlackboardCondition(string key, Operator op, Stops stopsOnChange, Node de
this.key = key;
this.stopsOnChange = stopsOnChange;
}



override protected void StartObserving()
{
this.RootNode.Blackboard.AddObserver(key, onValueChanged);
Expand Down
1 change: 1 addition & 0 deletions Scripts/Decorator/Decorator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
46 changes: 46 additions & 0 deletions Scripts/Decorator/ObservingDecorator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -41,6 +42,47 @@ override protected void DoStop()
Decoratee.Stop();
}

public override void 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.Push(child);
}
}
}
StopObserving();
}

public override void Resume()
{
StartObserving();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as for the service, I think it's safer to either call StartObserving() before calling base.Resume()or to check the Node's current state for being Active when calling StartObserving()
(although I'm still not quite sure what's better tbh)

base.Resume();
currentState = beforePauseState;
Evaluate();
}

protected override void DoChildStopped(Node child, bool result)
{
Assert.AreNotEqual(this.CurrentState, State.INACTIVE);
Expand All @@ -66,6 +108,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)
Expand Down
40 changes: 31 additions & 9 deletions Scripts/Decorator/Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,35 @@ 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();
stopService();
}

public override void Resume()
{
startService();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You maybe want to check that the CurrentState is ACTIVE before calling startService, as it could be that your underlying branch just decided to immediately stop during the Resume operation. Or it might be better if you do call the startService before calling the base.Resume

base.Resume();
}

private void startService()
{
if (this.interval <= 0f)
{
Expand All @@ -46,15 +75,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)
{
Expand All @@ -68,9 +91,8 @@ protected override void DoChildStopped(Node child, bool result)
{
this.Clock.RemoveTimer(InvokeServiceMethodWithRandomVariation);
}
Stopped(result);
}

private void InvokeServiceMethodWithRandomVariation()
{
serviceMethod();
Expand Down
12 changes: 12 additions & 0 deletions Scripts/Decorator/TimeMax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ 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);
}

private void TimeoutReached()
{
if (!waitForChildButFailOnLimitReached)
Expand Down
17 changes: 17 additions & 0 deletions Scripts/Decorator/WaitForCondition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ public WaitForCondition(Func<bool> condition, Node decoratee) : base("WaitForCon
}

protected override void DoStart()
{
addTimerOrStartImmediately();
}

public override void Pause()
{
base.Pause();
Clock.RemoveTimer(checkCondition);
}

public override void Resume()
{
addTimerOrStartImmediately();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might cause it to start the child node even though the base.Resume node might already have started it. You will need to do some more checking here.
You only want to call the addTimerOrStartImmediately if the Decoratee object wasn't yet started.

base.Resume();
}

private void addTimerOrStartImmediately()
{
if (!condition.Invoke())
{
Expand Down
16 changes: 13 additions & 3 deletions Scripts/Node.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum State
INACTIVE,
ACTIVE,
STOP_REQUESTED,
PAUSED,
}

protected State currentState = State.INACTIVE;
Expand Down Expand Up @@ -138,6 +139,16 @@ public void Stop()
#endif
DoStop();
}

public virtual void Pause()
{

}

public virtual void Resume()
{

}

protected virtual void DoStart()
{
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions Scripts/Root.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
10 changes: 10 additions & 0 deletions Scripts/Task/Task.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}