=> GuardHei

Illusory Walls Ahead

F+ or Deferred? Which one should I choose?


Here we go again!

Unity中自定义Coroutine的yield return返回条件

概述

当游戏开发涉及到人工智能设计的部分时,判断人物动画/寻路等这类持续一段时间而不是单帧的操作何时结束是一个能扰乱代码质量的地方。一种最简单的方法是在update()中每帧进行检验,但这种方式会使用大量的if else结构,使得代码混论难懂,耦合高,同时即使该帧没有操作正在进行也需要一个if else判断是否需要检测操作进度,消耗cpu性能。如果我们能使用coroutine协程来解决这个问题,进度检测代码就可以和主更新方法分离开了来了。
然而如果仍然使用if else判断动作是否结束,再yield return,固然可以实现功能,但是每一处都要写一遍,不利于代码的重用。因此,如果可以仿照Unity自己的诸如WaitForSecondsWaitUntil自定义出形如WaitForAnimationFinishedWaitForNavigationFinished的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来自定义检测动画片段播放完毕和寻路结束事件呢?

大体思路就是,新建WaitForAnimationFinishedWaitForNavgationFinished类,继承自CustomYieldInstruction类,然后在构造方法中传入相关组件进行初始化(如AnimatorNavMeshAgent这一类组件),最后重写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);
		}
	}
}

缺点

这里的缺点到不仅是针对自定义协程返回条件的,也有判断动画播放结束和寻路结束的一些潜在问题:

  1. 如果一个动作包含多个连续的动画片段,仅使用normalizedTime判断是否播放结束是不可行的,需要同时检测当前播放动画的名称。然而这样增加了代码量,动画名称可能是硬编码写入程序,不方便调试和策划或美工人员的编辑
  2. 可能会创建大量条件检测的实例,消耗cpu时间,可以考虑使用对象池模式对实例进行复用。
  3. NavMeshAgent在设置完目的地后到计算出路径之前,remainingDistance都为0,可能会造成判断的bug。

总结

主要也就是展示一下CustomYieldInstruction的用法和使用场景,如果各位有更好的检测动画播放结束或者寻路结束的方法,也可以拿出来讨论讨论。💻☕️

Recent Articles

Unity游戏开发中对本地文件的加密读写

概述在实际项目中,总是需要将一些数据持久化在本地,比如游戏存档,偏好设置或是热更新的一些资源。对于其中一些需求如偏好设置来说,直接以明文的方式写入PlayerPrefs就可以了,但是对于存档这类的文件一般都是在写入在项目的Application.persistentDataPath路径下。然而直接明文读写的话可能会导致存档被玩家篡改,或者重要的信息被第三方获取到(比如与服务器通讯的秘钥),因此,对于敏感的文件,我们需要对它进行加密操作。实现二进制存储第一种实现的方法非常简单,...…

UnityContinue
Earlier Articles

Waste Of Water

It is often heard by people like me living in a wet city of water scarcity. However, fewer people really gets a figure of what is really happening just around us - the human-beings are now touching on the bottom of the earth’s well.It is hard to...…

IB,EnglishContinue