meniku / NPBehave

Event Driven Behavior Trees for Unity 3D
MIT License
1.13k stars 194 forks source link

NPBehave - An event driven Behavior Tree Library for code based AIs in Unity

NPBehave Logo

NPBehave aims to be:

NPBehave builds on the powerful and flexible code based approach to define behavior trees from the BehaviorLibrary and mixes in some of the great concepts of Unreal's behavior trees. Unlike traditional behavior trees, event driven behavior trees do not need to be traversed from the root node again each frame. They stay in their current state and only continue to traverse when they actually need to. This makes them more performant and a lot simpler to use.

In NPBehave you will find most node types from traditional behavior trees, but also some similar to those found in the Unreal engine. Please refer to the Node Type Reference for the full list. It's fairly easy to add your own custom node types though.

If you don't know anything about behavior trees, it's highly recommended that you gain some theory first, this Gamasutra article is a good read.

Installation

Just drop the NPBehave folder into your Unity project. There is also an Examples subfolder, with some example scenes you may want to check out.

Example: "Hello World" Behavior Tree

Let's start with an example:

using NPBehave;

public class HelloWorld : MonoBehaviour
{
    private Root behaviorTree;

    void Start()
    {
        behaviorTree = new Root(
            new Action(() => Debug.Log("Hello World!"))
        );
        behaviorTree.Start();
    }
}

Full sample

When you run this, you'll notice that "Hello World" will be printed over and over again. This is because the Root node will restart the whole tree once traversal bypasses the last node in the tree. If you don't want this, you might add a WaitUntilStopped node, like so:

// ...
behaviorTree = new Root(
    new Sequence(
        new Action(() => Debug.Log("Hello World!")),
        new WaitUntilStopped()
    )
);
///... 

Up until now there really isn't anything event driven in this tree. Before we can dig into this, you need to understand what Blackboards are.

Blackboards

In NPBehave, like in Unreal, we got blackboards. You can think about them as beeing the "memory" of your AI. In NPBehave, blackboards are basically dictionaries that can be observed for changes. We mainly use Service to store & update values in the blackboards. And we use BlackboardCondition or BlackboardQuery to observe the blackboard for changes and in turn continue traversing the bahaviour tree. Though you are free to access or modify values of the blackboard everywhere else (you'll also access them often from Action nodes).

A blackboard is automatically created when you instantiate a Root, but you may also provide another instance with it's constructor (this is particularly useful for Shared Blackboards)

Example: An event-driven behavior tree

Here's a simple example that uses the blackboard for event-driven behavior:

/// ...
behaviorTree = new Root(
    new Service(0.5f, () => { behaviorTree.Blackboard["foo"] = !behaviorTree.Blackboard.Get<bool>("foo"); },
        new Selector(

            new BlackboardCondition("foo", Operator.IS_EQUAL, true, Stops.IMMEDIATE_RESTART,
                new Sequence(
                    new Action(() => Debug.Log("foo")),
                    new WaitUntilStopped()
                )
            ),

            new Sequence(
                new Action(() => Debug.Log("bar")),
                new WaitUntilStopped()
            )
        )
    )
);
behaviorTree.Start();
//...

Full sample | More sophisticated example

This sample will swap between printing "foo" and "bar" every 500 milliseconds. We use a Service decorator node to toggle the foo boolean value in the blackboard. We use a BlackboardCondition decorator node to decide based on this flag whether the branch gets executed or not. The BlackboardCondition also watches the blackboard for changes based on this value and as we provided Stops.IMMEDIATE_RESTART the currently executed branch will be stopped if the condition no longer is true, also if it becomes true again, it will be restarted immediately.

Please note that you should put services in real methods instead of using lambdas, this will make your trees more readable. Same is true for larger actions.

Stops Rules

Some Decorators such as BlackboardCondition, Condition or BlackboardQuery have a stopsOnChange parameter that allows to define stop rules. The parameter allows the Decorator to stop the execution of a running subtree within it's parent's Composite. It is your main tool to make power of the event-drivenness in NPBehave.

A lower priority node is a node that is defined after the current node within it's parent Composite.

The most useful and commonly used stops rules are SELF, IMMEDIATE_RESTART or LOWER_PRIORITY_IMMEDIATE_RESTART.

Be careful if you're used to Unreal though. In NPBehave BOTH and LOWER_PRIORITY have a slightly different meaning. IMMEDIATE_RESTART actually matches Unreal's Both and LOWER_PRIORITY_IMMEDIATE_RESTART matches Unreal's Lower Priority.

The following stop rules exist:

One caveat with both IMMEDIATE_RESTART variants is currently, that they may not actually always immediately restart your node. They won't immediately restart it in case the aborted branch leads the parent composite to evalutate to succeed. Therefor it's generally advised to make use of stop rules within Selector nodes and return Failed when your nodes are aborted.: In NPBehave 2.0 this might change.

Blackboard Alternatives

In NPBehave you define your behavior tree within a MonoBehaviour, as thus it isn't necessary to store everything in the blackboard. If you don't have BlackboardDecorator or BlackboardQuery with other stop rules than Stops.NONE, you probably don't need them to be in the blackboard at all. You can also just make use of plain member variables - it is often the cleaner, faster to write and more performant. It means that you won't make use of the event-drivenness of NPBehave in that case, but it's often not necessary.

If you want to be able to make use of stopsOnChange stops rules without using the Blackboard, two alternative ways exist in NPBehave:

  1. use a regular Condition decorator. This decorator has an optional stopsOnChange stops rules parameter. When providing any other value than Stops.NONE, the condition will frequently check the condition and interrupt the node according to the stops rule when the result of the given query function changes. Be aware that this method is not event-driven, it queries every frame (or at the provided interval) and as thus may lead to many queries if you make heavy use of them. However for simple cases it is often is sufficient and much simpler than a combination of a Blackboard-Key, a Service and a BlackboardCondition.
  2. Build your own event-driven Decorators. It's actually pretty easy, just extend from ObservingDecorator and override the isConditionMet(), StartObserving() and StopObserving() methods.

Node execution results

In NPBehave a node can either succeed or fail. Unlike traditional behavior trees, there is no result while a node is executing. Instead the node will itself tell the parent node once it is finished. This is important to keep in mind when you create your own node types.

Node Types

In NPBehave we have four different node types:

Stopping the Tree

In case your Monster gets killed or you just destroy your GameObject, you should always stop the tree. You could put sometihng like the following on your Script:

    // ...
    public void OnDestroy()
    {
        StopBehaviorTree();
    }

    public void StopBehaviorTree()
    {
        if ( behaviorTree != null && behaviorTree.CurrentState == Node.State.ACTIVE )
        {
            behaviorTree.Stop();
        }
    }
    // ...

The Debugger

You can use the Debugger component to debug the behavior trees at runtime in the inspector.

NPBehave Debugger

Check out the sample

Shared Blackboards

You have the option to share blackboards across multiple instances of an AI. This can be useful if you want to implement some kind of swarm behavior. Additionally, you can create blackboard hierarchies, which allows you to combine a shared with a non-shared blackboard.

You can use UnityContext.GetSharedBlackboard(name) to access shared blackboard instances anywhere.

Check out the sample

Extending the Library

Please refer to the existing node implementations to find out how to create custom node types, however be sure to at least read the following golden rules before doing so.

The golden rules

  1. Every call to DoStop() must result in a call to Stopped(result). This is extremely important!: you really need to ensure that Stopped() is called within DoStop(), because NPBehave needs to be able to cancel a running branch at every time immediately. This also means that all your child nodes will also call Stopped(), which in turn makes it really easy to write reliable decorators or even composite nodes: Within DoStop() you just call Stop() on your active children, they in turn will call of ChildStopped() on your node where you then finally put in your Stopped() call. Please have a look at the existing implementations for reference.
  2. Stopped() is the last call you do, never do modify any state or call anything after calling Stopped. This is because Stopped will immediately continue traversal of the tree on other nodes, which will completley fuckup the state of the behavior tree if you don't take that into account.
  3. Every registered clock or blackboard observer needs to be removed eventually. Most of the time you unregister your callbacks immediately before you call Stopped(), however there may be exceptions, e.g. the BlackboardCondition keeps observers around up until the parent composite is stopped, it needs to be able to react on blackboard value changes even when the node itself is not active.

Implementing Tasks

For tasks you extend from the Task class and override the DoStart() and DoStop() methods. In DoStart() you start your logic and once you're done, you call Stopped(bool result) with the appropriate result. Your node may get cancelled by another node, so be sure to implement DoStop(), do proper cleanup and call Stopped(bool result) immediately after it.

For a relatively simple example, check the source of the Wait Task.

As already mentioned in the golden rules section, in NPBehave you have to always call Stopped(bool result) after your node is stopped. So it is currently not supported to have cancel-operations pending over multiple frames and will result in unpredictable behaviour.

Implementing Observing Decorators

Writing decorators is a lot more complex than Tasks. However a special base class exists for convenience. It's the ObservingDecorator. This class can be used for easy implementation of "conditional" Decorators that optionally make use stopsOnChange stops rules.

All you have to do is to extend from it ObservingDecorator and override the method bool IsConditionMet(). If you want to support the Stops-Rules you will have to implement StartObserving() and StopObserving() too. For a simple example, check the source of the Condition Decorator.

Implementing Generic Decorators

For generic decorators you extend from the Decorator class and override the DoStart(), DoStop() and the DoChildStopped(Node child, bool result) methods.

You can start or stop your decorated node by accessing the Decoratee property and call Start() or Stop() on it.

If your decorator receives a DoStop() call, it's responsible to stop the Decoratee accordingly and in this case will not call Stopped(bool result) immediately. Instead it will do that in the DoChildStopped(Node child, bool result) method. Be aware that the DoChildStopped(Node child, bool result) doesn't necessarily mean that your decorator stopped the decoratee, the decoratee may also stop itself, in which case you don't need to immediately stop the Decoratee (that may be useful if you want to implement things like cooldowns etc). To find out whether your decorator got stopped, you can query it's IsStopRequested property.

Check the source of the Failer Node for a very basic implementation or the Repeater Node for a little more complex one.

In addition you can also implement the method DoParentCompositeStopped(), which may be called even when your Decorator is inactive. This is useful if you want to do additional cleanup work for listeners you kept active after your Decorator stopped. Check the ObservingDecorator for an example.

Implementing Composites

Composite nodes require a deeper understanding of the library and you usually won't need to implement new ones. If you really need a new Composite, feel free to create a ticket on the GitHub project or contact me and I'll try my best to help you getting through it correctly.

Node States

Most likely you won't need to access those, but it's still good to know about them:

The current state can be retrieved with the CurrentState property

The Clock

You can use the clock in your nodes to register timers or get notified on each frame. Use RootNode.Clock to access the clock. Check the Wait Task for an example on how to register timers on the clock.

By default the behavior tree will be using the global clock privoded by the UnityContext. This clock is updated every frame. There may be scenarious where you want to have more control. For example you may want to throttle or pause updates to a group of AIs. For this reason you can provide your own controlled clock instances to the Root node and Blackboard, this allows you to precisely control when your behavior trees are updated. Check the Clock Throttling Example.

Node Type Reference

Root

Composite Nodes

Selector

Sequence

Parallel

RandomSelector

RandomSequence

Task Nodes

Action

NavWalkTo (!!!! EXPERIMENTAL !!!!)

Wait

WaitUntilStopped

Decorator Nodes

BlackboardCondition

BlackboardQuery

Condition

Cooldown

Failer

Inverter

Observer

Random

Repeater

Service

Succeeder

TimeMax

TimeMin

WaitForCondition

Video Tutorials

Contact

NPBehave was created and is maintained by Nils Kübler (E-Mail: das@nilspferd.net, Skype: disruption@web.de)

Games using NPBehave

If you have built a game or are building a game using NPBehave, I would be glad to have it on this list. You can submit your game eiter via contacting me or creating a pull request on the Github page