目前网站开发应用到的技术有什么,wordpress 编辑器字号,网页设计模板素材营销型首页,商城网站设计公司Unity3D协程详解
游戏中的许多过程都是在多个帧的过程中发生的。你有“密集”的过程#xff0c;比如寻路#xff0c;每个帧都努力工作#xff0c;但会分成多个帧#xff0c;以免对帧速率产生太大影响。您拥有“稀疏”进程#xff0c;例如游戏触发器#xff0c;它们在大多…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() 计算序列中的下一个值如果没有更多值则返回 falseCurrent 检索它计算的值。
通常如果您想实现一个接口您必须编写一个类实现成员等等。迭代器块是实现 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; SortedListfloat, 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 dont 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也许您会发现以同样的方式利用它们很有用。