实现一个简易的敌人状态机。(以下代码以最简易的敌人AI为例:巡逻状态--->(发现敌人)-->追逐状态--->(进入攻击范围内)---> 攻击状态)
一:状态机基类:
1:StateActionSO (抽象的状态基类)参数是为了实现状态的复用。
public abstract class StateActionSO : ScriptableObject
{
public virtual void OnEnter(StateMachineSystem stateMachineSystem) { }
public abstract void OnUpdate(StateMachineSystem stateMachineSystem);
public virtual void OnExit(StateMachineSystem stateMachineSystem) { }
}
2:ConditionSO (抽象的转换类)
public abstract class ConditionSO : ScriptableObject
{
public abstract bool ConditionSetUp(StateMachineSystem stateMachineSystem);//条件是否成立
}
3:TransitionSO (转换控制器) 实现转换SO文件的复用(新增 isInit bool类型变量)
内部类:StateTransitionConfig 描述了每一条转换的信息(包括优先级)。
private class StateTransitionConfig
{
public StateActionSO fromState;
public StateActionSO toState;
public ConditionSO condition;
public int priority;
}
存储了两个容器:一个字典(游戏中实际使用进行转换),一个列表(编辑器列表)
//存储所有状态转换信息和条件
private Dictionary<StateActionSO, List<StateTransitionConfig>> states = new Dictionary<StateActionSO, List<StateTransitionConfig>>();
//获取状态配置,即外部面板的手动配置信息
[SerializeField] private List<StateTransitionConfig> configStateData = new List<StateTransitionConfig>();
private StateMachineSystem stateMachineSystem; //当前管理的状态机系统
初始化函数Init:从编辑器列表中获取到所有信息,存入到字典中。
//可以在OnValidate函数中实现自动配置(不再需要isInit变量)
public void Init(StateMachineSystem stateMachineSystem)
{
isInit=true;
SaveAllStateTransitionInfo();
}
/// <summary>
/// 保存所有状态配置信息
/// </summary>
private void SaveAllStateTransitionInfo()
{
foreach (var item in configStateData)
{
//这个时候外面面板已经配置好信息了。我们需要将它们的转换关系保存起来
if (!states.ContainsKey(item.fromState))
{
//检测现在存储字典是否有存在的Key,如果没有我们需要创建一个,并且初始化它的条件存储容器
states.Add(item.fromState, new List<StateTransitionConfig>());
states[item.fromState].Add(item);
}
else
{
states[item.fromState].Add(item);
}
}
}
转换的控制函数TryGetApplyCondition:
/// <summary>
/// 尝试去获取条件成立的新状态
/// </summary>
public void TryGetApplyCondition(StateMachineSystem stateMachineSystem)
{
int transitionPriority = -1;
StateActionSO toState = null;
//遍历当前状态能转的状态是否有条件成立
if (states.ContainsKey(stateMachineSystem.currentState))
{
foreach (var stateItem in states[stateMachineSystem.currentState])
{
if (stateItem.condition.ConditionSetUp(stateMachineSystem))
{
if (stateItem.priority > transitionPriority)
{
transitionPriority = stateItem.priority;
toState = stateItem.toState;
}
}
}
}
//字典中没有当前状态的描述 直接返回
else return;
//可以进行转换状态
if (toState != null)
{
stateMachineSystem.currentState.OnExit(stateMachineSystem);
Debug.Log(toState);
stateMachineSystem.currentState = toState;
stateMachineSystem.currentState.OnEnter(stateMachineSystem);
}
}
二:敌人的状态机控制类:(挂载在每个敌人上)
要点:1:记录每个敌人及其当前的状态
2:每个敌人根据模板生成自己的转换器(保证不冲突)// 代码已更改:实现了转换器复用
3:在进行tick之前判断是否需要转换状态。
public class StateMachineSystem : MonoBehaviour
{
public TransitionSO transition;
public StateActionSO currentState;
//敌人相关组件
public EnemyState currentStateType;
public EnemyController currentEnemy;
private void Awake()
{
currentEnemy = GetComponent<EnemyController>();
if(!transition.isInit)transition.Init();
}
private void Start()
{
if(currentState!=null)currentState.OnEnter(this);
}
private void Update()
{
StateMachineTick();
}
private void StateMachineTick()
{
if(transition!=null)transition.TryGetApplyCondition(this);//每一帧都去找是否有成立的条件
//TODO:当前默认为敌人的状态机,后续采用继承,继承出不同种类的状态机(敌人,NPC,友军)等等
if (CanEnemyAction())
if(currentState!=null)currentState.OnUpdate(this);
}
private bool CanEnemyAction()
{
if (currentEnemy == null || currentEnemy.IsDead || currentEnemy.IsHurt || currentEnemy.enemyCharacterStats.IsWeakState||currentEnemy.IsExecuted) return false;
return true;
}
}
三:简易AI案例
1:具体的状态(只包含状态的行为,不用考虑状态的转换)
要点:在传入的StateMachineSystem 中内含了调用敌人的EnemyController,实现不冲突的复用。由于状态类是被所有同种类的敌人复用的,故对于一些在状态中改变的值要上升到EnemyContrller类中,而不能再状态中定义在过程中变化的变量。
(1)巡逻状态 PatrolState
变量waitTime表示等待时间,对于同一种类的敌人来说是固定不变的,故在状态中定义。
逻辑是每次在出生点周围随机生成一个目标点,达到目标点后进行等待,等待时间完成后随机一个新的目标点进行移动。
[CreateAssetMenu(fileName = "New Patrol State", menuName = "State Machine/State/EnemyPatrolState")]
public class EnemyPartolState : StateActionSO
{
[SerializeField]protected float waitTime; //巡逻到点位后的等待时间
public override void OnEnter(StateMachineSystem stateMachineSystem)
{
EnemyController currentEnemy = stateMachineSystem.currentEnemy;
stateMachineSystem.currentStateType = EnemyState.PartolState;
currentEnemy.partolTargetPos=currentEnemy.GetRandomPartolPoint();
currentEnemy.StartCoroutine(currentEnemy.WaitPatrolTime(waitTime/2, currentEnemy.partolTargetPos));
}
public override void OnUpdate(StateMachineSystem stateMachineSystem)
{
//如果到达巡逻点 则进入等待,后选择下一个巡逻点
EnemyController currentEnemy = stateMachineSystem.currentEnemy;
if (currentEnemy == null || currentEnemy.IsDead||currentEnemy.IsHurt) return;
if (!currentEnemy.IsWait&&FinishTargetPos(currentEnemy))
{
TurnToNewPatrolPoint(currentEnemy);
}
}
//到达了指定的巡逻点
private bool FinishTargetPos(EnemyController currentEnemy)
{
return Vector3.Distance(currentEnemy.transform.position, currentEnemy.partolTargetPos) < 1f;
}
//Wait,之后转向新的巡逻点
private void TurnToNewPatrolPoint(EnemyController currentEnemy)
{
currentEnemy.partolTargetPos = currentEnemy.GetRandomPartolPoint();
currentEnemy.StartCoroutine(currentEnemy.WaitPatrolTime(waitTime,currentEnemy.partolTargetPos));
}
}
(2)追击状态 ChaseState
进入时改变取消之前可能的巡逻协程并更改速度。敌人朝向时刻朝着主角即可。Update中的条件可忽略(已经在StateMachineSystem的tick前进行了判断)。
[CreateAssetMenu(fileName = "New Chase State", menuName = "State Machine/State/EnemyChaseState")]
public class EnemyChaseState : StateActionSO
{
public override void OnEnter(StateMachineSystem stateMachineSystem)
{
stateMachineSystem.currentStateType = EnemyState.ChaseState;
stateMachineSystem.currentEnemy.StopAllCoroutines();
stateMachineSystem.currentEnemy.IsWait = false;
stateMachineSystem.currentEnemy.curSpeed = stateMachineSystem.currentEnemy.chaseSpeed;
//currentEnemy.StartCoroutine(currentEnemy.WaitPatrolTime(0, currentEnemy.player.transform.position)); 和update中的转视角冲突了
}
public override void OnUpdate(StateMachineSystem stateMachineSystem)
{
EnemyController currentEnemy = stateMachineSystem.currentEnemy;
if (currentEnemy == null || currentEnemy.IsDead || currentEnemy.IsHurt) return;
Vector3 playerPos = currentEnemy.player.transform.position;
//敌人的视野始终跟随人物
Quaternion toRotation = Quaternion.LookRotation(new Vector3(playerPos.x, currentEnemy.transform.position.y, playerPos.z) - currentEnemy.transform.position);
currentEnemy.transform.rotation=Quaternion.Slerp(currentEnemy.transform.rotation, toRotation, Time.deltaTime * currentEnemy.rotateSpeed);
}
}
(3)攻击状态 AttackState
1:停止移动 2:判断能否进行攻击 3:调整面向
[CreateAssetMenu(fileName = "New Attack State", menuName = "State Machine/State/EnemyAttackState")]
public class EnemyAttackState : StateActionSO
{
public override void OnEnter(StateMachineSystem stateMachineSystem)
{
stateMachineSystem.currentStateType = EnemyState.AttackState;
stateMachineSystem.currentEnemy.curSpeed = 0;//停下移动准备攻击
}
public override void OnUpdate(StateMachineSystem stateMachineSystem)
{
EnemyController currentEnemy = stateMachineSystem.currentEnemy;
if (currentEnemy == null || currentEnemy.IsDead||currentEnemy.IsHurt) return;
float distance = Vector3.Distance(currentEnemy.transform.position, currentEnemy.player.transform.position);
//敌人在攻击范围内,先判断敌人现在是否正在攻击
if(distance<currentEnemy.AttackStateDistance&&!currentEnemy.IsAttack)
{
if (!currentEnemy.IsHurt)
Attack(currentEnemy, distance);
}
//敌人的视野始终跟随人物
Vector3 playerPos = currentEnemy.player.transform.position;
Quaternion toRotation = Quaternion.LookRotation(new Vector3(playerPos.x, currentEnemy.transform.position.y, playerPos.z) - currentEnemy.transform.position);
currentEnemy.transform.rotation=(Quaternion.Slerp(currentEnemy.transform.rotation, toRotation, Time.deltaTime * currentEnemy.rotateSpeed));
}
public override void OnExit(StateMachineSystem stateMachineSystem)
{
stateMachineSystem.currentEnemy.curSpeed = stateMachineSystem.currentEnemy.chaseSpeed;
}
private void Attack(EnemyController currentEnemy,float distance)
{
//敌人在近战攻击距离内 使用近战攻击
if (distance < currentEnemy.AttackDistanceNear)
{
AttackNear(currentEnemy);
}
//否则,即如果敌人在远程攻击距离内,使用远程攻击
else if (distance < currentEnemy.AttackDistanceFar)
{
AttackFar(currentEnemy);
}
}
private void AttackNear(EnemyController currentEnemy)
{
currentEnemy.anim.SetTrigger("AttackNear");
}
private void AttackFar(EnemyController currentEnemy)
{
currentEnemy.anim.SetTrigger("AttackFar");
}
}
2:具体的转换条件
(1)巡逻 ---> 追击 发现敌人
(2)追击 ---> 巡逻 没有发现敌人
(2)追击 ---> 攻击 进入攻击范围
(2)攻击---> 追击 脱离攻击范围
3:编辑器中的TransitionSO配置: