更新日期:2024年7月24日。
项目源码:第五章发布(正式开始游戏逻辑的章节)
简介
本章我们将进入让整个游戏真正跑起来
的核心环节,那便是敌方阵营的AI
,能够调遣敌方所有角色
进行移动
、追击
、围堵
玩家角色,才是真正体现游戏性的标志。
一、敌方AI调遣策略
首先,我们来设计敌方AI
针对敌方所有角色
的调遣策略:
设敌方角色A(会为所有敌方角色执行此调遣策略):
1.【收集】:收集所有玩家角色的评估数据;
2.【评估】:评估所有玩家角色数据相对于自己的价值;
3.【行动】:向价值最高目标发起行动(攻击、追击等)。
简言之,此策略可归纳为如下三步:
收集 => 评估 => 行动
通过自定义收集
算法、评估
算法,可以使得敌方所有角色
始终保持向高价值
目标行动的趋势。
二、敌方AI基类
那么首先,我们依然是抽象一个敌方AI的基类AI
:
/// <summary>
/// 敌人AI
/// </summary>
public abstract class AI
{
/// <summary>
/// 调遣所有敌人行动
/// </summary>
/// <param name="level">关卡</param>
/// <param name="enemyRoles">敌人列表</param>
/// <param name="playerRoles">玩家列表</param>
/// <param name="endCallback">调遣结束回调</param>
public void DispatchAllEnemy(Level level, List<Role> enemyRoles, List<Role> playerRoles, HTFAction endCallback)
{
}
}
我们只需要使自己编写的AI类型继承至AI
类,其便作为一个AI控制器独立存在,通过定义不同的AI类型还可以实现各种风格或难度的AI,比如擅长围堵
的,擅长风筝
的,擅长逃跑
的…
三、为关卡指定AI类型
然后回到我们的关卡(Level)
类,在其中添加AI
相关字段:
/// <summary>
/// AI类型
/// </summary>
[Label("敌人AI类型"), ClassType(typeof(AI))] public string AIType = "<None>";
/// <summary>
/// 敌人AI实例
/// </summary>
public AI EnemyAI { get; private set; }
ClassType
使得其能够检索AI
类的所有子类,<None>
则代表无AI类型,这将是不被允许的。
然后在关卡(Level)
的检视器面板便可以直接选择所有的AI类型:
四、创建AI控制器
接着我们在关卡(Level)
的初始化(Initialize)
方法中创建AI控制器:
public virtual void Initialize()
{
if (!string.IsNullOrEmpty(AIType) && AIType != "<None>")
{
Type type = ReflectionToolkit.GetTypeInRunTimeAssemblies(AIType);
EnemyAI = Activator.CreateInstance(type) as AI;
}
else
{
Log.Error($"关卡【{Name}】的敌人AI类型为空,这是不被允许的!");
}
}
五、调遣所有敌人行动
我们在之前的章节中已经对EnemyAI
有所涉及了,也即是在玩家结束当前回合
时,将控制器交由AI时,AI便会开始调遣所有敌人行动
:
/// <summary>
/// 玩家结束当前回合
/// </summary>
public void PlayerEndRound()
{
//......
//调遣所有敌人行动
EnemyAI.DispatchAllEnemy(this, EnemyRoles, PlayerRoles, () =>
{
//行动结束后,进入下一回合
StartCoroutine(EnterRound(Round + 1, false));
});
}
到此,AI的生命周期差不多了,接下来我们进入正题,开始编写AI调遣策略的三个阶段(收集、评估、行动)。
六、AI调遣策略第一阶段:收集
要知道,在玩家操作角色行动之前,也会对整个战场的数据进行收集以便对接下来的行动做出评估,所以为了模拟真人的操作,AI在这里也将进行这个数据收集
的过程。
我们定义一个收集
方法:
public abstract class AI
{
/// <summary>
/// 收集评估数据(具体如何收集,交由具体的AI子类来实现)
/// </summary>
/// <param name="enemy">敌人</param>
/// <param name="player">玩家</param>
protected virtual EvaluationData GenerateEvaluationData(Role enemy, Role player)
{
return null;
}
}
AI收集评估数据
必然是一对一的(一个敌方角色对应一个玩家角色),只不过这个敌方角色会对所有玩家角色都收集一份评估数据
。
然后,我们定义评估数据
类:
/// <summary>
/// AI评估数据
/// </summary>
public class EvaluationData : IReference
{
/// <summary>
/// 评估的目标玩家
/// </summary>
public Role Target;
/// <summary>
/// 玩家的上方是否可抵达
/// </summary>
public bool IsUpCanMove;
/// <summary>
/// 玩家的下方是否可抵达
/// </summary>
public bool IsDownCanMove;
/// <summary>
/// 玩家的左方是否可抵达
/// </summary>
public bool IsLeftCanMove;
/// <summary>
/// 玩家的右方是否可抵达
/// </summary>
public bool IsRightCanMove;
/// <summary>
/// 可用来攻击玩家的要诀
/// </summary>
public Ability UseAbility;
/// <summary>
/// 针对玩家的行动模式(0:无法攻击,1:能直接攻击,2:移动后可攻击)
/// </summary>
public int ActMode;
/// <summary>
/// 玩家是否能反击
/// </summary>
public bool IsCanBackAttack;
/// <summary>
/// 玩家当前的路径距离(为0则无法抵达玩家位置)
/// </summary>
public int Distance;
/// <summary>
/// 重置数据
/// </summary>
public virtual void Reset()
{
Target = null;
UseAbility = null;
}
}
当然,针对不同的AI类型,这里的评估数据是可以做出调整的,毕竟越强大的AI必然依赖越多的战场数据。
然后,我们在进入调遣所有敌人行动(DispatchAllEnemy)
入口方法后,为所有敌方角色收集评估数据:
for (int i = 0; i < enemyRoles.Count; i++)
{
Role enemy = enemyRoles[i];
//敌方角色处于活动中,且未死亡
if (enemy.State == RoleState.Actived && !enemy.IsDead)
{
//回收评估数据缓存列表
for (int j = 0; j < _evaluationDatas.Count; j++)
{
Main.m_ReferencePool.Despawn(_evaluationDatas[j]);
}
_evaluationDatas.Clear();
//此敌方角色,针对所有玩家角色,开始收集评估数据
for (int j = 0; j < playerRoles.Count; j++)
{
Role player = playerRoles[j];
if (player.State == RoleState.Actived && !player.IsDead)
{
//将评估数据加入缓存列表
_evaluationDatas.Add(GenerateEvaluationData(enemy, player));
}
}
}
}
七、AI调遣策略第二阶段:评估
上一阶段已经收集到了敌方角色针对所有玩家角色的评估数据,那么接下来就是对所有数据做出评估,同时量化为可横向比较的价值
。
我们依然定义一个评估
方法:
public abstract class AI
{
/// <summary>
/// 评估一个玩家对于当前敌人的价值(具体如何评估,交由具体的AI子类来实现)
/// </summary>
/// <param name="enemy">敌人</param>
/// <param name="data">评估数据</param>
/// <returns>总价值,价值越高,敌人越优先向其发起行动</returns>
protected virtual int Evaluation(Role enemy, EvaluationData data)
{
return 0;
}
}
对上一阶段收集到的所有评估数据进行评估:
for (int i = 0; i < enemyRoles.Count; i++)
{
Role enemy = enemyRoles[i];
//收集评估数据......
//开始评估价值最高的目标
EvaluationData data = null;
int cost = -1;
for (int j = 0; j < _evaluationDatas.Count; j++)
{
int newCost = Evaluation(enemy, _evaluationDatas[j]);
if (newCost > cost)
{
data = _evaluationDatas[j];
cost = newCost;
}
}
}
到这一阶段,最高的价值cost
对应的目标数据data
便已经被筛选出来了,接下来只需向其指向的目标角色
发起行动即可。
八、AI调遣策略第三阶段:行动
为此,我们再定义一个行动
方法:
public abstract class AI
{
/// <summary>
/// 敌人向玩家发起行动(具体如何行动,交由具体的AI子类来实现)
/// </summary>
/// <param name="enemy">敌人</param>
/// <param name="data">AI评估数据</param>
protected virtual IEnumerator EnemyActing(Role enemy, EvaluationData data)
{
//行动完毕后,enemy 被标记为已行动,并等待1秒后,结束此角色的调遣
enemy.IsActed = true;
yield return YieldInstructioner.GetWaitForSeconds(1);
}
}
然后,对评估
阶段筛选出来的最佳目标发起行动:
for (int i = 0; i < enemyRoles.Count; i++)
{
Role enemy = enemyRoles[i];
//收集评估数据......
//评估价值最高目标......
//向价值最高目标发起行动,并等待行动结束
if (data != null)
{
yield return EnemyActing(enemy, data);
}
}
到此,AI调遣策略的三个阶段便简介明了的展现出来了,它的代码量极其精简,但可扩展的空间却相当的大,核心的算法都将由具体的AI类型在收集
、评估
、行动
这三个阶段做出具体的实现。
那么,下一篇我们将基于此探讨一个默认AI类型(AI_Normal)
的实现。