在Unity中,异步编程主要应用于长时间运行的操作或I/O操作,例如加载场景、资源(如纹理、音频文件)、进行网络请求或其他非阻塞操作。Unity提供了几种常见的方式来实现异步操作,通常通过协程和异步编程API(如async/await)来实现
Asynchronous
Unity 从 2017 版本开始支持 async/await 异步编程方式,它是 C# 的一部分,适用于处理 耗时的异步操作,如网络请求、文件操作等。通过 async 标记方法,并在需要等待的地方使用 await,可以简化代码并使其更加可读
示例:异步加载资源(UnityWebRequest)
假设你要从网络上下载文件,可以使用async/await来实现非阻塞的异步操作:
using UnityEngine;
using UnityEngine.Networking;
using System.Threading.Task;
public class AsyncExample : MonoBehaviour
{
async void Start()
{
string url = "https://example.com/resource";
string result = await DownloadDataAsync(url);
Debug.Log("下载完成:" + result);
}
// 异步下载数据
private async Task<string> DownloadDataAsync(string url)
{
using (UnityWebRequest webRequest = UnityWebRequest.Get(url))
{
// 发送请求并等待结果
await webRequest.SendWebRequest();
if (webRequest.result == UnityWebRequest.Result.Success)
return webRequest.downloadHandler.text; // 返回下载的文本内容
else return "错误:" + webRequest.error;
}
}
}
在这个例子中,DownloadDataAsync使用async/await来处理异步操作。awatiwebRequest.SendWebRequest()会等待请求完成,避免阻塞主线程
异步操作的常见用途:
- 网络请求:例如从服务器获取数据或上传数据
- 文件操作:读取/写入大文件时避免主线程阻塞
- 资源加载:异步加载资源(比如大型的纹理、音频文件等)
异步编程优缺点:
优点:
- 代码更简洁、易于理解
- 支持现代C#异步模式,错误处理更加方便
- 完全非阻塞主线程,不会影响UI和游戏的流畅性
缺点:
- 对于资源加载(如场景加载)等操作,仍然需要通过Unity自带的API来实现
- 不适用于每一类异步操作,尤其是涉及到Unity特有的对象和接口时
Coroutine
Unity Coroutine是一种允许在多帧中分布执行代码的机制,它通常用于处理一些需要在多个帧之间等待的任务,比如延时操作、动画播放、资源加载等
协程本质上是通过一种特殊的方式执行代码,它可以在执行过程中“暂停”并在后续的帧继续执行
协程是通过StartCoroutine()来启动的。协程通常返回一个IEnumerator类型的方法
using UnityEngine;
using System.Collections;
public class CoroutineExample : MonoBehaviour
{
void Start() => StartCoroutine(MyCoroutine());
IEnumerator MyCoroutine()
{
//在这里执行某些操作
Debug.Log("协程开始");
// Wait 2 seconds
yield return new WaitForSeconds(2);
// 等待结束后继续执行
Debug.Log("2秒后继续执行");
// 继续执行其他操作
yield return null; // 等待下一帧
Debug.Log("协程执行完毕");
}
}
在这个例子中,MyCoroutine协程将在开始时打印“协程开始”,然后等待2秒后打印“2秒后继续执行”,最后在下一帧打印“协程执行完毕”
协程可以通过yield return暂停执行,直到某个条件满足。常见的暂停类型有:
WaitForSeconds:等待指定的时间WaitForEndOfFrame:等待当前帧渲染结束后继续执行WaitForFixedUpdate:等待下一次物理更新null:等到下一帧执行
协程并不是自动停止的,你需要显示地停止它
使用StopCoroutine()方法可以停止某个协程,或者通过StopAllCoroutine()停止当前对象的所有协程
StartCoroutine(MyCoroutine());
StopCoroutine(MyCoroutine());
协程通常返回一个IEnumerator,但也可以有不同的返回类型,比如WaitForSeconds或其他等待条件类型
协程的优缺点
优点:
- 简洁性:相比于传统的
Update或者使用定时器的方式,协程让代码更简洁、更易于理解 - 灵活性:可以处理复杂的等待逻辑,比如按帧延迟、动态等待、分步执行等
- 性能优化:协程可以有效避免不必要的多次计算或事件处理,提升游戏性能
缺点:
- 容易受到Unity引擎主线程调度的影响
- 错误处理不如
async/await简单
注意
- 协程是在主线程中执行的,所以它们会被游戏的主循环驱动,而不能跨线程操作数据
- 协程一旦启动,默认会在对象生命周期内有效,如果对象被销毁,协程会自动停止
- 如果需要频繁控制协程的暂停或停止,可能需要考虑使用更复杂的状态机或事件系统来更好的管理它们
进阶应用
协程不仅仅仅限于等待固定时间,也可以与其他逻辑结合实现复杂的功能
等待某个条件满足后继续执行
IEnumerator WaitForCondition()
{
while (!someCondition) yield return null;
Debug.Log("条件满足,继续执行");
}
动态修改等待时间
IEnumerator DynamicWait(float time)
{
yield return new WaitForSeconds(time);
Debug.Log("等待结束");
}
实现动画或缓动 可以用协程来实现逐个改变的某个值,例如实现一个平滑的动画过渡
IEnumerator LerpPosition(Vector3 targetPosition, float duration)
{
Vector3 startPosition = transform.position;
float timeElapsed = 0f;
while (timeElapsed < duration)
{
transform.position = Vector3.Lerp(startPosition, targetPosition, timeElapsed / duration);
timeElapsed += Time.deltaTime;
yield return null;
}
transform.position = targetPosition;
}
协程的底层机制
在Unity中,协程的执行是通过IEnumerator类型的函数来定义的,协程的调用、暂停、恢复都与Unity的主线程紧密结合
协程不是传统意义上的线程,而是通过Unity引擎内部的协程调度系统来管理的
通过StartCoroutine()方法启动,这个方法接收一个IEnumerator类型的函数,或者是一个字符串(表示方法名)
StartCoroutine(MyCoroutine());
StartCoroutine("MyCoroutine");
当调用StartCoroutine()时,Unity会为该协程分配一个任务,并把它加入到协程调度队列中
之后Unity的主循环会负责在每一帧执行协程的代码
协程本质上时被Unity的引擎框架所调度的,协程代码并不会一次性执行完,而是会按需执行
调度流程
1.挂起状态:当协程执行到yield return语句时,Unity会暂停协程的执行,并把协程的执行状态保存下来(即当前的执行位置和上下文)
2.等待状态:协程会等待指定的时间、条件、或事件。在等待期间,协程的执行被挂起
3.恢复执行:当协程等待的条件满足,Unity会再次将协程的执行任务加入到下一帧的调度队列中,并从挂起点继续执行
协程调度的底层实现机制
大致底层实现:
- 每个协程有一个状态机,包含当前的执行位置、等待的条件等信息
- Unity会管理所有的协程的队列,在每帧中,根据协程的状态和等待条件,决定哪些协程应该继续执行,哪些需要暂停或恢复
- Unity引擎通过
MonoBehaviour类的Update()函数来调度协程,保证协程的状态更新和执行是与游戏主线程同步的 - 每次协程的恢复操作本质上是在下一帧的
Update()或LateUpdate()中继续执行。协程的执行是由Unity内部的协程管理系统控制的
协程的性能与限制
- 协程过多会影响性能:如果你创建了大量的协程,并且每个协程的执行时间都比较长,可能会导致性能下降。建议根据时间需求合理使用协程
- 协程不能跨线程:协程只能在主线程上允许,它们并不会生成新的线程,因此不能在协程中执行线程相关的任务
- 协程与对象生命周期:协程与对象的生命周期紧密关联,当独享被销毁时,所有挂载该对象上的协程都会自动停止
Unity中协程的状态是如何被保存的
Coroutine对象 协程的状态是通过
Coroutine对象来管理的,每个协程在运行时都会创建一个Coroutine对象,Unity会使用这个对象来跟踪协程执行状态IEnumerator状态机 协程通常返回一个
IEnumerator对象,这实际上就是一个状态机的实现。在协程函数中,你可以通过yield return语句控制协程的执行。当协程遇到yield语句时,Unity会保存当前的执行上下文(如执行位置、局部变量等),并在下一帧继续从这个位置开始执行内存和堆栈 在协程执行过程中,Unity会使用内存中的堆栈来保存函数调用的上下文,每次协程被挂起时,它的局部变量、执行位置等信息会被保存在堆栈中,当协程恢复时,这些信息会被取出,协程继续执行
协程调度器 Unity会管理一个协程调度器,它负责跟踪所有活动的协程,并在每一帧更新它们。协程调度器会检查每个协程的状态,如果协程已经完成,它就会被销毁
状态保存与恢复 Unity通过以下方式来保存和恢复协程状态:
- yield条件:
yield return语句时协程的暂停点,Unity会记录当前的暂停点(如等待的时间、是否等待某个事件等) - 协程生命周期:协程的生命周期和GameObject、MonoBehaviour的生命周期有关,只有当对象被销毁或协程被停止时,协程才会完全退出
- 暂停/恢复机制:Unity的协程机制基于状态机模型,每次
yield返回后,Unity会根据当前的yield值来决定合适恢复协程的执行
结束和清理
当协程结束时,Unity会清理相关的资源并移除协程。若协程被手动停止,Unity会在下一帧停止协程的执行,并释放相关资源
IEnumerator接口
IEnumerator是C#中的一个接口,它广泛用于实现迭代器模式,通过IEnumerator,我们可以控制集合的遍历、生成懒加载的数据、以及其他的一些需要“暂停”和“恢复”的操作,比如Unity中的协程
IEnumerator接口概述
IEnumerator是C#中用来实现迭代器模式的接口,它定义了两种方法和一个属性:
MoveNext():移动到集合中的下一个元素,返回true或false表示是否还有更多元素可供迭代Current:返回当前元素Reset():将迭代器重置到初始状态(不常用,部分实现会抛出异常)
public interface IEnumerator
{
bool MoveNect();
object Current { get; }
void Reset();
}
IEnumerator在迭代器中的使用
IEnumerator主要用于构建“迭代器”,用于按顺序遍历集合中的元素。一个典型的实现例子是一个自定义集合类,它提供了一个迭代器来遍历集合中的元素:
public class MyCollection : IEnumerable
{
private int[] numbers = {1, 2, 3, 4, 5};
public IEnumerator GetEnumerator() => return new MyEnumerator(numbers);
private class MyEnumerator : IEnumerator
{
private int[] _numbers;
private int _index = -1;
public MyEnumerator(int[] numbers) => _numbers = numbers;
public bool MoveNext()
{
_index++;
return _index < _numbers.Length;
}
public object Current => _numbers[_index];
public void Reset() => _index = -1;
}
}
在这个例子中,MyCollection实现了IEnumerable接口,这样它就可以被foreach遍历GetEnumerator方法返回一个IEnumerator实例,负责控制迭代过程
yield return和IEnumerator
在C#中,yield return是实现IEnumerator接口的一种简化方式,当你使用yield return时,编译器会自动生成一个迭代器类,并且每次yield作为一个暂停点来保存当前的状态
示例:使用yield return返回值
public class MyCollection
{
public IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
}
GetNumbers返回了一个IEnumerable<int>类型的对象,这意味着可以通过foreach来遍历这个集合。
每次迭代时,yield return会暂停方法的执行,并将当前值返回给调用者,这个过程在每次调用MoveNext时自动恢复
IEnumerator与Unity协程
Unity中的协程是基于IEnumerator实现的,它是延迟执行的核心。协程的执行通过yield return来暂停,Unity引擎管理协程的调度和恢复
yield return与状态机
在使用yield return时,C#编译器会将方法转化为状态机,每次遇到yield return时,编译器会保存当前方法的执行状态(局部变量、执行信息等),并返回一个IEnumerator迭代器,这个迭代器控制着何时继续执行代码,何时返回控制权
状态机生成示例 假设有协程:
IEnumerator ExampleCoroutine()
{
Debug.Log("Step 1");
yield return new WairForSeconds(1f);
Debug.Log("Step 2");
}
编译器会将这个代码转化成类似以下的状态机:
private class ExampleCoroutineStateMachine : IEnumerator
{
private int _state = 0;
private bool _isPaused = false;
public bool MoveNext()
{
switch (_state)
{
case 0:
Debug.Log("Step 1");
_state = 1;
return true; // 暂停,等待外部恢复
case 1:
if (_isPaused)
{
_isPaused = false; // 模拟等待1秒
Debug.Log("Step 2");
return false; // 完成
}
return true; // 继续执行
default:
return false; // 协程结束
}
}
public object Current => null;
}
每次协程执行时,状态机会检查当前状态,并决定是否继续执行或暂停
状态保存与恢复:IEnumerator和Unity协程
当Unity执行协程时,每次遇到yield return,它会将协程的执行状态(包括局部变量、执行栈、当前指令位置等)保存到内存中。
Unity引擎管理一个协程调度器,这个调度器负责在每一帧检查协程的状态,并恢复或继续执行这些协程yield表达式在背后通过IEnumerator实现,并通过Unity的调度器继续执行
自定义IEumerator
可以自定义自己的IEnumerator来控制更复杂的行为
public class WaitForCondition : CustomYieldInstruction
{
private Func<bool> _condition;
public WaitForCondition(Func<bool> condition) => _condition = condition;
public override bool keepWaiting => !_condition(); // 直到条件满足才结束
}
