[Unity] 基于迭代器的协程底层原理详解

这篇具有很好参考价值的文章主要介绍了[Unity] 基于迭代器的协程底层原理详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Unity 是单线程设计的游戏引擎, 所有对于 Unity 的调用都应该在主线程执行. 倘若我们要实现另外再执行一个任务, 该怎么做呢? 答案就是协程.

协程本质上是基于 C# yield 迭代器的, 使用 yield 语法生成的返回迭代器的方法, 其内部的逻辑执行, 是 “懒” 的, 只有在调用 MoveNext 的时候, 才会继续执行下一步逻辑.


Unity 生命周期

我们知道, Unity 在运行的时候, 本质上是有一个主循环, 不断的调用所有游戏对象的各个事件函数, 诸如 Update, LateUpdate, FixedUpdate, 以及在这个主循环中, 进行游戏主逻辑的更新. 其中协程的处理也是在这里完成的.

Unity 在每一个游戏对象中都维护一个协程的列表, 该对象启动一个协程的时候, 该协程的迭代器就会被放置到 “正在执行的协程” 列表中. Unity 每一帧都会对他们进行判断, 是否应该调用 MoveNext 方法.

又因为迭代器有 “懒执行” 的特性, 所以就能够实现, 等待某些操作结束, 然后执行下一段逻辑.

关于迭代器懒执行, 参考: [C#] 基于 yield 语句的迭代器逻辑懒执行


仿写协程

光是口述, 肯定是无法讲明白协程原理的, 下面将使用代码简单实现一个协程.

我们游戏引擎将有以下文件:

  • GameEngine : 游戏引擎, 存储所有的游戏对象
  • GameObject : 表示一个游戏对象, 将会存储其正在运行的协程
  • GameObjectStates : 表示一个游戏对象的状态, 例如它是否已经启动, 是否被销毁
  • Coroutine : 表示一个正在运行的协程
  • WaitForSeconds : 表示一个要等待的对象, 它将使协程暂停执行指定秒数
  • Program : 游戏引擎的主循环逻辑

以及用户的逻辑:

  • MyGameObject : 用户自定义的游戏对象

首先创建一个 GameEngine 类, 它将容纳当前创建好的所有游戏对象.

public class GameEngine
{
    // 私有构造函数, 使外部无法直接被调用
    private GameEngine()
    { }

    // 单例模式
    public static GameEngine Current { get; } = new();

    // 所有的游戏对象
    internal List<GameObject> _allGameObjects = new();

    // 通过 ReadOnlyList 向外暴露所有游戏对象
    public IReadOnlyList<GameObject> AllGameObjects => _allGameObjects;
    public int FrameNumber { get; internal set; }
}

创建一个 WaitForSeconds 类, 它和 Unity 中的 WaitForSeconds 类一样, 用于在写成中通过 yield 返回实现等待指定时间.

public class WaitForSeconds
{
    public WaitForSeconds(float seconds)
    {
        Seconds = seconds;
    }

    public float Seconds { get; }
}

接下来, 创建一个 Coroutine 类, 它表示一个正在运行的协程, 构造时, 传入协程要执行的逻辑, 也就是一个 IEnumerator. 其中, 包含一个 “当前的等待对象” 以及 “当前等待对象相关联的某些参数数据”. 它的 Update 方法会在游戏主循环中不断被调用.

using System.Collections;

public class Coroutine
{
    public Coroutine(IEnumerator enumerator)
    {
        Enumerator = enumerator;
    }

    public IEnumerator Enumerator { get; }

    // 当前等待对象
    object? currentWaitable;

    // 与当前等待对象相关联的参数信息
    object? currentWaitableParameter;

    public bool IsCompleted { get; set; }

    internal void Update()
    {
        // 如果当前协程已经结束, 就不再进行任何操作
        if (IsCompleted)
            return;

        // 如果当前没有要等待的对象
        if (currentWaitable == null)
        {
            // 执行迭代器的 "MoveNext"
            if (!Enumerator.MoveNext())
            {
                // 如果迭代器返回了 false, 也就是迭代器没有下一个数据了
                // 则表示当前协程已经运行结束, 做上标记, 然后返回
                IsCompleted = true;
                return;
            }

            // 如果当前等待对象是 "等待指定秒"
            if (Enumerator.Current is WaitForSeconds waitForSeconds)
            {
                // 保存当前等待对象
                currentWaitable = waitForSeconds;

                // 将当前时间作为参数存起来
                currentWaitableParameter = DateTime.Now;
            }
            else if (Enumerator.Current is Coroutine coroutine)
            {
                // 如果当前等待对象是另一个协程
                // 保存当前等待对象
                currentWaitable = coroutine;
            }
        }
        else   // 否则, 也就是当当前等待对象不为空时
        {
            // 如果当前等待对象是 "等待指定秒"
            if (currentWaitable is WaitForSeconds waitForSeconds)
            {
                DateTime startTime = (DateTime)currentWaitableParameter!;
                
                // 判断是否等待结束
                if ((DateTime.Now - startTime).TotalSeconds >= waitForSeconds.Seconds)
                {
                    // 如果等待结束, 那么就将当前等待对象置空
                    // 这样下一次被调用 Update 时, 就会通过调用迭代器 MoveNext
                    // 执行协程的下一段逻辑, 并且获取下一个等待对象
                    currentWaitable = null;
                }
            }
            else if (currentWaitable is Coroutine coroutine)
            {
                // 如果等待对象是协程, 并且对应协程已经执行完毕
                if (coroutine.IsCompleted)
                {
                    // 将当前等待对象置空
                    currentWaitable = null;
                }
            }
        }
    }
}

编写一个 GameObjectStates 来表示一个游戏对象的状态, 例如是否启动了, 是否被销毁了什么的.

internal class GameObjectStates
{
    // 对应游戏对象
    public GameObject Target { get; }

    // 是否已经启动
    public bool Started { get; set; }

    // 是否已经被销毁
    public bool Destroyed { get; set; }

    public GameObjectStates(GameObject target)
    {
        Target = target;
    }
}

下面, 编写一个 GameObject, 因为协程是运行在游戏对象中的, 所以游戏对象会有一个容器来承载当前游戏对象正在运行的协程. 当然, 它也有 StartUpdate 两个虚方法, 会被游戏的主逻辑调用.

using System.Collections;

public class GameObject
{
    // 当前游戏对象的状态
    internal GameObjectStates States { get; }

    // 所有正在运行的协程
    List<Coroutine> coroutines = new();

    // 即将开始运行的协程
    List<Coroutine> coroutinesToAdd = new();

    // 将要被删除的协程
    List<Coroutine> coroutinesToRemove = new();

    public GameObject()
    {
        // 初始化状态
        States = new(this);

        // 将当前游戏对象添加到游戏引擎
        GameEngine.Current._allGameObjects.Add(this);
    }

    // 由游戏引擎调用的 Start 和 Update
    public virtual void Start() { }
    public virtual void Update() { }

    // 由游戏引擎调用的, 更新所有协程的逻辑
    internal void UpdateCoroutines()
    {
        // 将需要添加的所有协程添加到当前正在运行的协程中
        foreach (var coroutine in coroutinesToAdd)
        {
            coroutines.Add(coroutine);
        }

        coroutinesToAdd.Clear();

        // 更新当前所有协程
        foreach (var coroutine in coroutines)
        {
            coroutine.Update();

            // 如果当前协程已经执行完毕, 则将其添加到 "删除列表" 中
            if (coroutine.IsCompleted)
            {
                coroutinesToRemove.Add(coroutine);
            }
        }

        // 将准备删除的所有协程从当前运行的协程列表中删除
        foreach (var coroutine in coroutinesToRemove)
        {
            coroutines.Remove(coroutine);
        }

        coroutinesToRemove.Clear();
    }

    // 开启一个协程
    public Coroutine StartCoroutine(IEnumerator enumerator)
    {
        Coroutine coroutine = new(enumerator);
        coroutinesToAdd.Add(coroutine);

        return coroutine;
    }

    // 停止一个协程
    public void StopCoroutine(Coroutine coroutine)
    {
        coroutinesToRemove.Add(coroutine);
    }

    // 停止一个协程
    public void StopCoroutine(IEnumerator enumerator)
    {
        int index = coroutines.FindIndex(c => c.Enumerator == enumerator);
        if (index != -1)
            coroutinesToRemove.Add(coroutines[index]);
    }

    // 销毁当前游戏对象
    public void DestroySelf()
    {
        States.Destroyed = true;
    }
}

自定义一个游戏对象 MyGameObject, 它在 Start 时启动一个协程.

using System.Collections;

class MyGameObject : GameObject 
{
    public override void Start()
    {
        base.Start();
        StartCoroutine(MyCoroutineLogic());
    }


    IEnumerator MyCoroutineLogic()
    {
        System.Console.WriteLine("Logic out");
        yield return StartCoroutine(MyCoroutineLogicInner());
        yield return new WaitForSeconds(3);
        System.Console.WriteLine("Logic out end");
    }

    IEnumerator MyCoroutineLogicInner() 
    {
        for (int i = 0; i < 5; i++)
        {
            yield return new WaitForSeconds(1);
            Console.WriteLine($"Coroutine inner {i}");
        }
    }
}

程序主逻辑, 创建自定义的游戏对象, 并执行主循环:

// 创建自定义的游戏对象
new MyGameObject();

// 要被销毁的游戏对象
List<GameObject> objectsToDestroy = new();

while (true)
{
    // 对所有游戏对象执行 Start
    foreach (var obj in GameEngine.Current.AllGameObjects)
    {
        if (!obj.States.Started)
        {
            obj.Start();
            obj.States.Started = true;
        }
    }

    // 调用所有游戏对象的 Update
    foreach (var obj in GameEngine.Current.AllGameObjects)
    {
        if (obj.States.Destroyed)
            continue;
            
        obj.Update();
    }

    // 更新所有游戏对象的协程
    foreach (var obj in GameEngine.Current.AllGameObjects)
    {
        if (obj.States.Destroyed)
            continue;

        obj.UpdateCoroutines();
    }

    // 将需要被销毁的游戏对象存起来
    objectsToDestroy.Clear();
    foreach (var obj in GameEngine.Current.AllGameObjects)
    {
        if (obj.States.Destroyed)
            objectsToDestroy.Add(obj);
    }

    // 从游戏引擎中移出游戏对象
    foreach (var obj in objectsToDestroy)
        GameEngine.Current._allGameObjects.Remove(obj);
}

执行结果:

Logic out
Coroutine inner 0
Coroutine inner 1
Coroutine inner 2
Coroutine inner 3
Coroutine inner 4
Logic out end

总结

综上所述, 可以了解到, Unity 协程的本质无非就是在合适的实际执行迭代器的 MoveNext 方法. 对当前正在等待的对象进行条件判断, 如果满足条件, 则 MoveNext, 否则就不执行.文章来源地址https://www.toymoban.com/news/detail-793536.html

到了这里,关于[Unity] 基于迭代器的协程底层原理详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Unity3D中的C#协程(概念、使用方法、底层原理)

             Unity3D 中的协程是针对 Unity3D 框架和 C# 编程语言定制的 ,具有便捷的使用方式和良好的效率。其他语言Python、Lua等也支持协程,但是底层实现的细节可能不同。在 Unity3D 引擎中, 协程被 Unity3D 引擎的主循环所驱动 。         协程(Coroutine)是一种编程概念

    2024年02月08日
    浏览(42)
  • Golang的协程调度器原理及GMP设计思想

    (1) 单进程时代不需要调度器 我们知道,一切的软件都是跑在操作系统上,真正用来干活(计算)的是CPU。早期的操作系统每个程序就是一个进程,知道一个程序运行完,才能进行下一个进程,就是“单进程时代” 一切的程序只能串行发生。 早期的单进程操作系统,面临2个问题

    2024年02月16日
    浏览(31)
  • Unity 协程底层 简单解释

    总纲功能 程序分帧执行 (挂起程序延迟执行) 在Update 之后 和 在 LastUpdate 之前 提高运行效率 协程应用上的小知识 协程和主程是 并行 执行的,当协程被挂起的时候 并不影响主程 StopCoroutine 停止的协程一定要是String类型的,不能使用方法调用的形式,有时候会出错,如可

    2024年03月15日
    浏览(31)
  • 【c#每日一记】所以迭代器的原理知识你还清楚吗?

    👨‍💻个人主页 :@元宇宙-秩沅 👨‍💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍💻 本文由 秩沅 原创 👨‍💻 收录于专栏 : unityc#专题 ⭐【Unityc#专题篇】之c#核心篇】 ⭐【Unityc#专题篇】之c#基础篇】 ⭐【Unity-c#专题篇】之c#入门篇) ⭐【Unityc#专题篇】—基础

    2024年02月07日
    浏览(24)
  • Golang中的协程(上)

    在Golang中,协程(Coroutine)是一种轻量级的执行单位,可以理解为独立的并发任务。在本篇博客中,我们将详细分析介绍Golang中的协程,包括协程的概念、存在的原因、实现方法、运行方式、案例讲解以及与主线程的关系等内容。 协程是一种轻量级的线程,拥有自己的堆栈和

    2024年02月15日
    浏览(29)
  • Python、Rust中的协程

    协程在不同的堆栈上同时运行,但每次只有一个协程运行,而其调用者则等待: F启动G,但G并不会立即运行,F必须显式的恢复G,然后 G 开始运行。 在任何时候,G 都可能转身并让步返回到 F。这会暂停 G 并继续 F 的恢复操作。 F再次调用resume,这会暂停F并继续G的yield。它们不

    2024年02月09日
    浏览(26)
  • Golang中的协程(goroutine)

    目录 进程 线程 并发 并行 协程(goroutine)  使用sync.WaitGroup等待协程执行完毕 多协程和多线程         进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程

    2024年02月05日
    浏览(29)
  • ET介绍——C#更好的协程

    上文讲了一串回调就是协程,显然这样写代码,增加逻辑,插入逻辑非常容易出错。我们需要利用异步语法把这个异步回调的形式改成同步的形式,幸好C#已经帮我们设计好了,看代码   在这段代码里面,WaitTimeAsync方法中,我们利用了TaskCompletionSource类替代了之前传入的Act

    2024年02月05日
    浏览(31)
  • go的协程和管道运用案例

    2024年01月19日
    浏览(30)
  • 【react】react18的学习(十二)– 底层原理(二)之 迭代器 iterator

    迭代器iterator 是一种 ES6 规范,具有这种机制的数据结构才可以使用 for of 循环:返回每一项的值; 原型链具有 Symbol.iterator 属性的数据结构都具备;如数组、部分类数组、字符串等; 普通对象就不能用; for-of循环原理:循环获取属性值; 执行可迭代原型链上的 Symbol.iterat

    2024年02月16日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包