天天看點

【AI】行為樹(Behaviour Tree)

接上篇,AI預定義邏輯可以把條件和行為邏輯看成一個Task,一個AI有多個Task,行為樹把單個Task中條件和行為、多個Task之間的關系進行細分成節點關系,通過樹形結構來分層,系統每幀從樹的根向下周遊,根據各節點的功能來執行子節點。

行為樹主要由以下四種節點抽象而成組合節點、裝飾節點、條件節點、行為節點。

①組合節點(Composites)

主要包含:Sequence順序條件,Selector選擇條件,Parallel平行條件以及他們之間互相組合的條件。

②修飾節點(Decorator)

連接配接樹葉的樹枝,就是各種類型的修飾節點,這些節點決定了 AI 如何從樹的頂端根據不同的情況,來沿着不同的路徑來到最終的葉子這一過程。

如讓子節點循環操作(LOOP)或者讓子task一直運作直到其傳回某個運作狀态值(Util),或者将task的傳回值取反(NOT)等等

③條件節點(Conditinals)

用于判斷某條件是否成立。目前看來,是Behavior Designer為了貫徹職責單一的原則,将判斷專門作為一個節點獨立處理,比如判斷某目标是否在視野内,其實在攻擊的Action裡面也可以寫,但是這樣Action就不單一了,不利于視野判斷處理的複用。一般條件節點出現在Sequence控制節點中,其後緊跟條件成立後的Action節點。

④行為節點(Action)

行為節點是真正做事的節點,行為節點在樹的最末端,都是葉子節點(注意葉子節點是沒有子節點的),就是這些 AI 實際上去做事情的指令;

通過使用行為樹内的節點之間的關聯來驅動角色的行為,比直接用具體的代碼告訴一個角色去做什麼事情,要來得有意思得多,這也是行為樹最讓人興奮的一點。這樣我們隻要抽象好行為,就不用去理會戰鬥中具體發生了什麼。

像修飾節點和條件節點可以放在組合節點中進行處理,是以組成三個節點:

1.根節點Root

2.邏輯節點:負責子節點的執行順序和子節點能否執行。

              Sequence          : 選擇第一個成功計算的子對象作為活動子對象。

              PrioritySelector  :計算算目前活動子節點,或者第一個子節點(如果沒有活動子節點)。如果通過計算,标記目前活動子節點,或者第一個子節點(如果沒有可用的活動子節點),如果結果是結束,那麼将活動子節點更改為下一個。

              Parallel               : 計算所有子節點,如果其中任何子節點計算失敗,則目前節點失敗。

3.行為節點 

案例:一個怪物在指定路徑巡邏,當玩家怪物與玩家距離小于等于5米時會追逐玩家,當大于5米時會繼續巡邏。

條件:大于5五米、小于等于5米

行為:巡邏、追逐玩家

【AI】行為樹(Behaviour Tree)

 所有節點的根節點:NodeBase

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

namespace VAE.BehaviourTree
{
    public abstract class NodeBase
    {
        public enum NodeResult              //節點傳回狀态 
        {
            Ended = 1,
            Running = 2,
        }


        public PreconditionBase preconditionBase;    // 用來檢查節點是否可以輸入
        public Database database;

        public bool activated;                                      //節點激活狀态
        public float interval;                                        //節點更新間隔

        private float lastTimeEvaluated = 0;

        protected List<NodeBase> children;
        public List<NodeBase> Children { get { return this.children; } }

        public NodeBase() : this(null) { }
        public NodeBase(PreconditionBase preconditionBase)
        {
            this.preconditionBase = preconditionBase;
        }

        //激活節點
        public virtual void Active(Database database)
        {
            if (this.activated) return;

            this.database = database;

            if (this.preconditionBase != null)
            {
                this.preconditionBase.Active(database);
            }

            if (this.children != null)
            {
                foreach (var child in this.children)
                {
                    child.Active(database);
                }
            }

            this.activated = true;

        }

        //沒幀進行校驗
        public virtual NodeResult Update()
        {
            return NodeResult.Ended;
        }

        protected virtual bool DoEvaluate() { return true; }

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

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


        public virtual void Clear() { }

        public virtual void AddChild(NodeBase nodeBase)
        {
            if (this.children == null)
            {
                this.children = new List<NodeBase>();
            }
            if (nodeBase != null)
            {
                this.children.Add(nodeBase);
            }
        }

        public virtual void RemoveChild(NodeBase nodeBase)
        {
            if (this.children != null && nodeBase != null)
            {
                this.children.Remove(nodeBase);
            }
        }

        //用來處理每個節點的檢查間隔
        private bool CheckTimer()
        {
            if (Time.time - this.lastTimeEvaluated > this.interval)
            {
                this.lastTimeEvaluated = Time.time;
                return true;
            }
            return false;
        }


    }
}
           

條件基類:PreconditionBase        條件會注冊到邏輯節點進行判定目前節點是否可以進行

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

namespace VAE.BehaviourTree
{
    public abstract class PreconditionBase : NodeBase
    {
        public PreconditionBase() : base(null) { }

        public abstract bool Check();       // 條件檢測

        public override NodeResult Update()
        {
            bool success = this.Check();
            if (success)
            {
                return NodeResult.Ended;
            }
            else
            {
                return NodeResult.Running;
            }
        }

    }
}
           

邏輯節點:PrioritySelector

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

namespace VAE.BehaviourTree
{
    /*
     * 優先級選擇器
        選擇第一個成功計算的子對象作為活動子對象。
     */
    public class PrioritySelector : NodeBase
    {
        private NodeBase activeChild;       //活動子對象

        public PrioritySelector(PreconditionBase preconditionBase = null) : base(preconditionBase)
        {

        }

        protected override bool DoEvaluate()
        {
            foreach (var child in this.children)
            {
                if (child.Evaluate())       //選擇計算成功的子對象
                {
                    if (this.activeChild != null && this.activeChild != child)
                    {
                        this.activeChild.Clear();
                    }
                    this.activeChild = child;
                    return true;
                }
            }
            this.activeChild = null;
            return false;
        }

        public override NodeResult Update()
        {
            if (this.activeChild == null)
            {
                return NodeResult.Ended;
            }

            var result = this.activeChild.Update();
            if (result != NodeResult.Running)
            {
                this.activeChild.Clear();
                this.activeChild = null;
            }
            return result;
        }


    }

}
           

行為節點:

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

namespace VAE.BehaviourTree
{
    /*
        行為節點基類
        該節點不能進行增加和删除子節點,隻用來處理行為
     */
    public class ActionBase : NodeBase
    {
        private enum ActionStatus
        {
            Ready = 1,
            Running = 2,
        }

        private ActionStatus actionStatus = ActionStatus.Ready;

        public ActionBase(PreconditionBase preconditionBase = null) : base(preconditionBase)
        {

        }

        protected virtual void Enter()
        {
            // 當行為進入時觸發的函數   一般用來處理:播放動畫等
        }

        protected virtual void Exit()
        {
            // 當行為退出時觸發的函數

        }

        // 執行邏輯
        protected virtual NodeResult Execute()
        {
            return NodeResult.Running;
        }


        public override NodeResult Update()
        {
            NodeResult result = NodeResult.Ended;

            if (this.actionStatus == ActionStatus.Ready)
            {
                this.Enter();                                                       // 進入執行行為
                this.actionStatus = ActionStatus.Running;
            }

            if (this.actionStatus == ActionStatus.Running)
            {
                result = this.Execute();
                if (result != NodeResult.Running)       //代表執行結束
                {
                    this.Exit();
                    this.actionStatus = ActionStatus.Ready;
                }
            }
            return result;
        }


        public override void Clear()
        {
            if (this.actionStatus != ActionStatus.Ready)
            {
                this.Exit();
                this.actionStatus = ActionStatus.Ready;
            }
        }


        public override void AddChild(NodeBase nodeBase)
        {
            Debug.LogError("Action: Cannot add a node into Action.");
        }

        public override void RemoveChild(NodeBase nodeBase)
        {
            Debug.LogError("Action: Cannot remove a node into Action.");
        }


    }
}
           

用于AI行為樹,各節點之間資料修改與擷取的:Database

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

namespace VAE.BehaviourTree
{
    public class Database : MonoBehaviour
    {

        private List<object> _database = new List<object>();
        private List<string> _dataNames = new List<string>();

        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)
        {
            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;
        }
    }
}
           

行為樹驅動腳本:BTTreeBase     處理行為樹初始化,條件判定和更新

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

namespace VAE.BehaviourTree
{
    /*
        
     */
    public class BTTreeBase : MonoBehaviour
    {
        protected NodeBase root = null;

        [HideInInspector]
        public Database database;

        [HideInInspector]
        public bool isRunning = true;

        public const string RESET = "RESET";

        private static int resetId;

        private void Awake()
        {
            this.Init();

            //當所有 限定節點 邏輯條件 添加完畢 再進行資料激活
            this.root.Active(this.database);
        }

        private void Update()
        {
            if (!this.isRunning) return;

            if (this.database.GetData<bool>(BTTreeBase.RESET))
            {
                this.Reset();
                this.database.SetData<bool>(BTTreeBase.RESET, false);
            }

            if (this.root.Evaluate())
            {
                this.root.Update();
            }

        }

        protected virtual void Init()
        {
            this.database = this.GetComponent<Database>();
            if (this.database == null)
            {
                this.database = this.gameObject.AddComponent<Database>();
            }

            BTTreeBase.resetId = this.database.GetDataId(BTTreeBase.RESET);
            this.database.SetData<bool>(BTTreeBase.resetId, false);
        }

        protected void Reset()
        {
            this.root.Clear();
        }

    }
}
           

外部邏輯:

處理限制條件:大于5五米、小于等于5米

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

using VAE.BehaviourTree;

public class Precdontion_TransformDistance : PreconditionBase
{
    public enum precdFunction
    {
        LessThan = 1,
        GreaterThan = 2,
    }

    private string minChaseDistanceStr;
    private int minChaseDistanceId;
    private Transform itemTran;                     // 目标對象
    private Transform enemyAI;

    private precdFunction func;

    public Precdontion_TransformDistance(string minChaseDistanceStr, Transform itemTran, precdFunction func)
    {
        this.minChaseDistanceStr = minChaseDistanceStr;
        this.itemTran = itemTran;
        this.func = func;
    }

    public override void Active(Database database)
    {
        base.Active(database);


        this.minChaseDistanceId = this.database.GetDataId(this.minChaseDistanceStr);

        this.enemyAI = this.database.transform;
    }


    public override bool Check()
    {
        if (itemTran == null) return false;

        Vector3 offset = itemTran.position - this.enemyAI.position;

        var minDistance = this.database.GetData<float>(this.minChaseDistanceId);

        if (this.func == precdFunction.GreaterThan)
        {
            return (offset.sqrMagnitude >= minDistance);
        }
        else
        {
            return (offset.sqrMagnitude <= minDistance);
        }

    }


}
           

行為邏輯:

巡邏:Action_Partoll

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VAE.BehaviourTree;

public class Action_Partoll : ActionBase
{
    private List<Transform> partollPos;
    private int index = 0;
    private Transform enemyAI;
    private float moveSpeed;

    public Action_Partoll(List<Transform> partollPos, float speed)
    {
        this.partollPos = partollPos;
        this.moveSpeed = speed;
    }

    public override void Active(Database database)
    {
        base.Active(database);

        this.enemyAI = this.database.transform;
    }

    protected override NodeResult Execute()
    {
        var distance = Vector3.Distance(this.enemyAI.position, this.partollPos[this.index].position);
        if (distance <= 0.1f)
        {
            this.index++;
            this.index %= this.partollPos.Count;
        }

        Vector3 direction = (this.partollPos[this.index].position - this.enemyAI.position).normalized;
        this.enemyAI.position += direction * this.moveSpeed * Time.deltaTime;

        return NodeResult.Running;
    }

}
           

追逐指定目标:Action_ChaseItem

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VAE.BehaviourTree;


public class Action_ChaseItem : ActionBase
{
    private Transform itemTran;      //目标

    private float moveSpeed;

    private Transform enemyAI;

    private string minChaseDistanceStr;
    private int minChaseDistanceId;

    public Action_ChaseItem(string minChaseDistanceStr, Transform itemTran, float speed)
    {
        this.itemTran = itemTran;
        this.moveSpeed = speed;
        this.minChaseDistanceStr = minChaseDistanceStr;
    }


    public override void Active(Database database)
    {
        base.Active(database);      // 基類函數一定要執行

        this.minChaseDistanceId = this.database.GetDataId(this.minChaseDistanceStr);

        this.enemyAI = this.database.transform;
    }


    protected override NodeResult Execute()
    {
        if (this.CheckArrived())
        {
            return NodeResult.Ended;
        }

        Vector3 direction = (this.itemTran.position - this.enemyAI.position).normalized;
        this.enemyAI.position += direction * this.moveSpeed * Time.deltaTime ;

        return NodeResult.Running;
    }

    //檢驗是否應超出追逐範圍
    private bool CheckArrived()
    {
        Vector3 offset = this.itemTran.position - this.enemyAI.position;       // 怪物AI -》目的地的向量

        float tmpMinDistance = this.database.GetData<float>(this.minChaseDistanceId);

        return offset.sqrMagnitude > tmpMinDistance * tmpMinDistance;
    }

}
           

EnemyAI:AI初始化

using System.Collections;
using System.Collections.Generic;
using VAE.BehaviourTree;
using UnityEngine;

public class EnemyAI : BTTreeBase
{
    public Transform itemTran;
    public float speed = 3;
    public List<Transform> partollPos;
    public float partollSpeed = 4;

    public float minChaseDistance = 5;      // 最小追逐距離

    public const string MINCHASEDISTANCE = "MINCHASEDISTANCE";

    protected override void Init()
    {
        base.Init();        // 一定要調用基類函數

        this.root = new PrioritySelector();         // 建立行為樹根節點

        //一個怪物在指定路徑巡邏,當玩家怪物與玩家距離小于等于5米時會追逐玩家,當大于5米時會繼續巡邏。這裡就分為了巡邏、追逐兩種狀态。
        //
        this.database.SetData<float>(MINCHASEDISTANCE, this.minChaseDistance);

        Precdontion_TransformDistance precd_TransformDistance_greater = new Precdontion_TransformDistance(MINCHASEDISTANCE, itemTran, Precdontion_TransformDistance.precdFunction.GreaterThan);
        Precdontion_TransformDistance precd_TransformDistance_less = new Precdontion_TransformDistance(MINCHASEDISTANCE, itemTran, Precdontion_TransformDistance.precdFunction.LessThan);

        
      
        PrioritySelector tree2_1 = new PrioritySelector(precd_TransformDistance_greater);
        Action_Partoll action_Partoll = new Action_Partoll(this.partollPos, this.partollSpeed);
        tree2_1.AddChild(action_Partoll);

        PrioritySelector tree2_2 = new PrioritySelector(precd_TransformDistance_less);
        Action_ChaseItem action_ChaseItem = new Action_ChaseItem(MINCHASEDISTANCE, itemTran, this.speed);
        tree2_2.AddChild(action_ChaseItem);

        this.root.AddChild(tree2_1);
        this.root.AddChild(tree2_2);


    }
}
           
【AI】行為樹(Behaviour Tree)

 像上述,條件節點,行為節點還可以繼續進行細劃分,分的越詳細代碼複用越高。比如:AI巡邏行為接着可劃分為朝指定位置移動行為,這就需要新的邏輯節點來處理,進而達到複用追逐和巡邏兩個地方的行為功能。

行為樹缺點:為樹是依賴設計者的固定架構的,很不靈活,做的選擇不一定是最優選擇,而且每次都要經過大量的邏輯判斷,性能消耗嚴重。

而後另一種AI算法:GOAP(目标導向型行動計劃)更好的解決問題。

目前對行為樹了解還不太深刻,後續會繼續增加。。。

主要參考:

https://zhuanlan.zhihu.com/p/94850561

https://www.jianshu.com/p/23f79a365c10

繼續閱讀