StartCoroutine/yield 返回模式在 Unity 中到底如何工作?

Unity3D协程详解

游戏中的许多过程都是在多个帧的过程中发生的。你有“密集”的过程,比如寻路,每个帧都努力工作,但会分成多个帧,以免对帧速率产生太大影响。您拥有“稀疏”进程,例如游戏触发器,它们在大多数帧中不执行任何操作,但偶尔会被要求执行关键工作。两者之间有各种各样的流程。

每当您创建一个将在多个帧上进行的进程(无需多线程)时,您需要找到某种方法将工作分解为可以每帧运行一个的块。对于任何具有中心循环的算法,这是相当明显的:例如,可以构造 A* 探路者,使其半永久地维护其节点列表,每帧仅处理打开列表中的少数节点,而不是尝试一口气完成所有工作。需要进行一些平衡来管理延迟 - 毕竟,如果您将帧速率锁定在每秒 60 或 30 帧,那么您的过程每秒只会执行 60 或 30 个步骤,这可能会导致该过程仅执行整体太长了。一个简洁的设计可能会在一个级别上提供尽可能最小的工作单元——例如处理单个 A* 节点——并在顶部分层一种将工作分组为更大块的方法——例如继续处理 A* 节点 X 毫秒。(有些人称之为“时间切片”,但我不这么认为)。

尽管如此,允许以这种方式分解工作意味着您必须将状态从一帧转移到下一帧。如果您要分解迭代算法,那么您必须保留迭代之间共享的所有状态,以及跟踪下一步要执行哪个迭代的方法。这通常不算太糟糕——“A*探路者类”的设计相当明显——但也有其他情况,不太令人愉快。有时,您将面临长时间的计算,这些计算在帧与帧之间执行不同类型的工作;捕获其状态的对象最终可能会产生一大堆半有用的“局部变量”,这些“局部变量”用于将数据从一帧传递到下一帧。如果您正在处理稀疏进程,您通常最终不得不实现一个小型状态机,只是为了跟踪工作何时应该完成。

如果您不必在多个帧中显式跟踪所有这些状态,也不必使用多线程并管理同步和锁定等,而只需将函数编写为单个代码块,那么这不是很简洁吗?标记函数应该“暂停”并稍后继续的特定位置?

Unity 以及许多其他环境和语言以协程的形式提供了这一点。

他们看起来怎么样?在“Unityscript”(Javascript)中:

function LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */

    // Pause here and carry on next frameyield;
}

}
在 C# 中:

IEnumerator LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */

    // Pause here and carry on next frameyield return null;
}

}
它们如何工作?我只想说,我不为 Unity Technologies 工作。我还没有看到Unity源代码。我从未见过 Unity 协程引擎的内部结构。但是,如果他们以与我将要描述的方式完全不同的方式实现它,那么我会感到非常惊讶。如果来自 UT 的任何人想要插话并谈论它的实际工作原理,那就太好了。

重要线索在 C# 版本中。首先,请注意该函数的返回类型是 IEnumerator。其次,请注意其中一个语句是yield return。这意味着yield 必须是一个关键字,并且由于Unity 的C# 支持是vanilla C# 3.5,因此它必须是vanilla C# 3.5 关键字。事实上,它在 MSDN 中- 谈论称为“迭代器块”的东西。发生什么了?

首先,有 IEnumerator 类型。IEnumerator 类型的作用类似于序列上的光标,提供两个重要成员:Current,它是一个属性,为您提供光标当前所在的元素;MoveNext(),一个移动到序列中下一个元素的函数。因为 IEnumerator 是一个接口,所以它没有具体指定这些成员是如何实现的;MoveNext() 可以只向 Current 添加一个,或者可以从文件加载新值,或者可以从 Internet 下载图像并对其进行哈希处理,然后将新哈希值存储在 Current 中……或者它甚至可以首先做一件事序列中的元素,而第二个元素则完全不同。如果您愿意,您甚至可以使用它来生成无限序列。MoveNext() 计算序列中的下一个值(如果没有更多值,则返回 false),Current 检索它计算的值。

通常,如果您想实现一个接口,您必须编写一个类,实现成员,等等。迭代器块是实现 IEnumerator 的一种便捷方法,没有那么多麻烦 - 您只需遵循一些规则,IEnumerator 实现就会由编译器自动生成。

迭代器块是一个常规函数,它 (a) 返回 IEnumerator,并且 (b) 使用yield 关键字。那么yield关键字实际上是做什么的呢?它声明序列中的下一个值是什么——或者没有更多的值。代码遇到yield return X 或yield break 的点就是IEnumerator.MoveNext() 应该停止的点;yield return X 会导致 MoveNext() 返回 true,并且 Current 被分配值 X,而yield break 会导致 MoveNext() 返回 false。

现在,这就是窍门。序列返回的实际值是什么并不重要。您可以重复调用MoveNext(),并忽略Current;计算仍将被执行。每次调用 MoveNext() 时,迭代器块都会运行到下一个“yield”语句,无论它实际生成什么表达式。所以你可以写这样的东西:

IEnumerator TellMeASecret()
{
PlayAnimation(“LeanInConspiratorially”);
while(playingAnimation)
yield return null;

Say(“I stole the cookie from the cookie jar!”);
while(speaking)
yield return null;

PlayAnimation(“LeanOutRelieved”);
while(playingAnimation)
yield return null;
}
您实际编写的是一个迭代器块,它生成一长串空值,但重要的是它计算空值的工作的副作用。您可以使用如下简单循环来运行此协程:

IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }
或者,更有用的是,您可以将其与其他工作混合在一起:

IEnumerator e = TellMeASecret();
while(e.MoveNext())
{
// If they press ‘Escape’, skip the cutscene
if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}
正如您所看到的,每个yield return 语句都必须提供一个表达式(如null),以便迭代器块有一些内容可以实际分配给IEnumerator.Current。一长串空值并不完全有用,但我们对副作用更感兴趣。我们不是吗?

实际上,我们可以用这个表达式做一些方便的事情。如果我们不只是产生 null 并忽略它,而是产生一些指示我们何时需要做更多工作的东西,该怎么办?当然,我们通常需要直接继续下一帧,但并非总是如此:很多时候我们希望在动画或声音播放完毕后,或者在经过特定时间后继续进行。那些 while(playingAnimation) 产生返回 null;构造有点乏味,你不觉得吗?

Unity 声明了 YieldInstruction 基类型,并提供了一些具体的派生类型来指示特定类型的等待。您有 WaitForSeconds,它会在指定的时间过后恢复协程。您有 WaitForEndOfFrame,它可以在同一帧稍后的特定点恢复协程。您已经获得了协程类型本身,当协程 A 产生协程 B 时,它会暂停协程 A 直到协程 B 完成。

从运行时的角度来看,这是什么样的?正如我所说,我不为 Unity 工作,所以我从未见过他们的代码;但我想它可能看起来有点像这样:

List unblockedCoroutines;
List shouldRunNextFrame;
List shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes;

foreach(IEnumerator coroutine in unblockedCoroutines)
{
if(!coroutine.MoveNext())
// This coroutine has finished
continue;

if(!coroutine.Current is YieldInstruction)
{// This coroutine yielded null, or some other value we don't understand; run it next frame.shouldRunNextFrame.Add(coroutine);continue;
}if(coroutine.Current is WaitForSeconds)
{WaitForSeconds wait = (WaitForSeconds)coroutine.Current;shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
}
else if(coroutine.Current is WaitForEndOfFrame)
{shouldRunAtEndOfFrame.Add(coroutine);
}
else /* similar stuff for other YieldInstruction subtypes */

}

unblockedCoroutines = shouldRunNextFrame;
不难想象如何添加更多的 YieldInstruction 子类型来处理其他情况 - 例如,可以添加对信号的引擎级支持,并使用 WaitForSignal(“SignalName”)YieldInstruction 支持它。通过添加更多的 YieldInstructions,协程本身可以变得更具表现力 - 如果你问我,yield return new WaitForSignal(“GameOver”) 比 while(!Signals.HasFired(“GameOver”)) 更容易阅读事实上,在引擎中执行此操作可能比在脚本中执行速度更快。

一些不明显的后果 关于这一切,有一些人们有时会忽略的有用的事情,我认为我应该指出。

首先,yield return 只是产生一个表达式——任何表达式——而 YieldInstruction 是一个常规类型。这意味着您可以执行以下操作:

YieldInstruction y;

if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);

yield return y;
特定的行 yield return new WaitForSeconds()、yield return new WaitForEndOfFrame() 等很常见,但它们本身并不是特殊形式。

其次,因为这些协程只是迭代器块,所以如果您愿意,您可以自己迭代它们 - 不必让引擎为您做这件事。我之前用它来向协程添加中断条件:

IEnumerator DoSomething()
{
/* … */
}

IEnumerator DoSomethingUnlessInterrupted()
{
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}
}
第三,您可以在其他协程上让出这一事实可以让您实现自己的 YieldInstructions,尽管性能不如引擎实现的那样。例如:

IEnumerator UntilTrueCoroutine(Func fn)
{
while(!fn()) yield return null;
}

Coroutine UntilTrue(Func fn)
{
return StartCoroutine(UntilTrueCoroutine(fn));
}

IEnumerator SomeTask()
{
/* … /
yield return UntilTrue(() => _lives < 3);
/
… */
}
然而,我真的不推荐这样做——启动一个协程的成本对我来说有点沉重。

结论 我希望这能够澄清您在 Unity 中使用协程时实际发生的一些情况。C# 的迭代器块是一个绝妙的小构造,即使您不使用 Unity,也许您会发现以同样的方式利用它们很有用。

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

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

相关文章

Shopee、Lazada卖家不得不看的提升销量技巧,自养号测评打造权重

近年来&#xff0c;大部分虾皮、Lazada卖家开始通过测评补单的方式来提升店铺权重和产品排名&#xff0c;以吸引更多流量。这种方式可以有效提高产品的销售转化率&#xff0c;对店铺的运营起到推动作用。然而&#xff0c;测评补单并非简单的购买过程&#xff0c;其中涉及到许多…

干洗店预约下单管理系统收衣开单拍照必备软件

随着生活水平的提高和节奏的加快&#xff0c;商务人士的衣物越来越多&#xff0c;但精力和时间却越来越少。于是&#xff0c;干洗店应运而生&#xff0c;在中国&#xff0c;几乎所有的中心城市干洗店都门庭若市。若每人每月需要干洗一套服装&#xff0c;一个城市每月则需干洗50…

面试中经常问道的问题二

深入理解前端跨域方法和原理 前言 受浏览器同源策略的限制&#xff0c;本域的js不能操作其他域的页面对象&#xff08;比如DOM&#xff09;。但在安全限制的同时也给注入iframe或是ajax应用上带来了不少麻烦。所以我们要通过一些方法使本域的js能够操作其他域的页面对象或者使…

HTML5语义化标签 header 的详解

&#x1f31f;&#x1f31f;&#x1f31f; 专栏详解 &#x1f389; &#x1f389; &#x1f389; 欢迎来到前端开发之旅专栏&#xff01; 不管你是完全小白&#xff0c;还是有一点经验的开发者&#xff0c;在这里你会了解到最简单易懂的语言&#xff0c;与你分享有关前端技术和…

idea使用Alibaba Cloud Toolkit实现自动部署

在日常开发过程中&#xff0c;经常会使用到jenkins进行项目部署&#xff0c;但对一些小项目来说&#xff0c;这就过于复杂&#xff0c;就可以使用Alibaba Cloud Toolkit插件配合shell脚本进行项目的远程部署工作。 一、下载Alibaba Cloud Toolkit插件 二、服务器安装nohup 1.…

Kafka - 消息队列的两种模式

文章目录 消息队列的两种模式点对点模式&#xff08;Point-to-Point&#xff0c;P2P&#xff09;发布/订阅模式&#xff08;Publish/Subscribe&#xff0c;Pub/Sub&#xff09; 小结 消息队列的两种模式 消息队列确实可以根据消息传递的模式分为 点对点模式发布/订阅模式 这两…

Power BI 实现日历图,在一张图中展示天、周、月数据变化规律

《数据可视化》这本书里介绍了一个时间可视化的案例&#xff08;如下图所示&#xff09;&#xff0c;以日历图的形式展示数据的变化&#xff0c;可以在一张图上同时观察到&#xff1a;&#xff08;1&#xff09;每一天的数据变化&#xff1b;&#xff08;2&#xff09;随周变化…

创建个人github.io主页(基础版)//吐槽:很多国内教程已经失效了

一、就跟着官网教程来很快就好了 官方文档的教程 GitHub Pages | Websites for you and your projects, hosted directly from your GitHub repository. Just edit, push, and your changes are live. // 简单跑通为例&#xff0c;第一个链接直接能行了&#xff0c;如果不想…

超全整理,服务端性能测试-docker部署tomcat/redis(详细步骤)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、docker部署tom…

sql server 生成连续日期和数字

在sqlserver里&#xff0c;可以利用系统表master..spt_values里面存储的连续数字0到2047&#xff0c;结合dateadd&#xff08;&#xff09;函数生成连续的日期 select convert (varchar(10),dateadd(d, number, getdate()),23) as workday from master..spt_values where type…

全面分享‘’找不到msvcp140.dll无法继续执行代码修复教程

计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“msvcp140.dll缺失”。这个错误通常会导致某些应用程序无法正常运行。为了解决这个问题&#xff0c;我们需要找到合适的解决办法。本文将介绍5种解决msvcp140.dll缺失问题的方法&#xff0c;帮…

【Python】基于非侵入式负荷检测与分解的电力数据挖掘

文章目录 前言一、案例背景二、分析目标三、分析过程四、数据准备4.1 数据探索4.2 缺失值处理 五、属性构造5.1 设备数据5.2 周波数据 六、模型训练七、性能度量文末送书&#xff1a;《Python数据挖掘&#xff1a;入门、进阶与实用案例分析》 前言 本案例将根据已收集到的电力…

Phthon下载库函数

在代码中使用import tushare as ts导入时编译会报错找不到tushare&#xff0c;此时则表示本地没有相应的库函数包 打开安装python的目录&#xff0c;找到pip.exe文件目录&#xff0c;例如D:\Python\Python311\Scripts&#xff0c;文件夹右键->在终端打开&#xff0c;输入pi…

Linux部署Redis哨兵集群 一主两从三哨兵(这里使用Redis6,其它版本类似)

目录 一、哨兵集群架构介绍二、下载安装Redis2.1、选择需要安装的Redis版本2.2、下载并解压Redis2.3、编译安装Redis 三、搭建Redis一主两从集群3.1、准备配置文件3.1.1、准备主节点6379配置文件3.1.2、准备从节点6380配置文件3.1.3、准备从节点6381配置文件 3.2、启动Redis主从…

在Vue.js中使用xlsx组件实现Excel导出

在现代Web应用程序中&#xff0c;数据导出到Excel格式是一项常见的需求。Vue.js是一种流行的JavaScript框架&#xff0c;允许我们构建动态的前端应用程序。本文将介绍如何使用Vue.js和xlsx组件轻松实现Excel数据导出功能。 1、项目设置 首先&#xff0c;在控制台执行以下命令安…

idea 设置serlvet 类模板(快捷生成servlet类)

我的版本是idea2020.3.4&#xff0c;博客中有相应安装教程&#xff0c;其他版本设置类似&#xff1a; 1.选择文件-->设置 2.选择编辑器-->文件和代码模板-->其他 3.选择Web-->Servlet Annotated Class.java-->复制相应模板&#xff0c;下面顺便设置了注释模板 …

区块链轻节点的问答

EOS的nodeos并没有获取merkle proof的功能&#xff0c;那应该怎样获取merkle proof nodeos&#xff08;EOS区块链节点软件&#xff09;本身并不提供Merkle Proof的功能&#xff0c;而是全节点或其他数据源通常提供Merkle Proof。获取Merkle Proof的过程通常需要与全节点或区块浏…

【Docker】Docker Compose的使用

我们知道使用一个Dockerfile模板文件&#xff0c;可以让用户很方便的定义⼀个单独的应用容器。然而&#xff0c;在日常工作中&#xff0c;经常会碰到需要多个容器相互配合来完成某项任务的情况。 例如要实现一个Web项目&#xff0c;除了Web服务容器本身&#xff0c;往往还需要…

如何在Linux将Spring Boot项目的Jar包注册为开机自启动系统服务

有时候我们需要将Spring Boot打包出来jar文件当做系统服务注册到系统中&#xff0c;本文教你如何操作 目录结构 以下是目录结构&#xff0c;jar文件是从maven package打包出来的&#xff0c;config/application.yml是原先在项目的resources文件夹里&#xff0c;外置出来方便适…

什么是IO多路复用?Redis中对于IO多路复用的应用?

IO多路复用是一种高效的IO处理方式&#xff0c;它允许一个进程同时监控多个文件描述符&#xff08;包括套接字、管道等&#xff09;&#xff0c;并在有数据可读或可写时进行相应的处理。这种机制可以大大提高系统的并发处理能力&#xff0c;减少资源的占用和浪费。 在Redis中&…