Giter Club home page Giter Club logo

npbehave's Introduction

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

NPBehave Logo

NPBehave aims to be:

  • lightweight, fast & simple
  • event driven
  • easily extendable
  • A framework to define AIs with code, there is no visual editing support

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:

  • Stops.NONE: the decorator will only check it's condition once it is started and will never stop any running nodes.
  • Stops.SELF: the decorator will check it's condition once it is started and if it is met, it will observe the blackboard for changes. Once the condition is no longer met, it will stop itself allowing the parent composite to proceed with it's next node.
  • Stops.LOWER_PRIORITY: the decorator will check it's condition once it is started and if it's not met, it will observe the blackboard for changes. Once the condition is met, it will stop the lower priority node allowing the parent composite to proceed with it's next node.
  • Stops.BOTH: the decorator will stop both: self and lower priority nodes.
  • Stops.LOWER_PRIORITY_IMMEDIATE_RESTART: the decorator will check it's condition once it is started and if it's not met, it will observe the blackboard for changes. Once the condition is met, it will stop the lower priority node and order the parent composite to restart the Decorator immediately.
  • Stops.IMMEDIATE_RESTART: the decorator will check it's condition once it is started and if it's not met, it will observe the blackboard for changes. Once the condition is met, it will stop the lower priority node and order the parent composite to restart the Decorator immediately. As in BOTH it will also stop itself as soon as the condition is no longer met.

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:

  • the root node: The root has one single child and is used to start or stop the whole behavior tree.
  • composite nodes: these have multiple children and are used to control which of their children are executed. Also the order and result is defined by this kind of node.
  • decorator nodes: these nodes have always exactly one child and are used to either modify the result of the child or do something else while executing the child (e.g. a service updating the blackboard)
  • task nodes: these are the leafs of the tree doing the actual work. These are the ones you most likely would create custom classes for. You can use the Action with lambdas or functions - For more complicated tasks, it is often a better option to create a new subclass of Task. Be sure to read the the golden rules if you do so.

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:

  • ACTIVE: the node is started and not yet stopped.
  • STOP_REQUESTED: the node is currently stopping, but has not yet called Stopped() to notify the parent.
  • INACTIVE: the node is stopped.

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

  • Root(Node mainNode): run the given mainNode endlessly, regardless of it's failure state
  • Root(Blackboard blackboard, Node mainNode): use the given blackboard instead of instantiating one; run the given mainNode endlessly, regardless of it's failure state
  • Root(Blackboard blackboard, Clock clock, Node mainNode): use the given blackboard instead of instantiating one; use the given clock instead of using the global clock from the UnityContext; run the given mainNode endlessly, regardless of it's success state

Composite Nodes

Selector

  • Selector(params Node[] children): Run children sequentially until one succeeds and succeed (succeeds if one of the children succeeds).

Sequence

  • Sequence(params Node[] children): Run children sequentially until one fails and fail (succeeds if none of the children fails).

Parallel

  • Parallel(Policy successPolicy, Policy failurePolicy, params Node[] children): Run children in parallel. When failurePolocity is Polociy.ONE, the Parallel will stop (with failing resi;t) as soon as one of the children fails. When successPolicy is Policy.ONE, the Parallel will stop (with succeeding result) when of the children fails. If the Parallel doesn't stop because of a Policy.ONE it will execute until all of the children are done, then it either succeeds if all children succeeded or fails.

RandomSelector

  • RandomSelector(params Node[] children): Run children in random order until one succeeds and succeed (succeeds if one of the children succeeds). Note that for abortion rules the original order defines priorities.

RandomSequence

  • RandomSequence(params Node[] children): Run children in random order until one fails and fail (succeeds if none of the children fails). Note that for abortion rules the original order defines priorities.

Task Nodes

Action

  • Action(System.Action action): fire and forget action (always finishes successfully immediately)
  • Action(System.Func<bool> singleFrameFunc): action which can succeed or fail (return false to fail).
  • 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.

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.

Wait

  • Wait(float seconds): Wait for given seconds with a random variance of 0.05 * seconds
  • Wait(float seconds, float randomVariance): Wait for given seconds with given random varaince
  • Wait(string blackboardKey, float randomVariance = 0f): wait for seconds set as float in given blackboardKey
  • Wait(System.Func<float> function, float randomVariance = 0f): wait for result of the provided lambda function

WaitUntilStopped

  • WaitUntilStopped(bool sucessWhenStopped = false): just wait until stopped by some other node. It's often used to park at the end of a Selector, waiting for any beforehead sibling BlackboardCondition, BlackboardQuery or Condition to become active.

Decorator Nodes

BlackboardCondition

  • BlackboardCondition(string key, Operator operator, object value, Stops stopsOnChange, Node decoratee): execute the decoratee node only if the Blackboard's key matches the op / value condition. If stopsOnChange is not NONE, the node will observe the Blackboard for changes and stop execution of running nodes based on the stopsOnChange stops rules.
  • BlackboardCondition(string key, Operator operator, Stops stopsOnChange, Node decoratee): execute the decoratee node only if the Blackboard's key matches the op condition (for one operand operators that just check for IS_SET for example). If stopsOnChange is not NONE, the node will observe the Blackboard for changes and stop execution of running nodes based on the stopsOnChange stops rules.

BlackboardQuery

  • BlackboardQuery(string[] keys, Stops stopsOnChange, System.Func<bool> query, Node decoratee): while BlackboardCondition allows to check only one key, this one will observe multiple blackboard keys and evaluate the given query function as soon as one of the value's changes, allowing you to do arbitrary queries on the blackboard. It will stop running nodes based on the stopsOnChange stops rules.

Condition

  • Condition(Func<bool> condition, Node decoratee): execute decoratee node if the given condition returns true
  • Condition(Func<bool> condition, Stops stopsOnChange, Node decoratee): execute decoratee node if the given condition returns true. Re-Evaluate the condition every frame and stop running nodes based on the stopsOnChange stops rules.
  • Condition(Func<bool> condition, Stops stopsOnChange, float checkInterval, float randomVariance, Node decoratee): execute decoratee node if the given condition returns true. Reevaluate the condition at the given checkInterval and randomVariance and stop running nodes based on the stopsOnChange stops rules.

Cooldown

  • Cooldown(float cooldownTime, Node decoratee): Run decoratee immediately, but only if last execution wasn't at least past cooldownTime
  • Cooldown(float cooldownTime, float randomVariation, Node decoratee): Run decoratee immediately, but only if last execution wasn't at least past cooldownTime with randomVariation
  • Cooldown(float cooldownTime, bool startAfterDecoratee, bool resetOnFailiure, Node decoratee): Run decoratee immediately, but only if last execution wasn't at least past cooldownTime with randomVariation. When resetOnFailure is true, the cooldown will be reset if the decorated node fails
  • Cooldown(float cooldownTime, float randomVariation, bool startAfterDecoratee, bool resetOnFailiure, Node decoratee) Run decoratee immediately, but only if last execution wasn't at least past cooldownTime with randomVariation. When startAfterDecoratee is true, the cooldown timer will be started after the decoratee finishes instead of when it starts. When resetOnFailure is true, the cooldown will be reset if the decorated node fails.
  • Cooldown(float cooldownTime, bool startAfterDecoratee, bool resetOnFailiure, bool failOnCooldown, Node decoratee): Run decoratee immediately, but only if last execution wasn't at least past cooldownTime with randomVariation. When resetOnFailure is true, the cooldown will be reset if the decorated node fails. If failOnCooldown is true, fail instead of wait in case the cooldown is still active.
  • Cooldown(float cooldownTime, float randomVariation, bool startAfterDecoratee, bool failOnCooldown, bool resetOnFailiure, Node decoratee) Run decoratee immediately, but only if last execution wasn't at least past cooldownTime with randomVariation. When startAfterDecoratee is true, the cooldown timer will be started after the decoratee finishes instead of when it starts. When resetOnFailure is true, the cooldown will be reset if the decorated node fails. If failOnCooldown is true, fail instead of wait in case the cooldown is still active.

Failer

  • Failer(Node decoratee): always fail, regardless of the decoratee's result.

Inverter

  • Inverter(Node decoratee): if decoratee succeeds, the inverter fails and if the decoratee fails, the inverter succeeds.

Observer

  • Observer(Action onStart, Action<bool> onStop, Node decoratee): runs the given onStart lambda once the decoratee starts and the onStop(bool result) lambda once the decoratee finishes. It's a bit like a special kind of Service, as it doesn't interfere in the execution of the decoratee directly.

Random

  • Random(float probability, Node decoratee): runs the decoratee with the given probability chance between 0 and 1.

Repeater

  • Repeater(Node decoratee): repeat the given decoratee infinitly, unless it fails
  • Repeater(int loopCount, Node decoratee): execute the given decoratee for loopCount times (0 means decoratee would never run). If decoratee stops the looping is aborted and the Repeater fails. If all executions of the decoratee are successful, the Repeater will succeed.

Service

  • Service(Action service, Node decoratee): run the given service function, start the decoratee and then run the service every tick.
  • Service(float interval, Action service, Node decoratee): run the given service function, start the decoratee and then run the service at the given interval.
  • Service(float interval, float randomVariation, Action service, Node decoratee): run the given service function, start the decoratee and then run the service at the given interval with randomVariation.

Succeeder

  • Succeeder(Node decoratee): always succeed, regardless of whether the decoratee succeeds or not

TimeMax

  • TimeMax(float limit, bool waitForChildButFailOnLimitReached, Node decoratee): run the given decoratee. If the decoratee doesn't finish within the limit, the execution fails. If waitForChildButFailOnLimitReached is true, it will wait for the decoratee to finish but still fail.
  • TimeMax(float limit, float randomVariation, bool waitForChildButFailOnLimitReached, Node decoratee): run the given decoratee. If the decoratee doesn't finish within the limit and randomVariation, the execution fails. If waitForChildButFailOnLimitReached is true, it will wait for the decoratee to finish but still fail.

TimeMin

  • TimeMin(float limit, Node decoratee): run the given decoratee. If the decoratee finishes sucessfully before the limit time is reached the decorator will wait until the limit is reached and then stop the execution with the result of the Decoratee. If the decoratee finishes failing before the limit time is reached, the decorator will immediately stop.
  • TimeMin(float limit, bool waitOnFailure, Node decoratee): run the given decoratee. If the decoratee finishes sucessful before the limit time is reached, the decorator will wait until the limit is reached and then stop the execution with the result of the Decoratee. If waitOnFailure is true, the decoratee will also wait when the decoratee fails.
  • TimeMin(float limit, float randomVariation, bool waitOnFailure, Node decoratee): run the given decoratee. If the decoratee finishes sucessful before the limit with randomVariation time is reached, the decorator will wait until the limit is reached and then stop the execution with the result of the Decoratee. If waitOnFailure is true, the decoratee will also wait when the decoratee fails.

WaitForCondition

  • WaitForCondition(Func<bool> condition, Node decoratee): Delay execution of the decoratee node until the condition gets true, checking every frame
  • WaitForCondition(Func<bool> condition, float checkInterval, float randomVariance, Node decoratee): Delay execution of the decoratee node until the condition gets true, checking with the given checkInterval and randomVariance

Video Tutorials

Contact

NPBehave was created and is maintained by Nils Kübler (E-Mail: [email protected], Skype: [email protected])

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

npbehave's People

Contributors

jessetg avatar meniku avatar mohheader avatar wanda0104 avatar wokarol avatar wqaetly avatar xerios avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

npbehave's Issues

BlackboardCondition seems can't stop lower priority repeater node when using a child blackboard

At first using the default blackboard and BlackboardCondition, it works. And then try to move the code into a node factory and use a shared blackboard. After some trial and error, found that if use a BlackboardCondition node and with a none default child board, even if set it to IMMEDIATE_RESTART and the condition is meet, the state would stuck in a lower priority repeater node, and swtich BlackboardCondition node to condition node, it works agin.

Still maintained?

Hey, is this library still maintained? Unity 2018/2019 supported?

A question about the source code of Clock.cs and Blackborad.cs

I have encountered a question while reading the source code. I'm not sure whether it's a bug or designed like that intentionally, so I just list it out here. If it's the latter case, I would be pleased if someone could tell me the intention behind it.

In methods AddObserver and RemoveObserver in Blackboard.cs, before add/remove a observer, it will check whether there is already a add/ remove request in addObservers/removeObservers. If so, this request will be ignored.

But in methods AddUpdateObserver and RemoveUpdateObserver in Clock.cs, before add/remove a observer, it does not check the same thing, which means that there could be two remove requests for the same observer.

So, what's the intention of this difference?

Thanks.

Pausing the behavior tree

Hi there,

is there a way to stop the execution of the behavior tree temporarily and then start it again.

I have different kind of enemies which behaviors are controlled by behavior trees.
When they get hurt, I want to stop the behavior tree for a moment so that a hurt animation can be played.

BlackboardCondition with Operator.ON_CHANGE

If I have something like this :

new BlackboardCondition ("SomeThing,,, Looking forward to be changed", Operator.ON_CHANGE, Stops.LOWER_PRIORITY,
	new Action(() => Debug.Log("Well,,, that something has been changed"))
)

Would fire with every loop( even though the key hasn't changed )

private bool IsConditionMet()
{
       if (op == Operator.ON_CHANGE)
       {
              return true;
       }
...
}

The expected behavior : Not to Fire unless the key really changed.

I thought that something like that would fix the issue :

        protected override void DoStart()
        {
            if (stopsOnChange != Stops.NONE)
            {
                this.RootNode.Blackboard.AddObserver(this.key, OnValueChanged);
            }

            if (this.op == Operator.ON_CHANGE || !IsConditionMet())
            {
                Stopped(false);
            }
            else
            {
                Decoratee.Start();
            }
        }

But now it don't fire at all.

I am feeling that may be I am the one who don't get it's intended use.

Thanks,

Debugger Window not scrollable on OSX

@Xerios Do you have an idea about this? The scrollbar is visible but there is no way of scrolling it. When clicking the "down" arrow it scrolls for 1 frame but immediately snaps back again

Assertion in Wait action

I am getting an assertion exception from within the Wait node in the Stop() method:

Assert.AreEqual(this.currentState, State.ACTIVE, "can only stop active nodes, tried to stop");

The node is nested under Condition with Stops.IMMEDIATE_RESTART

Creation Utility class

Can you add class that handles creation for nodes? That way instead of bunch of "new"
image
You would have this
image
Or even this
image

Naming nodes

I touched this asset again and I noticed big flaw when it comes to debugging. Currently we cannot name nodes like selector or sequence in easy way. This is key feature in my opinion.
Without this debugging is basically pointless
image
With naming we would be able to explicitly say "this part of tree is handling steering"

Infinite loop when two BlackboardCondition fire at once

Hi, :)

Unfortunately, I got no time to investigate it more.
But I am sure I did something wrong.

I have something like this :

new BlackboardCondition (Attacking.SHOULD_ATTACK, Operator.IS_SET, true, Stops.BOTH,
new Repeater (
	new Succeeder (
		new Sequence (
				new Attacking(Entity),
				new Idle(Entity, Idle.Duration.Short)
			)
		)
	)
),
new BlackboardCondition (Alerted.SHOULD_ALERT, Operator.IS_SET, true, Stops.BOTH,
new Sequence (
	new Alerted(Entity)
)
),

and in update :

if(PlayerDistance > 6)
{
	Entity.Memory.Unset (Attacking.SHOULD_ATTACK);
	Entity.Memory.Unset (Alerted.SHOULD_ALERT);
}
else if(PlayerDistance > Entity.Weapons.MaxRange())
{
	Entity.Memory.Unset (Attacking.SHOULD_ATTACK);
	Entity.Memory.Set (Alerted.SHOULD_ALERT, true);
}
else
{
	Entity.Memory.Set (Attacking.SHOULD_ATTACK, true);
	Entity.Memory.Unset (Alerted.SHOULD_ALERT);
}
  • Memory is just a normal Blackboard.

in a certain point, Unity Editor crash
through debug, I found an infinite loop
I think it is related to : DoParentCompositeStopped

But I am not sure about it,,,

will dig into it more, but thought to ask,

Thanks,

Randomized Nodes

It would be really handy to have nodes such as "Random Selector" or "Random Sequencer". They would be helpful for creating more non linear enemy behaviours. Especially for bosses.
(For clarity, by "Random Selector" I mean one where nodes are checked in random order and by "Random Sequencer" I mean one where nodes are sequenced in random order.)

NavMoveTo always fails when stopOnTolerance==true

I traced the bug and have my own fix, but thought I'd post it here in case it causes any unintended consequences.

Here's the offending code from NavMoveTo.cs lines 103-118, in moveToBlackboardKey():

// set new destination
agent.destination = destination;

bool destinationChanged = (agent.destination - lastDestination).sqrMagnitude > (DESTINATION_CHANGE_THRESHOLD * DESTINATION_CHANGE_THRESHOLD); //(destination - agent.destination).sqrMagnitude > (DESTINATION_CHANGE_THRESHOLD * DESTINATION_CHANGE_THRESHOLD);
bool distanceChanged = Mathf.Abs(agent.remainingDistance - lastDistance) > DESTINATION_CHANGE_THRESHOLD;

// check if we are already at our goal and stop the task
if (lastDistance < this.tolerance)
{
	if (stopOnTolerance || (!destinationChanged && !distanceChanged))
	{
		// reached the goal
		stopAndCleanUp(true);
		return;
	}
}

...

lastDestination = agent.destination;
lastDistance = agent.remainingDistance;

When agent.destination is changed, agent doesn't immediately update the path. Instead, it takes a frame or so to generate the new result (see: NavMeshAgent.destination documentation).

But immediately after changing the destination, this code is querying parameters that rely on the path having been generated. In particular, agent.remainingDistance will return 0 if the path hasn't been generated yet. And it will always return 0 after changing the destination because the destination is changed only two lines before it.

At the end of this method, the following line is called:
lastDistance = agent.remainingDistance;
Since agent.remainingDistance always returns 0 after the destination is changed, lastDistance is also always set to 0 after the destination is changed.

That's fine for the first pass through this method. It's when the method gets called on the second tick that the problem starts:

lastDistance will equal 0, since it was set on the last tick, before the path was ready. Which means that the conditional statement if (lastDistance < this.tolerance) will always evaluate to true on that second tick, because 0 is always smaller than any tolerance. Then the next conditional, if (stopOnTolerance || (!destinationChanged && !distanceChanged)) will also always evaluate to true if stopOnTolerance = true. These conditions cause the code to exit, apparently assuming that since the tolerance was met, then agent must have reached its destination (but in reality it hasn't even started to move yet).

To fix this problem, I changed the following line of code:

lastDistance = agent.remainingDistance;

to:

if (agent.pathPending)
{
	lastDistance = 99999999.0f;
}
else
{
	lastDistance = agent.remainingDistance;
}

agent.pathPending will return true if the path isn't yet set, meaning that we shouldn't trust the value of agent.remainingDistance, since it will return a false value of 0. So instead of using the unreliable agent.remainingDistance to set lastDistance, we instead set lastDistance to its default starting value of 99999999.0f.

That means that when we come around to the second tick, the conditional if (lastDistance < this.tolerance) does NOT trigger accidentally and we do NOT prematurely exit the navigation.

[Proposal] Making it more "unreal"

One of the things I was trying to do myself is to replicate the way Unreal did their behaviour trees.
Since the structure was too different and I had the urge to use UniRx, I made my own repository and tried replicating it basing on how they did it in their SDK. I never managed to finish it but the structure and code is there, never got to implement UniRx either since I realized that it wouldn't benefit at all in the end.

What I found really interesting is the way they structure their trees.
Notice how service sticks underneath their composition node? Or how decorators are always above either composites and tasks?

Seeing how this looks:
https://camo.githubusercontent.com/40566eaf7697442c61b3f4079f70a83d479f51ab/687474703a2f2f6c6162732e6e6b7565626c65722e64652f6e706265686176652f696d616765732f4e504265686176652d416e696d2e676966

I can't help but to think it could be better, and by better I mean by changing the way it's structured.

The way they do it is not that hard.

  • Composites can have Decorators and Services, can have childs
  • Tasks can have Decorators, no childs
  • Services, child
  • Root is just a Composite that doesn't have anything attached to it and can only have one task child ( see it more as an entry point to the BT )
  • Decorators and Services are also nodes, but they can only be part of a composite child ( which is explained further below ).

Imagine if you could stack all nodes that share same scope.
Instead of :

  • (Root)
    • (->)
      • (Wait)
        • (Service)
          • (Service)
            • (Repeater)
              • (Succeeder)
                • (->)
                  • (Wait)
                    • (=>) // Paralell

You could have this:

  • (Root)
    • (Wait) [->] (Service)(Service)
      • (Repeater) (Succeeder) [->]
        • (Wait) [=>]

In the end the structure for node class becomes like this :

  • Node
    • Composite
    • Task
    • Decorator ( pre-condition/effect )
    • Service ( post-node-execution )

The difference is that most logic is executed in the composites themselves, they're the ones who are active/inactive and know which child is currently active - not the task itself.
The children in the composite are stored with a struct CompositeChild, which looks like this:

  • CompositeChild
    • Decorators[]
    • Node
    • Services[]

In order to execute the node you need to pass through its decorators, and in order to execute its services you need to execute the node. All this done by the composite parent.

Details aside, I really thought it through and it makes sense to structure it like that.
Seeing that Unreal kept it this way for so long with no other change can only mean that it works.

What do you think of it?
I know it's a major change but it would look totally awesome and the behavior trees will be more readable.
Anyhow, look up their docs and tell me if you agree with this. I think we could create something powerful.

[Question] Clock timer with repetitions

I have a question about the clock behavior when used with repetitions. Don't know if it's intended behavior or not.

If I add a timer with a time of X seconds with repeats, the first time it waits X seconds, but after that it repeats every tick. Seeing your NavMoveTo script, I thought the time parameter was meant to be the update frequency.

I could solve the problem by adding a timer with no repeats every time the action is called, but if this is an issue, I'd rather fix this.

I'm using Unity 2019.2.01f and NPBehave latest version.

It is not possible to handle player input because the tree cannot wait for some action while execute other in parallel

I was trying to build a character controller script using NPBehave.
I have a blackboard with a key called "isMovePressed" that turns true or false depending on player input.
So i used the BlackboardCondition to set the animator key to true of false if the "isMovePressed" value is updated.

The problem is that the Parallel node only keeps executing its children if a SUCCESS or FAILED status is returned by the children,
if i return PROGRESS the other actions will keep waiting and the rest of the tree will not work.

But if i return SUCCESS or FAILED the action DoStop() will never be called on the BlackboardCondition class and is not possible to set the animator "isWalking" boolean back to false when the condition fails.

So based on this i believe it is not possible to handle player input on this library, because the tree cannot wait for some action while execute other in parallel.

My question is if this is right or do i miss something?

return new Root(
    blackboard,
    new Parallel(
        Parallel.Policy.ALL,
        Parallel.Policy.ALL,
        new BlackboardCondition(
            "isMovePressed",
            Operator.IS_EQUAL,
            true,
            Stops.SELF,
            new Action(
                (abort) =>
                {
                    if (abort)
                    {
                        animator.SetBool("isWalking", false);
                        return Action.Result.SUCCESS;
                    }
                    animator.SetBool("isWalking", true);
                    return Action.Result.PROGRESS;
                }
            )
        ),
        new Action(() => Debug.Log("ONLY RUN WHEN THE FIRST RETURN SUCCESS OR FAILURE;"))
    )
);

how to use BlackboardQuery ? I get confused. I need example, please.

Hello author!
Thanks for your sharing program first.

I tried to modifyEnemyAI example. I tried BlackboardQuery in Selector, like this:
new BlackboardQuery(keys, Stops.IMMEDIATE_RESTART, MyBoardQuery, new Sequence());

keys assigned like this:
private string[] keys = new string[2];
keys[0] = "OrcDistance"; //if Orc is near, run away.
keys[1] = "goblinDistance"; //if goblin is near, attack. both conditions should query simultaneously.

MyBoardQuery() method is defined like this:
private bool MyBoardQuery()
{
object o1 = behaviorTree.Blackboard.Get(keys[0]);
object o2 = behaviorTree.Blackboard.Get(keys[1]);
float f1 = (float)o1;
float f2 = (float)o2;

    if ((f1 > 0.99) && (f2 < 5.99f))
        return true;
    return false;
}

but I get errors in Unity :
Assertion failed. Value was False
Expected: True
UnityEngine.Assertions.Assert:IsTrue(Boolean)
NPBehave.Composite:.ctor(String, Node[]) (at Assets/NPBehaveTest01/NPBehave/Scripts/Composite/Composite.cs:12)
NPBehave.Sequence:.ctor(Node[])
NPBehaveExampleEnemyAI:CreateBehaviourTree() (at Assets/NPBehaveTest01/NPBehave/Examples/Scripts/NPBehaveExampleEnemyAI.cs:32)
NPBehaveExampleEnemyAI:Start() (at Assets/NPBehaveTest01/NPBehave/Examples/Scripts/NPBehaveExampleEnemyAI.cs:13)

ArgumentNullException: Argument cannot be null.
Parameter name: key
System.Collections.Generic.Dictionary2[System.String,System.Collections.Generic.List1[System.Action`2[NPBehave.Blackboard+Type,System.Object]]].ContainsKey (System.String key) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Collections.Generic/Dictionary.cs:458)

can only stop active nodes, tried to stop Sequence! PATH: Root/Service/Selector/BlackboardCondition/Sequence
Assertion failed. Values are not equal.
Expected: ACTIVE == INACTIVE
UnityEngine.Assertions.Assert:AreEqual(State, State, String)
NPBehave.Node:Stop() (at Assets/NPBehaveTest01/NPBehave/Scripts/Node.cs:111)

IndexOutOfRangeException: Array index is out of range.
NPBehave.Sequence.DoStop () (at Assets/NPBehaveTest01/NPBehave/Scripts/Composite/Sequence.cs:29)
NPBehave.Node.Stop () (at Assets/NPBehaveTest01/NPBehave/Scripts/Node.cs:117)

What's wrong ? Can you kindly enough to give an example of BlackboardQuery?

GC/Performance problems

Hello Author

I tried to put a method which calls another component function into the mutiframe action,like this:
new Action((bool _shouldCancel) =>
{ if (!_shouldCancel)
{
MoveInComp(true);
return Action.Result.PROGRESS;
} })

The caller method defined like this:
private void MoveInComp(bool isStart)
{
GetComponent().isStart = true;
GetComponent().enemyPos = new Vector3(21f, 2f, 2f);
}

And the really working function is in Update() of MoveComponent component
void Update()
{ if (isStart == true)
MoveTowardsEnemy(enemyPos); }

    public void MoveTowardsEnemy(Vector3 targetPos)
   {     gameObject.transform.position = Vector3.MoveTowards(gameObject.transform.position, targetPos, this.speed);        }

The running result in Unity is gameObject moved to the targePos just in One frame. My intent is to let it move in multiframe slowly.
How to do it then ?

//////*******//////
So I tried another way.
I didn't use Update() in MoveComponent this time. I just call the plain MoveTowardsEnemy() method.
public void MoveTowardsEnemy() //in MoveComponent
{ if (isStart == true && enemyPos != Vector3.zero)
gameObject.transform.position = Vector3.MoveTowards(gameObject.transform.position, enemyPos, this.speed); }

In behavior tree caller is like this:
private void MoveInComp(bool isStart)
{
GetComponent().isStart = true;
GetComponent().enemyPos = new Vector3(21f, 2f, 2f);
GetComponent().MoveTowardsEnemy() ;
}

But the result is same, gameObject moved to the targePos just in One frame.
Please give me some advice. Thanks

Question about order of timers

In

public void ShouldListenToEvents_WhenUsingChildBlackboard()

In Unity, although child blackboard notify is added to timer first, root blackboard notify is called first (Set branch 1 true).

But As far as I know, C# have no guarantee the order of the Map's keys.

Why this order is reversed?

Test above can be failed if the order of the keys is changed. Am I misunderstanding something?

Null check on Condition node works but not on BlackboardCondition node

Hello, i am making a space fight game and i am using NPBehave to implement AI, however i encountered this weird issue.

So basically there is a "Target" entry in my blackboard and if it is null i want to select a new target, which i implemented as these:

new BlackboardCondition("Target", Operator.IS_EQUAL, null, Stops.IMMEDIATE_RESTART,

    new Action(() => SelectANewTarget())

)

and

new Condition(() => Target == null, Stops.IMMEDIATE_RESTART,

    new Action(() => SelectANewTarget())

)

Target value is GameObject, and i am destroying the ships with Destroy(gameobject). So when target ship is destroyed these implementations suppose to stop the lower nodes and start itself. While implementation with Condition node works implementation with BlackboardCondition does not work.

My test scene starts with every ship having null value for Target so when scene starts every ship finds a target, the thing is BlackboardCondition actually does it's job once, meaning that ships can take find their first target, but once that target is destroyed BlackboardCondition node doesnt restart itself. So i am guessing BlackboardCondition node cant observe that the value has changed.

simpleAI example problem

new Sequence(

                            // Do once 
                            new NBAction(() => {
                                                
                                                SetColor(Color.red); 
                                                }) 
                            { Label = "Change to Red" },

                            // go towards player as long as playerDistance is less than 7.5 ( in that case, _shouldCancel is false )
                            new NBAction((bool _shouldCancel) =>
                            {
                                //_shouldCancel turns TRUE when playerDiscance is more than 7.5 => means " do I have to abort"
                                if (!_shouldCancel)
                                {
                                    MoveTowards(blackboard.Get<Vector3>("playerLocalPos"));
                                    //inform that this action is being performed until the condition changes => waits for distance to be greater (it will run else statement)
                                    return NBAction.Result.PROGRESS;
                                }
                                else
                                {
                                    
                                    //upon returning false starts the next Node in the Node[]
                                    return NBAction.Result.SUCCESS;
                                }
                            })
                            { Label = "Follow" }
                            ,
                            new NBAction(() => {
                                Debug.Log("yellow");
                                SetColor(Color.yellow);
                            })
                            { Label = "Change to Yellow" }
                            
                        );

It is from simpleAI example. It is run inside a condition with Stops.IMMEDIATE_RESTART flag.
I don't quite understand why when I return SUCCESS in the second action it never runs the Change to Yellow action. If I change PROGRESS to SUCCESS it runs all 3 Actions constantly. Why doesn't it run the last action before restarting if we stop returning PROGRESS and return SUCCESS instead? I thought Sequence should finish all its actions if we don't return FAILED ?
In other words how would I add another condition before we restart ?

NPBehave 2.0 Changes - Wishlist

Looking at how things work in NPBehave two years after I wrote it, I realise it has some flaws in the aborting/Stops Rules, that cannot be resolved without major changes to the API. That’s why I aim for a new major version.

If you also have suggestions, feel free to comment on this ticket.

My Suggestions:

Base C# Library

As I currently don’t work in Unity, but in Monogame, I would like to split the NPBehave in a base C# library and have separate git-projects for each of the specific engines: Unity, Monogame etc.

Stops Rules

IMO one of the biggest flaws NPBehave has, is the Stops Rules and the aborting of nodes.

  1. Aborted nodes can influence the way the Behaviour Tree continues processing by returning Succeed or Failed. This is a property of NPBehave that I like over UE4, where aborting a node results in the whole branch to be aborted. You can get UE4s way by a combination of IMMEDIATE_RESTART and returning the right result in the aborted node, however the way it works right now is not very obvious. I would like to get rid of any IMMEDIATE_RESTART versions of the Stops Rules, and give the control over what “aborting” actually means completely to the aborted node. In case a node gets aborted, the node could control the flow by finish with: Succeed, Failed or Aborted (the latter is a replacement for IMMEDIATE_RETART but will always guarantee that the aborting decorator is restarted)
  2. I would like to introduce an “Aborting” state inside nodes. Nodes that are requested to be aborted shouldn’t be forced to immediately finish their work, but allowed to block the flow until they are finished cleaning up ( sometimes you want to wait for some animation for example ). This could be quite problematic in NPBehave, so I will have to see if I can actually make it happen.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.