One article is enough: Unity & Behavior Tree

Table of contents

foreword

Introduction to Unity Behavior Tree

A simple enemy AI

text

Personal understanding of behavior trees

Finite State Machines and Behavior Trees

basic framework

BTNode

DataBase

behavior tree entry

Behavior tree event GraphEvent

send event

listen event

Script sends event

Behavior tree management & operation

 1. Operating a single tree

 2. Manage all trees

Custom Task task

1. Introduce namespace:

2. Explicitly inherited Task type:

3. Know the execution flow of the internal functions of the Task:

Summarize

The following advantages of behavior trees

> Static

> Intuitive

> Reusability

> Extensibility


foreword

Introduction to Unity Behavior Tree

At present, the general complex AI in the Unity3D game can see the figure of the behavior tree, and the simple AI can be realized by using a state machine. It is recommended to study in advance and prepare well. This is called "do not fight the unprepared battle" hahaha .

The concept of behavior tree has been around for many years. In general, it is the combination of various classic control nodes + behavior nodes to realize complex AI.

       In the Behavior Designer plug-in, there are four main concept nodes, which are called Task. include:

       (1)  Composites   combination nodes , including classic: Sequence, Selector, Parallel

       (2)  Decorator   decoration node , as the name implies, is to add some additional functions to only one child node, such as letting the child task run until it returns a certain running status value, or inverting the return value of the task, etc.

       (3)  Actions   action node , action node is the node that actually does things, which is a leaf node. The Behavior Designer plug-in comes with a lot of Action nodes. If it is not enough, you can also write your own Action. Generally speaking, you have to write your own Action, unless the user is an artist or planner who does not understand scripting, and just wants to simply control the properties of some objects.

       (4)  Conditinals   condition node , which is used to judge whether a certain condition is true. At present, it seems that in order to implement the principle of single responsibility, Behavior Designer treats the judgment as a node independently, such as judging whether a certain target is in the field of view, in fact, it can also be written in the action of the attack, but the Action is not single. It is not conducive to the reuse of visual field judgment processing. The general condition node appears in the Sequence control node, followed by the Action node after the condition is established.

Behavior Tree has the following characteristics:
 
  Its 4 types of nodes: 1. Composite 2. Decorator 3. Condition 4. Action Node
  After any Node is executed, it must report the execution result to its Parent Node: success/failure .
  This simple success/failure reporting principle is cleverly used to control the decision direction of the entire tree.

A simple enemy AI

When in the surveillance range, run to the player, when in the attack range, attack the player, otherwise stay in place, which is represented by the behavior tree as follows:

text

Personal understanding of behavior trees

My understanding so far is that sometimes a behavior tree can be seen as a state machine .

The selector selects the large state, and the selector in the large state selects the small state. These same-level states have priorities from left to right, which simplifies some judgment conditions.

Since there is a state, there is a judgment statement to judge whether the state is executed or not. The judgment statement can be used in combination with the sequencer and the condition, or the conditional node can be directly used in real time.

This is not enough, because it is not dynamic, each frame after entering an action will wait for the task to complete, and will not re-detect the conditions from left to right to select the task. (For example, if the mobs are patrolling, he may not attack the player when he sees it. At this time, it enters the patrol state, and the player detection statement is not executed, so the player cannot be seen.) In this way, the selector should be set to Dynamic, although the patrol task does not End, but each frame will first judge the condition on the left according to the priority, and see the player will switch to the chase state. 

if(){}

else if(){}

else if(){

    if(){}

    else{}

}

if(){

    if(){}

    else if()  {}

    else{}

    else{}

Finite State Machines and Behavior Trees

Why do many people think that finite state machines are cumbersome?

Because in some respects, the finite state machine discards the priority of each state, and in exchange for high scalability , each new state only needs to add transition conditions. Also keeping each state separate increases maintainability. However, because the priority is discarded and the transition of any two states is implemented by conditional judgment, the inconvenience is that each state has to write transition conditions for the states it can transition to , which undoubtedly increases the workload. You can refer to Unity's animation state machine when there are too many states.

The behavior tree is more like the script we usually write. It not only retains the priority relationship of each state, but also omits the state transition conditions added by the state machine due to abandoning the state priority, and can modularize each state to achieve high scalability. And high maintenance (the subtree under each selector is a state, if the priority and the hierarchical relationship of the tree are well designed, you can make a state machine taste, this behavior tree is a beautiful thing. Below), the structure of the well-written code designed by the behavior tree must also be very clear.

Tips: When building a behavior tree, you should not be blind, but have a clear structure planned by selectors and sequencers as a whole, so as not to blindly connect nodes.

basic framework

BTNode

The behavior tree node (BTNode) is used as the base class of all nodes in the behavior tree. It needs to have the following basic attributes and functions/interfaces:

  • Attributes

    • Node name ( name)

    • list of child nodes ( childList)

    • Node access condition ( precondition)

    • Blackboard ( Database)

    • Cooling interval ( interval)

    • Whether to activate ( activated)

  • function/interface

    • Node initialization interface ( public virtual void Activate (Database database))
    • Personalized check interface ( protected virtual bool DoEvaluate ())
    • Check whether the node can execute: whether it is activated, whether the cooling is completed, whether it passes the admission conditions, and whether it is a personalized check ( public bool Evaluate ())
    • Node execution interface ( public virtual BTResult Tick ())
    • node clear interface ( public virtual void Clear ())
    • add/remove child node function ( public virtual void Add/Remove Child(BTNode aNode))
    • Check cooldown time ( private bool CheckTimer ())

The two most important interfaces that BTNode provides to subclasses are DoEvaluate() and Tick().

DoEvaludate provides subclasses with an interface for personalized inspection (note that it is different from Evaluate ). For example, the inspection of Sequence is different from that of Priority Selector. For example, Sequence and Priority Selector have nodes A, B, and C. During the first inspection,

Sequence can only check A, because A does not pass Evaluate, then this Sequence cannot be executed from the beginning, so the DoEvaludate of Sequence also does not pass.

The Priority Selector checks A first. If A fails, it checks B, and so on. Only when all child nodes fail to pass Evaluate, it will fail DoEvaludate.

Tick ​​is the interface executed by the node, which will only be executed when Evaluate passes. Subclasses need to overload Tick to achieve the desired logic. For example, Sequence and Priority Selector, their Ticks are also different:

In the Sequence, when the active child node A Tick returns Ended, the Sequence will set the current active child as node B (if there is B), and return Running. When the last child node N Tick of Sequence returns Ended, Sequence also returns Ended.

Priority Selector is when the current active child returns Ended, it also returns Ended. When Running, it also returns Running.

It is through overloading DoEvaluate and Tick that the BT framework implements the logical nodes of Sequence, PrioritySelector, Parallel, and ParallelFlexible. If you have special needs, you can also overload DoEvaluate and Tick to achieve:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace BT {
	/// <summary>
	/// BT node is the base of any nodes in BT framework.
	/// </summary>
	public abstract class BTNode {
		//节点名称
		public string name;
		//孩子节点列表
		protected List<BTNode> _children;
		//节点属性
		public List<BTNode> children {get{return _children;}}

		// Used to check the node can be entered.
		//节点准入条件
		public BTPrecondition precondition;
		//数据库
		public Database database;
		//间隔
		// Cooldown function.
		public float interval = 0;
		//最后时间评估
		private float _lastTimeEvaluated = 0;
		//是否激活
		public bool activated;


		public BTNode () : this (null) {}

		/// <summary>
        /// 构造
        /// </summary>
        /// <param name="precondition">准入条件</param>
		public BTNode (BTPrecondition precondition) {
			this.precondition = precondition;
		}
		
		// To use with BTNode's constructor to provide initialization delay
		// public virtual void Init () {}
		/// <summary>
        /// 激活数据库
        /// </summary>
        /// <param name="database">数据库</param>
		public virtual void Activate (Database database) {
			if (activated) return ;
			
			this.database = database;
			//			Init();
			
			if (precondition != null) {
				precondition.Activate(database);
			}
			if (_children != null) {
				foreach (BTNode child in _children) {
					child.Activate(database);
				}
			}
			
			activated = true;
		}

		public bool Evaluate () {
			bool coolDownOK = CheckTimer();

			return activated && coolDownOK && (precondition == null || precondition.Check()) && DoEvaluate();
		}

		protected virtual bool DoEvaluate () {return true;}

		public virtual BTResult Tick () {return BTResult.Ended;}

		public virtual void Clear () {}
		
		public virtual void AddChild (BTNode aNode) {
			if (_children == null) {
				_children = new List<BTNode>();	
			}
			if (aNode != null) {
				_children.Add(aNode);
			}
		}

		public virtual void RemoveChild (BTNode aNode) {
			if (_children != null && aNode != null) {
				_children.Remove(aNode);
			}
		}

		// Check if cooldown is finished.
		private bool CheckTimer () {
			if (Time.time - _lastTimeEvaluated > interval) {
				_lastTimeEvaluated = Time.time;
				return true;
			}
			return false;
		}
	}
	public enum BTResult {
		Ended = 1,
		Running = 2,
	}
}

DataBase

As a place to store all data, the database can retrieve any data through key-value. You can understand it as a global variable blackboard. We can manually add data and access data through nodes:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;

/// <summary>
/// Database is the blackboard in a classic blackboard system. 
/// (I found the name "blackboard" a bit hard to understand so I call it database ;p)
/// 
/// It is the place to store data from local nodes, cross-tree nodes, and even other scripts.

/// Nodes can read the data inside a database by the use of a string, or an int id of the data.
/// The latter one is prefered for efficiency's sake.
/// </summary>
public class Database : MonoBehaviour {

	// _database & _dataNames are 1 to 1 relationship
	private List<object> _database = new List<object>();
	private List<string> _dataNames = new List<string>();
	

	// Should use dataId as parameter to get data instead of this
	public T GetData<T> (string dataName) {
		int dataId = IndexOfDataId(dataName);
		if (dataId == -1) Debug.LogError("Database: Data for " + dataName + " does not exist!");

		return (T) _database[dataId];
	}

	// Should use this function to get data!
	public T GetData<T> (int dataId) {
		if (BT.BTConfiguration.ENABLE_DATABASE_LOG) {
			Debug.Log("Database: getting data for " + _dataNames[dataId]);
		}
		return (T) _database[dataId];
	}
	
	public void SetData<T> (string dataName, T data) {
		int dataId = GetDataId(dataName);
		_database[dataId] = (object) data;
	}

	public void SetData<T> (int dataId, T data) {
		_database[dataId] = (object) data;
	}

	public int GetDataId (string dataName) {
		int dataId = IndexOfDataId(dataName);
		if (dataId == -1) {
			_dataNames.Add(dataName);
			_database.Add(null);
			dataId = _dataNames.Count - 1;
		}

		return dataId;
	}

	private int IndexOfDataId (string dataName) {
		for (int i=0; i<_dataNames.Count; i++) {
			if (_dataNames[i].Equals(dataName)) return i;
		}

		return -1;
	}

	public bool ContainsData (string dataName) {
		return IndexOfDataId(dataName) != -1;
	}
}
// IMPORTANT: users may want to put Jargon in a separate file
//public enum Jargon {
//	ShouldReset = 1,
//}

behavior tree entry

The previous code is the behavior tree framework itself. Now, we need to build this behavior tree entry through nodes, so that we can actually use it:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using BT;

// How to use:
// 1. Initiate values in the database for the children to use.
// 2. Initiate BT _root
// 3. Some actions & preconditions that will be used later
// 4. Add children nodes
// 5. Activate the _root, including the children nodes' initialization

public abstract class BTTree : MonoBehaviour {
	protected BTNode _root = null;

	[HideInInspector]
	public Database database;

	[HideInInspector]
	public bool isRunning = true;

	public const string RESET = "Rest";
	private static int _resetId;


	void Awake () {
		Init();

		_root.Activate(database);
	}
	void Update () {
		if (!isRunning) return;
		
		if (database.GetData<bool>(RESET)) {
			Reset();	
			database.SetData<bool>(RESET, false);
		}

		// Iterate the BT tree now!
		if (_root.Evaluate()) {
			_root.Tick();
		}
	}

	void OnDestroy () {
		if (_root != null) {
			_root.Clear();
		}
	}

	// Need to be called at the initialization code in the children.
	protected virtual void Init () {
		database = GetComponent<Database>();
		if (database == null) {
			database = gameObject.AddComponent<Database>();
		}

		_resetId = database.GetDataId(RESET);
		database.SetData<bool>(_resetId, false);
	}

	protected void Reset () {
		if (_root != null) {
			_root.Clear();	
		}
	}
}

Behavior tree event GraphEvent

When an event is sent, all oweners in the scene can respond to the event at the same time.

It is also possible to send events through scripts, and it is feasible to respond to attacks.

send event

listen event

Script sends event

Behavior tree management & operation

 1. Operating a single tree

This is the behavior tree script created automatically by an enemy bound to the behavior tree in our project:

Enlarge the red box to see:

The Behavior Tree component contains the following properties:

So when we need it, how do we code to manipulate these variables?

(1) We must first find the tree to operate on

Method 1 to find the tree: define a Public BehaviorTree tree = new BehaviorTree();, and then drag and drop the assignment from the panel.

Method 2 to find the tree: define a privarte's BehaviorTree tree = new BehaviorTree();, then find the object through the GameObject's Find, and then get the components above the object.

(2) The code operates the tree

using BehaviorDesigner.Runtime.Tasks;//引用不可少

using BehaviorDesigner.Runtime;

 

public class Tree : MonoBehaviour {

public BehaviorTree tree = new BehaviorTree();

void Start () {

    tree.enabled = false;

    var a = tree.GetAllVariables();

    tree.StartWhenEnabled = false;

    var b = tree.FindTasksWithName("AI_Daze");

  }

}

The above code is just a simple demonstration, you can manipulate the data of the behavior tree. In fact, all variables in the panel screenshot can be manipulated. In addition, tree has many properties and methods that can be manipulated.

 2. Manage all trees

When the behavior tree runs, a new game object with a behavior manager component is automatically created, and the behavior manager component is bound to it. This component manages the behavior tree of all executions in your scene

 You can control the type of update of the behavior tree, the update time, etc.

Update Interval : update frequency

Every Frame : Update the behavior tree every frame

Specify Seconds : define an update interval

Manual : Manually call the update, after selecting this, you need to call the update of the behavior tree through a script

Task Execution Type : Task execution type

No Duplicates : no duplicates

Repeater Task : Repeat the task node. If set to 5, then each frame is executed 5 times


BehaviorManager.instance.Tick();
In addition, if you want different behavior trees to have their own update intervals, you can do this:
BehaviorManager.instance.Tick(BehaviorTree);

For more methods, see the BehaviorManager class

Custom Task task

Generally, the tasks of composite classes and decorative classes are sufficient, and some are not even used at all, while the specific behavior tasks and conditional tasks have never been able to meet our needs, and writing such tasks by yourself can greatly simplify The entire behavior tree structure.

The steps to write your own Task are as follows:

1. Introduce namespace:

using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;

2. Explicitly inherited Task type:

public class MyInputMove : Action
public class MyIsInput : Conditional

3. Know the execution flow of the internal functions of the Task:

very important picture

 

Observing the above figure, you will find that it is similar to writing scripts in Unity. The difference is that the Update here has a return value. To return the execution status of the task, it is only called every frame when it is in the Running state:

using UnityEngine;
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;

public class MyInputMove : Action
{
    public SharedFloat speed = 5f;
    public override TaskStatus OnUpdate()
    {
        float inputX = Input.GetAxis("Horizontal");
        float inputZ = Input.GetAxis("Vertical");
        if (inputX != 0 || inputZ != 0)
        {
            Vector3 movement = new Vector3(inputX, 0, inputZ);
            transform.Translate(movement*Time.deltaTime*speed.Value);
            return TaskStatus.Running;
        }
        return TaskStatus.Success;
    }
}

Summarize

The following advantages of behavior trees

> Static

The more complex the function, the more simple the foundation is needed , otherwise, you will not be able to play by yourself in the end.
Static is a very important point to use with behavior trees: even if the system needs some "dynamic" properties .
In fact, dynamically placed Nodes such as Stimulus seem to be powerful, but they destroy the static nature that is easy to understand, and do more harm than good.
One of the improvements to the BT AI that Halo3 has over Halo2 is the removal of the dynamics of the Stimulus. Instead, use Behavior Masks, Encounter Attitude, Inhibitions.
The principle is to keep all Node static and just check if Node is enabled based on events and environment.
The direct benefit of staticity is that the planning of the entire tree does not need to be dynamically adjusted at runtime, which brings convenience to many optimizations and pre-editing.

> Intuitive

Behavior trees can easily organize complex AI knowledge items very intuitively. The default Composite Node iteration method from begin to end of Child Node is like processing a
preset priority policy queue, which is also very consistent with the normal human thinking mode: the first is the best and the second is the best.
Behavior tree editors are also readily available to good programmers.

> Reusability

Various Nodes, including Leaf Node, are highly reusable. Achieving personality differentiation for NPC AI can even be achieved by placing Impulses in different positions on a shared behavior tree. Of course, when an NPC needs a completely different brain, such as a level 100 BOSS, instead of racking their brains to install Impulse in a public BT, it is better to design a dedicated BT from scratch.

> Extensibility

Although the combinations and collocations between the above Nodes cover almost all AI needs.
But it is also easy to tailor a new Composite Node or Decorator Node to the project.
You can also accumulate a project-related Node Lib, which is very valuable in the long run.

Guess you like

Origin blog.csdn.net/flyTie/article/details/126440816