一款RPG的战斗模块中,怪物AI是又一基础单元。在经典的即时战斗中,比较简单的流程是角色进入怪物的仇恨范围->怪物持续的追踪角色到达攻击距离以内->角色与怪物的战斗->角色或怪物的死亡。这个Demo主要实现了以上的简单表现。
这个Demo的第一步就是制作一个怪物的模型及表现动画,通常而言骨骼动画包括以下几种:
根据不同的事件触发动画之间的转换来控制怪物的动画表现,显然这是一个经典的状态机模型,而Unity3D也提供了Animator模块来可视化的编辑状态图。我实现的Animator大概如下:
状态 | 玩家进入仇恨范围 | 玩家离开仇恨范围 | 玩家在怪物攻击范围内 | 玩家在怪物攻击范围外 | 玩家击杀怪物 | 怪物击杀玩家 |
---|---|---|---|---|---|---|
待机 | 移动 | \ | \ | \ | \ | \ |
移动 | 移动 | 待机 | 攻击 | 移动 | \ | \ |
攻击 | \ | \ | 攻击 | 移动 | 死亡 | 待机 |
死亡 | \ | \ | \ | \ | \ | \ |
根据上表制作相应的Animation与Animator,再加上之前制作的血条系统,模型即可完成。
怪物AI则主要实现Introduction中的流程转换逻辑,这里我主要通过一个Enemy.cs来记录怪物的各种属性(血量,攻击力,仇恨范围阈值等等),怪物的控制则由ZombieControl.cs来完成(转发消息到表现层,通知后台类的数据更新),实现思路如下:
在实现过程中的主要问题是NavMesh与动画的结合,特别是对于寻路结束的判断。
由于怪物自身的骨骼动画或多或少的会改变怪物的位置和浮点数计算的精度误差,因此在使用NavMesh寻路时结束的动画切换会造成小幅度的位移,而位移又会导致新的寻路产生,从而产生“抖动”的现象。
我的主要解决办法是设置一个偏移值,在每一帧中计算距离,当距离小于偏移值时将NavMesh的Enabled置为False,让NavMesh不可用来避免新的寻路产生,最后只要在怪物位移触发时重新置为True即可。
主要代码如下:
void update()
{
Vector3 playerPosition = GameObject.Find ("A03").transform.position;
Vector3 ZombieToPlayer = playerPosition - this.transform.position;
distance = ZombieToPlayer.magnitude;
if ((distance < 5f && distance > 1.4f) || isTracing)
{
ZombieTrace (playerPosition);
}
if (distance < 5f)
{
isTracing = true;
}
CheckNavMesh ();
}
void ZombieTrace(Vector3 target)
{
if (GetComponent ().Blood.value == 0)
{
mr.enabled = false;
return;
}
GetComponent ().SetBool ("hasPlayer", true);
GetComponent ().SetBool ("inAttackArea", false);
mr.enabled = true;
mr.SetDestination (target);
}
此外,由于是在每一帧中进行逻辑判断与处理,所以在调用怪物攻击时触发的的IsHited方法时应该加上一个Time.deltaTime来固定调用的时间间隔,这里我的实现是每秒调用一次:
void CheckNavMeshAndHit()
{
if (distance < 1.4f)
{
GetComponent ().SetBool ("inAttackArea", true);
mr.enabled = false;
if (hitPerSecond >= 1)
{
hitPerSecond = 0;
mainPlayer.isHited (this.attackDamage);
}
hitPerSecond += Time.deltaTime;
}
}
最后,只要实现IsHited函数即可,这里主要通过Tween来实现,并在结束的回调函数中进行动画切换:
public void isHited(float damage)
{
if (Blood == null)
{
return;
}
bloodValue -= damage;
if (bloodValue < 0) bloodValue = 0;
Tweener tween = DOTween.To (() => Blood.value, x => Blood.value = x, bloodValue, 2);
if (bloodValue == 0)
{
tween.OnComplete (hasDead);
}
}
public void hasDead()
{
this.GetComponent<Animator> ().SetBool ("dead", true);
}
以上可以看出,触发怪物追击玩家的条件是玩家与怪物的距离小于阈值,简单的实现则可以穷举每一个玩家与每一个怪物进行计算,复杂度将是O(N^2)的,而在MMORPG的世界中有着大量的怪物和玩家分布在不同区域,如果直接穷举的话服务器将会消耗较大的性能。
其实仔细思考可以发现,有些距离玩家较远的怪物实际上根本不用计算,如果能划分出可能触发AOI和不可能触发的怪物集合将会大大减少计算量,事实上典型的AOI算法也是基于这个划分的思路。如划分地形的灯塔算法,以及基于坐标的十字链表。
综上所述,只要加上血条与伤害表现,一个基本的怪物AI系统就完成啦!这个Demo主要是基于阶段转换的状态机来实现怪物AI,模块也比较少并不复杂,后续可以扩展出新的
功能,比如技能系统啦,精英怪物战斗的阶段转换(残血时变狂暴这样)啦等等,还是有很多东西可以去做的!