游戏开发的承诺(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/promises-for-game-development-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 43 分钟阅读 - 21270 个词 阅读量 0游戏开发的承诺(译文)
原文地址:https://www.codeproject.com/Articles/1008068/Promises-for-Game-Development
原文作者:Ashley Davis, Adam Single
译文由本站 robot-v1.0 翻译
前言
In this article we talk about our experience making the promises pattern work for game development. We aim to explain how promises can improve your game development process.
在本文中,我们将讨论使承诺模式适用于游戏开发的经验.我们旨在说明承诺如何改善您的游戏开发流程.
游戏开发的承诺(Promises for Game Development)
首次发表于:(First published at:) 可能出错的地方(What Could Possibly Go Wrong) 如果您发现本文有问题或有反馈,请在以下位置记录问题(If you find an issue with this article or have feedback please log an issue at) github页面(the github page) .(.)
介绍(Introduction)
在本文中,我们将讨论我们制作(In this article we talk about our experience making the) 承诺模式(promises pattern) 为游戏开发工作.我们旨在说明承诺如何改善您的游戏开发流程.(work for game development. We aim to explain how promises can improve your game development process.) 这不是初学者级别的文章.您需要对C#有基本到中级的了解.如果您已经尝试过游戏开发,并且知道要从较小的游戏扩展到较大的游戏,这将对您有所帮助.您应该已经了解了管理异步操作链和相互依存且按时间顺序排列的游戏逻辑的复杂网络有多么困难.您正在寻找一种更好的方法来构造这种代码.如果您在这里找到自己,那么本文将为您提供一个新的选择.(This isn’t a beginner level article. You’ll need a basic to intermediate level understanding of C#. It will help if you have already tried game development and know the struggle to scale up from smaller to larger games. You should already understand how difficult it can be to manage chains of asynchronous operations and complex webs of interdependent and time-sequenced game logic. You are looking for a better way to structure this kind of code. If this is where you find yourself, then this article will give you a new option.) 在本文中,我们将花一些时间介绍承诺,并大体上解释它们如何在游戏开发中有用.然后,我们将更具体地介绍如何将诺言应用于游戏开发.这就引发了关于我们如何扩展承诺模式以应对更高级的游戏开发问题的讨论:从较小的单元组成游戏逻辑,对基于时间的逻辑进行排序以及构造有条件的门控逻辑.最后,我们将诺言与您可能已经使用或正在考虑的其他选择进行比较.(In this article we will spend some time introducing promises and generally explaining how they can be useful in game development. Then we’ll get more specific about how promises can be applied to game development. This leads into a discussion about how we have extended the promises pattern to cope with more advanced game development problems: composing game logic from smaller units, sequencing time-based logic and structuring conditionally gated logic. We finish by comparing promises to the other alternatives you may have used or be considering.) 当您阅读本文时,请记住,我们永远不会断言在每种情况下承诺都是正确的.当然,它们并不是游戏开发中唯一需要的工具. Promise是用于构造复杂异步代码的强大工具,一开始您可能会发现它们难以理解.在异步代码变得越来越普遍的世界中,我们需要这样的模式来帮助控制代码中不断增加的复杂性.(As you read this article keep in mind that we will never assert that promises are right for every situation. They certainly aren’t the only tool you’ll ever need in game development. Promises are a powerful tool for structuring complex asynchronous code and at first you might find they are difficult to appreciate. In a world where asynchronous code is becoming more and more prevalent we need patterns like this to help tame the ever-increasing complexity in our code.) 在Real Serious Games中,我们将C#与Unity结合使用.如果您打算将诺言与C#结合使用(与任何C#游戏引擎,而不仅仅是Unity结合使用),则可以立即使用诺言库开始使用.如果您打算将诺言与Javascript一起使用,则可以使用很多好的库,而最新版本的Javascript甚至包含诺言.如果您使用其他语言,这些技术仍然可以为您服务,但是如果找不到现有的Promise库,您将需要做一些工作才能上手.(At Real Serious Games we use C# with Unity. If you intend to use promises with C# (combined with any C# game engine, not just Unity) you can get started straight away using our promises library. If you intend to use promises with Javascript you have plenty of good libraries at your disposal and the latest version of Javascript even includes promises. If you work with other languages, these techniques can still work for you, but you’ll have some work ahead to get started if you can’t find an existing promises library.) 我们的C#promises库在github上可用:(Our C# promises library is available on github:) https://github.com/Real-Serious-Games/c-sharp-promise(https://github.com/Real-Serious-Games/c-sharp-promise) .(.) 一个显示承诺的Unity示例也可以在github上找到:(A Unity example showing promises can also be found on github:) https://github.com/adamsingle/PromisesUnityDemo(https://github.com/adamsingle/PromisesUnityDemo)
内容(Contents)
- 承诺简介(Introduction to Promises)
- 游戏开发的承诺(Promises for Game Development)
- 承诺使用C#进行救援(Promises to the Rescue in C#)
- 承诺在游戏开发中营救(Promises to the Rescue in Game Dev)
- 构成游戏逻辑的承诺(Promises for Composing Game Logic)
- 基于时间的逻辑的承诺(Promises for Time-based Logic)
- 有条件选通的承诺(Promises for Conditional Gating)
- 诺言vs Unity协程(Promises vs Unity Coroutines)
- 几乎是行为树?(Almost Behaviour Trees?)
- 结论(Conclusion)
- 关于作者(About the Authors)
承诺简介(Introduction to Promises)
承诺是一个(Promises are a) 设计模式(design pattern) 来构造异步代码并消除(依赖)异步操作的运行序列的复杂性.(to structure asynchronous code and smooth over the complexities of running sequences of (dependent) asynchronous operations.) 我们注意到(We noticed an) 文章(article) 最近,这帮助我准确地理解了诺言如何帮助我们.通常,在运行异步函数时,没有简单的方法可以返回结果.此外,我们还难以处理错误和异常.如果异步代码引发异常,将会发生什么?如此雄辩地写在那篇文章中(recently that helped me concisely understand exactly how promises help us. Normally when running an asynchronous function there is no easy way to get a result back. In addition we have the difficulties of handling errors and exceptions. What exactly will happen if the async code throws an exception? As so eloquently put in that article)承诺的重点是将我们还给我们(the point of promises is to give us back) 功能组成(functional composition) 和(and) 错误冒泡(error bubbling) 在异步世界中(in the async world).(.) 一个承诺是(A promise is a) 软件抽象(software abstraction) 可以将多个异步操作绑定到一个(that can bind together multiple asynchronous operations into a single)** 管道(pipeline) **表示为(*represented as a*)** 一流的对象(first-class object) **.签订了一项合同,承诺在将来的某个时候异步操作将成功完成或已被拒绝(表明在流水线的某个阶段发生了错误).(*. A contract is made with the promise that at some time in the future the async operation(s) will either complete successfully or have been rejected (indicating that an error occurred at some stage of the pipeline).*) 每个异步操作的结果都将通过管道传递.这使我们能够轻松地从异步代码中检索结果并对结果采取行动.管道成功完成后,我们的(*Results from each async operation are passed through the pipeline. This allows us to easily retrieve and act on results from our async code. When the pipeline has completed successfully our*)**成功回调(*success callback*)**将被调用.(*will be invoked.*)
承诺具有强大的错误处理机制(概念上类似于(Promises have a powerful error handling mechanism (conceptually similar to) 异常处理(exception handling) ),允许在管道的任何阶段注入错误处理程序.如果您只对整个异步操作序列的成功或失败感兴趣,则将错误处理放在管道的末尾.如果流水线的任何阶段都发生错误,则其余流水线阶段将被短路,并将控制权转移到错误处理程序.对于异步代码,这相当于try/catch异常处理.() that allows error handlers to be injected at any stage of the pipeline. If you are only interested in the success or failure of the entire sequence of async operations, then put your error handling at the end of the pipeline. If an error occurs at any stage of the pipeline, the rest of the pipeline stages will be short-circuited and control transferred to the error handler. This gives the equivalent of try/catch exception handling with respect to our async code.)
我们通过使用Java开发Web应用程序的经验首先注意到了承诺.只是我们在这里很清楚,我们指的是真正的Javascript,而不是(We first noticed promises through our experience developing web applications in Javascript. Just so we’re clear here, we mean real Javascript, not) Unity伪Javascript(Unity pseudo-Javascript) .从阅读(. From reading) 维基百科页面(the wikipedia page) 似乎诺言从迁移到Javascript(it appears promises migrated to Javascript from) 功能语言(functional languages) ,因此产生了许多好主意.(, where so many good ideas originate.) Java语言中的承诺经常被誉为是一种称为"反模式"的治疗方法(Promises in Javascript are often lauded as the cure for the antipattern known as) 回调地狱(callback hell) .当然,有许多用于异步代码管理的Javascript库,但是Promise模式已成为众人瞩目的解决方案,它是选择的解决方案和某种(. Of course there are many Javascript libraries for management of async code, however the promises pattern has risen above the herd as the solution of choice and something of a) 标准(standard) .(.) 现在概述承诺如何帮助您成为游戏开发者…(Now for an overview of how promises can help you as a game developer…)
游戏开发的承诺(Promises for Game Development)
游戏的结构如何,承诺如何改善游戏开发?(How are games structured and how can promises improve game development?) 您可能会争辩说,游戏是一系列与时间有关的活动,一个接一个地发生,并且依赖于先前的活动和用户输入并受其影响.当玩家满足一组目标时,游戏或关卡完成.目标取决于活动和先前的目标.如果您可以想象从4维的角度来看游戏,那么游戏看起来就像是目标和活动的交织在一起的挂毯,它们之间有着复杂的依赖关系.(You could argue that a game is a sequence of time-dependent activities happening one after the other, dependent on and influenced by previous activities and user input. Game or level completion happens when the player satisfies a set of objectives. Objectives depend on activities and prior objectives. If you can imagine looking at it from a 4-dimensional perspective, a game looks like an interwoven tapestry of objectives and activities with complex dependencies between them.) 任何游戏开发工具箱中的一些奖励工具都是那些以优雅,富有表现力且易于随游戏发展而变化的方式将目标和活动依赖关系联系在一起的工具.当然,我们已经可以使用许多模式来构造游戏代码:(Some of the prize tools in any game development toolbox are those that help wire together objective and activity dependencies in a manner that is elegant, expressive and easy to change as the game is evolved. Of course we already have many patterns at our disposal for structuring game code: the) 策略模式(strategy pattern) ,(,) 有限状态机(finite state machines) (fsms),分层fsms,((fsms), hierarchical fsms,) 行为树(behavior trees) ,脚本,协程,消息传递/事件系统,实体组件系统,甚至可能更多.都值得理解.(, scripting, coroutines, messaging/event systems, entity component systems and probably more. All are worth understanding.) 我们想证明承诺属于这种混合.它们是一种简单的替代方法,对各种各样的任务非常有用,您可能可以立即开始使用它们.承诺的力量来自将一系列活动视为单个实体的能力.这是具有承诺的重用基础,您可以创建越来越大的构建基块,从中构建各种游戏逻辑和行为.结果代码是可读,可理解和可维护的.(We’d like to show that promises belong in this mix. They are a simple alternative, very useful for a great variety of tasks and you can probably start using them straight away. The power of promises comes from the ability to treat a sequence of activities as a single entity. This is the base of reuse with promises, you can create bigger and bigger building blocks from which to compose a variety of game logic and behaviour. The resulting code is readable, understandable and maintainable.) 对于许多传统的异步操作(例如加载资产),承诺已经立即有用.我们还将它们用于将运动,动画和声音排序为可能容易称为过场动画的序列.但是对于某些更复杂的逻辑,我们不得不升级promise模式.在此之前,让我们介绍一下C#中Promise的更基本用法.(Promises are already immediately useful for many traditionally asynchronous operations such as loading assets. We also use them for sequencing movements, animations and sound into sequences you might easily call a cutscene. But for some of the more complex logic we had to upgrade the promises pattern. Before we get to that, let’s cover the more basic use of promises in C#.)
承诺使用C#进行救援(Promises to the Rescue in C#)
在(At) 真正的严肃游戏(Real Serious Games) 我们将promises模式转换为C#,它立即使异步代码中的设计改进得以完成,例如加载资产.在这些情况下,通常首选(we translated the promises pattern to C# and it immediately enabled a design improvement in our async code for tasks such as loading assets. In these cases it is typically preferred) 异步加载而不是阻塞(to load asynchronously rather than to block) 并在加载完成后获取回调.这样可以释放线程以进行其他处理,或者仅仅是为了使UI保持响应状态.(and get a callback when the load has completed. This frees up the thread for other processing or simply just for keeping the UI responsive.) 让我们来看一些示例,以便您了解诺言的基础.(Let’s work through some examples so you’ll understand the basics of promises.) 异步操作通常由返回promise的函数开始,所以让我们从这样的函数开始:(An async operation is usually started by a function that returns a promise, so let’s start with such a function:)
<code>public IPromise<MyAsset> LoadAsset(string assetPath)
{
// ...
}
</code>
**负荷资产(LoadAsset)**启动基于promise的异步操作:(initiates a promise-based async operation:)
<code>var promise = new Promise<MyAsset>();
// ... start the async operation ...
return promise;
</code>
在返回承诺之前,将启动异步操作.呼叫者可以立即使用返回的承诺(通过其(The async operation is started before returning the promise. The caller can immediately use the returned promise (via its)** 流利的API(fluent API) **),即使异步操作仍在进行中且尚未完成.(*) even though the asynchronous operation is still in flight and has not yet completed.*) 异步操作完成后,(*Upon completion of the async operation, in*)**无论如何(*whatever way*)**该通知实际上发生了,(*that notification actually occurs,*)**负荷资产(*LoadAsset*)**兑现承诺:(*resolves the promise:*)
<code>promise.Resolve(theLoadedAsset);
</code>
解决promise会触发调用方使用以下功能定义的下游回调管道:(Resolving the promise triggers a pipeline of downstream callbacks that have been defined by the caller using functions such as)然后(Then),(,)**然后全部(ThenAll)**和(and)完成了(Done).(.) 最简单的管道只需使用(The simplest pipeline simply uses)**完成了(Done)**附加回调作为管道的最后阶段:(to attach a callback as the last stage of the pipeline:)
<code>LoadAsset(someAssetPath)
.Done(myAsset => OnAssetLoaded(myAsset));
</code>
可以使用链接多个操作(Multiple operations can be chained using)然后(Then),例如,加载链接的资产:(, for example loading a linked asset:)
<code>LoadAsset(someAssetPath)
.Then(myAsset => LoadAsset(myAsset.LinkedAssetPath))
.Done(nextAsset => OnAssetLoaded(nextAsset));
</code>
的(The) 匿名功能(anonymous function) 仅在上一个阶段解决后,才在每个管道阶段定义回调.每个管道阶段都会返回一个新的Promise,表示一个新的异步操作,该操作必须在管道可以继续到下一个阶段(或由(that defines the callback at each pipeline stage is invoked only after the preceding stage has resolved. Each pipeline stage returns a new promise that represents a new async operation which must complete before the pipeline can continue to the next stage (or the final stage as specified by)**完成了(Done)**在这种情况下).(in this case).) 这是一个简单的例子,但是希望您开始看到诺言的力量.(This is a trivial example, but hopefully you are starting to see the power of promises.) 让我们加注.对于RSG Promise库,我们添加了一个新功能,该功能允许链接多个从属的Promise.这个例子使用(Let’s up the ante. For the RSG Promise library we have added a new function that allows multiple dependent promises to be chained. This example uses)**然后全部(ThenAll)**加载多个链接资产:(to load multiple linked assets:)
<code>LoadAsset(someAssetPath)
.ThenAll(myAsset =>
myAsset.LinkedAssets.Select(path => LoadAsset(path))
)
.Done(linkedAssets => OnAllAssetsLoaded());
</code>
**然后全部(ThenAll)**传递了一个回调,该回调返回一个(is passed a callback that returns a) 采集(collection) 的承诺.所有这些承诺必须在管道继续之前完成.(of promises. All of these promises must complete before the pipeline continues.) LINQ(LINQ) ** 选择(Select) **转换(transforms the)**关联资产的收集(collection of linked assets)**变成一个(into a)**承诺的集合(collection of promises)**加载这些资产.作为C#程序员,我们更喜欢LINQ.(to load those assets. As C# programmers we prefer LINQ’s) **方法语法(method syntax)**在其之上(over its)查询语法(query syntax) .(.) Promise的优点在于,管理异步操作的链式序列非常容易.是否需要在管道中插入新操作?管道很容易修改:(The great thing about promises is just how easy it is to manage chained sequences of async operations. Need to insert a new operation into your pipeline? The pipeline is easily modified:)
<code>LoadAsset(someAssetPath)
.ThenAll(myAsset =>
myAsset.LinkedAssets.Select(path => LoadAsset(path))
)
.Then(linkedAssets => SomeOtherAsyncOperation(linkedAssets))
.Then(somethingElse => AndAnotherAsyncOperation(somethingElse))
.Done(somethingElseAgain => OnAllAssetsLoaded(somethingElseAgain));
</code>
C#Promise库和文档(The C# promises library and documentation) 在github上可用(is available on github) .(.)
承诺在游戏开发中营救(Promises to the Rescue in Game Dev)
游戏开发中有许多情况需要等待一些异步操作完成或满足某些条件.这里有些例子:(There are many situations in game development that require waiting for some async operation to complete or for some condition to be fulfilled. Here are some examples:)
- 从网络下载文件或数据(Downloading files or data from a network)
- 在动画结束时播放粒子效果(Playing a particle effect at the end of an animation)
- 在动画结束时播放声音效果(Playing a sound effect at the end of an animation)
- 将相机移动到特定位置(Moving a camera to a certain position)
- 等待玩家死亡(Waiting for the player to die)
- 等待玩家丧生(Waiting for the player to lose all their lives)
- 等待用户输入(Waiting for user input)
- 等待计时器用完(Waiting for a timer to run out)
- 等待成就的所有要求得到满足(Waiting for all requirements of an achievement to be met) 在以下各节中,我们将通过示例(从基本到更高级)来研究专门用于游戏开发的Promise.(In the following sections we will work through examples (from basic to more advanced) of promises used specifically for game development.)
异步资产加载的承诺(Promises for Asynchronous Asset Loading)
游戏通常需要异步操作.用于加载和初始化,或者用于在多个框架上执行的运行时操作.(Games often need asynchronous operations. Either for loading and initialisation or for runtime operations that execute over multiple frames.) 在启动时,必须加载一个级别和其他资产.也许数据是从云中的数据库中提取的.其他系统的初始化可能取决于此数据的加载.(At startup a level and other assets must be loaded. Maybe data is pulled from a database in the cloud. Initialisation of other systems may depend on the loading of this data.) 天真的尝试可能是频繁轮询以查看加载是否已完成:(A naive attempt might be frequent polling to see if the loading has completed:)
<code>public void Update(float deltaTime)
{
if (!gameLoaded)
{
if (gameLoadFinished)
{
// Loading has finished.
gameLoaded = true;
// Initialize systems...
}
else
{
// Not loaded yet, maybe progress the loading screen animation.
return;
}
}
else
{
// Game is loaded...
// Update AI, world, etc.
}
}
</code>
现在,轮询可能会有用,但是通常(Now polling can be useful but it is often) 被认为是坏习惯(considered a bad practice) .您会发现它笨拙且不雅.(. And you see that it is clunky and inelegant.) 添加初始化依赖项后,这种编码方式很快变得复杂.更不用说我们确实应该考虑的其他问题.我们如何处理故障并从中恢复?我们是否还要轮询另一个指示错误情况的变量?(This style of coding gets complicated quickly as initialisation dependencies are added. Not to mention the other issues we really should be considering. How do we handle and recover from failure? Do we poll yet another variable that indicates an error condition?) 这样的轮询使我想到(Polling like this brings to mind the)**拉模型(pull model)**在计算中(of computing in which we)**问(ask)**有关状态更改的对象.(objects about state changes.) 根据李`坎贝尔(According to Lee Campbell) 世界已经移到(the world has already moved to the)推模型(push model),但我们的开发人员仍在追赶.在推模型中,我们是(, but us developers are still catching up. In the push model we are)**已通知(notified)**状态变化,而不必(of state changes instead of having to)问(ask).(.) 您已经在(You are already in the)**推模型(push model)**领域,如果您正在使用(realm if you are using) C#事件(C# events) .像这样:(. Something like this:)
<code>public void Startup()
{
// No need to poll, this event will notify
// when the level has been loaded.
levelLoader.LevelLoaded += levelLoader_LevelLoaded;
levelLoader.Load("SomeLevel");
}
private void levelLoader_LevelLoaded(object sender, LevelLoadedEventArgs e)
{
// Initialize dependent systems...
}
</code>
C#事件更好,但是代码仍然可以很快变得复杂.想象一下,具有异步依赖关系的蜘蛛网,必须在游戏开始之前对其进行解析. C#事件可以走多远?同样,随着复杂性的增加,混乱也急剧增加,并且代码变得更加难以管理和维护.(C# events are better but the code can still get complicated very quickly. Imagine having a spider web of asynchronous dependencies that must be resolved before your game starts. How far can you go with C# events? Again, the confusion rises sharply as the complexity increases and the code becomes more difficult to manage and maintain.) 从我们可以使用的事件继续(Moving on from events we can use) 回叫(callbacks) (使用匿名方法)((with anonymous methods)) 代替事件(instead of events) ,这是Javascript和(, a technique that is ubiquitous in Javascript and) a(Lua) :(:)
<code>public void Startup()
{
levelLoader.Load("SomeLevel", loadedLevel =>
{
// Initialize dependent systems...
});
}
</code>
您可以从回调中获得很多收益.它们更好地允许一连串的加载和初始化:(You can get a lot of mileage out of callbacks. They better allow for a chain of loading and initialization:)
<code>public void Startup()
{
// Load the level.
levelLoader.Load("SomeLevel", loadedLevel =>
{
// After level has loaded, load behaviours required by the level.
behaviorLoader.LoadBehaviors(loadedLevel.RequiredBehaviours,
loadedBehaviors =>
{
// Initialise dependent systems...
}
);
});
}
</code>
回调的问题在于,它们导致匿名函数的深层嵌套,并导致许多混乱.这是一个非常普遍的问题,已被命名为(The problem with callbacks is that they lead to deep nesting of anonymous functions and much resulting confusion. This is a problem so prevalent that is has been given the name) 回调地狱(callback hell) .(.) 这就是我们实现承诺的地方.您需要一个更好的工具来构造和管理复杂的异步代码.如果您可以理解本文已经描述的问题,那么Promise将成为工具箱中值得欢迎的补充.(So this is where we arrive at promises. You need a better tool to structure and manage your complex asynchronous code. If you can appreciate the problems described already in this article, then promises are going to be a welcome addition to your toolbox.) 上面介绍的方法可以非常迅速地转变为难以调试,测试和维护的复杂代码网络.使用Promises,您可以以一种优雅且易读的方式来布局依赖链:(The methods presented above can very quickly turn into a tangled web of code that is difficult to debug, test and maintain. Using Promises you can layout your chain of dependencies in an elegant and readable manner:)
<code>levelLoader.LoadLevel("SomeLevel")
.Then(loadedLevel => LoadSomethingElse(loadedLevel))
.Then(loadedLevel => LoadAnotherThing(loadedLevel)))
.Done(loadedLevel => StartGame(loadedLevel));
</code>
关于promise的另一个好处是:随着代码的逐步重构,可以转换为promise.我们知道,因为我们这样做了,所以用Promise逐步取代了Unity协程(并且在每个阶段都进行了非常仔细的测试).(Another nice thing about promises: with gradual refactoring of your code it is possible to convert to promises. We know because we did this, incrementally replacing Unity coroutines with promises (and testing very carefully at each stage).) 这实际上只是对诺言可以做的事情的品尝.我们需要更深入地探索它们,以便您了解它们在您的开发过程中将具有怎样的变革性.(This is really just a taster of what promises can do. We need to explore them in more depth so you’ll understand how transformative they can be for your development process.)
构成游戏逻辑的承诺(Promises for Composing Game Logic)
承诺具有通过流利的API编写操作序列的强大功能.我们通过添加合成运算符来解决游戏开发特有的问题,从而扩展了Promise规范.在本节中,我们将介绍这些新的运算符.(Promises have a powerful ability to compose sequences of operations via the fluent API. We have extended the promises specification by adding composition operators to solve problems specific to game development. In this section we will look at these new operators.)**所有(All)**用于组成并行操作,(for composing parallel operations,)**种族(Race)**当第一个并行操作完成时完成(for completing when the first parallel operation completes and)**顺序(Sequence)**用于安排一系列顺序操作.(for scheduling a collection of sequential operations.)
无极(Promise.All)
**所有(All)**打包多个promise,仅当所有打包的promise已解决时,结果promise才会解析.(packages up multiple promises, the resulting promise resolves only when all of the packaged promises have resolved.) 在许多情况下这可能非常有用.例如成就系统.想象一下一款益智游戏,其成就在玩家玩了20分钟并在该时间内完成10个关卡后便被解锁:(This can be very useful in a number of situations. For example an achievement system. Imagine a puzzle game with an achievement that is unlocked after the player has played the game for 20 minutes and finished 10 levels in that time:)
<code>IPromise OpeningGambitAchievement()
{
return Promise.All(
WaitForTimeSpentOnPuzzles(60 * 20),
FinishedSpecificNumberOfLevels(10)
);
}
</code>
**WaitForTimeSpentOn拼图(WaitForTimeSpentOnPuzzles)**和(and)**FinishedSpecificNumberOfLevels(FinishedSpecificNumberOfLevels)**都是返回承诺的函数.他们在内部做的事情将在后面解释,因为现在重要的是要了解(are both functions that return promises. What they are doing internally will be explained later, for now it is important to understand that the promise returned by)**OpeningGambit成就(OpeningGambitAchievement)**将解决何时(will resolve when)**都(both)**不论这两个事件发生的距离有多远,内在的诺言都已经解决.(of the inner promises have resolved, regardless of how far apart those two events occur.) 要使用此承诺,您只需开除并忘记:(To use this promise, you simply fire and forget:)
<code>OpeningGambitAchievement()
.Done(() => ShowAchievementNotification("OpeningGambit");
</code>
**所有(All)**也可以承诺解决的承诺,例如玩家必须完成的目标集合:(can also take a collection of promises to resolve, e.g. a collection of objectives the player must complete:)
<code>IEnumerable<IPromise> ObjectivesToComplete()
{
// ... return a collection of promises ...
}
</code>
现在,当玩家完成目标时,很容易收到通知:(It is now easy to receive a notification when the player has completed the objectives:)
<code>Promise.All(ObjectivesToComplete())
.Done(() => OnObjectivesCompleted());
</code>
**所有(All)**可以通过以下方式在链内使用(can be used within a chain via)然后全部(ThenAll):(:)
<code>LoadLevel(someLevel)
.ThenAll(theLevel => ObjectivesToComplete(theLevel))
.Done(() => OnLevelCompleted());
</code>
无极种族(Promise.Race)
**种族(Race)**类似于(is similar to)所有(All),但在第一个内部承诺解决后立即解决.每个承诺都是(, but it resolves immediately when the first inner promise resolves. Each of the promises are)**竞速(racing)**彼此完成.这使我们可以轻松地在代码中建立超时.(each other for completion. This allows us to easily build timeouts into our code.) 想象一下这样一种情况,用户需要提供一些输入,但是机会有限.在此示例中,返回的promise根据玩家的输入或在超时到期后解析,以先到者为准:(Imagine a situation where the user needs to provide some input, but there is a time limit on their opportunity to do so. In this example the returned promise resolves either on input from the player or after the timeout has expired, whichever comes first:)
<code>IPromise WaitForTimedUserInput(float timeoutSeconds)
{
return Promise.Race(WaitForInput(), Timeout(timeoutSeconds));
}
</code>
请注意,其他内部承诺将继续运行,直到完成为止.虽然(Be warned that the other inner promises will continue running until they complete. Although the)**种族(Race)**第一个诺言完成后,诺言就会解析并调用链式逻辑,其他诺言将继续运行直到完成.因此,请谨慎使用会进行异步状态更改并引起副作用的promise.(promise resolves and invokes chained logic as soon as the first promise completes, the other promises will continue to run until completion. So be careful using promises that are making asynchronous state changes and causing side effects.) **种族(Race)**可以通过以下方式在链内使用(can be used within a chain via)然后种族(ThenRace).(.)
承诺顺序(Promise.Sequence)
某些场景(例如,对动态生成的运动序列进行动画处理)可以使用(Some scenarios, for example animating a dynamically generated sequence of movements, can be simplified using the)**顺序(Sequence)**功能.您可能具有一系列硬编码的操作:(function. You may have a hard-coded sequence of operations:)
<code>PromiseOne()
.Then(PromiseTwo())
.Then(PromiseThree())
.Then(PromiseFour())
// ... etc ...
.Done();
</code>
**顺序(Sequence)**需要将其更改为动态生成的操作集合(可能从数据加载).(is needed to change this to a dynamically generated collection of operations (possibly loaded from data).) **顺序(Sequence)**被赋予函数集合,每个函数都返回一个promise.每个功能都依次调用,但只能在先前的承诺解决后才能调用.这使得一系列异步操作可以串行执行:(is given a collection of functions, each returning a promise. Each function is invoked in-turn one after the other, but only after the previous promise has resolved. This enables a sequence of asynchronous operations to be executed serially:)
<code>IEnumerable<Func<IPromise>> GenerateMovementSequence()
{
// Generate a collection of functions that each initiate an async operation
// and returns a promise.
}
Promise.Sequence(GenerateMovementSequence())
.Done(() =>
{
// The sequence has completed.
});
</code>
这听起来比实际要复杂.我们需要一个功能集合,而不是一个Promise集合,因为我们需要推迟启动每个异步操作,直到轮到它为止.我们不能简单地传递一个Promise集合,因为已经有了一个Promise意味着异步操作已经开始,这意味着序列中的每个操作都将同时开始,这不是我们想要的(这就是(This sounds more complicated than it actually is. We need a collection of functions, rather than a collection of promises, since we need to hold off starting each async operation until its turn arrives. We can’t simply pass in a collection of promises, as already having a promise implies that the async operation has already started, meaning that every operation in the sequence would start at the same time and this is not what we want (this is what)**所有(All)**确实).所以(does). So)**顺序(Sequence)**必须采用尚未被调用的功能.(must take functions that are yet to be called.) 举个例子,假设我们有一个NPC守卫,他遵循一系列航路点.行为完成后,NPC将重置为稍后由播放器再次触发.(As an example, let’s say we have an NPC guard who follows a series of waypoints. When the behaviour is completed the NPC is reset to later be triggered again by the player.) 我们可以通过上述方式使用诺言,将其链接在一起(We could use promises in the way mentioned above, chaining together)然后(Then),以建立一个序列,使NPC随时间推移通过每个路标.这意味着我们需要在编译时知道航路点.我们希望以此数据为驱动力,从数据中加载一组路标.这是一个绝佳的机会(s to build a sequence that moves the NPC over time through each of the waypoints. This means we need to know the waypoints at compile time. We’d like to make this data driven, loading the set of waypoints from data. This is a perfect opportunity for)顺序(Sequence).(.) 假设我们有一个移动我们的NPC的函数,该函数返回一个诺言.当NPC就位时,诺言就解决了:(Let’s assume we have a function for moving our NPC that returns a promise. The promise resolves when the NPC has moved into position:)
<code>IPromise MoveToPosition(Vector3 endPosition, float movementSpeed);
</code>
这足以让我们手动链接它.但是要用(This would be enough for us to manually chain it. But to use)**顺序(Sequence)**我们需要返回一个返回promise的函数,允许(we need to return a function that returns a promise, allowing)**顺序(Sequence)**在正确的时间调用它:(to call it at the right time:)
<code>Func<IPromise> PrepMoveToPosition(Vector3 endPosition, float movementSpeed)
{
return () => MoveToPosition(endPosition, movementSpeed);
}
</code>
**PrepMoveToPosition(PrepMoveToPosition)**包裹(wraps)**MoveToPosition(MoveToPosition)**在一个匿名函数中并返回新函数.这使我们能够构建此类函数的集合以传递给(in an anonymous function and returns the new function. This allows us to build a collection of such functions to pass to)顺序(Sequence):(:)
<code>Vector3[] waypoints = ... loaded from data ...
void MoveAlongWaypoints(float movementSpeed)
{
// Generate collection of functions for moving through the way points.
var moves = waypoints
.Select(waypoint => PrepMoveToPosition(waypoint, movementSpeed);
Promise.Sequence(moves)
.Done(() => Reset()) // Reset the NPC ready to be triggered again.
}
</code>
您可以看到promise如何不仅实现而且鼓励我们分离逻辑,将我们的功能分解为易于阅读和管理的紧密逻辑组件.(You can see how promises not only enable, but encourage us to separate our logic, breaking our functions down to tight, logical components that are easy to read and manage.) **顺序(Sequence)**可以通过以下方式在链内使用(can be used within a chain via)然后序列(ThenSequence)
基于时间的逻辑的承诺(Promises for time based logic)
在上述情况下使用诺言有明显的好处,但是当我们将诺言扩展为时间因素时,它们确实发挥了全部潜力.(Using promises in the above scenarios has clear benefits, however they really hit their full potential when we extend promises to factor in time.) 大多数游戏开发人员都会以某种方式使用计时器,跟踪时间是游戏开发的必要和重要组成部分.例子包括持续有限时间的加电,基于所花费时间的结束条件,赛车游戏中每圈的时间以及复杂性的最高点,整个过场动画都是许多定时操作的序列.(Most game developers will have used a timer in some way, tracking time is a necessary and important part of game development. Examples include power ups that last for a limited amount of time, end conditions based on time taken, time per lap in a racing game and at the top end of complexity, entire cutscenes that are sequences of many timed operations.) 在最简单的形式中,计时器用于在经过一定时间后触发逻辑.一旦结束,这是在代码中解决的一个简单问题.这是一个浮点值,它会增加(In it’s simplest form a timer is used to trigger logic after a certain amount of time has passed. Once-off, this is a simple problem to solve in code. It’s a floating point value that is incremented by) 经过时间(delta elapsed time) 每次更新:(each update:)
<code> void Update(float deltaTime)
{
curTime += deltaTime;
if (curTime < triggerTime)
{
// Still counting towards the timer
}
else
{
// Time is up
}
}
</code>
这是一种简单的技术,但是扩大规模会使事情复杂化.运行多个计时器变得难以调试和维护.(This is a simple technique, but scaling up complicates things. Having multiple timer running becomes hard to debug and maintain.) 让我们考虑一个更复杂的示例.(Let’s consider a more complicated example.) 想象一下,您的玩家角色是一个形状改变者,能够采取多种形式,每种形式都有其特定的优势.玩家只能从人类形式激活这些形式,并且在改变时它们无法控制何时变回,这是在随机时间之后发生的.更复杂的是,在每种形式的游戏中,玩家都可以激活特定于该形式的加电,后者也有自己的计时器.(Imagine your player character is a shape changer and capable of taking on multiple forms, with each form having specific benefits. The player can only activate these forms from the human form and while changed they cannot control when they change back, which happens after a random amount of time. To further complicate this, while in each of these forms, the player can activate power-ups specific to that form, which also have their own timers.)
<code>class Character
{
float currentFormTimer; // The amount of time left in the current form.
enum Form
{
Human,
Wolf,
Hawk,
Dolphin
}
Form currentForm = Form.Human;
float currentPowerupTimer;
enum Powerup
{
None,
Werewolf,
HawkSpeed,
DolphinEcho
}
Powerup currentPowerup = Powerup.None;
public void Update(float deltaTime)
{
if (currentForm != Form.Human)
{
currentFormTimer -= deltaTime;
if (currentFormTimer <= 0f)
{
// Return to human form
currentForm = Form.Human;
// Disable any powerups that were active
currentPowerup = Powerup.None;
}
if (currentPowerup != Powerup.None)
{
currentPowerupTimer -= deltaTime;
if (currentPowerupTimer <= 0f)
{
currentPowerup = Powerup.None;
}
}
}
}
}
</code>
这个例子仍然很简单,但是这样做太过复杂,丑陋且难以阅读.您可以想象,随着我们添加更多功能和更多计时器,外观会如何.(This example is still simple, but well on it’s way too becoming overly complicated, also ugly and difficult to read. You can imagine how it will look as we add more features and more timers.) 通过对promises库的另一个扩展,我们可以改善计时器的使用.这将使我们能够将这种逻辑移出(With another extension to our promises library we can improve our use of timers. This will allow us to move this logic out of)**更新资料(Update)**并分成单独的功能,每个功能都有(and into separate functions, each with a) 明确界定的责任(clearly defined responsibility) .(.) 我们已经实施了(We have implemented the)**PromiseTimer(PromiseTimer)**这个类可以弥合promise和timer之间的差距.的(class which can bridge the gap between promises and timers. The)**等待(WaitFor)**函数返回一个承诺,该承诺将在请求的时间过去后解析:(function returns a promise that resolves after the requested time has passed:)
<code> PromiseTimer promiseTimer = new PromiseTimer();
promiseTimer.WaitFor(timeToWait)
.Done(() =>
{
// Time is up
});
</code>
让我们在同一类中定义一个便捷函数:(Let’s define a convenience function within the same class:)
<code>PromiseTimer promiseTimer = new PromiseTimer();
IPromise WaitFor(float timeToWait)
{
return promiseTimer.WaitFor(timeToWait);
}
</code>
现在,我们的计时器仍然必须更新.让我们在(Now somewhere our timers still must be updated. Let’s do that in the)**字符(Character)**类:(class:)
<code>void Update(float deltaTime)
{
promiseTimer.Update(deltaTime);
}
</code>
现在,我们重写(Now let’s rewrite the)**字符(Character)**使用示例(example using)等待(WaitFor).您会记得其中的逻辑(. You’ll remember the logic in the)更新资料(Update).在这里,我们可以将其移至适当的辅助函数.(. Here we can move that out to appropriate helper functions.)
<code>class Character
{
PromiseTimer promiseTimer = new PromiseTimer();
enum Form
{
Human,
Wolf,
Hawk,
Dolphin
}
Form currentForm = Form.Human;
enum Powerup
{
None,
Werewolf,
HawkSpeed,
DolphinEcho
}
Powerup currentPowerup = Powerup.None;
public void Update(float deltaTime)
{
promiseTimer.Update(deltaTime);
}
// Activates wolf mode if possible
public IPromise BeWolf()
{
if (currentForm != Form.Human)
{
// Resolve this promise straight away,
// we already have one running for a transformation.
return Promise.Resolved();
}
// Wait for a random time between 10 and 20 seconds
return RunTimer(Random.Range(10f, 20f))
.Then(() => currentForm = Form.Human); // Return to human mode.
}
...
// Similar functions for the other transformations
...
// Activate power-up.
public IPromise ActivateWerewolfPowerup()
{
if (currentForm != Form.Wolf || currentPowerup == Powerup.Werewolf)
{
return Promise.Resolved();
}
currentPowerup = Powerup.Werewolf;
return promiseTimer.WaitFor(Random.Range(1f, 5f))
.Then(() => currentPowerup = Powerup.Werewolf);
}
}
</code>
而且,我们所有基于计时器的逻辑都由(And all our timer based logic is neatly handled for us by the)PromiseTimer(PromiseTimer).(.) 您可能已经注意到我们shapeshapeer代码的小问题!应该(You may have noticed the small problem with our shapechanger code! Should the)**狼的形式(wolf form)**在狼人上电时间结束之前,上电将保持活动状态.我们可以通过扩展有条件选通的承诺来轻松解决这一问题.(time end before the werewolf power-up time, the power-up would remain active. We can address that easily by extending promises for conditional gating.)
有条件选通的承诺(Promises for Conditional Gating)
您可能已经意识到(You may have already picked up on the fact that)所有(All),(,)**种族(Race)**和(and)**顺序(Sequence)**与标准编程结构非常相似(are very similar to the standard programming constructs)和(and),(,)**要么(or)**和(and)对于(for).以这种方式思考它们将帮助您理解如何将它们用于构造条件逻辑和行为.(. Thinking about them in this way will help you understand how they can be used to construct conditional logic and behaviour.) 的(The)**PromiseTimer(PromiseTimer)**通过添加基于时间的条件操作扩展了这些内容(extends on these by adding the time-based conditional operations)**等到(WaitUntil)**和(and)等待(WaitWhile).(.) **等到(WaitUntil)**返回一个诺言,当我们的自定义条件评估为true时,该诺言将在以后的某个时间解决.此条件可能是时间限制,使其与(returns a promise that resolves at some future time when our custom condition has evaluated to true. This condition could be a time limit, making it perform exactly the same as)等待(WaitFor),也可以是其他任何返回布尔结果的表达式.(, or it could be any other expression that returns a boolean result.) 在上一节中,我们提到了上电功能中的逻辑错误:(In the last section we mentioned the logic error in the power-up function:)
<code>IPromise ActivateWerewolfPowerup()
{
if (currentForm != Form.Wolf)
{
return Promise.Resolved();
}
return promiseTimer.WaitFor(Random.Range(1f, 5f));
}
</code>
如果(If)**currentForm(currentForm)**应该在之后改变(should change after the)**等待(WaitFor)**已经开始,但尚未完成,这意味着我们的角色已恢复为人形,尽管角色不再以狼的形式出现,但狼人的加电将一直活跃到时间用完.这可以用解决(has started but before it finishes, meaning our character has reverted to human form, the werewolf powerup will remain active until it’s time runs out, despite the character not being in wolf form anymore. This can be fixed with)等到(WaitUntil):(:)
<code>IPromise ActivateWerewolfPowerup()
{
var powerupTime = Random.Range(1f, 5f);
return promiseTimer.WaitUntil(t =>
{
return t.elapsedTime >= powerupTime || currentForm != Form.Wolf);
});
}
</code>
一旦时间耗尽,或者角色不是狼(以先到者为准),我们的承诺就会在这里得到解决.(Here our promise will resolve once the time has run out, or the character is not a wolf, whichever comes first.) 早些时候,当谈论(Earlier, when talking about)无极(Promise.All),我们讨论了用于检测成就的两个兑现承诺的功能.他们是(, we talked about two promise-returning functions that were used to detect an achievement. They were)**WaitForTimeSpentOn拼图(WaitForTimeSpentOnPuzzles)**和(and)FinishedSpecificNumberOfLevels(FinishedSpecificNumberOfLevels).两者都很容易使用(. Both are very simple to construct using the)PromiseTimer(PromiseTimer):(:)
<code>IPromise WaitForTimeSpentOnPuzzles(float seconds)
{
return promiseTimer.WaitUntil(timeData =>
{
var timePlaying = 0f;
if (!paused)
{
timePlaying += timeData.deltaTime;
}
return timePlaying >= seconds;
});
}
IPromise FinishedSpecificNumberOfLevels(int levelCount)
{
return promiseTimer.WaitUntil(_ => levelsCompleted >= levelCount);
}
</code>
使用(Using)**等到(WaitUntil)**我们可以快速轻松地构建仍可管理的复杂系统.例如,让我们假设我们疯狂的shapechanger游戏具有怪异的成就系统.在海豚赛中,在星期二还剩1秒的水平时,它能取得一个不错的成就(我不知道你为什么会这么做,但这可能很有趣).您可以想象这些成就类型对代码来说有多么复杂.特别是如果您的游戏中有150多个.我们可以用(we can quickly and easily build complicated systems that are still manageable. For instance, let us assume our crazy shapechanger game has a whacky achievement system. It has an achievement for completing a level with 1 second left on the clock, on a Tuesday, while in dolphin form (I don’t know why you’d do this, but it could be fun). You can imagine how complicated these types of achievements can be to code. Especially if your game has 150+ of them. We can use)**等到(WaitUntil)**监视一组复杂的条件并动态响应:(to monitor a complex set of conditions and dynamically respond:)
<code>class LevelManager
{
DateTime timeOfLevelStart;
Character character;
float curTimeRemaining; // The amount of time left on the clock
float totalLevelTime = 60f; // 1 minute to finish the level.
PromiseTimer promiseTimer = new PromiseTimer();
public LevelManager()
{
timeOfLevelStart = DateTime.Now();
curTimeRemaining = totalLevelTime;
// Timer for the level,
// this time counting down so we can catch that last second
promiseTimer.WaitUntil(timeData =>
{
curTimeRemaining = totalLevelTime - timeData.elapsedTime;
return curTimeRemaining <= 0;
})
.Done();
promiseTimer.WaitUntil(_ =>
{
return
timeOfLevelStart.DayOfTheWeek == DayOfTheWeek.Tuesday &&
character.IsInDolphinMode &&
curTimeRemaining <= 1f &&
curTimeRemaining > 0f;
})
.Then(() => ShowAchievementNotification("NailedItOnDolphinDay"))
.Done();
}
}
</code>
嵌套的承诺(Nested Promises)
我们已经看到了嵌套在其他承诺中的承诺示例,现在让我们更深入地研究如何使用它们.(We have already seen examples of promises nested within other promises, now let’s take a more in-depth look at using them.) 想象一下,我们正在构建一个游戏,该游戏在开始时就有一个教程.本教程显示了一些操作,然后在等待玩家以特定方式进行交互然后继续之前暂停.我们可以通过从较小的承诺中建立更大的承诺来构建这一点:(Imagine we are building a game that has a tutorial at the beginning. The tutorial shows some action, then pauses while it waits for the player to interact in a specific manner before moving on. We can construct this by building a larger promise from smaller promises:)
<code>IPromise MoveCamera(Vector3 endPosition, float durationSeconds)
{
var startPosition = camera.CurPosition;
return promiseTimer.WaitUntil(timeData =>
{
camera.CurPosition = Vector3.Lerp(
startPosition,
endPosition,
timeData.elapsedTime / durationSeconds
);
return IsPositionCloseEnough(camera.CurPosition, endPosition);
});
}
</code>
好的,所以没有新内容.现在一个等待玩家输入的函数:(OK so nothing new yet. Now a function that waits for player input:)
<code>IPromise WaitForKeyInput(KeyCode key)
{
return promiseTimer.WaitUntil(_ => InputManager.GetKey(key));
}
</code>
现在,我们对文本信息进行排序,从而可以选择等待用户按下空格以从一条消息前进到另一条消息.布尔值使其可选,因为序列中的最后一个文本框不需要它:(Now we sequence textual information, allowing the option of waiting for the user to press space to progress from one message to another. The boolean makes it optional because it isn’t needed for the last text box in the sequence:)
<code>Func<IPromise> PrepTextBoxesForStage(string text, bool waitForInput)
{
return () =>
{
TextBox.Text = text;
TextBox.Show(); // Sets up the text and displays it on screen.
if (waitForInput)
{
return WaitForKeyInput(KeyCode.Space);
}
return Promise.Resolved();
}
}
</code>
现在,我们来构建本教程的序列.我们将相机移至某个位置,显示1条或更多信息,然后等待输入:(Now let’s build the sequence for the tutorial. We move the camera to a certain position, show 1 or more messages and wait for input:)
<code>Func<IPromise> PrepTutorialStage(
Vector3 cameraPosition,
string[] texts,
KeyCode key
)
{
// If the index is the last one in the array,
// then we don't want to wait for the user's input
var textBoxes = texts.Select(
(text, index) =>
PrepTextBoxesForStage(text, index != texts.length - 1)
);
return () =>
{
return MoveCamera(cameraPosition, 2f)
.ThenSequence(() => textBoxes)
.Then(() => WaitForKeyInput(key));
}
}
</code>
接下来,我们假设我们在数据库或电子表格中描述了阶段,从中可以加载以下内容的集合:(Next we’ll assume that we have our stages described in a database or spreadsheet from which we can load a collection of)**教程阶段(TutorialStage)**结构:(structures:)
<code>struct TutorialStage
{
public Vector3 Position;
public string[] Texts;
public KeyCode key;
}
</code>
最后,将其全部放到一个非常小的函数中,该函数返回一个可在本教程完成时解决的承诺:(Finally pulling it altogether into a very small function, which returns a promise that resolves when the tutorial has completed:)
<code>IPromise RunTutorial(IEnumerable<TutorialStage> tutorialData)
{
var tutorialStages = tutorialData
.Select(data =>
PrepTutorialStage(data.Position, data.Texts, data.key)
);
return Promise.Sequence(tutorialStages);
}
</code>
ew,现在我们有了一系列教程步骤,每个步骤都是根据上下文定制的,能够等待用户交互,由数据驱动,并且过程的每个部分都是分离和隔离的.为了对此进行更具体的实现,我们建立了一个演示它的Unity项目.请随意将其拉下并戳入代码.在这里能找到它(Phew, now we have a sequence of tutorial steps each customised to context, able to wait for user interaction, driven by data and with each part of the process separated and isolated. For a more specific implementation of this we have set up a Unity project demonstrating it. Please feel free to pull it down and poke around the code. It can be found here) 在GitHub上(on GitHub)
诺言vs Unity协程(Promises vs Unity Coroutines)
承诺可以替代(Promises are an alternative to) 协程(coroutines) 在Unity中协程在我们的工具箱中占有一席之地,但我们更喜欢诺言.如果您曾经使用过协程任何程度的复杂性,您就会知道,随着代码变得更加复杂,理解协程的能力会迅速降低.(in Unity. Coroutines have their place in our toolbox, but we prefer promises. If you have ever used coroutines to any degree of complexity you will know that the ability to comprehend them diminishes quickly as the code gets more complex) Unity中的协程基于C#(Coroutines in Unity build on C#) 迭代器(iterators) . Unity在这里有控制权…它运行我们的协程,该协程返回一个迭代器.然后,Unity逐个元素地逐步推进迭代器,从而推进迭代器的执行.每次我们的协程(. Unity has control here… it runs our coroutine which returns an iterator. Unity then steps that iterator forward, element by element, advancing the execution of the iterator. Each time our coroutine) 产量(yields) 我们将控制权交还给Unity,在以后的Unity中,迭代器将再次前进,以使协程得以继续进行,直到再次产生或最终完成为止.(we give control back to Unity, in the future Unity steps the iterator forward again allowing our coroutine to progress until it yields again or finally completes.) 承诺来自Javascript世界,Javascript没有协程,尽管(Promises are from the Javascript world and Javascript doesn’t have coroutines, although) 发电机(generators) 进来(are coming in) ES6(ES6) 人们已经在谈论如何做(and people are already talking about how to do) Java语言中的协程(coroutines in Javascript) 和(and the) 优点(pros) 和(and) 缺点(cons) 这样.协程似乎激发了爱与恨的平等衡量标准.(of doing so. Coroutines seem to inspire equal measures of love and hate.) 当然,我们相信使用promise会比使用协程产生更多的表达性和可维护性的代码.(Of course we believe the use of promises results in more expressive and maintainable code than using coroutines.) 但是,协程确实具有一个主要优点:读取代码流容易得多.这使得调试线性代码序列变得更加容易(即使使用协程,整体调试效率会大大降低).唯一阻碍阅读代码流的方法是经常使用(However, coroutines do have one major advantage: it is much easier to read the flow of the code. This makes it easier to debug a linear sequence of code (even though overall debugging effectiveness is greatly reduced with coroutines). The only thing that gets in the way of reading the flow of the code is the frequent requirement to use)**收益回报…(yield return …)**相对于(as opposed to)然后(Then),(,)**然后全部(ThenAll)**和使用诺言时的类似功能.两者都有碍您发展,但在协程中,绝对可以通过较短的线性代码序列轻松阅读和调试.(and similar functions when working with promises. Both get in your way, but in coroutines it is definitely easier to read and debug over a short linear sequence of code.) 尽管这是使用协程的唯一优点,但存在多个缺点.(While this is the only advantage to using coroutines, there are multiple disadvantages.) 从较小的协程中构建较大的协程是困难的.嵌套的协程很难管理,您当然可以做到,但是起初并不清楚如何实现.当然,只要完成一两次,就足够简单了.尝试执行100次.另一方面,promise可以缝合在一起并以多种方式嵌套以产生更大的逻辑序列.这种(It is difficult to build larger coroutines out of smaller coroutines. Nested coroutines are tough to manage, you can do it of course but at first it isn’t obvious how to achieve it. Sure, it’s simple enough if you’ve done it once or twice. Try doing it 100s of times. On the other hand promises can be stitched together and nested in many ways to produce larger sequences of logic. This sort of) 可组合性(composability) 结果更好(results in better) 代码重用(code reuse) .最后,您将获得一个用于常见逻辑的高级承诺返回函数库,您可以将这些库组成以创建新行为,从而创建新的和不同的游戏逻辑.重组和重排承诺是微不足道的,可以提高您进行快速实验的能力,这对于在寻找使游戏充满乐趣的调整时提高生产力至关重要.(. You’ll end up with a library of higher-level promise-returning functions for common logic that you can compose to build new behaviour creating new and different game logic. Recomposing and rewiring promises is trivial and increases your ability for fast experimentation, something that is essential to being productive while searching for the tweaks that put the fun in your game.) 协程的另一个问题是缺乏对它们的控制. Unity自动运行协程,您无法控制何时以及如何推进协程,并且您不知道在任何特定时刻运行了哪些协程.这就是为什么协程特别难以调试的原因(即使调试单个协程实际上更容易).对于理解您的代码在做什么,这是一场彻底的灾难.您对协程的唯一控制权是从协程内部屈服的能力(因此将控制权交还给Unity),或者您可以停止一个或所有协程.(Another problem with coroutines is the lack of control you have over them. Unity runs coroutines automatically, you don’t have any control over when and how coroutines are advanced and you have no idea what coroutines are running at any particular point. This is why coroutines are particularly hard to debug (even though debugging a single coroutine is in fact easier). This is a complete disaster for understanding what your code is doing. The only control you have over coroutines is the ability to yield from within the coroutine (thus giving control back to Unity) or you can stop one or all coroutines.) 停止一个协程有其自身的问题.我们只能通过将协程名称指定为(Stopping a single coroutine has its own problems. We can only stop it by specifying the name of the coroutine as a) 魔术弦(magic string) (每个人都知道这不好吗?).这影响了((everyone knows this is bad right?). This impacts the) 类型安全(type-safety) 的代码,破坏了Visual Studio中的自动重构工具.单个诺言或一组诺言可以通过停止(of our code and breaks the automated refactoring tools in Visual Studio. Single promises or a group of promises can easily be stopped by stopping the)**PromiseTimer(PromiseTimer)**对他们负责.没有(that is responsible for them. No) 魔术弦(magic strings) 是实现这一目标所必需的.(are necessary to achieve this.) 使用(Using the)**PromiseTimer(PromiseTimer)**给了我们背部控制权.我们可以在任何地方更新承诺,(启用调试时)可以显示当前正在运行的承诺列表.我们可以使用多个(gives us back control. We can update promises wherever we want and (when debugging is enabled) we can display a list of the promises that are currently running. We can use multiple)**PromiseTimer(PromiseTimer)**可以以不同的方式控制不同的承诺组,例如,我们可以暂停一组承诺,而允许另一个组继续运行.(s to control different groups of promises in different ways, for instance we could pause a group of promises while we allow another group to continue running.) 想知道使用协程的主要缺点吗?它们只能从(Want to know the major disadvantage of using coroutines? They can only be used from a) 单性行为(MonoBehaviour) .如果您使用协程,则您的代码为(. If you use coroutines then your code is) 紧密耦合(tightly coupled) 到Unity,并且没有机会在它之外运行代码.这极大地限制了代码重用和测试驱动开发的机会.如果您不了解此需求,也许对您来说不会有问题,请继续使用协程.(to Unity and there is no chance that you’ll run your code outside of it. This drastically limits opportunities for code reuse and test driven development. If you don’t understand the need for this, maybe it won’t be a problem for you, go ahead and use coroutines.)
几乎是行为树?(Almost Behaviour Trees?)
当我们与承诺一起工作并将其扩展到游戏开发中时,我们开始感到自己几乎在重新发明(As we worked with promises and extended them for game development we started to get the feeling that we were almost reinventing) 行为树(behaviour trees) .(.) 我们并没有完全构建AI(行为树是做什么的).我们正在建立互动游戏行为.对声音,动画,用户交互等进行排序.对级别流程进行排序,并评估进入下一个级别所需的条件.(We weren’t quite building AI (what behaviour trees are for). We were building interactive game behaviour. Sequencing sound, animation, user interaction, etc. Sequencing the flow of levels and the evaluation of the conditions required to progress to the next level.) 承诺(当我们使用它们时)和行为树具有一些重叠的功能…(Promises (as we use them) and behaviour trees have some overlapping functionality…) 使用扩展的Promise API,我们可以并行运行异步操作(使用(With the extended promises API we can run async operations in parallel (using)**所有(All)**和(and)然后全部(ThenAll)).这与使用(). This is very similar to using a) 并行节点(parallel node) 在行为树中.(in a behaviour tree.) 我们可以按顺序运行异步操作(使用(We can run async operations sequentially (using)**顺序(Sequence)**和(and)然后序列(ThenSequence)),这与使用() and this is very similar to using a) 序列节点(sequence node) 在行为树中.(in a behaviour tree.) 承诺可以分层嵌套.也就是说,promise可以嵌套嵌套的promise,而父promise将在其子promise解析后解决.行为树也可以嵌套,它们毕竟是(Promises can be nested hierarchically. That is to say that promises can have nested promises and the parent promise will resolve when its child promise(s) have resolved. Behaviour trees as well can be nested, they are after all a) 树(tree) ,固有地(, an inherently) 递归的(recursive) 数据结构.(data structure.) 行为树有一个(Behaviour trees have a) 选择器节点(selector node) ,Promise API中没有相应功能,因此,如果需要添加一个功能很困难.(, to which there is no corresponding feature in the promises API, not that it would be difficult to add one should it be needed.) 当您达到诺言的极限时,您可能需要考虑使用行为树,它们比诺言具有许多优势…(When you come to the limits of promises you may want to consider using behaviour trees, they have a number of advantages over promises…) 行为树通常由数据驱动,由游戏设计师使用编辑器构建.这允许非程序员直接构建,调整和平衡,并且是构建游戏逻辑和AI的理想方法.对于独立游戏开发者来说,使用行为树编辑器可能不是一种选择,而实际上拥有一名游戏设计师也可能不是一种选择,因为程序员经常也是游戏设计师.在这些情况下,代码驱动的游戏AI可以很好地工作.我们当然已经在这个领域中将承诺推了很远,并对结果感到非常满意.但是我们可以(Behaviour trees are typically data-driven and built by game designers using an editor. This allows non-programmers to directly build, tweak and balance and is the ideal way to build game logic and AI. For indie game devs it may not be an option to use a behaviour tree editor and it may not even be an option to actually have a game designer as often the programmer will also be the game designer. In these situations code-driven game AI can work very well. We have certainly pushed promises very far already in this space and have been very happy with the result. We can however)**想像(imagine)**行为树的代码驱动fluent-API,在某些情况下更适合我们.(a code-driven fluent-API for behaviour trees that would better suit us in some circumstances.) 行为树更易于控制.它们被设计为暂停,停止,重新启动等.尽管我们至少可以暂停或停止通过运行的基于时间的承诺,但承诺并非旨在执行这些操作.(Behaviour trees are easier to control. They are designed to be paused, stopped, restarted, etc. Promises are not designed to do these things, although we can at least pause or stop a time-based promise that is running via the)PromiseTimer(PromiseTimer),但是很难(即使不是不可能)停止正在进行中的异步操作,例如等待网络事务处理的结果.并不是说我们不能通过承诺解决这些问题,但是如果您对承诺做出足够的承诺以解决这些问题,您可能会开始考虑使用行为树.我们的意思是,您应该在可能有用的地方使用承诺…并为那些变得难以实现的领域引入行为树.(, however it is difficult if not impossible to stop a mid-flight asynchronous operation such as awaiting the result of an network transaction. It’s not that we can’t work around these issues with promises, but if you take promises far enough that you hit these issues, you might want to start thinking about using behaviour trees. What we’re saying is you should use promises where they are useful… and bring in behaviour trees for those areas where promises become unwieldy.) 根据情况,行为树的调试也容易得多.如果您拥有行为树编辑器的奢侈选择,那么您可能还可以使用某种视觉行为树调试器.这很棒,但是当然不能替代在真正的调试器中调试实际代码来确定正在发生的事情.调试承诺可能很困难,但是至少您必须在真正的调试器中做到.我们认为,幻想行为树fluent-API比promises API更容易调试.(Debugging can also be much easier for behaviour trees depending on the situation. If you have the luxury of having a behaviour tree editor you may also have some kind of visual behaviour tree debugger. This is great, but of course there is no substitute for debugging real code in a real debugger to work out what is going on. Debugging promises can be difficult, but at least you get to do it in a real debugger. We imagine that our fantasy behaviour tree fluent-API is easier to debug than the promises API.)
结论(Conclusion)
通过本文,我们描述了使用promises模式进行游戏开发的经验.任何先进的技术都有其优点和缺点,您应该被充分了解,了解风险并就将哪种技术引入您的开发过程做出有根据的决策.(Through this article we have described our experience using the promises pattern for game development. Any advanced technique has both benefits and disadvantages and you should be well informed, understand the risks and make educated decisions on which tech to bring into your development process.) 将错误的技术引入您的流程可能会造成灾难性的后果.万物都有它们的位置,但是如果使用不当或在错误的情况下使用,它们可能会对您的生产率产生负面影响,因此请当心,并制定针对特定技术无法使用的计划B.(Bringing the wrong technique or technology into your process can be disastrous. All things have their places, but used badly or in the wrong situation they can negatively impact your productivity, so be careful and have a plan B for when a particular technology doesn’t work for you.) 确保计划好学习时间.您不能急于寻求某些东西,并且希望立即有效.您需要了解有用的情况,并了解如何解决和减轻潜在的陷阱.(Make sure you plan time to learn. You can’t rush into something and expect to be effective with it immediately. You need to understand the situations where it is useful and understand how to workaround and mitigate the potential pitfalls.) 请不要让它关闭你!我们只想确保您知道自己正在进入的领域.一定要切入并探索诺言,我们希望本文能帮助您为此做准备.请注意,任何先进技术都可能是一个黑洞.(Please don’t let this turn you off! We just want to make sure you are aware of what you are getting yourself into. By all means jump in and explore promises and we hope this article has helped you prepare for that. Just be aware that any advanced technology can be a black hole…) 请转到github下载C#promises库,然后(Please go to github to download the C# promises library and)PromiseTimer(PromiseTimer):(:) https://github.com/Real-Serious-Games/c-sharp-promise(https://github.com/Real-Serious-Games/c-sharp-promise) .(.) 可以在此处找到Unity示例:(The Unity example can be found here:) https://github.com/adamsingle/PromisesUnityDemo(https://github.com/adamsingle/PromisesUnityDemo) 谢谢阅读.灰和亚当.(Thanks for reading. Ash and Adam.)
关于作者(About the Authors)
阿什莉`戴维斯(Ashley Davis)(Ashley Davis)
Ash是在澳大利亚布里斯班生活和工作的软件开发人员.(Ash is a software developer living and working in Brisbane, Australia.) 自1998年以来,他一直从事专业游戏的开发,并在其他行业从事过一些活动.在2012年,Ash进入了严肃游戏和模拟世界,在Real Serious Games担任首席开发人员,利用他的经验使游戏技术为企业服务.(He has been developing games professionally since 1998 with a few interludes in other industries. In 2012 Ash moved into the world of serious games and simulations taking on the role of Lead Developer at Real Serious Games where he uses his experience to make game technology work for business.) Ash既是专职开发人员,也是承包商的名字(Ash is both a full time developer and a contractor under the name) 代码雀跃(Code Capers) 并正在开发云和移动产品.(and is working on cloud and mobile products.) Ash经常为开源社区做出贡献,并且是布里斯班游戏行业聚会的创始人和组织者:游戏技术布里斯班和游戏开发布里斯班.(Ash regularly contributes to the open source community and is a founder and organiser of games industry meetups in Brisbane: Game Technology Brisbane and Game Development Brisbane.) 对于更长的生物,请参阅Ash的个人资料(For a longer bio please see Ash’s profile on) 链接到(linked in) .(.)
亚当`辛格(Adam Single)
亚当是丈夫,父亲,专业开发人员,独立开发人员,音乐爱好者和游戏玩家.他是7Bit Hero的编码器,是Real Serious Games技术团队的程序员,Sly Budgie的联合创始人,程序员和联合设计师,以及游戏技术和游戏开发布里斯班聚会的共同组织者.(Adam is a Husband, Father, Professional Developer, Indie Developer, lover of music and gamer. He’s the coder for 7Bit Hero, a programmer on the tech team at Real Serious Games, co founder, programmer and co designer at Sly Budgie and co organiser of the Game Technology and Game Development Brisbane Meetups.) 自2011年进入专业游戏开发行业以来,Adam从事过许多手机游戏,包括Android热门Photon和专为迪士尼日本特定手机设计的预装游戏.他是一个团队的程序员,该团队在昆士兰科技大学令人惊叹的多点触摸屏装置The Cube上创建了一个巨大的交互式显示器,这是澳大利亚首个数字写作驻地计划的一部分,并在Real Serious Games的一个团队工作,创建了大规模的交互式模拟对于采矿和建筑业,其中一些是使用Oculus Rift的基于虚拟现实的解决方案.所有这些都是使用Unity游戏引擎完成的.(Since entering the professional game development industry in 2011, Adam has worked on numerous mobile games, including the Android hit Photon and a pre-install game designed for specific Disney Japan handsets. He’s been the programmer on a team that created a huge, interactive display at Queensland University of Technology’s amazing multi touch screen installation The Cube as a part of Australia’s first Digital Writing Residency and worked on a team at Real Serious Games creating large scale, interactive simulations for the mining and construction industries, some of which are Virtual Reality based solutions using the Oculus Rift. All of this has been done using the Unity game engine.) 亚当对现代技术固有的独特和引人入胜的可能性充满热情.当他不为Sly Budgie开发令人兴奋的新游戏机制时,他正在尝试使用手机技术进行"自制VR",利用现代Web开发技术构建移动应用程序,并将令人兴奋的想法推到7Bit Hero的现场音乐/多人游戏互动背后引人入胜的迷人之路.(Adam has a passion for the unique and engaging possibilities inherent in modern technology. When he’s not working on exciting new game mechanics for Sly Budgie, he’s experimenting with “home made VR” using mobile phone technology, building mobile applications utilizing modern web development technologies and pushing the exciting ideas behind 7Bit Hero’s live music/multiplayer game interaction down whichever fascinating path it should happen to lead.)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C# Dev 新闻 翻译