1. 程式人生 > >行為樹 和 Behavior Designer

行為樹 和 Behavior Designer

對於遊戲AI,行為樹是個簡單可擴充套件的解決方案。

本文簡單介紹 行為樹 以及 Unity 的 Behavior Designer 外掛。

行為樹

行為樹(Behavior Tree,簡稱 BT),是由行為節點組成的樹狀結構。

行為樹中的節點,會在某一幀中被呼叫,然後立即且必須得到以下之一的結果:成功 Success、失敗 Failure、執行中 Running。然後根據返回值進行下一步操作。

行為樹會自頂向下的,根據節點定義的規則和節點返回值來搜尋這顆樹,最終確定需要執行的行為節點。

節點不需要維護向其他節點的轉換, 因此擁有更好的封裝性和模組性,讓遊戲邏輯更加直觀。

通過 Behavior Designer 外掛 瞭解行為樹

官方文件的介紹:Behavior Designer is a behavior tree implementation designed for everyone - programmers, artists, designers. Behavior Designer offers an intuitive visual editor with a powerful API allowing you to easily create new tasks. It also includes hundreds of tasks, PlayMaker integration, and extensive third party integration making it possible to create complex AIs without having to write a single line of code!(大意就是說 Behavior Designer 這個行為樹外掛,提供了方便使用的視覺化編輯器,他提供的強大api, 配合 PlayMaker 等外掛,可以在不寫程式碼的情況下構建複雜的 AI )

Behavior Designer 中存在四種節點:
- Actions 動作節點 : 官方文件
- Composites 組合節點 : 官方文件
- Conditionals 條件節點 : 官方文件
- Decorators 修飾節點 : 官方文件

Behavior Designer 中基本操作是用 Decorators 和 Composites 搭邏輯框架,然後自定義 Conditionals 來實際判斷和執行 Actions。

下面看一個具體的例子:

  1. 匯入 Unity:可以在官網或者在 Unity Asset Store 中下載 package 檔案。

  2. 檢視 Editor 面板:匯入 Behavior Designer 後,可以在 選單欄/Tools/Behavior Designer/Editor 開啟編輯器。

  3. 視窗 Tab 介紹

    • Behavior: 可以設定行為樹的名子和屬性
    • Tasks: 所有 Conditional(條件)和 Action(動作)的列表(包括內建條件、動作和自定義條件和動作)
    • Variables: 公共變數視窗,具體見後面介紹。
    • Inspector: 行為樹節點的Inspector,是針對某個節點的詳細屬性。設定引數和繫結變數用。
  4. 新增一個 Behavior Tree:在 Hierarchy 視窗選中一個 GameObject,在剛剛開啟的 Behavior Designer 視窗右鍵, 點選 Add Behavior Tree, 如果看到 GameObject 被添加了一個 Behavior Tree 指令碼,說明新增成功。

  5. 設計一個士兵的AI邏輯:

    • 士兵能夠發現距離5以內的敵人;
    • 士兵總是發現距離最近的敵人;
    • 當敵人在距離1內時,使用噴子攻擊;
    • 當敵人在距離2內時,使用步槍攻擊;
    • 當敵人在距離3內時,使用狙擊槍攻擊;
    • 當敵人在距離超過3時,使用望遠鏡觀察;
  6. 建立如下指令碼 (程式碼在文末)

    • FindEnemy, 繼承自 Conditional,用於發現敵人
    • WithInDistance 繼承自 Conditional,用於判斷是否在生效距離內
    • Shotgun、Sniper、Rifle 和 Scope 繼承自 Action
    • Enemy 繼承自 MonoBehavior,用於扮演敵人
  7. 給剛剛新增的 Behavior Tree 新增 建立行為樹如圖所示

  8. 解釋上一步中新增節點的作用:

    • Conditional 是條件節點,繼承 Conditional 的節點一般用於邏輯判定,返回值用於邏輯判定結果, 此處 FindEnemy 指令碼用於判定是否發現敵人
    • Sequence: 序列的 AND。它從左到右,每幀只執行一個子節點。
      • 子節點返回 Running,Sequence 返回 Running,下一幀繼續執行當前子節點
      • 子節點返回 Failure,Sequence 返回 Failure
      • 子節點返回 Success,Sequence 返回 Running,下一幀會執行下一個子節點,若無下一個子節點,返回 Success
    • Parallel Selector: 並行的 OR。它是並行的,每楨執行所有子節點一次
      • 所有子節點返回 Running,Parallel Selector 返回 Running。
      • 任何一個節點返回失敗,Parallel Selector 本身返回 Running,如果所有子節點都失敗了,返回 Failure
      • 任何一個節點返回成功,Parallel Selector 返回 Success。
    • Action 是動作節點,繼承 Action 的節點用於執行動作,返回值用於動作執行成功或者失敗, 此處 Shotgun 指令碼就是執行噴子開槍的動作
  9. 為行為樹內設定引數

    • 設定 全域性變數 Variables
    • 設定 FindEnemy 的 Inspector
    • 設定 WithInDistance 的 Inspector
  10. 建立Enemy的例項: 建立一個GameObject, 新增Enemy指令碼, 通過鍵盤控制 Enemy的位置。

  11. SharedXXXX變數:

    • 是什麼:SharedFloat, SharedBool, SharedTransform 等變數是專門用在 Behavior Designer 內部的變數。所有節點都可以訪問和修改。
    • 如何建立:開啟Behavior Designer 視窗, 切換到 Variables 變數視窗, 新增名字和型別就可以 Add 一個變數。
    • 如何使用:在指令碼中定義上面建立的變數,點選節點,點選 Inspector, 擊白色圓點,切換一下繫結方式。
    • 適用範圍:只用於 AI 的變數,放在行為樹的 Variable s裡面(其他屬性放角色腳本里即可,保持解耦)
  12. 行為樹如何複用

    • Editor視窗右鍵可以儲存.asset到本地
    • 通過程式碼繫結行為樹
var bt = gameObject.AddComponent<BehaviorTree> ();  
var extBt = Resources.Load<ExternalBehaviorTree> ("Behavior");  
bt.StartWhenEnabled = false;  
bt.ExternalBehavior = extBt;  

行為樹和狀態機簡單比較

- Behaviour Tree (行為樹) Finite State Machines (有限狀態機)
本質 以行為邏輯為框架,以具體行動為節點的一種樹狀圖 以多個狀態為核心,以狀態轉移為線索的一種圖表
節點 每個節點表示一個行為 每個節點表示一個狀態,同時維護執行在該狀態的行為以及向其他節點的轉換
狀態切換 提供大量的流程控制方法,狀態之間的切換很直觀 狀態較多的情況下狀態切換十分繁瑣
並行處理 支援。Composites 下的 Parallel Selector 就是並行的 OR,每楨執行所有子節點一次 無法同時處理兩個狀態
擴充與複用 增加控制節點的型別,可以達到複用的目的。更好的封裝性和模組性,讓遊戲邏輯更直觀 新增狀態時,要給這個新狀態新增連線其他狀態的跳轉邏輯,狀態的邏輯會隨著狀態的增加變複雜。單個狀態很難複用。
編寫難度 方便製作編輯器,方便程式碼編寫,方便除錯,方便檢視,方便編輯 狀態少時很方便,狀態多時維護費力。

指令碼

public class FindEnemy : Conditional
{
    public float rangeOfVisibility;
    public string targetTag;

    public SharedTransform target;
    public SharedVector3 targetPos;
    public SharedFloat targetDistance;

    private Transform[] possibleTargets;

    public override void OnAwake()
    {
        var targets = GameObject.FindGameObjectsWithTag(targetTag);
        possibleTargets = new Transform[targets.Length];
        for (int i = 0; i < targets.Length; ++i)
        {
            possibleTargets[i] = targets[i].transform;
        }
    }

    public override TaskStatus OnUpdate()
    {
        var currentTarget = target.Value;
        var currentTargetDistance = targetDistance.Value;
        for (int i = 0; i < possibleTargets.Length; ++i)
        {
            float distance = GetDistance(possibleTargets[i]);
            if (distance <= rangeOfVisibility)
            {
                if (target.Value == null || distance < currentTargetDistance)
                {
                    currentTarget = possibleTargets[i];
                    currentTargetDistance = distance;
                }
            }
        }

        target.Value = currentTarget;
        targetDistance.Value = currentTargetDistance;
        if (target.Value != null)
        {
            targetPos.Value = target.Value.position;
            return TaskStatus.Success;
        }

        return TaskStatus.Running;
    }

    public float GetDistance(Transform targetTransform)
    {
        Vector3 direction = targetTransform.position - transform.position;
        return direction.magnitude;
    }
}
public class WithInDistance : Conditional
{
    public float rangeOfVisibility;
    public SharedFloat targetDistance;

    public override TaskStatus OnUpdate()
    {
        if (targetDistance.Value <= rangeOfVisibility)
            return TaskStatus.Success;

        return TaskStatus.Running;
    }
}
/* 噴子 */
public class Shotgun : Action
{
    public override TaskStatus OnUpdate ()
    {
        Debug.Log ("Shotgun!!!");
        return TaskStatus.Success;
    }
}
/* 狙擊槍 */
public class Sniper : Action
{
    public override TaskStatus OnUpdate ()
    {
        Debug.Log ("Sniper!!!");
        return TaskStatus.Success;
    }
}
/* 步槍 */
public class Rifle : Action
{
    public override TaskStatus OnUpdate ()
    {
        Debug.Log ("Rifle!!!");
        return TaskStatus.Success;
    }
}
/* 瞄準鏡 */
public class Scope : Action
{
    public override TaskStatus OnUpdate ()
    {
        Debug.Log ("Scope!!!");
        return TaskStatus.Success;
    }
}
/* 敵人 */
public class Enemy : MonoBehaviour {

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            transform.localPosition -= 10 * Time.deltaTime * Vector3.right;
        }
        if (Input.GetKeyDown(KeyCode.S))
        {
            transform.localPosition = new Vector3(0, 0, 0);
        }
        if (Input.GetKeyDown(KeyCode.D))
        {
            transform.localPosition += 10 * Time.deltaTime * Vector3.right;
        }
    }

    private void OnGUI()
    {
        GUI.Label(new Rect (100, 100, 200, 50), transform.localPosition.ToString());
    }
}

如有錯誤,歡迎指出。