[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, 否则就不执行.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/220505.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从任意时间作为起始点读取intel深度相机录制的bag文件

首先介绍两个函数&#xff0c;第一个是seek()。seek()是RealSense SDK中rs2::playback类的一个方法&#xff0c;用于在回放时设置当前时间。 playback.seek(timestamp); 其中&#xff0c;timestamp是希望回放跳转到的时间点&#xff0c;以微秒为单位。以下是一个实例&#xf…

RocketMQ容器化最佳实践

前言 在上一篇文章基于RocketMQ实现分布式事务我们完成基于消息队列实现分布式事务&#xff0c;为了方便后续的开发和环境统一&#xff0c;我们决定将RocketMQ容器化部署到服务器上。所以这篇文章就来演示一下笔者基于docker-compose完成RocketMQ容器化的过程。 本篇文章为了…

【笔试强化】Day 1

文章目录 一、单选1.2.3.4.5.6. &#xff08;写错&#xff09;7. &#xff08;不会&#xff09;8. &#xff08;常错题&#xff09;9.10. &#xff08;写错&#xff09; 二、编程1. 组队竞赛题目&#xff1a;题解&#xff1a;代码&#xff1a; 2. 删除公共字符题目&#xff1a;…

大数据企业如何使用IP代理进行数据抓取

目录 一、引言 二、IP代理概述 三、为什么大数据企业需要使用IP代理 四、使用IP代理进行数据抓取的步骤 1、获取可用的代理IP 2、配置代理IP 3、设置请求头部信息 4、开始数据抓取 5、错误处理和重试 五、IP代理的注意事项 六、总结 一、引言 随着互联网的快速发展…

freeRtos信号量的使用

一.信号量的基本概念 "give"给出资源&#xff0c;计数值加1&#xff1b;"take"获得资源&#xff0c;计数值减1 二.创建信号量 一开始的时候任务1计算&#xff0c;计算完之后信号量里面的计数值增加1&#xff0c;任务2获得信号量&#xff0c;但是任务2里…

Duplicate keys detected: This may cause an update error.【Vue遍历渲染报错的解决】

今天在写项目时&#xff0c;写到一个嵌套评论的遍历时&#xff0c;控制台出现了一个报错信息&#xff0c;但是并不影响页面的渲染&#xff0c;然后一看这个错的原因是 key值重复&#xff0c;那么问题的解决方式就很简单了。&#xff08;vue for循环读取key值时&#xff0c; key…

Nacos配置Mysql数据库

目录 前言1. 配置2. 测试前言 关于Nacos的基本知识可看我之前的文章: Nacos基础版 从入门到精通云服务器 通过docker安装配置Nacos 图文操作以下Nacos的版本为1.1.3 1. 配置 对应的配置文件路径如下: 对应的application.properties为配置文件 需配置端口号 以及 mysql中的…

价值财务:以业务与财务的双向奔赴,成就合规与增长双赢

随着我国多层次资本市场体系建设的推进以及注册制的实施&#xff0c;越来越多的企业有机会进入资本市场获得更丰富的发展资源和更加广阔的发展空间。但是&#xff0c;无论是已上市公司还是走在 IPO 路上的拟上市公司&#xff0c;持续合规化运行和运营效率与效益的持续提升永远是…

java-集合的补充

常见基础集合汇总 数据结构&#xff1a;栈 数据结构分为&#xff1a; &#xff08;1&#xff09;逻辑结构 &#xff1a;--》思想上的结构--》卧室&#xff0c;厨房&#xff0c;卫生间 ---》线性表&#xff08;数组&#xff0c;链表&#xff09;&#xff0c;图&#xff0c;树&…

国密SSL证书有哪些?一起来看国密SSL证书品牌大合集

早在2011年&#xff0c;我国国家密码管理局就已经对网络安全高度重视&#xff0c;在《关于做好公钥密码算法升级工作的通知》中&#xff0c;明确提出在建和拟建公钥密码基础设施电子认证系统和密钥管理系统应使用国密算法。并随之陆续颁布了《网络安全法》、《密码法》、《关键…

java 执行linux 命令

文章目录 前言一、linux命令执行二、使用步骤三、踩坑 前言 java 执行linux 命令&#xff1b; 本文模拟复制linux文件到指定文件夹后打zip包后返回zip名称&#xff0c;提供给下载接口下载zip&#xff1b; 一、linux命令执行 linux命令执行Process process Runtime.getRunti…

C++ 常函数 常对象 const

this 指针的本质: Person * const this; 常函数 用 const 修饰成员函数时&#xff0c;const 修饰 this 指针指向的内存区域&#xff0c;成员函数体内不可以修改本类中的任何普通成员变量。 void show() const { }//const Person const this;//限制了 this 指针当成员变量类型符…

Linux中ps命令使用指南

目录 1 前言2 ps命令的含义和作用3 ps命令的基本使用4 常用选项参数5 一些常用情景5.1 查看系统中的所有进程&#xff08;标准语法&#xff09;5.2 使用 BSD 语法查看系统中的所有进程5.3 打印进程树5.4 获取线程信息5.5 获取安全信息5.6 查看以 root 用户身份&#xff08;实际…

vue2+Echarts数据可视化 【帕累托图】

接口得到的数据如下 要经过排序 &#xff0c;计算累计百分比得到数据 蓝色 柱状图数据&#xff1a; 取count字段值 横坐标&#xff1a;取 id值 折线图&#xff1a;根据柱状图的数据计算累计百分比 getInterface(data) {getParetoChart(data).then((res) > {if (res) {thi…

使用YOLOv8训练图集详细教程

准备自己的数据集 训练YOLOv8时&#xff0c;选择的数据格式是VOC&#xff0c;因此下面将介绍如何将自己的数据集转换成可以直接让YOLOv8进行使用。 1、创建数据集 我的数据集都在保存在mydata文件夹&#xff08;名字可以自定义&#xff09;&#xff0c;目录结构如下&#xf…

AI编译器及TVM概述

AI编译器 AI编译器有许多不同的类型和品牌&#xff0c;以下是一些常见的AI编译器&#xff1a; TensorFlow&#xff1a;谷歌开发的深度学习框架&#xff0c;它包含了一个用于优化和编译TensorFlow模型的编译器。 PyTorch&#xff1a;一个基于Python的开源深度学习框架&#xf…

03.HTML常用标签

HTML常用标签 0.思维导图大纲 1.基本标签 详细介绍 注意 不要用 br 来增加文本之间的行间隔&#xff0c;应使用 p 元素&#xff0c;或后面即将学到的 CSS margin 属性hr 的语义是分隔&#xff0c;如果不想要语义&#xff0c;只是想画一条水平线&#xff0c;那么应当使用 CSS…

node-static 任意文件读取漏洞复现(CVE-2023-26111)

0x01 产品简介 node-static 是 Node.js 兼容 RFC 2616的 HTTP 静态文件服务器处理模块&#xff0c;提供内置的缓存支持。 0x02 漏洞概述 node-static 存在任意文件读取漏洞&#xff0c;攻击者可通过该漏洞读取系统重要文件&#xff08;如数据库配置文件、系统配置文件&#…

扩展学习|商务智能与社会计算

一、概念介绍 &#xff08;一&#xff09;商务智能 商务智能&#xff08;Business Intelligence&#xff0c;简称BI&#xff09;是一种基于数据分析的决策支持系统&#xff0c;旨在帮助企业或组织更好地理解和利用自身数据&#xff0c;发现其中的模式和趋势&#xff0c;并提供…

Spring配置动态数据库

首先创建一个SpringWeb项目——dynamicdb&#xff08;spring-boot2.5.7&#xff09; 然后引入相关依赖lombok、swagger2、mybatis-plus&#xff0c;如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven…