2011-03-16 27 views
8

这个例子中的另一个问题是用来说明如何协同程序可在视频游戏中使用脚本过场动画:替代协程

bob.walkto(jane) 
bob.lookat(jane) 
bob.say("How are you?") 
wait(2) 
jane.say("Fine") 
... 

每个功能收率到主发动机,其确实动画,定时等在恢复协程之前。协程的一个可能的替代方案是事件队列而不是代码,但是然后必须实现控制逻辑和循环作为事件。有没有其他的可以用来实现这种功能的协程的替代品?我看过一些文章中提到的回调,但我不确定代码的外观。

+0

协程,FSM和基于事件的编程的替代方法是CSP(通信顺序进程)。查看LuaCSP实现(在协程上): https://github.com/loyso/LuaCSP注意:人们通常缺少的是协调和沟通方面。 – 2013-03-12 10:10:01

回答

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,PlayAnimationOpenDoor也将调用Sleep产生。要么基于已知的动画持续时间,要定期唤醒以查看探路者和运动系统是否已经走路或者其他什么。

如果NPC遇到的情况,他将不得不应对的方式向大门,会发生什么?您不希望在基于协同程序的代码中检查所有这些事件。拥有一个事件系统补充协程会使这一点变得简单:

垃圾桶倾覆:垃圾桶可以向所有附近的NPC播放一个事件。 NPC对象决定在他的堆栈上推送一个新的行为对象来修复它。 WalkTo行为在某处产生Sleep调用,但现在FixTrashcan行为由于该事件而正在运行。当FixTrashcan完成WalkTo行为将从Sleep醒来,并永远不知道有关垃圾箱事件。但它仍将继续前进,在它下面我们仍然在运行EnterHouse

爆炸发生:爆炸像垃圾桶一样广播事件,但此时NPC对象决定重置它的运行行为并推动FleeInPanic行为。他不会回到EnterHouse

我希望你明白我的有事件和协程共同生活在一个AI系统的意思。您可以使用协同程序保持本地状态,同时仍让步到您的脚本调度程序,并且您可以使用事件来处理中断并保持逻辑以集中处理它们而不会污染您的行为。

如果您还没有看到如何实现C/C单线程协同程序this article by Thomas Tong ++我可以强烈推荐它。

他仅使用内联组件(单指令)的最小的位保存在堆栈指针,该代码是容易移植到一大堆平台。我已经运行在Wintel,Xbox 360,PS3和Wii上。

有关调度程序/脚本设置的另一个好处是,它变得微不足道,如果你需要的资源用于别的东西饿死离屏或遥远的AI角色/脚本对象。只需将它与调度程序中的优先级系统连接起来即可,而且您可以轻松前往。

+0

有趣的垃圾桶和爆炸的例子!我没看过这篇文章,但还有另一篇文章[使用状态机实现协程,并使用一些预处理器技巧隐藏细节](http://www.chiark.greenend.org.uk/~sgtatham/coroutines .html)(无内联汇编)。 – absence 2011-03-18 10:06:56

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 - 它已经内置了对期货的支持,比上面一个更好的语法。

+0

感谢您的回调示例。除了不方便之外,循环会导致堆栈溢出。 – absence 2011-03-18 10:02:51

3

你没有提到你正在使用哪种语言,所以我将要在Lua与中产阶级提供面向对象的写这篇 - https://github.com/kikito/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的子类,它们将定义startstop方法,并且默认情况下将状态初始化为'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 

子类可以提高在这些方法,并实现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 

唯一缺少的实现部分是过场动画。过场动画将主要有三个方面: *动作列表执行 * A参考当前动作,或动作列表 对行动的指数*如下所示的更新方法:

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)。而且你消除了对协程的需求。