行為樹簡易敵人AI
前言:
有些天沒更新新文章了,主要是最近科一有些頭疼,而且最近琢磨這個行為樹代碼有些難受,但是終于熬出頭了,MonoGame的系列會繼續更新的,今天不說別的就說困擾我兩三天的行為樹
有限狀態機 -》分層狀態機 -》 行為樹:
首先我們得先理解一個概念:有限狀態機,在游戲開發中因為團隊協作開發,和編程效率的緣故,人們在游戲編程模式中發明了一種編程模式:有限狀態機,他的邏輯簡單而且編寫起來也很優雅,我非常喜歡這種游戲編程模式,在我早期接觸游戲開發的時候百分之九十九的項目都是使用有限狀態機來編寫玩家操控和怪物AI,但是很快就會遇到難題,普通小怪倒還好,簡單的行為模式時的有限狀態機來編寫邏輯清晰沒什么缺點,但是一旦到了Boss級別敵人邏輯復雜,技能繁多,如果用有限狀態機來處理的話機會面臨以下問題:狀態切換太過復雜,狀態切換條件繁多,從邏輯上來說這個有限狀態機變不再適合開發這種技能繁多的BossAI了,聰明的你想到:欸!要是把意思相同的邏輯封裝成一個集合,比如:移動,站立歸為Move層,魔法攻擊,普通攻擊,等等攻擊歸為戰斗層,跳躍,下落歸為跳躍層等等,這樣我們的邏輯就清晰了很多了,把所有狀態設為二級節點,但是這樣依舊不是最佳選擇,那么我們本篇文章的:行為樹
行為樹:
1.行為樹是為了簡化游戲的邏輯,在很多游戲開發過程中人們都會選擇行為樹來開發這款游戲的AI,甚至可以說一款游戲的大多數時間和代碼都是來源游戲AI,行為樹都是由一個個節點組成的主要包括:
- 葉子節點(Node)
- 順序節點(SequenceNode)
- 選擇節點 (SelectorNode)
- 裝飾節點 (DecoratorNode)
以上這些節點都是在游戲開發中常用的節點,當然不妨還有一些擴展節點但最為常用的還是這些節點的狀態,每個節點都由三種狀態:
- Success
- Failure
- Running
葉子節點(Node):
所謂葉子節點也是樹的最底部,也就是他沒有子節點,葉子節點沒有子節點,這也就意味著葉子節點必須得執行游戲中Boss/Monster的具體邏輯,因為他沒有子節點無法再繼續往下遍歷了,所以我們必須得使用葉子節點來實現Boss的具體功能;
順序節點(SequenceNode):
順序節點是一個父級節點,他會一次從左向右遍歷所有的子樹,一旦遍歷到返回失敗節點返回失敗,就意味著這個節點失敗了。
選擇節點 (SelectorNode):
這種節點和順序節點一樣是一種父級節點,但是不同的是這個節點會選擇,從左往右數的子樹中第一個返回成功或者運行的節點。
裝飾節點 (DecoratorNode):
這種節點通常都是再Sequence節點下的前置節點,通常用來判斷條件,一條條件不滿足直接返回失敗,那么相應的Sequence節點也會返回失敗;
代碼部分:
首先我們得先完成一個示例的簡單AI邏輯來實踐一下,這個AI邏輯代碼很簡單就是一個Boss在Idle, Walk, Attack三個形態之間的切換,會了這個就相當與只要你畫出行為樹的邏輯圖,那么搞定這個也就簡單起來了:
演示:
首先我們得先寫一下基礎的節點代碼:
BTNode
using System.Collections.Generic;namespace ETFramework
{public class BTNode{protected NodeState state;public BTNode parent;public List<BTNode> children = new List<BTNode>();public BTNode(){parent = null;}public BTNode(List<BTNode> children){foreach (BTNode child in children)AddNode(child);}private void AddNode(BTNode node){node.parent = this;children.Add(node);}public virtual NodeState Evaluate() => NodeState.Failure;}
}
SelectorNode
using System.Collections;
using System.Collections.Generic;namespace ETFramework
{public class SelectorNode : BTNode{public SelectorNode() : base() {}public SelectorNode(List<BTNode> children) : base(children) {}public override NodeState Evaluate(){foreach (BTNode node in children){switch (node.Evaluate()){case NodeState.Failure:continue;case NodeState.Success:state = NodeState.Success;return state;case NodeState.Running:state = NodeState.Running;return state;default:continue;}}state = NodeState.Failure;return state;}}
}
SequenceNode
using System.Collections.Generic;namespace ETFramework
{public class SequenceNode : BTNode{public SequenceNode() : base() {}public SequenceNode(List<BTNode> children) : base(children) {}public override NodeState Evaluate(){bool AnyChildIsRunning = false;foreach (BTNode node in children){switch (node.Evaluate()){case NodeState.Failure:state = NodeState.Failure;return state;case NodeState.Success:continue;case NodeState.Running:AnyChildIsRunning = true;continue;default:state = NodeState.Success;return state;}}state = AnyChildIsRunning ? NodeState.Running : NodeState.Success;return state;}}
}
BTree
using UnityEngine;namespace ETFramework
{[RequireComponent(typeof(Rigidbody2D))][RequireComponent(typeof(Animator))][RequireComponent(typeof(SpriteRenderer))]public abstract class BTree : MonoBehaviour{/// <summary>/// 這個實例的名字/// </summary>public string InstanceName;/// <summary>/// 實例類型/// </summary>public InstanceType TypeIns;/// <summary>/// 渲染組件/// </summary>public SpriteRenderer Render;/// <summary>/// 動畫組件/// </summary>public Animator animator;/// <summary>/// 剛體組件/// </summary>public Rigidbody2D Rb;private BTNode Root = null;protected virtual void Awake(){/*初始化添加組件*/animator = GetComponent<Animator>();Render = GetComponent<SpriteRenderer>();Rb = GetComponent<Rigidbody2D>();}protected void Start(){Root = SetupTree();}private void Update(){if (Root != null)Root.Evaluate();}protected abstract BTNode SetupTree();}
}
接下來寫具體的游戲AI邏輯代碼,就是Boss具體的行動和行為:
包括Idle,Walk, Attack,那么把這棵行為樹畫出來就搞定了
Code :
using ETFramework;
using UnityEngine;public class DarkBossIdle : BTNode
{private Rigidbody2D Rb;private Animator animator;private InstanceCheck instanceCheck;public DarkBossIdle(Rigidbody2D Rb, Animator animator, InstanceCheck instanceCheck){this.Rb = Rb;this.animator = animator;this.instanceCheck = instanceCheck;}public override NodeState Evaluate(){if (!instanceCheck.isEnter){state = NodeState.Running;Rb.velocity = Vector2.zero;animator.SetBool("DarkWalk", false);}else{state = NodeState.Failure;}return state;}
}
using ETFramework;
using UnityEngine;public class DarkBossWalk : BTNode
{private SpriteRenderer Sr;private Animator animator;private Rigidbody2D Rb;private InstanceCheck instanceCheck;private Transform transform;private float MoveSpeed;public DarkBossWalk(SpriteRenderer Sr, Rigidbody2D Rb, Animator animator, InstanceCheck instanceCheck, Transform transform, float MoveSpeed){this.Sr = Sr;this.Rb = Rb;this.animator = animator;this.instanceCheck = instanceCheck;this.transform = transform;this.MoveSpeed = MoveSpeed;}public override NodeState Evaluate(){Collider2D collider = instanceCheck.Collider;if (collider == null){state = NodeState.Failure;animator.SetBool("DarkWalk", false);return state;}animator.SetBool("DarkWalk", true);if (collider.transform.position.x > transform.position.x){Rb.velocity = new Vector2(MoveSpeed, 0);Sr.flipX = true;transform.GetComponent<AttackCheck>().Offset.x = 5;transform.GetComponent<SearchInstanceCheck>().Offset.x = 5;}if (collider.transform.position.x < transform.position.x){Rb.velocity = new Vector2(-MoveSpeed, 0);Sr.flipX = false;transform.GetComponent<AttackCheck>().Offset.x = -5;transform.GetComponent<SearchInstanceCheck>().Offset.x = -5;}state = NodeState.Running;return state;}
}
using ETFramework;
using UnityEngine;public class DarkBossAttack : BTNode
{private AttackCheck attackCheck;private SearchInstanceCheck searchInstanceCheck;private Transform transform;private Animator animator;private Rigidbody2D Rb;private int temp = 0;public DarkBossAttack(Animator animator, AttackCheck attackCheck, Transform transform, Rigidbody2D Rb, SearchInstanceCheck searchInstanceCheck){this.animator = animator;this.attackCheck = attackCheck;this.transform = transform;this.Rb = Rb;this.searchInstanceCheck = searchInstanceCheck;}public override NodeState Evaluate(){AnimatorStateInfo info = animator.GetCurrentAnimatorStateInfo(0);if (attackCheck.isEnter && temp == 0){temp++;}if (info.normalizedTime <= 0.9f && info.IsName("DarkBossAttack")){state = NodeState.Running;return state;}if (searchInstanceCheck.isEnter){state = NodeState.Running;animator.SetBool("DarkAttack", true);return state;}state = NodeState.Failure;animator.SetBool("DarkAttack", false);return state;}
}
using System.Collections.Generic;
using ETFramework;
using UnityEngine;public class DarkBoss : BTree
{[SerializeField][Tooltip("攻擊檢測")] private AttackCheck attackCheck;[SerializeField][Tooltip("實例搜索")] private InstanceCheck instanceCheck;[SerializeField][Tooltip("檢測實例")] private SearchInstanceCheck searchInstanceCheck;[SerializeField][Tooltip("面向方向")] private float MoveSpeed;protected override BTNode SetupTree(){BTNode root = new SelectorNode(new List<BTNode>{new SelectorNode(new List<BTNode>{new DarkBossAttack(animator, attackCheck, transform, Rb, searchInstanceCheck),new DarkBossWalk(Render, Rb, animator, instanceCheck, transform, MoveSpeed)}),new DarkBossIdle(Rb, animator, instanceCheck)});return root;}public void AttackEnter(){attackCheck.IsStart = true;}public void AttackExit(){attackCheck.IsStart = false;}
}
結語:
這個是我困擾兩天的代碼問題,我最近在開發一個能快速成型游戲的Unity框架,這個框架我打算免費發行,我計劃的是集有限狀態機,UI模式,單例模式,行為樹,代碼模板,場景切換組件合為一體的只要給出美術資源能快速幫助我構建出一個游戲的模板框架,為什么突發奇想想開發一個這個,因為我要備戰明年的Game Jam我得趕緊疊疊我的技術棧,和發展一下我的弱項:3D游戲開發!不然到時候沒人要嚶嚶嚶!還有就是騰訊的游戲開發大賽我也想參與大家一起加油!!