当前位置: 首页 > news >正文

[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 和 Updatepublic 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)
{// 对所有游戏对象执行 Startforeach (var obj in GameEngine.Current.AllGameObjects){if (!obj.States.Started){obj.Start();obj.States.Started = true;}}// 调用所有游戏对象的 Updateforeach (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, 否则就不执行.

相关文章:

  • LRU算法(面试遇到两次)
  • 代码随想录——链表 刷题记录
  • Scrapy的crawlspider爬虫
  • 普通二叉树和右倾斜二叉树--LeetCode 111题《Minimum Depth of Binary Tree》
  • 可视化数据监控大屏网页界面,数据大屏模版PS资料(免费UI源文件)
  • conda命令克隆(复制)环境
  • windows wsl2 ubuntu上部署 redroid云手机
  • 英语四六级作文常用高级表达汇总(持续更新)
  • Apache Web 服务器监控工具
  • SpringBoot+Netty+Websocket实现消息推送
  • OpenHarmony应用开发——创建第一个OpenHarmonry工程
  • 广州旅游攻略(略说一二)
  • 算法leetcode|92. 反转链表 II(rust重拳出击)
  • 【面经】2024年软件测试面试题大全(持续更新)附答案
  • Angular中使用Intersection Observer API实现无限滚动
  • Angular 2 DI - IoC DI - 1
  • Angular 响应式表单之下拉框
  • - C#编程大幅提高OUTLOOK的邮件搜索能力!
  • JavaScript HTML DOM
  • java中的hashCode
  • leetcode386. Lexicographical Numbers
  • Python学习之路16-使用API
  • quasar-framework cnodejs社区
  • rc-form之最单纯情况
  • vue+element后台管理系统,从后端获取路由表,并正常渲染
  • Webpack 4 学习01(基础配置)
  • 不上全站https的网站你们就等着被恶心死吧
  • 测试如何在敏捷团队中工作?
  • 从输入URL到页面加载发生了什么
  • 基于OpenResty的Lua Web框架lor0.0.2预览版发布
  • 理解IaaS, PaaS, SaaS等云模型 (Cloud Models)
  • 聊聊directory traversal attack
  • 那些年我们用过的显示性能指标
  • 如何编写一个可升级的智能合约
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 《天龙八部3D》Unity技术方案揭秘
  • 3月7日云栖精选夜读 | RSA 2019安全大会:企业资产管理成行业新风向标,云上安全占绝对优势 ...
  • 宾利慕尚创始人典藏版国内首秀,2025年前实现全系车型电动化 | 2019上海车展 ...
  • 哈罗单车融资几十亿元,蚂蚁金服与春华资本加持 ...
  • ​​​​​​​ubuntu16.04 fastreid训练过程
  • # Swust 12th acm 邀请赛# [ E ] 01 String [题解]
  • $.ajax,axios,fetch三种ajax请求的区别
  • (1) caustics\
  • (笔试题)分解质因式
  • (草履虫都可以看懂的)PyQt子窗口向主窗口传递参数,主窗口接收子窗口信号、参数。
  • (待修改)PyG安装步骤
  • (非本人原创)我们工作到底是为了什么?​——HP大中华区总裁孙振耀退休感言(r4笔记第60天)...
  • (附源码)springboot 基于HTML5的个人网页的网站设计与实现 毕业设计 031623
  • (附源码)计算机毕业设计SSM疫情下的学生出入管理系统
  • (求助)用傲游上csdn博客时标签栏和网址栏一直显示袁萌 的头像
  • (十五)devops持续集成开发——jenkins流水线构建策略配置及触发器的使用
  • (原創) 如何解决make kernel时『clock skew detected』的warning? (OS) (Linux)
  • (转)ABI是什么
  • (转载)Linux 多线程条件变量同步
  • (转载)从 Java 代码到 Java 堆