协程是Unity中一种强大的编程工具,它允许你在不阻塞主线程的情况下执行跨越多个帧的操作。协程提供了一种优雅的方式来处理需要随时间推移执行的任务,如动画、延时操作、分步骤处理等。
1. 协程的基本概念
什么是协程?
协程是一种特殊的函数,它可以在执行过程中暂停(yield),然后在之后的某个时间点恢复执行。这与普通函数不同,普通函数一旦开始执行就会一直运行到完成。
协程的关键特性:
- 可以暂停执行并在之后恢复
- 不会阻塞主线程
- 使用yield语句来指示暂停点
- 返回类型必须是IEnumerator
- 必须使用StartCoroutine()方法启动
协程与线程的区别
协程不是线程。关键区别:
- 协程在主线程上运行,不是并行执行
- 协程没有线程切换的开销
- 协程不需要线程同步机制
- 协程是协作式多任务处理,而不是抢占式
2. 协程的基本语法
定义协程
private IEnumerator MyCoroutine()
{
Debug.Log("协程开始");
// 等待5秒
yield return new WaitForSeconds(5f);
Debug.Log("5秒后继续执行");
// 等待下一帧
yield return null;
Debug.Log("在下一帧继续执行");
}
启动协程
void Start()
{
// 方法1:直接启动
StartCoroutine(MyCoroutine());
// 方法2:通过字符串名称启动(不推荐,因为没有编译时检查)
StartCoroutine("MyCoroutine");
// 方法3:保存协程引用以便之后停止
Coroutine coroutineHandle = StartCoroutine(MyCoroutine());
}
停止协程
// 方法1:通过协程引用停止特定协程
Coroutine coroutineHandle = StartCoroutine(MyCoroutine());
StopCoroutine(coroutineHandle);
// 方法2:通过方法引用停止
StopCoroutine(MyCoroutine());
// 方法3:通过字符串名称停止(不推荐)
StopCoroutine("MyCoroutine");
// 方法4:停止所有协程
StopAllCoroutines();
3. 协程的yield指令
协程使用yield return语句来指定暂停点和恢复条件。Unity提供了多种内置的yield指令:
yield return null
等待到下一帧的Update函数执行完毕后继续。
IEnumerator WaitForNextFrame()
{
Debug.Log("当前帧");
yield return null;
Debug.Log("下一帧");
}
yield return new WaitForSeconds(float seconds)
等待指定的秒数后继续。这个时间是受Time.timeScale影响的。
IEnumerator WaitForFiveSeconds()
{
Debug.Log("开始等待");
yield return new WaitForSeconds(5f);
Debug.Log("5秒后");
}
yield return new WaitForSecondsRealtime(float seconds)
等待指定的秒数,但不受Time.timeScale影响(真实时间)。
IEnumerator WaitRealtime()
{
Time.timeScale = 0.5f; // 游戏时间减慢
Debug.Log("开始等待真实时间");
yield return new WaitForSecondsRealtime(5f);
Debug.Log("5秒真实时间后"); // 无论timeScale如何,都是5秒后执行
}
yield return new WaitForFixedUpdate()
等待到下一次FixedUpdate执行完毕后继续。
IEnumerator WaitForPhysicsUpdate()
{
Debug.Log("等待物理更新");
yield return new WaitForFixedUpdate();
Debug.Log("物理更新后");
}
yield return new WaitForEndOfFrame()
等待到当前帧中的所有摄像机和GUI渲染完成后继续。
IEnumerator CaptureScreenshot()
{
Debug.Log("准备截图");
yield return new WaitForEndOfFrame();
// 此时所有渲染已完成,适合截图
ScreenCapture.CaptureScreenshot("screenshot.png");
Debug.Log("截图完成");
}
yield return new WaitUntil(Func<bool> predicate)
等待直到指定的条件变为true。
private bool isConditionMet = false;
IEnumerator WaitForCondition()
{
Debug.Log("等待条件满足");
yield return new WaitUntil(() => isConditionMet);
Debug.Log("条件已满足");
}
// 在其他地方设置条件
public void SetCondition()
{
isConditionMet = true;
}
yield return new WaitWhile(Func<bool> predicate)
等待直到指定的条件变为false。
private bool isProcessing = true;
IEnumerator WaitForProcessToComplete()
{
StartProcess();
Debug.Log("等待处理完成");
yield return new WaitWhile(() => isProcessing);
Debug.Log("处理已完成");
}
private void StartProcess()
{
// 启动某个处理过程
// 在处理完成时设置 isProcessing = false
}
yield return StartCoroutine(IEnumerator routine)
嵌套协程,等待另一个协程完成后继续。
IEnumerator MainCoroutine()
{
Debug.Log("主协程开始");
yield return StartCoroutine(SubCoroutine());
Debug.Log("子协程完成后继续");
}
IEnumerator SubCoroutine()
{
Debug.Log("子协程开始");
yield return new WaitForSeconds(2f);
Debug.Log("子协程完成");
}
yield return new CustomYieldInstruction
创建自定义的yield指令。
public class WaitForKeyPress : CustomYieldInstruction
{
private KeyCode keyCode;
public WaitForKeyPress(KeyCode key)
{
keyCode = key;
}
public override bool keepWaiting
{
get
{
return !Input.GetKeyDown(keyCode);
}
}
}
// 使用自定义yield指令
IEnumerator WaitForSpaceKey()
{
Debug.Log("请按空格键继续");
yield return new WaitForKeyPress(KeyCode.Space);
Debug.Log("空格键已按下");
}
yield return AsyncOperation
等待异步操作完成,如场景加载、资源加载等。
IEnumerator LoadSceneAsync()
{
Debug.Log("开始加载场景");
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("NextScene");
asyncLoad.allowSceneActivation = false;
while (asyncLoad.progress < 0.9f)
{
Debug.Log("加载进度: " + (asyncLoad.progress * 100) + "%");
yield return null;
}
Debug.Log("加载完成,等待激活");
yield return new WaitForSeconds(1f);
asyncLoad.allowSceneActivation = true;
}
4. 协程的高级用法
参数化协程
可以向协程传递参数:
IEnumerator FadeObject(GameObject obj, float duration, float targetAlpha)
{
CanvasGroup canvasGroup = obj.GetComponent<CanvasGroup>();
float startAlpha = canvasGroup.alpha;
float time = 0;
while (time < duration)
{
time += Time.deltaTime;
canvasGroup.alpha = Mathf.Lerp(startAlpha, targetAlpha, time / duration);
yield return null;
}
canvasGroup.alpha = targetAlpha;
}
// 使用
StartCoroutine(FadeObject(myPanel, 2.0f, 0.0f));
协程返回值
协程本身不能直接返回值,但可以通过回调或引用传递来获取结果:
IEnumerator CalculateWithCallback(int a, int b, System.Action<int> callback)
{
Debug.Log("开始计算");
yield return new WaitForSeconds(2f); // 模拟耗时操作
int result = a + b;
callback(result);
}
// 使用回调获取结果
void Start()
{
StartCoroutine(CalculateWithCallback(5, 3, result => {
Debug.Log("计算结果: " + result);
}));
}
协程链
连续执行多个协程:
IEnumerator CoroutineChain()
{
Debug.Log("开始协程链");
yield return StartCoroutine(FirstTask());
Debug.Log("第一个任务完成");
yield return StartCoroutine(SecondTask());
Debug.Log("第二个任务完成");
yield return StartCoroutine(ThirdTask());
Debug.Log("第三个任务完成");
Debug.Log("协程链完成");
}
IEnumerator FirstTask() { /* ... */ }
IEnumerator SecondTask() { /* ... */ }
IEnumerator ThirdTask() { /* ... */ }
协程状态机
使用协程实现状态机:
private IEnumerator currentState;
void Start()
{
ChangeState(IdleState());
}
void ChangeState(IEnumerator newState)
{
if (currentState != null)
{
StopCoroutine(currentState);
}
currentState = newState;
StartCoroutine(currentState);
}
IEnumerator IdleState()
{
Debug.Log("进入空闲状态");
while (!Input.GetKeyDown(KeyCode.Space))
{
yield return null;
}
Debug.Log("空闲状态结束");
ChangeState(WalkingState());
}
IEnumerator WalkingState()
{
Debug.Log("进入行走状态");
float walkTime = 0;
while (walkTime < 5f)
{
walkTime += Time.deltaTime;
// 执行行走逻辑
yield return null;
}
Debug.Log("行走状态结束");
ChangeState(IdleState());
}
5. 协程的实际应用场景
1. 平滑动画和过渡
IEnumerator SmoothMove(Transform objectToMove, Vector3 targetPosition, float duration)
{
Vector3 startPosition = objectToMove.position;
float elapsedTime = 0;
while (elapsedTime < duration)
{
objectToMove.position = Vector3.Lerp(startPosition, targetPosition, elapsedTime / duration);
elapsedTime += Time.deltaTime;
yield return null;
}
objectToMove.position = targetPosition;
}
2. 延时执行
IEnumerator DelayedAction(float delay, System.Action action)
{
yield return new WaitForSeconds(delay);
action?.Invoke();
}
// 使用
StartCoroutine(DelayedAction(3f, () => {
Debug.Log("3秒后执行");
}));
3. 分帧处理大量数据
IEnumerator ProcessLargeDataSet(List<Data> dataSet)
{
int processedCount = 0;
int totalCount = dataSet.Count;
// 每帧处理10个数据项
const int itemsPerFrame = 10;
while (processedCount < totalCount)
{
int itemsToProcess = Mathf.Min(itemsPerFrame, totalCount - processedCount);
for (int i = 0; i < itemsToProcess; i++)
{
ProcessDataItem(dataSet[processedCount + i]);
}
processedCount += itemsToProcess;
// 更新进度
float progress = (float)processedCount / totalCount;
UpdateProgressBar(progress);
yield return null; // 等待下一帧
}
Debug.Log("所有数据处理完成");
}
4. 异步加载资源
IEnumerator LoadAssetAsync<T>(string path, System.Action<T> onLoaded) where T : UnityEngine.Object
{
ResourceRequest request = Resources.LoadAsync<T>(path);
while (!request.isDone)
{
Debug.Log($"加载进度: {request.progress * 100}%");
yield return null;
}
T asset = request.asset as T;
onLoaded?.Invoke(asset);
}
5. 序列化游戏事件
IEnumerator GameIntroSequence()
{
// 淡入场景
yield return StartCoroutine(FadeScreen(0f, 1f, 2f));
// 显示标题
titleText.gameObject.SetActive(true);
yield return new WaitForSeconds(3f);
// 播放介绍动画
introAnimation.Play();
yield return new WaitForSeconds(introAnimation.clip.length);
// 显示"按任意键继续"
pressAnyKeyText.gameObject.SetActive(true);
// 等待玩家输入
yield return new WaitUntil(() => Input.anyKeyDown);
// 开始游戏
StartGame();
}
6. 实现计时器
private Coroutine timerCoroutine;
public void StartTimer(float duration)
{
if (timerCoroutine != null)
{
StopCoroutine(timerCoroutine);
}
timerCoroutine = StartCoroutine(TimerCoroutine(duration));
}
IEnumerator TimerCoroutine(float duration)
{
float remainingTime = duration;
while (remainingTime > 0)
{
UpdateTimerDisplay(remainingTime);
yield return null;
remainingTime -= Time.deltaTime;
}
remainingTime = 0;
UpdateTimerDisplay(remainingTime);
OnTimerComplete();
}
6. 协程的性能考虑
性能优势
- 避免了每帧重复检查条件的开销
- 不需要维护复杂的状态变量
- 代码更清晰,逻辑更连贯
性能注意事项
- 协程创建开销:启动协程有一定开销,不应在每帧大量创建
- GC压力:yield指令可能产生垃圾回收压力
- 过多活跃协程:同时运行太多协程会影响性能
- 协程嵌套深度:过深的嵌套可能导致栈溢出
优化技巧
重用WaitForSeconds对象以减少GC
// 重用WaitForSeconds对象以减少GC
private readonly WaitForSeconds wait1s = new WaitForSeconds(1f);
IEnumerator OptimizedCoroutine()
{
// 使用缓存的对象而不是每次创建新对象
yield return wait1s;
// 其他代码...
}
对象池复用协程
// 对象池复用协程(示例)
public class CoroutinePool {
static Queue<IEnumerator> pool = new Queue<IEnumerator>(10);
public static IEnumerator Get() {
return pool.Count > 0 ? pool.Dequeue() : new WaitForEndOfFrame();
}
public static void Release(IEnumerator coroutine) {
pool.Enqueue(coroutine);
}
}
7. 协程的限制和注意事项
主要限制
- 不能直接返回值:协程不能像普通函数那样返回值
- 异常处理:协程中的异常可能难以捕获和处理
- 不是真正的异步:协程仍在主线程上运行,不能执行真正的并行操作
- 生命周期绑定:协程绑定到MonoBehaviour,如果对象被禁用或销毁,协程会停止
常见陷阱
- 忘记启动协程:定义了协程但忘记用StartCoroutine启动
- 忘记yield:协程中没有yield语句会立即执行完毕
- 协程停止但没有清理:协程被停止时没有适当清理资源
- 在禁用的GameObject上启动协程:这样的协程不会执行
// 错误示例:在禁用的对象上启动协程
gameObject.SetActive(false);
StartCoroutine(MyCoroutine()); // 不会执行
// 正确做法:确保对象处于激活状态
gameObject.SetActive(true);
StartCoroutine(MyCoroutine());
8. 协程与其他异步模式的比较
协程 vs Unity事件
- 协程:适合顺序执行的操作,代码更线性
- 事件:适合响应式编程,组件间解耦
协程 vs C# async/await
// 协程方式
IEnumerator LoadDataCoroutine()
{
Debug.Log("开始加载");
yield return StartCoroutine(FetchData());
Debug.Log("加载完成");
}
// async/await方式 (需要Unity 2017+)
async void LoadDataAsync()
{
Debug.Log("开始加载");
await FetchDataAsync();
Debug.Log("加载完成");
}
主要区别:
- async/await更现代,支持直接返回值
- 协程更适合Unity的生命周期
- async/await有更好的异常处理
- 协程与Unity的其他系统集成更好
协程 vs UniTask
UniTask是一个第三方库,提供了更强大的异步编程支持:
// 使用UniTask
async UniTask LoadResourcesAsync()
{
var texture = await Resources.LoadAsync<Texture>("texture");
var sprite = await Resources.LoadAsync<Sprite>("sprite");
// 可以直接获取结果
myImage.texture = texture.asset as Texture;
mySprite.sprite = sprite.asset as Sprite;
}
UniTask优势:
- 零GC分配
- 取消支持
- 更好的异常处理
- 与协程兼容但性能更好
9. 实际案例:完整的协程管理系统
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CoroutineManager : MonoBehaviour
{
private static CoroutineManager _instance;
public static CoroutineManager Instance
{
get
{
if (_instance == null)
{
GameObject go = new GameObject("CoroutineManager");
_instance = go.AddComponent<CoroutineManager>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
private Dictionary<string, List<Coroutine>> namedCoroutines = new Dictionary<string, List<Coroutine>>();
// 启动并跟踪协程
public Coroutine RunCoroutine(string name, IEnumerator routine)
{
Coroutine coroutine = StartCoroutine(routine);
if (!namedCoroutines.ContainsKey(name))
{
namedCoroutines[name] = new List<Coroutine>();
}
namedCoroutines[name].Add(coroutine);
return coroutine;
}
// 停止特定名称的所有协程
public void StopCoroutines(string name)
{
if (namedCoroutines.TryGetValue(name, out List<Coroutine> coroutines))
{
foreach (Coroutine coroutine in coroutines)
{
if (coroutine != null)
{
StopCoroutine(coroutine);
}
}
coroutines.Clear();
}
}
// 延迟执行
public Coroutine DelayedCall(float delay, System.Action action, string name = "")
{
return string.IsNullOrEmpty(name)
? StartCoroutine(DelayedCallRoutine(delay, action))
: RunCoroutine(name, DelayedCallRoutine(delay, action));
}
private IEnumerator DelayedCallRoutine(float delay, System.Action action)
{
yield return new WaitForSeconds(delay);
action?.Invoke();
}
// 重复执行
public Coroutine RepeatCall(float interval, System.Action action, int repeatCount = -1, string name = "")
{
return string.IsNullOrEmpty(name)
? StartCoroutine(RepeatCallRoutine(interval, action, repeatCount))
: RunCoroutine(name, RepeatCallRoutine(interval, action, repeatCount));
}
private IEnumerator RepeatCallRoutine(float interval, System.Action action, int repeatCount)
{
int count = 0;
while (repeatCount < 0 || count < repeatCount)
{
yield return new WaitForSeconds(interval);
action?.Invoke();
count++;
}
}
// 平滑值变化
public Coroutine LerpValue(float from, float to, float duration, System.Action<float> onUpdate,
System.Action onComplete = null, string name = "")
{
return string.IsNullOrEmpty(name)
? StartCoroutine(LerpValueRoutine(from, to, duration, onUpdate, onComplete))
: RunCoroutine(name, LerpValueRoutine(from, to, duration, onUpdate, onComplete));
}
private IEnumerator LerpValueRoutine(float from, float to, float duration,
System.Action<float> onUpdate, System.Action onComplete)
{
float time = 0;
while (time < duration)
{
float t = time / duration;
float value = Mathf.Lerp(from, to, t);
onUpdate?.Invoke(value);
time += Time.deltaTime;
yield return null;
}
onUpdate?.Invoke(to);
onComplete?.Invoke();
}
// 序列执行多个操作
public Coroutine Sequence(IEnumerable<IEnumerator> actions, string name = "")
{
return string.IsNullOrEmpty(name)
? StartCoroutine(SequenceRoutine(actions))
: RunCoroutine(name, SequenceRoutine(actions));
}
private IEnumerator SequenceRoutine(IEnumerable<IEnumerator> actions)
{
foreach (IEnumerator action in actions)
{
yield return StartCoroutine(action);
}
}
}
使用示例:
void Start()
{
// 延迟调用
CoroutineManager.Instance.DelayedCall(2f, () => {
Debug.Log("2秒后执行");
}, "delayedActions");
// 重复调用
CoroutineManager.Instance.RepeatCall(1f, () => {
Debug.Log("每秒执行一次");
}, 5, "repeatedActions");
// 平滑变化值
CoroutineManager.Instance.LerpValue(0f, 1f, 3f,
value => { fadeImage.color = new Color(1, 1, 1, value); },
() => { Debug.Log("淡入完成"); },
"fadeEffect");
// 序列执行
List<IEnumerator> sequence = new List<IEnumerator>
{
FadeIn(2f),
ShowMessage(3f),
FadeOut(2f)
};
CoroutineManager.Instance.Sequence(sequence, "introSequence");
}
// 在需要时停止特定组的协程
void OnDisable()
{
CoroutineManager.Instance.StopCoroutines("introSequence");
}
10. 总结
Unity的协程是一种强大而灵活的工具,适用于各种需要随时间推移执行的任务。它们提供了一种清晰、线性的方式来编写异步代码,而不需要复杂的状态管理。
协程的主要优势
- 简化时序操作:轻松实现延迟、动画和序列化操作
- 代码可读性:使异步逻辑更线性、更易理解
- 与Unity集成:与Unity的生命周期和系统紧密集成
- 灵活性:可以暂停、恢复和取消操作
- 低开销:比线程更轻量级
何时使用协程
- 实现随时间推移的效果和动画
- 延迟执行操作
- 分帧处理大量数据
- 等待异步操作完成
- 实现游戏事件序列
- 创建状态机和AI行为
何时避免使用协程
- 需要真正并行执行的CPU密集型任务
- 需要直接返回值的操作(考虑使用回调或async/await)
- 需要精确控制执行时机的操作
- 需要复杂错误处理的操作
协程是Unity开发中不可或缺的工具,掌握它们可以让你的代码更清晰、更高效,并能实现各种复杂的时序效果和游戏机制。