此示例已用于另一个问题,以说明协同程序如何用于编写视频游戏中的过场动画:
bob.walkto(jane)
bob.lookat(jane)
bob.say("How are you?")
wait(2)
jane.say("Fine")
...
每个函数产生主动引擎,它在恢复协程之前进行动画,定时等。协程的可能替代方案是事件队列而不是代码,但是必须实现控制逻辑和循环作为事件。是否有其他可用于实现此类功能的协同程序的替代方案?我在一些文章中看到过回调,但我不确定代码的外观。
答案 0 :(得分:3)
你没有提到你正在使用的是什么语言,所以我将在Lua中用中间层提供面向对象的方式写这个 - https://github.com/kikito/middleclass(免责声明:我是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
的子类,它们将定义start
和stop
方法,并默认情况下将状态初始化为'new'
。还有一个默认的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。 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)
。而且你不需要协同程序。
答案 1 :(得分:3)
协同程序非常适合这种情况,因为您可以毫不费力地保留所有本地状态变量。即无需手动将其存储在某处的上下文中。
但我没有看到事件系统作为替代方案。除了基于协程的脚本系统之外,您最有可能想要的补充。
示例(在一些连贯的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)
调用将产生脚本调度程序并允许其他脚本运行。 WalkTo
,FaceObject
,PlayAnimation
和OpenDoor
也会调用Sleep
来屈服。根据已知的动画持续时间,定期唤醒以查看探路者和运动系统是否已完成行走或其他任何事情。
如果NPC遇到他在去门途中必须处理的情况会怎样?您不希望必须在基于协程的代码中检查所有这些事件。有一个事件系统补充协程将使这很容易:
垃圾桶翻倒:垃圾桶可以向附近的所有NPC广播活动。 NPC对象决定在其堆栈上推送新的行为对象以进行修复。 WalkTo
行为在某处产生Sleep
调用,但现在由于事件而导致FixTrashcan
行为正在运行。当FixTrashcan
完成后,WalkTo
行为将从Sleep
唤醒,并且永远不会知道垃圾邮件事件。但它仍将在通往大门的路上,在它下面,我们仍在运行EnterHouse
。
爆炸发生:爆炸像垃圾箱一样广播一个事件,但这次NPC对象决定重置它的运行行为并推送FleeInPanic
行为。他不会回到EnterHouse
。
我希望你能通过在AI系统中共享事件和协同程序来了解我的意思。您可以使用协同程序保持本地状态,同时仍然可以使用脚本调度程序,并且可以使用事件来处理中断并保持逻辑以集中处理它们而不会污染您的行为。
如果你还没有看到this article by Thomas Tong如何在C / C ++中实现单线程协同程序,我强烈推荐它。
他只使用最小的内联汇编(单个指令)来保存堆栈指针,并且代码可以轻松移植到一大堆平台上。我已经在Wintel,Xbox 360,PS3和Wii上运行了它。
调度程序/脚本设置的另一个好处是,如果您需要其他资源,那么在屏幕外或远程AI字符/脚本对象上挨饿就变得微不足道了。只需将它与您的日程安排程序中的优先级系统联系起来就可以了。
答案 2 :(得分:2)
回调(C#式伪代码):
bob.walkto(jane, () => {
bob.lookat(jane), () => {
bob.say.....
})
})
绝对不是最便捷的方式。
另一种方法是期货(也称为承诺):
futureChain = bob.walkto(jane)
.whenDone(bob.lookAt(jane))
.whenDone(...)
.after(2 seconds, jane.Say("fine"));
futureChain.run();
一种有趣的语言是E - 它内置了对期货的支持,语法比上面的更好。