概述
当游戏开发涉及到人工智能设计的部分时,判断人物动画/寻路等这类持续一段时间而不是单帧的操作何时结束是一个能扰乱代码质量的地方。一种最简单的方法是在
update()
中每帧进行检验,但这种方式会使用大量的if else
结构,使得代码混论难懂,耦合高,同时即使该帧没有操作正在进行也需要一个if else
判断是否需要检测操作进度,消耗cpu性能。如果我们能使用coroutine
协程来解决这个问题,进度检测代码就可以和主更新方法分离开了来了。
然而如果仍然使用if else
判断动作是否结束,再yield return
,固然可以实现功能,但是每一处都要写一遍,不利于代码的重用。因此,如果可以仿照Unity
自己的诸如WaitForSeconds
,WaitUntil
自定义出形如WaitForAnimationFinished
,WaitForNavigationFinished
的yield return
返回条件,一来代码看得更加直观,二来代码风格统一,三来高度解耦合,方便代码重构。
实现
那么如何实现自定义yield return
返回条件呢?其实Unity
已经为我们考虑到了这一点,特地提供了一个父类CustomYieldInstruction
,方便我们继承它来自定条件。下面我们新建一个新的WaitForCustomCondition
脚本继承自CustomYieldInstruction
来演示下用法:
using System.Collections;
using UnityEngine;
public class WaitForCustomCondition : CustomYieldInstruction {
public override bool keepWaiting {
get {
// 这里进行是否返回的判断,注意true为继续等待,即不返回,false才是返回
return true;
}
}
}
可以看到代码其实非常的简单,只需要重写keepWaiting
这个属性访问器即可,非常方便。如果返回true,则继续等待,如果返回false,则执行yield return
后面的代码。
现在让我们回到之前提到的问题,怎么样使用CustomYieldInstruction
来自定义检测动画片段播放完毕和寻路结束事件呢?
大体思路就是,新建WaitForAnimationFinished
和WaitForNavgationFinished
类,继承自CustomYieldInstruction
类,然后在构造方法中传入相关组件进行初始化(如Animator
,NavMeshAgent
这一类组件),最后重写keepWaiting
进行判断,相关代码如下:
public class WaitForAnimationFinished : CustomYieldInstruction {
AnimatorStateInfo animatorStateInfo;
public override bool keepWaiting {
get {
// 动画控制器的标准化时间小于1,说明没有播放完,不返回
return animatorStateInfo.normalizedTime < 1f;
}
}
// 传入Animator组件初始化
public WaitForAnimationFinished(AnimatorStateInfo animatorStateInfo) {
this.animatorStateInfo = animatorStateInfo;
}
}
public class WaitForNavigationFinished : CustomYieldInstruction {
public static readonly float FLOAT_PRECISION = 0.00001f;
NavMeshAgent agent;
public override bool keepWaiting {
get {
// 寻路代理器的剩余路程大于制动距离,说明没有寻完路,不返回,注意浮点数误差
return agent.remainingDistance > agent.stoppingDistance + FLOAT_PRECISION;
}
}
// 传入NavMeshAgent组件初始化
public WaitForAnimationFinished(NavMeshAgent agent) {
this.agent = agent;
}
}
这里的代码都十分的直观,我就不多解释了。下面来举个实际使用场景的例子:
// 下面代码只是演示,没有通用性
[RequireComponent(typeof(Animator))] // 要求附着物体有Animator组件
[RequireComponent(typeof(NavMeshAgent))] // 要求附着物体有NavMeshAgent组件
public class AICharater : MonoBehaviour {
Animator animator;
NavMeshAgent agent;
bool alive;
void Awake() {
// 获取相关组件
animator = GetComponent<Animator>();
agent = GetComponent<NavMeshAgent>();
}
void Start() {
alive = true;
// 开始动画与寻路的协程
StartCoroutine(ExeAnimationTask());
StartCoroutine(ExeNavigationTask());
}
IEnumerator ExeAnimationTask() {
while (alive) {
// 这里的_Animate参数是随便填的,不要在意
animator.SetBool("_Animate", true);
// 像new WaitForSeconds(float time)一样使用
yield return new WaitForAnimationFinished(animator.GetCurrentAnimatorStateInfo(0));
animator.SetBool("_Animate", false);
}
}
IEnumerator ExeNavigationTask() {
while (alive) {
// 这里的destination的值是随便填的,不要在意
agent.destination = new Vector(1f, 1f, 1f);
// 像new WaitForSeconds(float time)一样使用
yield return new WaitForNavigationFinished(agent);
}
}
}
缺点
这里的缺点到不仅是针对自定义协程返回条件的,也有判断动画播放结束和寻路结束的一些潜在问题:
- 如果一个动作包含多个连续的动画片段,仅使用
normalizedTime
判断是否播放结束是不可行的,需要同时检测当前播放动画的名称。然而这样增加了代码量,动画名称可能是硬编码写入程序,不方便调试和策划或美工人员的编辑 - 可能会创建大量条件检测的实例,消耗cpu时间,可以考虑使用对象池模式对实例进行复用。
NavMeshAgent
在设置完目的地后到计算出路径之前,remainingDistance
都为0,可能会造成判断的bug。
总结
主要也就是展示一下CustomYieldInstruction
的用法和使用场景,如果各位有更好的检测动画播放结束或者寻路结束的方法,也可以拿出来讨论讨论。💻☕️