协程的替代方案

发布于 2024-10-22 12:37:32 字数 289 浏览 6 评论 0原文

这个例子已在另一个问题中使用,以说明如何使用协程来编写视频游戏中的过场动画脚本:

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

旧城烟雨 2024-10-29 12:37:32

协程非常适合这种情况,因为您可以轻松地保留所有本地状态变量。即无需手动将其存储在某个上下文中。

但我不认为事件系统可以作为替代方案。除了基于协程的脚本系统之外,更多的是您最可能想要的补充。

示例(在某种程度上连贯的 C++ 中):

您已经按照以下方式使用协程实现了一种行为:

class EnterHouse : public NPCBehavior
{
    EnterHouse(House theHouse) { _theHouse = theHouse; }
    void Begin() { _theHouse.AddNPC(NPC()); }
    void Update()
    {
        NPC().WalkTo(_theHouse.GetDoor().Position());
        NPC().FaceObject(_theHouse.GetDoor());
        NPC().PlayAnimation(NPC().Animations().Get(eAnimKnockOnDoor));
        Sleep(1.0f);       
        NPC().OpenDoor(_theHouse.GetDoor());
    }
    void End() { /* Nothing */ }

    private House _theHouse;
}

想象一下 NPC 上的方法本身将创建 NPCBehavior 对象,将它们推送到某种行为堆栈上并从这些行为完成时的调用。

Sleep(1.0f) 调用将让位于您的脚本调度程序并允许其他脚本运行。 WalkToFaceObjectPlayAnimationOpenDoor 也会调用 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:

class EnterHouse : public NPCBehavior
{
    EnterHouse(House theHouse) { _theHouse = theHouse; }
    void Begin() { _theHouse.AddNPC(NPC()); }
    void Update()
    {
        NPC().WalkTo(_theHouse.GetDoor().Position());
        NPC().FaceObject(_theHouse.GetDoor());
        NPC().PlayAnimation(NPC().Animations().Get(eAnimKnockOnDoor));
        Sleep(1.0f);       
        NPC().OpenDoor(_theHouse.GetDoor());
    }
    void End() { /* Nothing */ }

    private House _theHouse;
}

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. The WalkTo, FaceObject, PlayAnimation and OpenDoor will also call Sleep 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 yielding Sleep call somewhere, but now the FixTrashcan behavior is running due to the event. When FixTrashcan completes the WalkTo behavior will wake up from Sleep and never know about the trashcan incident. But it will still be on its way to the door, and underneath it we are still running EnterHouse.

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 to EnterHouse.

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.

離人涙 2024-10-29 12:37:32

回调(C# 风格的伪代码):

bob.walkto(jane, () => {
    bob.lookat(jane), () => {
        bob.say.....
    })
})

绝对不是最方便的方法。

另一种方法是Futures(也称为承诺):

futureChain = bob.walkto(jane)
  .whenDone(bob.lookAt(jane))
  .whenDone(...)
  .after(2 seconds, jane.Say("fine"));

futureChain.run();

一种有趣的语言是E - 它内置了对 futures 的支持,语法比上面更好。

Callbacks (C#-style pseudocode):

bob.walkto(jane, () => {
    bob.lookat(jane), () => {
        bob.say.....
    })
})

Definitely not the most convenient way.

A different approach is Futures (also known as promises):

futureChain = bob.walkto(jane)
  .whenDone(bob.lookAt(jane))
  .whenDone(...)
  .after(2 seconds, jane.Say("fine"));

futureChain.run();

One interesting language to look at is E - it has built-in support for futures, with a nicer syntax than above.

蘑菇王子 2024-10-29 12:37:32

你没有提到你使用的是什么语言,所以我将用中间类提供的面向对象的 Lua 来编写这个 - https://github.com/kikito/middleclass(免责声明:我是中产阶级的创造者)

另一种选择是将过场动画分成“动作列表”。如果您已经有一个在对象列表上调用“更新”方法的游戏循环,这可能会更好地与您的代码融合。

像这样:

helloJane = CutScene:new(
  WalkAction:new(bob, jane),
  LookAction:new(bob, jane),
  SayAction:new(bob, "How are you?"),
  WaitAction:new(2),
  SayAction:new(jane, "Fine")
)

操作将有一个 status 属性,该属性具有三个可能的值:'new''running''finished'< /代码>。所有“操作类”都是 Action 的子类,它将定义 startstop 方法,并将状态初始化为 > 默认情况下为“新”。还有一个默认的 update 方法会抛出错误。

Action = class('Action')

function Action:initialize() self.status = 'new' end

function Action:stop() self.status = 'finished' end

function Action:start() self.status = 'running' end

function Action:update(dt)
  error('You must re-define update on the subclasses of Action')
end

Action 的子类可以改进这些方法,并实现 update。例如,这里是 WaitAction

WaitAction = class('WaitAction', Action) -- subclass of Action

function WaitAction:start()
  Action.start(self) -- invoke the superclass implementation of start
  self.startTime = os.getTime() -- or whatever you use to get the time
end

function WaitAction:update(dt)
  if os.getTime() - self.startTime >= 2 then
    self:stop() -- use the superclass implementation of stop
  end
end

唯一缺少的实现部分是 CutScene。过场动画主要包含三件事:
* 要执行的操作列表
* 对当前操作的引用,或该操作在操作列表中的索引
* 如下所示的更新方法:

function CutScene:update(dt)
  local currentAction = self:getCurrentAction()
  if currentAction then
    currentAction:update(dt)
    if currentAction.status == 'finished' then
      self:moveToNextAction()
      -- more refinements can be added here, for example detecting the end of actions
    end
  end
end

使用此结构,您唯一需要的就是游戏循环在每次循环迭代时调用 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:

helloJane = CutScene:new(
  WalkAction:new(bob, jane),
  LookAction:new(bob, jane),
  SayAction:new(bob, "How are you?"),
  WaitAction:new(2),
  SayAction:new(jane, "Fine")
)

Actions would have a status attribute with three possible values: 'new', 'running', 'finished'. All the "action classes" would be subclasses of Action, which would define start and stop methods, as well as initialize the status to 'new' by default. There would be also a default update method that throws an error

Action = class('Action')

function Action:initialize() self.status = 'new' end

function Action:stop() self.status = 'finished' end

function Action:start() self.status = 'running' end

function Action:update(dt)
  error('You must re-define update on the subclasses of Action')
end

Subclasses of Action can improve upon those methods, and implement update. For example, here's WaitAction:

WaitAction = class('WaitAction', Action) -- subclass of Action

function WaitAction:start()
  Action.start(self) -- invoke the superclass implementation of start
  self.startTime = os.getTime() -- or whatever you use to get the time
end

function WaitAction:update(dt)
  if os.getTime() - self.startTime >= 2 then
    self:stop() -- use the superclass implementation of stop
  end
end

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:

function CutScene:update(dt)
  local currentAction = self:getCurrentAction()
  if currentAction then
    currentAction:update(dt)
    if currentAction.status == 'finished' then
      self:moveToNextAction()
      -- more refinements can be added here, for example detecting the end of actions
    end
  end
end

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文