开发角色AI时,通常会使用到两种方式,分别是状态机和行为树。状态机和行为树都有各自的使用场景。
状态机
当需要的角色AI执行的是简单、线性的行为逻辑,则优先使用状态机,执行效率高,资源消耗低。但当系统变得复杂时,状态机的状态数量和转换关系会变得难以维护,容易出现状态爆炸问题。扩展性较差,新增状态或修改逻辑时可能会导致整个状态图的重构。
行为树
行为树更适合复杂、层次化的行为逻辑,具有更高的灵活性和扩展性。行为树可以通过组合节点动态扩展行为逻辑,及动态调整行为的执行顺序和优先级,适合处理动态环境下的复杂行为。缺点就是执行效率可能较低,资源消耗较高。
实现简单的行为树
Node类,树中所有结点对象的父类
using System.Collections.Generic;
//结点状态
public enum NodeState
{
SUCCESS,
FAILURE,
RUNNING
}
public class Node
{
//父结点
public Node parent;
//子结点集合
public List<Node> childs = new List<Node>();
//当前结点状态
public NodeState nodeState;
public Node()
{
parent = null;
}
//初始化当前结点所有子结点,将子结点的父结点引用指向当前结点
public Node(List<Node> childrens)
{
foreach (var child in childrens)
{
Attach(child);
}
}
public void Attach(Node node)
{
node.parent = this;
childs.Add(node);
}
//结点执行的虚方法
public virtual NodeState Evaluate() => NodeState.FAILURE;
}
Tree类,当前脚本挂载在角色上
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor.SceneManagement;
using UnityEngine;
public abstract class Tree : MonoBehaviour
{
//根结点
private Node root;
void Awake()
{
root = SetupTree();
}
//执行根结点的结点评估方法
void Update()
{
if (root != null)
{
root.Evaluate();
}
}
//构建树的抽象方法
public abstract Node SetupTree();
}
SequenceNode顺序节点,用于控制流程的结点:
1.当子结点的结点状态为Success或Running时,将当前结点状态设为Success或Running,继续执行下一个子结点
2.当子结点的结点状态为Failure时,直接终止后续子结点运行,直接返回当前结点状态为Failure
3.当所有子结点都正常运行,最后返回当前结点状态为Success或Running
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SequenceNode : Node
{
public SequenceNode()
: base() { }
public SequenceNode(List<Node> childs)
: base(childs) { }
public override NodeState Evaluate()
{
bool hasRuningChildNode = false;
//遍历所有子结点,并且调用其Evaluate(评估)方法
foreach (var child in childs)
{
switch (child.Evaluate())
{
case NodeState.SUCCESS:
nodeState = NodeState.SUCCESS;
continue;
case NodeState.RUNNING:
hasRuningChildNode = true;
nodeState = NodeState.RUNNING;
continue;
case NodeState.FAILURE:
nodeState = NodeState.FAILURE;
return nodeState;
}
}
return nodeState = hasRuningChildNode ? NodeState.RUNNING : NodeState.SUCCESS;
}
}
SelectorNode选择节点,用于控制流程的结点:
1.当子结点的结点状态为Failure时,将当前结点状态设为Failure,继续执行下一个子结点
2.当子结点的结点状态为Success或Running时,直接终止后续子结点运行,直接返回当前结点状态为Success或Running
3.当所有子结点都运行失败,最后返回当前结点状态为Failure
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SelectorNode : Node
{
public SelectorNode()
: base() { }
public SelectorNode(List<Node> childs)
: base(childs) { }
public override NodeState Evaluate()
{
foreach (var child in childs)
{
switch (child.Evaluate())
{
case NodeState.SUCCESS:
nodeState = NodeState.SUCCESS;
return nodeState;
case NodeState.RUNNING:
nodeState = NodeState.RUNNING;
return nodeState;
case NodeState.FAILURE:
nodeState = NodeState.FAILURE;
continue;
default:
continue;
}
}
nodeState = NodeState.FAILURE;
return nodeState;
}
}
此时已写好当前行为树的所有基础类,接下来编写角色AI特定的行为结点。
下面的行为结点为:
1.角色在一定范围内执行巡逻行为的巡逻结点。
2.判断视角范围是否存在Enemy的结点。
3.存在则追击Enemy的追击结点。
4.当enemy脱离视角则角色回归巡逻结点。
Patrol结点
执行巡逻任务
这里会使用Unity自带的AI寻路系统
创建当前类之前,需要给地面添加NavMeshSurface组件,并烘焙,生成寻路所需要的Mesh。
给角色对象挂载NavMeshAgent组件,用于控制角色在烘焙的Mesh上进行移动
using UnityEngine;
using UnityEngine.AI;
public class PatrolNode : Node
{
public Transform transform;
//角色巡逻后的当前休息时间
private float waitTime;
//默认巡逻后的休息时间
private float originalWaitTime = 1f;
//Unity的AI寻路功能对象
private NavMeshAgent navMeshAgent;
//巡逻范围
private float patrolRange = 1f;
//当前巡逻点坐标
private Vector3 currentPatrolPoint;
//角色生成的初始坐标
private Vector3 orginalPoint;
public PatrolNode(Transform transform)
{
//创建结点时通过构造方法传递角色Transform属性
this.transform = transform;
生成初始坐标,用于固定巡逻点的范围
orginalPoint = new Vector3(
transform.position.x,
transform.position.y,
transform.position.z
);
navMeshAgent = transform.GetComponent<NavMeshAgent>();
//为角色生成第一个巡逻点
currentPatrolPoint = GetNextPatrolPoint();
//初始化休息时间
waitTime = originalWaitTime;
}
public override NodeState Evaluate()
{
//角色在原地休息
if (waitTime > 0)
{
waitTime -= Time.deltaTime;
transform.position = transform.position;
}
else
{
//休息结束
//判断角色和巡逻点之间的距离
if (Vector3.Distance(transform.position, currentPatrolPoint) > 0.5f)
{
//朝向巡逻点
transform.rotation = Quaternion.LookRotation(currentPatrolPoint);
//移动至巡逻点,若巡逻点不在导航Mesh上则不生效
navMeshAgent.SetDestination(currentPatrolPoint);
}
else
{
//到达巡逻点,重置等待时间
waitTime = originalWaitTime;
//获取下一个巡逻点
currentPatrolPoint = GetNextPatrolPoint();
}
}
//返回状态为结点状态为Running
nodeState = NodeState.RUNNING;
return nodeState;
}
//获取随机巡逻点
private Vector3 GetNextPatrolPoint()
{
var rangeX = Random.Range(-patrolRange, patrolRange);
var rangeZ = Random.Range(-patrolRange, patrolRange);
NavMeshHit hit;
//获取随机巡逻点在导航Mesh上最近的点,如果存在则返回最近点坐标,不存在则返回当前角色坐标
var nextPatrolPoint = NavMesh.SamplePosition(
new Vector3(orginalPoint.x + rangeX, orginalPoint.y, orginalPoint.z + rangeZ),
out hit,
patrolRange,
1
)
? hit.position
: transform.position;
return nextPatrolPoint;
}
}
CheckEnemyInFovNode结点
判断敌人是否在范围内
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CheckEnemyInFovNode : Node
{
private Transform transform;
//视野范围
private float fovRange = 2f;
public CheckEnemyInFovNode(Transform transform)
{
this.transform = transform;
}
public override NodeState Evaluate()
{
获取球形检测范围内的所有collider
var colliders = Physics.OverlapSphere(transform.position, fovRange);
foreach (var collider in colliders)
{
//存在敌人对象则保存敌人对象,并且返回当前结点状态为Success
if (collider.CompareTag("Enemy"))
{
var heroBT = transform.GetComponent<HeroBT>();
heroBT.target = collider.gameObject;
nodeState = NodeState.SUCCESS;
return nodeState;
}
}
//未找到则返回当前结点状态为Failure
nodeState = NodeState.FAILURE;
return nodeState;
}
}
GoToTarget结点
追击Enemy
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI;
public class GoToTargetNode : Node
{
private Transform transform;
private HeroBT heroBT;
private NavMeshAgent navMeshAgent;
private float range = 0.5f;
public GoToTargetNode(Transform transform)
{
this.transform = transform;
heroBT = transform.GetComponent<HeroBT>();
navMeshAgent = transform.GetComponent<NavMeshAgent>();
}
public override NodeState Evaluate()
{
//创建Enemy的在角色同等高度的坐标
var target = new Vector3(
heroBT.target.transform.position.x,
transform.position.y,
heroBT.target.transform.position.z
);
if (Vector3.Distance(transform.position, target) > 0.01f)
{
//看向Enemy
transform.rotation = Quaternion.LookRotation(target);
//因为当前enemy坐标不在导航Mesh上,所以需要返回当前坐标点最近的导航点
NavMeshHit hit;
NavMesh.SamplePosition(target, out hit, range, 1);
navMeshAgent.destination = hit.position;
}
else
{
//到达Enemy后停止移动,返回当前结点状态为Success
transform.position = transform.position;
nodeState = NodeState.SUCCESS;
return nodeState;
}
//追击中则返回当前结点状态为Running
nodeState = NodeState.RUNNING;
return nodeState;
}
}
HeroBT结点
控制角色的行为树,脚本挂载在角色上
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HeroBT : Tree
{
public GameObject target;
public override Node SetupTree()
{
//为保证每一次只有一个节点会执行,最外层使用Selector选择结点
var root = new SelectorNode(
new List<Node>
{
//CheckEnemyInFovNode和GoToTargetNode是共同协作的结点,
且CheckEnemyInFovNode执行不成功,GoToTargetNode结点不会执行,CheckEnemyInFovNode执行成功,
GoToTargetNode结点继续执行,所以需要放在同一个控制结点中,且满足条件的为SequenceNode顺序结点
new SequenceNode(
new List<Node>
{
new CheckEnemyInFovNode(transform),
new GoToTargetNode(transform)
}
),
//PatrolNode结点只有在找不到Enemy的时候执行,优先级低于前两个结点,所以顺序放在前两个结点之后
new PatrolNode(transform)
}
);
return root;
}
}
最终的执行效果