Advertisement

Flow of data in a behavior tree

Started by December 30, 2021 12:27 PM
2 comments, last by frob 2 years, 11 months ago

I'm currently working on a project that will include an enemy who stalks the player. One of the things I want to practice in this prototype is implementing a Behavior Tree to control the enemy stalking patterns. I've done some research, and I think I understand how the tree works conceptually, as well as different ways to implement it. For this endeavor, since the enemy behaviors will be relatively limited and I don't want to complicate it any more than necessary, I decided to go with an implementation that simply ticks the whole tree every frame. I'm working in Unity/C# by the way.

However, I've ran into a few questions related to the design of the tree:

  • From what I gather, trees are accompanied by a 'blackboard', which keeps track of variables important to the decision-making process (player position, sight/sound sensor states, player health etc.). I've placed these inside a dictionary accessible through string keys, and when a new node is created, it gets passed the data it needs to operate. My problem here is, how do I keep these variables updated in the blackboard itself? I'm tempted to give the blackboard component a reference to the GameObjects it needs to track, and just update them every frame in the component's Update() loop, but it sounds a bit inefficient. Some of these can be updated with events (player health whenever the player is damaged) but for others, like player position, it would make no difference - I'd just be firing an event in the slightest movement.
  • Another issue I have is how I'm currently passing arguments to the nodes. The way I've built the system, the enemy GameObject has two components: a BehaviorTree component and a Blackboard component. The BehaviorTree is responsible for building the actual node structure -- nodes don't derive from MonoBehaviour -- in its Awake() method -- it basically uses the 'new' keyword to create instances of task nodes and 'hooks' them to composite nodes (selectors, sequences etc.). The BehaviorTree component also has a reference to the Blackboard component.

    I think an example will help illustrate my point in a better way. Let's say we have an IsInSight conditional node that checks whether an entity's ‘sight sensor’ (i.e. ray, cone) has been triggered. The way I intend to track this is with a boolean variable inside the Blackboard which, in this case, is supposedly monitoring the enemy's sight cone and when a trigger enters it, the value switches to true. Since the tree is ticked every frame, when execution reaches the IsInSight node, it should check whether that bool value is true and return ‘SUCCESS’, so that the sequence can evaluate the next node. This means I should somehow pass a reference to that Blackboard variable in the node's constructor, so at the time of creation, it knows what it needs to track. However, boolean values are passed by value, so the node would be ‘stuck’ with the initial value it was passed. One solution to this I can see is just passing the GameObject references to every node, and let it grab the values it needs, but this completely eliminates the purpose of a blackboard that could be used in different contexts.

To sum up, my problems are mostly related to good practices, and not so much an inability to create the system in code. I understand that things should be kept simple when possibly, however a behavior tree is by its very nature modular, and I would like it to be implemented in a SOLID way so that it's robust enough to accomodate changes. I'm unable to find any examples that address these issues, as most articles/talks on the subject are either too abstract/conceptual, or documentation for existing solutions (i.e. UE's Behavior Tree implementation).

To update the blackboard, I know of four main ways:

  1. Add references to objects, rather than properties. E g, don't add “playerlocation” to the blackboard; add “player” and each time you want the location, do “player.GetLocation()” The separation of concerns then gets dispatched to the user of the blackboard, rather than the blackboard, which isn't a bad way to go. Another benefit is that it re-uses existing accessors and doesn't add additional structure.
  2. Make properties reactive, and add subscriptions in the blackboard. A reactive property has a “I changed” signal and a list of subscribers; blackboard adds itself as subscriber and updates the value when it's changed. Making each property reactive is a bit of a chore in most systems, and systems that make it easy, still have to implement all the indirection for each separate property, so the overhead can be significant, but it's also very robust and allows full separation of concerns. This can also be re-used for networking, GUI, gameplay triggers, and other data dependencies – as long as you can afford the overhead.
  3. Add decorators on the blackboard that poll the appropriate values at appropriate intervals. This is quick-and-dirty to implement, but generally less efficient than the first two because it tends to over-poll whether it's needed or not. It also adds a little bit of coupling in that the “poller” object needs to know about the target being polled, in addition to where it goes.
  4. Teach objects/actors about blackboards, and configure a list of blackboards to update on each object/actor. I like this the least, because it's high-coupling, but it's straightforward and can be efficient.

loxagossnake said:

documentation for existing solutions (i.e. UE's Behavior Tree implementation).

To be fair, you could do much worse than taking inspiration from an existing, working, system. If you learned how it worked in UE, and implemented the same exact concepts on top of Unity, and got it working, you would probably learn a lot, both about structure, and about implementation concerns, and once you had that, you would be in a better position try try other “what if?”

enum Bool { True, False, FileNotFound };
Advertisement

Ultimately it is entirely about your own implementation details. You need to consider who you are developing for.

The concept can be implemented entirely in code with switch statements or similar branches, entirely in data with everything configured externally, and anything imaginable in between.

The big benefits from data-driven systems is the designers, producers, and even QA or other team members can potentially adjust and tune the values. But the drawback is the systems can easily become a full time job for multiple people. The simple systems kept in code are often far less work and more suitable for solo development.

If you implement it with a block of data you pass around, then you will need to address how the data changes over time. The system can quickly become complex. You can spend tremendous time on the system, but it usually only makes sense if you will get a good return by enabling others to work with the data. If it is just you or a very small team, it is usually more cost effective to avoid the complexity and keep it in code. The systems in Unreal and Unity make far more sense for projects with many workers.

This topic is closed to new replies.

Advertisement