行为树简易敌人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
javascript
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
javascript
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
javascript
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
javascript
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 :
javascript
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;
}
}
javascript
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;
}
}
javascript
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;
}
}
javascript
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游戏开发!不然到时候没人要嘤嘤嘤!还有就是腾讯的游戏开发大赛我也想参与大家一起加油!!