Unity-c#-协程

Unity协程(Coroutine)详解

1. 协程基础概念

1.1 什么是协程?

协程是Unity中一种特殊的程序执行方式,它允许我们将一个长时间的操作分散到多个帧中执行,而不是在一帧内完成所有操作。可以将协程理解为一种"能够暂停执行"的函数。

与普通函数相比,协程具有以下特点:

  1. 可暂停性

    • 可以在执行过程中暂停
    • 可以在特定条件下恢复执行
    • 不会阻塞主线程
  2. 延时执行

    • 可以等待指定时间后继续执行
    • 可以等待某个条件满足后继续
    • 可以分帧执行耗时操作
  3. 灵活控制

    • 可以随时启动和停止
    • 可以同时运行多个协程
    • 可以控制执行顺序和时机

1.2 为什么需要协程?

在游戏开发中,协程解决了许多实际问题:

  1. 性能优化

    • 将耗时操作分散到多帧执行
    • 避免卡顿和掉帧
    • 更好的资源利用
  2. 时序控制

    • 实现延时执行
    • 控制动画和特效的播放
    • 管理游戏流程和状态转换
  3. 异步操作

    • 加载资源
    • 网络请求
    • 场景切换

2. 协程的基本用法

2.1 协程的声明和启动

public class CoroutineExample : MonoBehaviour
{
    
    
    // 声明协程
    private IEnumerator SimpleCoroutine()
    {
    
    
        Debug.Log("协程开始");
        yield return new WaitForSeconds(2f);  // 等待2秒
        Debug.Log("协程结束");
    }

    // 启动协程的几种方式
    private void Start()
    {
    
    
        // 方式1:直接启动
        StartCoroutine(SimpleCoroutine());

        // 方式2:通过字符串启动(不推荐,因为不类型安全)
        StartCoroutine("SimpleCoroutine");

        // 方式3:保存协程引用
        Coroutine coroutine = StartCoroutine(SimpleCoroutine());
    }
}

2.2 协程的暂停和等待

private IEnumerator WaitExample()
{
    
    
    // 等待一帧
    yield return null;

    // 等待固定时间
    yield return new WaitForSeconds(1f);

    // 等待固定时间(不受时间缩放影响)
    yield return new WaitForSecondsRealtime(1f);

    // 等待直到下一个固定物理更新
    yield return new WaitForFixedUpdate();

    // 等待直到条件满足
    yield return new WaitUntil(() => someCondition == true);

    // 等待直到条件不满足
    yield return new WaitWhile(() => someCondition == true);

    // 等待异步操作完成
    yield return StartCoroutine(AnotherCoroutine());
}

3. 实际应用示例

3.1 渐变效果实现

public class FadeEffect : MonoBehaviour
{
    
    
    private Image fadeImage;
    
    private IEnumerator FadeIn(float duration)
    {
    
    
        float elapsedTime = 0f;
        Color color = fadeImage.color;

        while (elapsedTime < duration)
        {
    
    
            elapsedTime += Time.deltaTime;
            float alpha = Mathf.Lerp(1f, 0f, elapsedTime / duration);
            fadeImage.color = new Color(color.r, color.g, color.b, alpha);
            yield return null;
        }
    }

    private IEnumerator FadeOut(float duration)
    {
    
    
        float elapsedTime = 0f;
        Color color = fadeImage.color;

        while (elapsedTime < duration)
        {
    
    
            elapsedTime += Time.deltaTime;
            float alpha = Mathf.Lerp(0f, 1f, elapsedTime / duration);
            fadeImage.color = new Color(color.r, color.g, color.b, alpha);
            yield return null;
        }
    }
}

3.2 技能冷却系统

public class SkillSystem : MonoBehaviour
{
    
    
    private Dictionary<string, bool> skillCooldowns = new Dictionary<string, bool>();

    private IEnumerator SkillCooldown(string skillName, float cooldownTime)
    {
    
    
        skillCooldowns[skillName] = true;
        yield return new WaitForSeconds(cooldownTime);
        skillCooldowns[skillName] = false;
    }

    public void CastSkill(string skillName, float cooldownTime)
    {
    
    
        if (!skillCooldowns.ContainsKey(skillName) || !skillCooldowns[skillName])
        {
    
    
            // 使用技能
            Debug.Log($"使用技能: {
      
      skillName}");
            // 启动冷却
            StartCoroutine(SkillCooldown(skillName, cooldownTime));
        }
        else
        {
    
    
            Debug.Log($"技能 {
      
      skillName} 正在冷却中");
        }
    }
}

4. 协程的高级特性

4.1 协程的嵌套

private IEnumerator MainCoroutine()
{
    
    
    Debug.Log("主协程开始");
    
    // 等待子协程完成
    yield return StartCoroutine(SubCoroutine());
    
    Debug.Log("主协程结束");
}

private IEnumerator SubCoroutine()
{
    
    
    Debug.Log("子协程开始");
    yield return new WaitForSeconds(1f);
    Debug.Log("子协程结束");
}

4.2 协程的取消和管理

public class CoroutineManager : MonoBehaviour
{
    
    
    private List<Coroutine> activeCoroutines = new List<Coroutine>();

    public void StartManagedCoroutine(IEnumerator routine)
    {
    
    
        Coroutine coroutine = StartCoroutine(routine);
        activeCoroutines.Add(coroutine);
    }

    public void StopAllManagedCoroutines()
    {
    
    
        foreach (var coroutine in activeCoroutines)
        {
    
    
            if (coroutine != null)
                StopCoroutine(coroutine);
        }
        activeCoroutines.Clear();
    }

    private void OnDisable()
    {
    
    
        StopAllManagedCoroutines();
    }
}

5. 最佳实践和注意事项

5.1 性能考虑

  1. 避免过多协程

    • 协程虽然轻量,但数量过多会影响性能
    • 考虑使用对象池管理频繁创建的协程
    • 及时停止不需要的协程
  2. 合理使用yield

    • 避免空的yield return
    • 使用适当的等待类型
    • 考虑使用WaitForSecondsRealtime代替WaitForSeconds

5.2 常见陷阱

  1. 生命周期问题

    • 协程会在GameObject禁用时自动停止
    • 在OnDisable中停止所有协程
    • 注意协程和MonoBehaviour的生命周期关系
  2. 错误处理

    • 协程中的异常不能用try-catch捕获
    • 需要在协程内部处理异常
    • 使用自定义的协程包装器处理错误

5.3 调试技巧

public class CoroutineDebugger : MonoBehaviour
{
    
    
    private IEnumerator DebugCoroutine(IEnumerator routine)
    {
    
    
        Debug.Log($"协程开始: {
      
      Time.time}");
        
        while (true)
        {
    
    
            try
            {
    
    
                if (!routine.MoveNext())
                    break;
            }
            catch (System.Exception e)
            {
    
    
                Debug.LogError($"协程执行错误: {
      
      e.Message}");
                yield break;
            }

            yield return routine.Current;
        }

        Debug.Log($"协程结束: {
      
      Time.time}");
    }

    public void StartDebugCoroutine(IEnumerator routine)
    {
    
    
        StartCoroutine(DebugCoroutine(routine));
    }
}

6. 实际应用场景

6.1 资源加载

private IEnumerator LoadAssetAsync()
{
    
    
    // 异步加载资源
    ResourceRequest request = Resources.LoadAsync<GameObject>("Prefabs/Character");
    
    // 等待加载完成
    while (!request.isDone)
    {
    
    
        float progress = request.progress * 100f;
        Debug.Log($"加载进度: {
      
      progress}%");
        yield return null;
    }

    // 使用加载的资源
    GameObject character = Instantiate(request.asset as GameObject);
}

6.2 场景切换

private IEnumerator LoadSceneWithTransition(string sceneName)
{
    
    
    // 开始淡出
    yield return StartCoroutine(FadeOut(1f));

    // 异步加载场景
    AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
    asyncLoad.allowSceneActivation = false;

    while (asyncLoad.progress < 0.9f)
    {
    
    
        Debug.Log($"场景加载进度: {
      
      asyncLoad.progress * 100}%");
        yield return null;
    }

    // 等待淡出完成
    asyncLoad.allowSceneActivation = true;
    
    // 开始淡入
    yield return StartCoroutine(FadeIn(1f));
}

协程是Unity中非常强大的功能,合理使用可以让游戏逻辑更加清晰,性能更好。在实际开发中,需要根据具体情况选择合适的使用方式,并注意避免常见陷阱。