对 Unity 协程的调研

本文详细探讨了Unity中协程的概念、原理及应用场景,包括如何使用协程实现延时执行和依赖操作完成后的代码执行。文章还对比了协程与线程的区别,介绍了Unity中与协程相关的类和需要注意的事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对 Unity 协程的调研

1. 什么是协程 #

A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done. → 协程是一个部分执行, 遇到条件 (yield return) 会挂起, 直到条件满足才会被唤醒继续执行后面代码的一种函数.

2. 为什么要有协程 #

协程的作用一共有两点 :

1. 延时等待一段时间执行代码
2. 等某个操作完成之后再执行后面的代码

总而言之, 协程控制了代码在特定的时机执行, 是对单线程控制权的交替.

3. 协程的原理 #

Coroutines 不是多线程, 不是异步技术, 协程都在 MainThread 中执行, 而且每个时刻只有一个 Coroutine 在执行. Coroutine 是一个 function, 可以部分执行, 当条件满足时, 未来会被再次执行直到整个函数执行完毕.

协程能够把一个计算或操作,分解成若干步,并且可以在任何一步停下来,并在需要的时候继续执行剩下的步骤。这样的模型给予了更细粒度的控制一个操作或是功能, 比如, 一个非常耗时间的操作, 被分步执行可以更好的控制程序响应. 比如, 一个操作需要依赖各种条件, 可以更好的处理条件不满足的时候的情况. 也能够更好的把操作或是计算过程中的状态变化, 与其他的状态变化交互, 然而, 程序运行的过程就是抽象数据结构和结构不断变化的过程, 协程能够优雅自然的进行这个变化过程的需求.

Unity 在每一帧都会去处理 GameObject 里带有的 Coroutine Function, 直到 Coroutine Function 被执行完毕. 当一个 Coroutine 开始启动时, 它会执行到遇到 yield 为止, 遇到 yield 的时候 Coroutine 会暂停执行, 直到满足 yield 语句的条件, 会开始执行 yield 语句后面的内容, 直到遇到下一个 yield 为止 ... 如此循环直到整个函数结束, 这就是可以将一个函数分割到多个帧里去执行的思想.

也就是说, Coroutines 最棒的就是函数的执行可以不在一次 Frame 里完成, 可以在多个 Frame 中完成. 比如, 我们希望看到物体透明度的改变, 如果让 color.a 的变化在一帧内完成, 那么我们是看不出来这其中的变化得, 因为太快, 所以让 color.a 的变化必须在多个帧内完成, 我们才有可能用肉眼看到这一变化的过程.

Unity 的协程系统基于 C# 的接口 : IEnumerator, 允许为自己的集合类型编写枚举类.

Unity 函数执行图

4. 协程与线程的区别 #

  1. 历史上先有的协程, 是操作系统用来模拟多任务并发, 协程是非抢占式的, 多任务时间片不能公平分享, 线程是抢占式的;

  2. 线程能利用多核达到真正的并行计算, 如果任务设计的好, 线程能几乎成倍的提高你的计算能力, 但是线程的缺点也很明显, 那就是没有设计好导致大量的锁 | 切换 | 等待, 这些很多都是应用层的问题. 而协程因为是非抢占式, 所以需要用户自己释放使用权来切换到其他协程, 因此同一时间其实只有一个协程拥有运行权, 相当于单线程的能力.

  3. 协程相对线程的最大优点就是 : 让原来要使用异步 + 回调方式写的非人类代码, 可以用看似同步的方式写出来.

5. Unity 中协程相关的类 #

  1. yield return null; → 等待下一帧中的 Update 函数执行完之后再继续执行;
  2. yield return new WaitForSeconds(10); → 延迟 10 秒后再继续执行;
  3. yield return new WaitForFixedUpdate(); → 等待所有脚本中的 FixedUpdate 函数结束之后再继续执行;
  4. yield return new WaitForEndOfFrame(); → 等待该帧中所有 Camera 和 GUI 对象渲染完毕, 在帧被显示到屏幕之前恢复执行前面的代码;
  5. yield return new WWW(url); → 等待 url 下载完后再继续执行;
  6. yield return StartCoroutine(MyFunc()); → 等待协程结束后再执行;

6. 需要注意的几点 #

- 协程是 C# 线程的替代品, 是 Unity 不使用线程的解决方案. 但是, 协程不是线程, 不能进行异步执行, 协程和 MonoBehaviour 的 Update 函数一样也是在 MainThread 中执行的.
  • 使用协程不用考虑同步和锁的问题.

  • 在程序中调用 StopCoroutine() 方法只能终止以字符串形式启动(开始)的协程;

  • 多个协程可以同时运行,它们会根据各自的启动顺序来更新;

  • 协程可以嵌套任意多层;

  • 如果你想让多个脚本访问一个协程,那么你可以定义静态的协程;

  • 协程不是多线程(尽管它们看上去是这样的),它们运行在同一线程中,跟普通的脚本一样;

  • 如果你的程序需要进行大量的计算,那么可以考虑在一个随着时间进行的协程中处理它们;

  • IEnumerator 类型的方法不能带 ref 或者 out 型的参数,但可以带被传递的引用;

  • 目前在 Unity 中没有简便的方法来检测作用于对象的协程数量以及具体是哪些协程作用在对象上。

  • 协程可以减少 callback 的使用, 但是不能完全替换 callback. 基于事件驱动的编程里面反而不能发挥协程的作用, 而用 callback 更适合. 想象一下用协程来写 GUI 的事件处理你怎么写, 计算密集型的异步代码里面也只能用 callback. 而 NodeJs 那种 io 瓶颈单任务流程用协程的确很适合, 但是也需要 callback 作补充.

  • 状态机用协程其实也有问题, 比如状态里面嵌套子状态, 再由子状态切换到其他状态的子状态, 开销和代码都会变差, 反而不如经典的状态机简单明了高效.

7. 几条准则 #

  1. 协程的返回值必须是 IEnumerator;
  2. 协程的参数不能加关键字 ref 或 out;
  3. 在 C# 脚本中, 必须通过 StartCoroutine 来启动协程;
  4. yield 语句要用 yield return 来代替;
  5. 在函数 Update 和 FixedUpdate 中不能使用 yield 语句, 但是可以启动协程;
  6. yield return 语句不能位于 try-catch 语句块中, 但可以位于 try-finally 的 try 语句块中;
  7. yield return 语句不能放在匿名方法中;
  8. yield return 语句不能放在 unsafe 语句块中;

8. 本文参考来源 #

Book <<Unity Case Study Manual>>
http://blog.youkuaiyun.com/u010153703/article/details/38557237
http://blog.youkuaiyun.com/huang9012/article/details/38492937
https://www.zhihu.com/question/20511233?rf=23290260
https://www.zhihu.com/question/20511233

End.

分类: ScriptC#, Unity3D
标签: 协程
<think>好的,我需要解释C#协程Unity协程的区别。首先,我得确认两者的基本概念。 C#协程通常是基于IEnumerator的迭代器方法,通过yield return来控制流程。而Unity协程虽然也使用IEnumerator,但它是基于Unity自己的生命周期和帧循环来驱动的。 接下来,执行环境的区别。C#原生协程需要开发者自己管理执行,比如用MoveNext()来推进。而Unity协程由引擎自动管理,依赖于MonoBehaviour和StartCoroutine方法。 然后要考虑调度机制。Unity协程与游戏帧率绑定,比如yield return null会在下一帧继续执行,而C#原生协程的执行时机由用户控制,可能不依赖帧率。 生命周期管理方面,Unity协程和GameObject绑定,物体禁用或销毁时协程终止。C#协程的生命周期需要手动控制,可能更灵活但也更复杂。 功能扩展性上,Unity提供了WaitForSeconds等定制指令,C#协程需要自己实现类似的功能,或者依赖其他库。 最后,适用场景不同。Unity协程主要用于游戏逻辑,如动画、延时等,而C#协程可能用于更通用的异步处理,但需要更多的底层管理。 需要确保术语准确,比如Unity的Yield Instruction,还有例子是否恰当。可能需要检查是否有遗漏点,比如线程安全或者异常处理方面的差异。 总结时应该分点说明,结构清晰,让用户容易理解两者的核心区别。</think>C#协程Unity协程的主要区别体现在以下几个方面: 1. **实现机制** - **C#原生协程**:基于`IEnumerator`迭代器,通过`yield return`语句实现控制流暂停/恢复。需要手动调用`MoveNext()`推进执行 - **Unity协程**:基于MonoBehaviour的生命周期系统,通过`StartCoroutine()`自动调度,与游戏主线程和帧循环深度集成 2. **执行环境** - **C#原生协程**:可在任意C#环境运行(控制台/WPF等),不依赖特定框架 - **Unity协程**:必须继承自MonoBehaviour,依赖Unity引擎的更新循环 3. **调度机制** - **C#原生协程**:执行时机完全由开发者控制,可自定义恢复条件 ```csharp IEnumerator MyCoroutine() { yield return new WaitUntil(() => condition); // 需自定义等待条件 } ``` - **Unity协程**:内置丰富调度指令,自动与帧率同步 ```csharp yield return new WaitForSeconds(1); // 等待1秒(受Time.timeScale影响) yield return new WaitForFixedUpdate(); // 等待物理更新 ``` 4. **生命周期** - **C#原生协程**:需要手动管理协程对象的生命周期 - **Unity协程**:自动关联GameObject生命周期,当物体被禁用/销毁时自动终止 5. **线程模型** - **C#原生协程**:本质是单线程的迭代器,不涉及多线程 - **Unity协程**:严格运行在主线程,与Unity API调用规则一致 6. **应用场景** - **C#原生协程**:适用于通用异步流程控制(如分步加载、状态机等) - **Unity协程**:专为游戏开发设计(角色动画序列、延时触发、资源分帧加载等) **典型代码对比** ```csharp // C#原生协程(控制台应用示例) IEnumerator CountToThree() { Console.WriteLine("One"); yield return null; // 简单暂停 Console.WriteLine("Two"); yield return new CustomWait(1000); // 需自定义等待逻辑 Console.WriteLine("Three"); } // Unity协程 IEnumerator FireProjectile() { while(true) { Instantiate(bulletPrefab); yield return new WaitForSeconds(0.5f); // 内置时间等待 } } ``` **关键差异总结表** | 特性 | C#协程 | Unity协程 | |---------------------|-------------------|--------------------| | 依赖框架 | 无 | 必须依赖Unity引擎 | | 生命周期管理 | 手动控制 | 自动关联GameObject | | 内置等待指令 | 需自定义 | 提供丰富YieldInstruction | | 线程安全 | 需自行处理 | 强制主线程执行 | | 典型应用场景 | 通用异步逻辑 | 游戏对象行为控制 | 理解这些差异有助于避免在Unity开发中错误使用原生C#协程特性,同时也能更好地利用Unity协程优化游戏逻辑实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值