这个例子已经在另一个问题中被用来说明如何使用协程来编写视频游戏中的动画:
bob.walkto(jane)
bob.lookat(jane)
bob.say("How are you?")
wait(2)
jane.say("Fine")
...在恢复协程之前,每个函数都会让给主引擎进行动画、计时等工作。协程的一个可能的替代方案是一个事件队列,而不是代码,但然后必须将控制逻辑和循环实现为事件。有没有其他可以用来实现这种功能的协程的替代方案?我看过一些文章中提到的回调,但我不确定代码看起来会是什么样子。
发布于 2011-03-16 20:08:10
协程非常适合这样做,因为您可以轻松地保留所有本地状态变量。即不必手动将其存储在某处的上下文中。
但我不认为事件系统是一种替代方案。作为补充,除了基于协程的脚本系统之外,您很可能还想拥有它。
示例(在一些连贯的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;
}想象一下,NPCs上的方法本身将创建NPCBehavior对象,将它们推送到某种行为堆栈上,并在这些行为完成时从调用中返回。
Sleep(1.0f)调用将提交给您的脚本调度程序,并允许其他脚本运行。WalkTo、FaceObject、PlayAnimation和OpenDoor也将调用Sleep来让步。或者基于已知的动画持续时间,周期性地唤醒,看看探路器和运动系统是否完成了行走或其他什么。
如果全国人民代表大会在上门的路上遇到了他必须处理的情况怎么办?您不希望在基于协程的代码中到处检查所有这些事件。有一个事件系统来补充协程将会使这件事变得简单:
垃圾桶在上倒下:垃圾桶可以向所有附近的NPCs广播事件。NPC对象决定将一个新的behavior对象推送到他的堆栈中去修复它。WalkTo行为在某个正在产生结果的Sleep调用中,但现在由于该事件,FixTrashcan行为正在运行。当FixTrashcan完成时,WalkTo行为将从Sleep中唤醒,并且永远不会知道垃圾桶事件。但它仍然在通往大门的路上,在它下面,我们仍然在运行EnterHouse。
发生爆炸:爆炸广播一个事件,就像垃圾桶一样,但这次NPC对象决定重置它的运行行为并推送一个FleeInPanic行为。他不会再回EnterHouse了。
我希望你明白我所说的让事件和协程一起生活在AI系统中是什么意思。您可以使用协程来保持本地状态,同时仍然服从于您的脚本调度程序,并且您可以使用事件来处理中断,并保持集中处理它们的逻辑,而不会污染您的行为。
如果你还没有看过关于如何用C/C++实现单线程协程的this article by Thomas Tong,我强烈推荐它。
他只使用了最小的内联汇编(单个指令)来保存堆栈指针,并且代码可以很容易地移植到整个平台上。我已经在Wintel、xbox360、PS3和Wii上运行过。
调度器/脚本设置的另一个好处是,如果您需要资源用于其他事情,那么让屏幕外或遥远的AI字符/脚本对象挨饿就变得微不足道了。只要将它与你的调度程序中的优先级系统结合起来,你就可以开始工作了。
发布于 2011-03-16 19:53:26
您没有提到您使用的是什么语言,所以我将使用middleclass https://github.com/kikito/middleclass提供的面向对象的Lua来编写本文(免责声明:我是middleclass的创建者)
另一种选择是将你的场景分割成“动作列表”。如果你已经有一个在对象列表上调用“update”方法的游戏循环,这可能会更好地与你的代码相融合。
如下所示:
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')
endAction的子类可以改进这些方法,并实现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)。这样就消除了对协程的需求。
发布于 2011-03-16 19:23:27
回调(C#风格的伪代码):
bob.walkto(jane, () => {
bob.lookat(jane), () => {
bob.say.....
})
})绝对不是最方便的方式。
另一种方法是期货(也称为promises):
futureChain = bob.walkto(jane)
.whenDone(bob.lookAt(jane))
.whenDone(...)
.after(2 seconds, jane.Say("fine"));
futureChain.run();值得关注的一种有趣的语言是E -它内置了对未来的支持,具有比上面更好的语法。
https://stackoverflow.com/questions/5324487
复制相似问题