大家好,欢迎来到IT知识分享网。
协程
协程有
IEnumerator类型返回值,所以总要有至少一个yield return xxx
基础入门直接看这个就够了:gamedevbeginner – Unity 中的协程(如何以及何时使用它们)
考虑使用协程的典型情况:
- 将对象移动到某个位置。
- 为对象提供要执行的任务列表(如前面的待办事项列表示例中所示)。
- 淡入淡出的视觉或音频。
- 等待资源加载。
协程的特点:
- 无法显式检查协程是否已在运行
- 协程的执行持续的时间是不确定的。协程允许开发者在多个帧上执行游戏逻辑(这里就涉及到Unity如何依靠IEnumerator将协程拆分成多个帧执行)
- 协程的执行顺序也是不确定的,它是由Unity引擎控制的,它可能会在主循环的不同阶段执行。
在Unity中,协程的执行是由Unity引擎来管理的。当你启动一个协程时,Unity会在后台为该协程创建一个迭代器(iterator,这要求协程的返回值是IEnumerator),并在每一帧中逐步执行该迭代器。
- 按顺序执行:Unity会按照协程启动的顺序来执行它们。如果有多个协程在同一帧启动,它们将按照启动的顺序进行排列执行。
- 帧更新执行:协程的执行是在Unity的每一帧更新中进行的。每当Unity引擎执行一帧时,它会检查当前正在执行的所有协程,并根据协程的当前状态来决定是否继续执行、暂停执行或者完成执行。
- 暂停和继续:当协程执行到
yield return语句时,Unity引擎会暂停该协程的执行,并将执行控制权交还给引擎。在等待的条件满足或者时间间隔过去后,Unity引擎会重新激活协程,并从yield return语句之后的代码开始继续执行。 - 时间管理:当协程使用
yield return new WaitForSeconds(time)等待一段时间时,Unity引擎会在等待时间结束后再次激活该协程。这样可以确保协程在指定的时间间隔后再次执行。 - 性能优化:虽然协程可以在多个帧中执行,但过多的协程同时执行也可能会影响性能。因此,在使用协程时应注意控制协程的数量,避免不必要的性能消耗。
协程的启动
- 以字符串传入协程名字启动
- 直接传递协程方法的引用
这种方法必须要求协程和启动写在一个文件里。如果不在一个文件里,Unity会在当前对象所附加的脚本中查找对应的协程方法,并且启动方法只能是直接传递协程方法的引用
注:当使用字符串启动协程时,只能传入一个参数
// 以字符串传入协程名字启动 StartCoroutine("MyCoroutine"); // 以字符串传入协程名字启动最多一个参数 StartCoroutine("MyCoroutine", 1); IEnumerator MyCoroutine(int value) {
// Code goes here... } // 直接传递协程方法的引用 StartCoroutine(MyCoroutine()); // 传递协程方法的引用在别的脚本 StartCoroutine(OtherScriptName.MyCoroutine());
协程与yield
任何协程暂停的代码都需要从yield return开始,yield 表示该方法是一个迭代器,它将在多个帧上执行,而 return 与常规函数一样,在该点终止执行并将控制权传递回调用方法。不同之处在于,使用协程时,Unity 知道从中断的地方继续该方法。yield return 后面的内容将指定 Unity 在继续之前等待多长时间。
等待到下一帧再执行
通过yield return null通常可以实现帧计数:
IEnumerator MyCoroutine() {
// 执行一些操作 yield return null; // 暂停协程,等待下一帧 // 在暂停之后是从这里继续执行 }
等待一定时间
暂停协程的执行一段时间,然后在指定的时间后继续执行。
IEnumerator MyCoroutine() {
// 执行一些操作 yield return new WaitForSeconds(2.0f); // 暂停协程,等待2秒 // 继续执行 }
如果要反复delay(如在while中),缓存 WaitforSeconds 对象的性能会稍好一些。
WaitForSeconds delay = new WaitForSeconds(1);
而WaitForSecondsRealtime(5);会在游戏暂停也执行,这是和WaitForSeconds在游戏暂停时等待时间不流逝是不一样的
等待帧结束
这个用法会暂停协程的执行直到当前帧结束后继续执行。通常用于在当前帧渲染完成后执行一些操作,典型用途是截取屏幕截图。
IEnumerator MyCoroutine() {
// 执行一些操作 yield return new WaitForEndOfFrame(); // 暂停协程,等待当前帧结束 // 继续执行 }
等待某bool表达式为true
下面这个则是等待值为true才执行后面的
IEnumerator CheckFuel() {
yield return new WaitWhile(() => fuel > 0); print("tank is empty"); }
等待另一个协程执行完
IEnumerator MyCoroutine() {
print("Coroutine has started"); yield return StartCoroutine(MyOtherCoroutine()); // 在启动的协程完成后,代码将继续 print("Coroutine has ended"); }
什么情况需要使用在当前帧结束完执行协程?主要是那些避免影响游戏性能和用户体验的场景:
- UI更新:有时你可能需要等待当前帧渲染完成后再更新UI,以确保UI元素在渲染前已经准备就绪,避免出现闪烁或不良的渲染效果。(那为什么不放在
LateUpdate呢?) - 延迟加载资源:在某些情况下,可能需要在当前帧结束后加载资源,以确保不影响当前帧的性能。例如,当某些资源仅在某些条件下才需要加载时,可以在下一帧加载这些资源,而不是立即加载。
- 执行复杂计算:如果有复杂的计算或处理需要在游戏逻辑更新之后执行,可以将这些操作放入协程中,在下一帧执行,以避免影响当前帧的性能。
- 网络请求或数据库操作:当需要进行网络请求或数据库操作时,可能需要等待当前帧结束后再执行,以避免阻塞主线程,从而提高游戏的流畅性和响应性。
- 场景切换后的操作:在场景切换后,可能需要进行一些初始化或清理操作,这些操作最好在当前帧结束后执行,以确保场景切换的流畅性和正确性。
yield的特殊情况
IEnumerator FetchData() {
Todo[] todos; User[] users; // USERS var www = new WWW(USERS_URL); yield return www; if (!string.IsNullOrEmpty(www.error)) {
Debug.Log("An error occurred"); yield break; } var json = www.text; try {
var userRaws = JsonHelper.getJsonArray<UserRaw>(json); users = userRaws.Select(userRaw => new User(userRaw)).ToArray(); } catch {
Debug.Log("An error occurred"); yield break; } // TODOS www = new WWW(TODOS_URL); yield return www; if (!string.IsNullOrEmpty(www.error)) {
Debug.Log("An error occurred"); yield break; } json = www.text; try {
var todoRaws = JsonHelper.getJsonArray<TodoRaw>(json); todos = todoRaws.Select(todoRaw => new Todo(todoRaw)).ToArray(); } catch {
Debug.Log("An error occurred"); yield break; } // OUTPUT foreach (User user in users) {
Debug.Log(user.Name); } foreach (Todo todo in todos) {
Debug.Log(todo.Title); } } void Start() {
StartCoroutine(FetchData()); } }
注意这里可以yield return www是因为在Unity中,WWW类实现了IEnumerator接口,因此可以在协程中使用yield return来暂停协程的执行,直到网络请求完成。一旦请求完成,协程会继续执行下面的代码,完成数据的处理和输出。
协程的停止
协程在执行其代码后自动结束,无需显式结束
- 协程内部中断使用
yield break - 协程外部中断使用
StopCoroutine("MyCoroutine"); - 停止 MonoBehaviour 上的所有协程
StopAllCoroutines();
StopAllCoroutines();停止由调用它的脚本启动的所有协程,因此它不会影响在其他地方运行的其他协程,但是如果脚本 A 运行的协程启动了脚本B上的一个协程,那从脚本 A 调用StopAllCoroutines();会把这俩都关了,但是从脚本B上调用则一个也不会停止。
销毁或禁用游戏对象将终止从该对象调用的任何协程,也就是说存在一种情况是销毁了游戏对象A但是其上的协程没有终止,因为那个协程是由B调用启动的。
协程与Thread的区别
Unity的协程(Coroutine)和C#的线程(Thread)是用于实现异步操作的两种不同的机制
- 如果要使用异步执行来表达游戏逻辑,请使用协程。
- 如果要使用异步执行来利用多个 CPU 内核,请使用线程,但是在子线程不要接触任何Unity原生功能,因为它完全不知道 Unity 主循环目前处于什么状态
区别如下:
- 执行上下文:
- 协程在 Unity 中是基于协程管理器(Coroutine Manager)的,运行在主线程中。因此,协程可以访问 Unity 的 API 和对象,但不能进行跨线程操作。
- 线程是独立的执行线程,与主线程分离。线程可以在后台执行任务,允许进行耗时操作,但在访问 Unity 的 API 和对象时,需要使用线程安全的方式,例如通过主线程调用
UnityMainThreadDispatcher或使用UnityMainThreadDispatcher等工具。
- 开销:
- 协程相对于线程来说,开销较小。因为协程是基于迭代器的,不需要像线程那样创建和管理额外的线程。
- 线程的创建和销毁需要更多的系统资源,因此在某些情况下,线程可能会导致更大的性能开销。
- 并发性:
- 协程在 Unity 中是顺序执行的,并不能真正实现多线程并发。即使使用了多个协程,它们仍然是在主线程中按顺序执行的。
- 线程是真正的并发执行机制,可以在多个线程上同时执行不同的任务。这使得线程更适合处理需要同时进行的耗时操作,如网络请求、文件IO等。
- 线程安全:
- 协程可以直接访问 Unity 的对象和 API,因此不需要担心线程安全的问题。
- 线程需要考虑线程安全的问题,需要使用锁、互斥量等机制来保护共享资源,以避免多线程同时访问导致的数据竞争和不确定性。
Unity 的 API 大部分是单线程访问的,这意味着在非主线程上直接访问 Unity 的 API 可能会导致异常或不确定的行为。如果没有特别需要,建议尽量避免在 Unity 中使用线程,而是使用协程等 Unity 提供的异步机制来处理异步操作。
协程更适合处理需要在多个更新周期中分批执行的任务,而线程更适合处理需要利用多核CPU的任务
协程、Invoke与Async
在Unity中,有几种常用的用于处理异步操作的方法:
- 协程(Coroutines):协程是Unity中用于处理异步操作的一种技术,它允许在多帧中分解一个操作,以避免阻塞主线程。
- Invoke:一种用于延迟调用方法的方法。可以使用
Invoke方法在一定时间后调用指定的方法。 - Async:C#中用于处理异步操作的关键字,通常与
await一起使用。在Unity中,可以使用Async/Await来处理异步操作,比如网络请求、文件读写等。
Async 和 Await 函数与协程之间的最大区别之一是它们可以返回一个值,而通常协程不能这样做。
Unity Documentation – MonoBehaviour.Invoke
Medium – Unity: Leveling up with Async / Await / Tasks
关于Coroutines与Async:
- 旧版本Unity可能不支持C#的Async/Await特性
- 协程侧重于分帧处理,Async侧重于异步
协程与迭代器
协程是一种特殊类型的迭代器方法,允许在多个帧之间分段执行代码。可以用来处理时间延迟、异步操作和顺序执行的任务,而不阻塞主线程。
在C#中,谈及迭代器,有这样几个特性:
- 迭代器必须实现
IEnumerable接口 - 每次调用
yield return时会保存当前状态,并在下次调用时从上次保存的状态继续执行 - Unity根据
yield return返回的对象类型来判断到底应该延迟多长时间来执行下一段代码 yield break则用于结束序列的生成,不再返回更多的元素。- 迭代器不会在创建时就生成所有元素,而是等到需要时才生成
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/112466.html