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

Implement CoroutineAction #36

Open
wants to merge 2 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
108 changes: 108 additions & 0 deletions Editor/Tests/Task/CoroutineAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using NUnit.Framework;

namespace NPBehave
{
public class CoroutineActionTest
{
private int firstStepRunCount;
private int secondStepRunCount;
private int thirdStepRunCount;

[SetUp]
public void SetUp()
{
this.firstStepRunCount = 0;
this.secondStepRunCount = 0;
this.thirdStepRunCount = 0;
}

private System.Collections.IEnumerator TestCoroutine()
{
this.firstStepRunCount++;
yield return new WaitForSeconds(0.05f);
this.secondStepRunCount++;
yield return null;
this.thirdStepRunCount++;
}

[Test]
public void AllStepsShouldRun_WhenDoesntYieldFailure()
{
var timer = new Clock();
var blackboard = new Blackboard(timer);
var action = new CoroutineAction(TestCoroutine);
var tree = new TestRoot(blackboard, timer, action);

tree.Start(); // First step should run
timer.Update(0.1f); // Tick - Second step should run
timer.Update(0.1f); // Tick - Third step should run

Assert.AreEqual(1, firstStepRunCount);
Assert.AreEqual(1, secondStepRunCount);
Assert.AreEqual(1, thirdStepRunCount);
}

[Test]
public void ShouldEndWithSuccess_WhenDoesntYieldFailure()
{
var timer = new Clock();
var blackboard = new Blackboard(timer);
var action = new CoroutineAction(TestCoroutine);
var tree = new TestRoot(blackboard, timer, action);

tree.Start(); // First step should run
timer.Update(0.1f); // Tick - Second step should run
timer.Update(0.1f); // Tick - Third step should run

Assert.AreEqual(1, firstStepRunCount);
Assert.AreEqual(1, secondStepRunCount);
Assert.AreEqual(1, thirdStepRunCount);

Assert.AreEqual(Node.State.INACTIVE, action.CurrentState); // Action should have ended...
Assert.IsTrue(action.DebugLastResult); // ... with success
}

private System.Collections.IEnumerator TestFailCoroutine()
{
this.firstStepRunCount++;
yield return new WaitForSeconds(0.05f);
this.secondStepRunCount++;
yield return Action.Result.FAILED;
this.thirdStepRunCount++;
yield return null;
}

[Test]
public void StepAfterFailureShouldNotRun_WhenYieldsFailure()
{
var timer = new Clock();
var blackboard = new Blackboard(timer);
var action = new CoroutineAction(TestFailCoroutine);
var tree = new TestRoot(blackboard, timer, action);

tree.Start(); // First step should run
timer.Update(0.1f); // Tick - Second step should run
timer.Update(0.1f); // Tick - Third step should run

Assert.AreEqual(1, firstStepRunCount);
Assert.AreEqual(1, secondStepRunCount);
Assert.AreEqual(0, thirdStepRunCount); // Third step should not run as coroutine is failing earlier
}

[Test]
public void ShouldEndWithFailure_WhenYieldsFailure()
{
var timer = new Clock();
var blackboard = new Blackboard(timer);
var action = new CoroutineAction(TestFailCoroutine);
var tree = new TestRoot(blackboard, timer, action);

tree.Start(); // First step should run
timer.Update(0.1f); // Tick - Second step should run
timer.Update(0.1f); // Tick - Third step should run

Assert.AreEqual(Node.State.INACTIVE, action.CurrentState); // Action should have ended...
Assert.IsFalse(action.DebugLastResult); // ... with failure
}
}
}
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,45 @@ There may be scenarious where you want to have more control. For example you may
- **`Action(Func<bool, Result> multiframeFunc)`**: action that can be ticked over multiple frames (return `Result.BLOCKED` when your action is not yet ready, `Result.PROGRESS` when you're busy with the action, `Result.SUCCESS` or `Result.FAILED` when your action failed). The bool parameter that is passed to the delegate turns true when the task has to be aborted - in this case you are only allowed to return `Result.SUCCESS` or `Result.FAILED`. When considering using this type of Action, you should also think about creating a custom subclass of the `Task` instead.
- **`Action(Func<Request, Result> multiframeFunc2)`**: similar to above, but the passed `Request` will give you a state information: `Request.START` means it's the first tick to your action or you returned `Result.BLOCKED` last tick; `Request.UPDATE` means the last time you returned `Request.PROGRESS`; `Request.CANCEL` means that you need to cancel your action and return `Result.SUCCESS` or `Result.FAILED`. When considering using this type of Action, you should also think about creating a custom subclass of the `Task` instead.

#### CoroutineAction
- **`CoroutineAction(Func<IEnumerator> action)`**: action that is implemented as
a coroutine. Yielding instance of `NPBehave.WaitForSeconds` causes execution
to be scheduled after provided number of seconds. Yielding null causes
execution to be scheduled for the next clock tick. Yielding `Action.Result.FAILED` fails the action. Yielding anything else
causes an exception.

Example Implementation:
```csharp
// ...
private System.Collections.IEnumerator TestCoroutine()
{
Debug.Log("First Step");

yield return new WaitForSeconds(1f);

Debug.Log("Second Step");

if (...)
{
yield return Action.Result.FAILED;
}

yield return null;

Debug.Log("Third Step");
}
///...
```

Example Usage;
```csharp
// ...
behaviorTree = new Root(
new CoroutineAction(TestCoroutine)
);
///...
```

#### NavWalkTo (!!!! EXPERIMENTAL !!!!)
- **`NavMoveTo(NavMeshAgent agent, string blackboardKey, float tolerance = 1.0f, bool stopOnTolerance = false, float updateFrequency = 0.1f, float updateVariance = 0.025f)`**: move a NavMeshAgent `agent` to either a transform or vector stored in the given `blackboardKey`. Allows a `tolerance` distance to succeed and optionally will stop once in the tolerance range (`stopOnTolerance`). `updateFrequency` controls how often the target position will be updated and how often the task checks wether it's done.

Expand Down
68 changes: 68 additions & 0 deletions Scripts/Task/CoroutineAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Collections;

namespace NPBehave
{
public struct WaitForSeconds
{
public readonly float Seconds;
public WaitForSeconds(float seconds)
{
this.Seconds = seconds;
}
}

public class CoroutineAction : Task
{
private System.Func<IEnumerator> action;
private IEnumerator actionCoroutine;

public CoroutineAction(System.Func<IEnumerator> action) : base("Action")
{
this.action = action;
}

private void Progress()
{
if (actionCoroutine.Current is Action.Result.FAILED)
{
Stopped(false);
}
else if (actionCoroutine.Current is WaitForSeconds w)
{
if (actionCoroutine.MoveNext())
{
Clock.AddTimer(w.Seconds, 0, Progress);
}
else
{
Stopped(true);
}
}
else if (actionCoroutine.Current == null)
{
if (actionCoroutine.MoveNext())
{
Clock.AddTimer(0, 0, Progress);
}
else
{
Stopped(true);
}
}
else
{
throw new System.Exception("unsupported coroutine return value");
}
}

protected override void DoStart()
{
this.actionCoroutine = action();
Progress();
}

protected override void DoStop()
{
}

Choose a reason for hiding this comment

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

I have tried your implemetation in my game. When another node tried to stop the CoroutineAction node, it didn't stop immediately. It seems like you didn't take this case into consideration. So the DoStop method should be rewritten like this:

protected override void DoStop()
{
    Clock.RemoveTimer(Progress);
    Stopped(false);
}

}
}