Unity BehaviorDesigner行為樹基礎總結


BehaviorDesigner——行為樹,用於控制和實現AI邏輯,類似於這樣:

 

上面這個行為樹實現了這樣的邏輯:

當Player有Input時按照Input值來移動,無Input時查找最近的可攻擊目標,如果能找到就執行攻擊;當既沒有Input也沒有找到攻擊目標時,那就一直處於Idle狀態。

 

下面總結BehaviorDesigner最常見的基礎知識:

首先要明確一個行為樹必須有一個依賦對象,它詮釋的是該對象的一系列行為模式。

這些行為模式由Task節點構成,圖中的每一個可執行的方框就是一個Task節點,將這些節點按照設計的邏輯進行連接,就組成了該對象的行為樹。

行為樹從根節點開始,從上至下,從左至右依次執行其下每一Task節點,任何被執行的Task將返回一種狀態,當根節點Task返回成功(或失敗)狀態時,意味着該行為樹單次執行結束。

 

Task有以下狀態:

正在執行,執行成功,執行失敗,未激活。

任何Task都處於這幾種狀態之一。

 

Task分為不同類型,每種類型的Task作用各不相同,首先必須弄清楚每類Task的大致作用:

Composites(復合類):主要用於控制行為樹的走向,也是用的最多最重要的一類,任何一個相對復雜的行為樹都包含這類Task節點,但它本身不做任何具體行為,所以它們一般位於父節點或根節點。

Decorators(裝飾類):多用於對其下的子Task節點執行額外操作,例如反轉結果,重復執行等。

Actions(行為類):數量最多,為具體執行行為的Task,一般位於行為樹的葉子節點右側,該類Task可能並非單幀就能完成。沒必要每個Action都搞清楚,因為可以很容易的自己擴展Action。后面會具體介紹如何擴展。

Conditionals(條件類):一般放在Action節點左側進行約束,只有當條件滿足(或不滿足)時才繼續往下執行,單幀內完成一次判斷。更多時候配合復合節點進行打斷或任務跳轉,這一點后面會詳細說明。

 

最基礎最常用的復合類Task是下面兩個:

Selector(選擇):相當於Or操作,下面的子Task節點只要有一個返回成功了它就返回成功,只有當所有的都返回失敗了才返回失敗。

Sequence(序列):相當於And操作,下面的子Task節點只要有一個返回失敗了它就返回失敗,只有當所有的都返回成功了才返回成功。

其余的只是在這兩個的基礎上變形,例如子節點同時執行或隨機順序執行等,具體可看官方文檔說明。

 

復合類Task的優先級和打斷:

這一點非常重要,是復合類Task的唯一特殊屬性:分為四種——不打斷,可打斷自身,可打斷低於該Task優先級的其他Task,既可以打斷自身也可以打斷低於其優先級的。

需要注意的是,該復合節點的打斷條件是其下子節點必須有條件節點,此時該條件節點的判斷一直處於運行狀態,一旦該條件節點在某一刻發生改變,此時行為樹將重新跳轉到該復合節點位置繼續運行,從而打斷其他正在運行的低優先級節點。

例如最上面的行為樹中,Player通過判斷是否接入Input移動指令可以打斷比它優先級低的攻擊節點和Idle節點的運行,而攻擊節點可以打斷Idle節點。

這意味着,當Player處於Idle狀態時一旦成功找到敵人,那么就變為攻擊狀態,攻擊或Idle狀態時一旦接受到Input指令又跳轉到移動;只有當既沒有Input指令輸入又沒有查找到敵人時,才Idle。

一旦某一時刻比它優先級高的節點滿足了前置條件,就會跳轉執行優先級高的。

所以在設計行為樹時,一般會把優先級高的Task節點置於行為樹的左側,將優先級低的置於右側,因為復合節點並不能打斷比該它優先級高的Task節點。

開啟了Abort Type后Task方框的左上角會出現向右或向下的箭頭作為標志提示。

 

自定義Task任務:

一般復合類和裝飾類的Task是夠用的,甚至有些根本用不到,而具體的行為類Task和條件類Task從來都不能滿足我們的需求,而且自己寫這類Task可以很大程度的簡化整個行為樹結構。

自己寫Task的步驟如下:

1.引入命名空間:

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

2.明確繼承的Task類型:

public class MyInputMove : Action
public class MyIsInput : Conditional

3.知曉Task內部函數的執行流程:(可以看官方文檔,寫的很清楚,這里把最重要的一張圖貼上來)

 

觀察上圖就會發現和Unity中編寫腳本大同小異,不一樣的地方就是這里的Update有返回值,要返回該任務的執行狀態,只有在Running狀態時才每幀調用。

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

還有一點不同,就是Task中封裝了一層Share的類型的屬性,它和非Share類型有何區別呢?

下面是對比:

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

public class MyLog : Action
{
    public string staticLog;
    public SharedString shareLog;

    public override TaskStatus OnUpdate()
    {
        Debug.Log(staticLog + shareLog.Value);
        return TaskStatus.Success;
    }
}

可以看到,這里的Share的類型就是一個方便在行為樹中傳遞和修改的變量,因為Task之間是不方便直接修改其他Task變量的,那怎么辦呢,於是就增加一種Share的類型變量在行為樹的各個Task之間進行交流。

比如這里,每次找到的最近的敵人是不一樣的,要根據上一個Task返回的值去執行下一個Task的攻擊或打印結果,這時固定的屬性就無法滿足要求,但直接調用別的Task又增加了耦合性,於是就單獨用Share變量來傳遞值。這樣也方便在面板中統一查看管理。

另外Share變量也可以增加自定義類型,全局的和本地變量的區別就是一個在所有的行為樹中有,一個只有這棵樹中有。

上面就是將查找到的最近的敵人和名字返回,其他Task例如攻擊和打印時直接就可以取到這里返回的值。在取Share變量值時需要.Value。

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

public class MyCanFindLatestTarget : Conditional
{
    public SharedGameObjectList origins;
    public bool bUseTag = false;
    public SharedString tag;
    public SharedGameObject result;
    public SharedString resultName;

    public override void OnStart()
    {
        if (bUseTag)
        {
            origins.Value.Clear();
            origins.Value.AddRange(GameObject.FindGameObjectsWithTag(tag.Value));
        }
    }
    public override TaskStatus OnUpdate()
    {
        if (origins.Value.Count > 0)
        {
            float minDistance=Mathf.Infinity;
            GameObject minDistanceObj=null;
            foreach(var item in origins.Value)
            {
                if (item == null)
                    continue;
                float sqrDistance = (item.transform.position - transform.position).sqrMagnitude;
                if (sqrDistance < minDistance)
                {
                    minDistance = sqrDistance;
                    minDistanceObj = item;
                }
            }
            result.Value = minDistanceObj;
            resultName.Value = minDistanceObj.name;
            return TaskStatus.Success;
        }
        return TaskStatus.Failure;
    }
}
using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;
using BehaviorDesigner.Runtime;

public class MyAttack : Action
{
    public SharedGameObject target;

    public SharedFloat damage=10f;
    public SharedFloat attackSpeed = 10f;

    private Enemy enemy;
    private float timer;

    public override void OnStart()
    {
        if (target.Value != null)
        {
            enemy = target.Value.GetComponent<Enemy>();
        }
        timer = 0f;
    }
    public override TaskStatus OnUpdate()
    {
        if (enemy == null)
            return TaskStatus.Failure;
        timer += Time.deltaTime;
        if (timer > (10f /attackSpeed.Value))
        {
            timer = 0f;
            enemy.Hit(damage.Value);
            Debug.Log(target.Value.name + "剩余HP:" + enemy.hp);
            if (enemy.hp <= 0)
            {
                enemy.Dead();
                return TaskStatus.Success;
            }
        }
        return TaskStatus.Running;
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM