协程的替代方案
这个例子已在另一个问题中使用,以说明如何使用协程来编写视频游戏中的过场动画脚本:
bob.walkto(jane)
bob.lookat(jane)
bob.say("How are you?")
wait(2)
jane.say("Fine")
...
每个函数都会交给主引擎,该引擎在恢复协程之前执行动画、计时等操作。协程的一种可能替代方案是事件队列而不是代码,但必须将控制逻辑和循环实现为事件。是否有其他协程替代方案可用于实现此类功能?我在一些文章中看到过回调,但我不确定代码会是什么样子。
This example has been used in another question to illustrate how coroutines can be used to script cutscenes in a video game:
bob.walkto(jane)
bob.lookat(jane)
bob.say("How are you?")
wait(2)
jane.say("Fine")
...
Each function yields to the main engine which does animation, timing, etc. before resuming the coroutine. A possible alternative to coroutines would be an event queue instead of code, but then one has to implement control logic and loops as events. Are there any other alternatives to coroutines that can be used to implement this kind of functionality? I've seen callbacks mentioned in some articles, but I'm not sure how the code would look.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
协程非常适合这种情况,因为您可以轻松地保留所有本地状态变量。即无需手动将其存储在某个上下文中。
但我不认为事件系统可以作为替代方案。除了基于协程的脚本系统之外,更多的是您最可能想要的补充。
示例(在某种程度上连贯的 C++ 中):
您已经按照以下方式使用协程实现了一种行为:
想象一下 NPC 上的方法本身将创建 NPCBehavior 对象,将它们推送到某种行为堆栈上并从这些行为完成时的调用。
Sleep(1.0f)
调用将让位于您的脚本调度程序并允许其他脚本运行。WalkTo
、FaceObject
、PlayAnimation
和OpenDoor
也会调用Sleep
来让出。基于已知的动画持续时间,定期唤醒以查看探路器和运动系统是否完成行走或其他操作。如果NPC在去门口的路上遇到必须处理的情况怎么办?您不想在基于协程的代码中到处检查所有这些事件。使用事件系统来补充协程将使这一切变得简单:
垃圾桶翻倒:垃圾桶可以向附近的所有 NPC 广播事件。 NPC 对象决定将一个新的行为对象推送到他的堆栈上来修复它。
WalkTo
行为处于某处的生成Sleep
调用中,但现在FixTrashcan
行为由于该事件而正在运行。当FixTrashcan
完成时,WalkTo
行为将从Sleep
中唤醒,并且永远不会知道垃圾桶事件。但它仍然会在通往门口的路上,而在它的下面我们仍然在运行EnterHouse
。发生爆炸:爆炸会像垃圾桶一样广播事件,但这次 NPC 对象决定重置其运行行为并推送
FleeInPanic
行为。他不会返回EnterHouse
。我希望您明白我所说的让事件和协程同时存在于人工智能系统中是什么意思。您可以使用协程来保持本地状态,同时仍然屈服于脚本调度程序,并且您可以使用事件来处理中断并保持集中处理它们的逻辑,而不会污染您的行为。
如果您还没有看过 Thomas Tong 撰写的这篇文章,了解如何实现单-C/C++ 中的线程协程 我强烈推荐它。
他只使用最少量的内联汇编(一条指令)来保存堆栈指针,并且代码可以轻松移植到一大堆平台。我已经在 Wintel、Xbox 360、PS3 和 Wii 上运行过它。
调度程序/脚本设置的另一个好处是,如果您需要资源做其他事情,那么饿死屏幕外或远处的人工智能角色/脚本对象就变得微不足道了。只需将其与调度程序中的优先级系统结合起来,就可以开始了。
Coroutines are well suited for this since you get to keep all your local state variables with no hassle. I.e. without having to manually store it in a context somewhere.
But I don't see an event system as an alternative. More as a supplement that you will most likely want to have in addition to a coroutine-based scripting system.
Example (in somewhat coherent C++):
You have implemented a behavior using coroutines along these lines:
Imagine that the methods on the NPCs will themselves create NPCBehavior objects, push them on some sort of behavior stack and return from the call when those behaviors complete.
The
Sleep(1.0f)
call will yield to your script scheduler and allow other scripts to run. TheWalkTo
,FaceObject
,PlayAnimation
andOpenDoor
will also callSleep
to yield. Either based on a known animation duration, to wake up periodically to see if the pathfinder and locomotion system are done walking or whatever.What happens if the NPC encounters a situation he will have to deal with on the way to the door? You don't want to have to check for all these events everywhere in your coroutine-based code. Having an event system supplement the coroutines will make this easy:
An trashcan topples over: The trashcan can broadcast an event to all nearby NPCs. The NPC object decides to push a new behavior object on his stack to go and fix it. The
WalkTo
behavior is in a yieldingSleep
call somewhere, but now theFixTrashcan
behavior is running due to the event. WhenFixTrashcan
completes theWalkTo
behavior will wake up fromSleep
and never know about the trashcan incident. But it will still be on its way to the door, and underneath it we are still runningEnterHouse
.An explosion happens: The explosion broadcasts an event just like the trashcan, but this time the NPC object decides to reset it running behaviors and push a
FleeInPanic
behavior. He will not return toEnterHouse
.I hope you see what I mean by having events and coroutines live together in an AI system. You can use coroutines to keep local state while still yielding to your script scheduler, and you can use events to handle interruptions and keep the logic to deal with them centralized without polluting your behaviors.
If you haven't already seen this article by Thomas Tong on how to implement single-threaded coroutines in C/C++ I can highly recommend it.
He uses only the tiniest bit of inline assembly (a single instruction) to save the stack pointer, and the code is easily portable to a whole bunch of platforms. I've had it running on Wintel, Xbox 360, PS3 and Wii.
Another nice thing about the scheduler/script setup is that it becomes trivial to starve off-screen or far-away AI characters/scripted objects if you need the resources for something else. Just couple it with a priority system in you scheduler and you are good to go.
回调(C# 风格的伪代码):
绝对不是最方便的方法。
另一种方法是Futures(也称为承诺):
一种有趣的语言是E - 它内置了对 futures 的支持,语法比上面更好。
Callbacks (C#-style pseudocode):
Definitely not the most convenient way.
A different approach is Futures (also known as promises):
One interesting language to look at is E - it has built-in support for futures, with a nicer syntax than above.
你没有提到你使用的是什么语言,所以我将用中间类提供的面向对象的 Lua 来编写这个 - https://github.com/kikito/middleclass(免责声明:我是中产阶级的创造者)
另一种选择是将过场动画分成“动作列表”。如果您已经有一个在对象列表上调用“更新”方法的游戏循环,这可能会更好地与您的代码融合。
像这样:
操作将有一个
status
属性,该属性具有三个可能的值:'new'
、'running'
、'finished'< /代码>。所有“操作类”都是
Action
的子类,它将定义start
和stop
方法,并将状态初始化为> 默认情况下为“新”
。还有一个默认的update
方法会抛出错误。Action 的子类可以改进这些方法,并实现
update
。例如,这里是WaitAction
:唯一缺少的实现部分是 CutScene。过场动画主要包含三件事:
* 要执行的操作列表
* 对当前操作的引用,或该操作在操作列表中的索引
* 如下所示的更新方法:
使用此结构,您唯一需要的就是游戏循环在每次循环迭代时调用
helloJane:update(dt)
。并且您消除了对协程的需要。You didn't mention what language you were using, so I'm going to be writing this in Lua with object orientation provided by middleclass - https://github.com/kikito/middleclass (disclaimer: I'm middleclass' creator)
Another option would be splitting your cutscenes into "lists of actions". This will probably blend better with your code, if you already have a game loop that invokes an 'update' method on lists of objects.
Like this:
Actions would have a
status
attribute with three possible values:'new'
,'running'
,'finished'
. All the "action classes" would be subclasses ofAction
, which would definestart
andstop
methods, as well as initialize the status to'new'
by default. There would be also a defaultupdate
method that throws an errorSubclasses of Action can improve upon those methods, and implement
update
. For example, here'sWaitAction
:The only missing implementation part is CutScene. A CutScene will mainly have three things:
* A list of actions to execute
* A reference to the current action, or the index of that action on the action list
* An update method like the following:
With this structure, the only thing you need is your game loop calling
helloJane:update(dt)
on every loop iteration. And you eliminate the need of coroutines.