上一篇已经介绍了人物的状态切换以及动画切换的效果,其实有很多共通之处,这篇文章就不多做解释了。我们将会分析AI的各种状态之间切换的条件以及方法的制作。作者在写AI逻辑的时候脑子是有点混乱的,但是经过测试,理论上是没有任何问题的,能用!至于后续的优化,我将在以后的文章会写到的。
在各种RPG游戏,或者MOBA游戏也好,AI都是已经事先设定好的,例如某热门荣耀手游,里面的小兵,也有各种各样的属性,以及方法。
例如,
1.发现敌方小兵,会优先攻击敌方小兵;
2.当己方英雄被敌方英雄攻击时,己方小兵或者己方防御塔的第一仇恨会变成敌方英雄,这里就涉及到一个检测的范围;
3.当周围小兵追逐玩家的时候,突然丢失了玩家的视野或者玩家离敌方小兵太远的时候,敌方小兵会回到原来的位置,然后继续按原来设计好的路线行走;
4.好了,没有第4了。
实际上游戏的AI说复杂也不复杂,因为是人为设定好的,该干啥干啥,它与广泛意义上的人工智能也不一样(其实我很喜欢、很感兴趣的,这个以后再说)。所以制作游戏的AI理论上还是逻辑上的问题。
在我设计的这款游戏中,AI暂时有4个状态(因为状态太多的话就要去看分层有限状态机或者行为树或者各种个月的技术啦。这些很有必要学的东西以后再说),这个4个状态分别是:死亡、巡逻、追逐、攻击。
现从Enemy挂载的AIEnemy开始说起,首先是一些AI需要的各种参数,
private Player _targPlayer; //保存扫描到的player
public float ScanRadius; //扫描范围
public float ChaseRange;//追击范围
public float AttackRange;//攻击范围
public float OutOfRangeTime;//追击超时
private float _attackCoolDownTime;//攻击间隔计时
public float AtkCoolDown;//攻击间隔
public float ChasingTime; //追逐时间
private float _countChasingTime;//追逐计时
public float GuardTime;//警戒时间
private float _countGuardTime;//警戒时间计时
public float PatrolOffset; //巡逻坐标偏移量
public float PatrolTime; //巡逻循环时间
[SerializeField]
private float _countPatrolTime;//巡逻循环时间计时
//原始地点
private Vector3 _oriPosition;
//生成地点
private Vector3 _spawnPosition;
private Animator _anim;
private NavMeshAgent _nav;
同样的,为了达到监控状态的效果,所以我也在Update中使用了switch用于状态的计算以及切换。
void Update () {
FsMachine.OnUpdate();
//当目标角色不为空的时候,注视目标角色
if (_targPlayer != null)
{
transform.LookAt(_targPlayer.transform);
}
//当目标玩家不为空的时候 感觉可以用switch-case来做的
AllCharacter.StateType aiState = FsMachine.CurBaseState.GetStateType();
switch (aiState)
{
case StateType.STATE_PATROL:
//当找到目标的时候
if (IsHaveTarget())
{
//巡逻状态切换到追击状态
if (LessThanChaseRange())
{
_countGuardTime += Time.deltaTime;
//打开境界模式
GuardMode(true);
if (_countGuardTime >= GuardTime)
{//大于警戒时间
GuardMode(false);
StateChange(StateType.STATE_CHASE);
}
else
{
GuardMode(false);
}
}
}
else
{
_countPatrolTime += Time.deltaTime;
}
break;
case StateType.STATE_CHASE:
_countChasingTime += Time.deltaTime;
//追击状态切换到攻击状态
//若enemy与player的距离小于attackrange就进入攻击状态
if (_countChasingTime < ChasingTime)
{
if (LessThanAttackRange())
{
StateChange(StateType.STATE_ATTACK);
}
else if (!LessThanScanRange())
{//如果enemy与player的距离大于ScanRange
StateChange(StateType.STATE_PATROL);
BackToOriginPosition();
}
}
else
{//超过追逐时间则回到巡逻状态
StateChange(StateType.STATE_PATROL);
}
break;
case StateType.STATE_ATTACK:
//计算攻击时间的间隔
_attackCoolDownTime += Time.deltaTime;
//攻击状态,目标击杀
if (_targPlayer.IsPlayerDead())
{
StateChange(StateType.STATE_PATROL);
}
else if (!LessThanAttackRange() && LessThanScanRange())
{
//若enemy与player的距离大于attackrange 小于 scanrange,则继续回到chasestate
StateChange(StateType.STATE_CHASE);
}
else if (Time.deltaTime > OutOfRangeTime)
{
//若enemy进入战斗,则开始计时,超过计时并且范围太远就脱离战斗
StateChange(StateType.STATE_PATROL);
}
break;
}
}
死亡的状态不多做赘述了,一般就是AI血量为0然后扔到缓存池(Pool) 再SetActive(false)掉,要用的时候再从缓存池拿出来。剩下三个一点一点细说吧。
该状态负责巡逻(Patrolling),以及扫描(Scanning)附近的目标。至于巡逻的方法我本意是想每个一段时间随机生成一个目标点再去巡逻,但是这样似乎挺好性能。所以在后续的制作上,我计划是随机生成n个点,然后就在这n个点内进行巡逻,这样会比较好。但是先放现在的方法吧。
//巡逻方法 以后使用伪随机方法,设置几个点好吧
public void Patrolling()
{
if (_countPatrolTime >= PatrolTime)
{
Vector3 newPosition = GetRandomPosition();
//新点与出生点的长度小于PatrolOffset圆形范围
if (EnemyAndTargetDistance(newPosition,_spawnPosition)< PatrolOffset)
{
_nav.SetDestination(newPosition);
_anim.SetFloat("MoveFactor", 0.2f);
_nav.speed = 2.0f;
_nav.stoppingDistance = 0.2f;
_countPatrolTime = 0;
if (EnemyAndTargetDistance(transform.position, newPosition) < _nav.stoppingDistance)
{
_anim.SetFloat("MoveFactor", 0);
}
}
else
{
GetRandomPosition();
_anim.SetFloat("MoveFactor", 0);
_countPatrolTime = 0;
}
}
}
我再画个图辅助理解一下
大概的意思就是说,在Enemy周围随机生成一个点,如果这个点在设定好的巡逻范围内(橙点),那么就作为Enemy巡逻的目标点;如果这个点在设定好的巡逻范围以外(黄点),则重新生成一个目标点。
扫描的方法Scanning应该是很有趣的才对,我计划是一个扇形范围的扫描,赋予该Enemy的视野效果,不过暂时使用了别的方法实现Scan的效果。不过这样不好,我过几天写好再补回来。
在巡逻状态时,当Enemy发现目标以后,首先是执行Guard警戒模式,大多数RPG都是这样设计的,在超过这个警戒时间以后,目标依旧在扫描范围内,就开始进行追击,进入到攻击状态。这里的bool值tik是控制是否需要进入到GuardMode,当大于警戒时间后或者目标远离了Enemy,此时就应该退出警戒模式然后进入到别的状态。
private void GuardMode(bool tik)
{
transform.LookAt(_targPlayer.transform);
if (tik)
{
Debug.Log("i found the player");
_nav.isStopped = true;
}
else
{
_nav.isStopped = false;
_countGuardTime = 0;
}
}
AIPatrolState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AIPatrolState : BaseState
{
private readonly AIEnemy _enemy;
public AIPatrolState(AIEnemy enemy)
{
this._enemy = enemy;
}
public override AllCharacter.StateType GetStateType()
{
return AllCharacter.StateType.STATE_PATROL;
}
public override void EnterState(FiniteStateMachine fsMachine, BaseState preState)
{
if (preState != null)
{
Debug.Log("Enter the PatrolState the preState is:" + preState.GetStateType());
}
else
{
Debug.Log("Enter the PatrolState");
}
//Debug.Log(_player.CurBaseState);
_enemy.ClearTarget();
}
public override void UpdateState()
{
//巡逻移动的方法
//没有目标的时候执行
if (!_enemy.IsHaveTarget())
{
_enemy.Patrolling();
_enemy.ScanAround();
}
}
public override void ExitState(BaseState preState)
{
Debug.Log("Exit Patrol State. " + preState.GetStateType());
}
public override void InputHandle()
{
}
}
只有在Scan到角色以后,才有机会进入追击状态,在追击状态中,声明了一个float的变量作为追击时间。因为在MMORPG中,大多数怪物都不是一直追着你的,当他们追逐玩家超过一定距离以后,就会开始计算脱离战斗的时间,当到了这个时间以后,怪物就会回到原地,然后重新进入巡逻状态。但是当Enemy与目标的距离小于Enemy的攻击距离的时候,将会进入攻击状态。
//判断小于攻击距离
private bool LessThanAttackRange()
{
if (Vector3.Distance(transform.position, _targPlayer.transform.position) < AttackRange)
return true;
return false;
}
//判断小于扫描距离
private bool LessThanScanRange()
{
if (Vector3.Distance(transform.position, _targPlayer.transform.position) < ScanRadius)
return true;
return false;
}
//追击目标
public void ChasingTarget()
{
_anim.SetFloat("MoveFactor", 1f);
_nav.speed = 5f;
_nav.stoppingDistance = AttackRange-0.1f;
if (!LessThanAttackRange())
{//追击目标时,Enemy与Target距离小于Enemy攻击距离时
_nav.SetDestination(_targPlayer.transform.position);
}
}
//退出追击
public void ExitChase()
{
_anim.SetFloat("MoveFactor", 0);
}
AIChaseState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AIChaseState : BaseState
{
private readonly AIEnemy _enemy;
public AIChaseState(AIEnemy enemy)
{
this._enemy = enemy;
}
public override AllCharacter.StateType GetStateType()
{
return AllCharacter.StateType.STATE_CHASE;
}
public override void EnterState(FiniteStateMachine fsMachine, BaseState preState)
{
if (preState != null)
{
Debug.Log("Enter the ChaseState the preState is:" + preState.GetStateType());
}
else
{
Debug.Log("Enter the PatrolState");
}
_enemy.ResetCountChasingTime();
//Debug.Log(_player.CurBaseState);
}
public override void UpdateState()
{
Debug.Log("i'm in the chase state");
_enemy.ChasingTarget();
}
public override void ExitState(BaseState preState)
{
Debug.Log("Exit Chase State. " + preState.GetStateType());
_enemy.ResetCountChasingTime();
_enemy.ExitChase();
}
public override void InputHandle()
{
}
}
在进入攻击状态后,就会有一个与Player相同的攻击间隔时间的计时,这里就不多说了,详细参考上一篇Player的攻击状态就好了。另外的还要说的是,当攻击的目标死亡以后,Enemy就会回到Patrol的状态。当攻击的目标远离了Enemy的时候,Enemy就会回到Chase的状态。
AIAttackState.cs
using System.Collections;
using System.Collections.Generic;
using BehaviorDesigner.Runtime.Tasks.Basic.Math;
using UnityEngine;
public class AttackState : BaseState
{
private readonly Player _player;
public AttackState(Player player)
{
//向状态机发送事件,请求转换状态
this._player = player;
}
public override AllCharacter.StateType GetStateType()
{
return AllCharacter.StateType.STATE_ATTACK;
}
public override void EnterState(FiniteStateMachine fsMachine, BaseState preState)
{
if (preState != null)
{
Debug.Log("Enter the AttackState the preState is:" + preState.GetStateType());
}
else
{
Debug.Log("Enter the AttackState");
}
_player.SetType(AllCharacter.StateType.STATE_ATTACK);
_player.ResetAttackCoolDown();
_player.Attack();
//Debug.Log(_player.CurBaseState);
}
public override void UpdateState()
{
//战斗状态不间断平A
//Debug.Log("I'm in Battle Mode .");
bool canAttack = _player.CanAttack();
//攻击间隔
if (canAttack)
{
_player.Attack();
}
}
public override void ExitState(BaseState preState)
{
_player.StopAttack();
_player.ResetAttackCoolDown();
Debug.Log("Exit Attack State. " + preState.GetStateType());
}
public override void InputHandle()
{
}
}
游戏的AI还有有各种细节的优化,以后将会再继续改进。至于为什么我把状态的方法都放在角色或者是Enemy内,是因为我认为方法是他们独有的,但是状态只是负责执行方法。不知道这个想法对不对?