游戏设计中的协程?

16

我听说协程是一种良好的游戏结构方式(例如,PEP 342:“协程是表达许多算法的自然方式,比如模拟,游戏...”),但我很难理解它实际上应该如何完成。

通过这篇文章,我了解到协程可以表示状态机中的状态,这些状态使用调度程序相互转换,但我不清楚它如何适用于游戏,因为游戏状态是基于多个玩家的移动而改变的。

有没有编写使用协程编写的游戏的简单示例可用?或者有人能够提供一份草图,说明它可能是如何完成的?

5个回答

10

协程最显著的案例可能是旧的图形“点击”冒险游戏,在这些游戏中,它们被用来编写剧情场景和其他动画序列。一个简单的代码示例看起来像这样:

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

整个过程无法像普通代码一样编写,因为你想在执行 bob.walkto(jane) 后看到 bob 做他的步行动画,而不是直接跳转到下一行。然而,为了播放步行动画,你需要将控制权交回给游戏引擎,这就是协程发挥作用的地方。

整个过程被执行为协程,意味着你可以随意暂停和恢复它。像 bob.walkto(jane) 这样的命令会告诉引擎端的 bob 对象它的目标,然后暂停协程,等待 bob 到达目标时的唤醒调用。

在引擎端,事情可能看起来像这样(伪代码):

class Script:
    def __init__(self, filename):
        self.coroutine  = Coroutine(filename)
        self.is_wokenup = True

    def wakeup(self):
        self.is_wokenup = False;

    def update():
        if self.is_wokenup:
          coroutine.run()            


class Character:
   def walkto(self, target, script):
       self.script = script
       self.target = target

   def update(self):
       if target:
           move_one_step_closer_to(self.target)
           if close_enough_to(self.target):
               self.script.wakeup()

               self.target = None
               self.script = None

objects = [Character("bob"), Character("jane")]
scripts = [Script("script_file.scr")]

while True:
    for obj in objects:
        obj.update()

    for scr in scripts:
        scr.update()

然而,需要提醒的是,虽然使用协程可以使编写这些序列变得非常简单,但并非您找到的每个实现都考虑了串行化的问题,因此如果您大量使用协程,游戏保存将成为一个麻烦的问题。

这个例子只是在游戏中使用协程的最基本情况,协程本身也可以用于很多其他任务和算法。


10

协程允许创建大量非常轻量级的“微线程”,实现协作式多任务处理(即,微线程自愿地暂停以允许其他微线程运行)。请参阅Dave Beazley关于这个主题的文章

现在,这样的微线程如何在游戏编程中有用就显而易见了。考虑一个实时策略游戏,你有数十个单位 - 每个单位都有自己的思想。对于每个单位的人工智能来说,在您的模拟多任务环境中作为这样的微线程运行可能是方便的抽象。这只是一个例子,我相信还有更多。

在谷歌上搜索“协程游戏编程”会带来一些有趣的结果。


7

协程在游戏中的一种应用是作为一个轻量级线程,类似于Kamaelia中的actor模型。

你的游戏中的每个对象都将成为一个Kamaelia '组件'。组件是一个可以通过yield暂停执行的对象。这些组件还具有消息系统,允许它们异步地安全地相互通信。

所有对象都将同时执行各自的任务,并在交互发生时向彼此发送消息。

因此,这并不仅限于游戏,任何具有大量并发组件相互通信的情况都可以受益于协程。


2

1

想要使用协程来表示单个角色AI脚本是非常普遍的。不幸的是,人们往往忘记协程具有与线程相同的所有同步和互斥问题,只是在更高的级别上。因此,您通常需要首先尽可能多地消除本地状态,其次编写强大的方法来处理协程中出现的错误,当您引用的某些内容停止存在时。

因此,在实践中,它们很难从中获得好处,这就是为什么像UnrealScript这样使用某种协程的语言将大部分有用的逻辑推向原子事件的原因。一些人可以从中获得良好的效益,例如EVE Online的人们,但他们必须基本上围绕该概念构建整个系统。(以及他们如何处理共享资源的争用并没有得到很好的记录。)


协程存在一些同步问题,但只有当它们的状态不能在 yield() 调用之间保持一致时才会出现。在所有 yield() 调用之间的代码周围有一个隐式锁可以极大地简化事情。最大的问题是,每次创建一个例程时,都应该当场决定该例程是否总是能够在必须 yield() 之前完成。如果例程的调用者不期望例程进行 yield(),则在调用例程时可能不会有一致的状态。添加 yield() 将是破坏性的更改。 - supercat
问题在于对于这样的例程,您通常希望能够在任何地方产生收益才有用(例如 UnrealScript 中的“潜在函数”隐含地产生收益),但这会导致协同程序的组合以不可预测的方式进行交互。例如,如果一个是巡逻协同程序,它说“向北走;看一看;向南走;看一看;重复;”,另一个是反应例程,它说“如果(听到西边的声音){向西走;看一看;向东走;}”,那么交替两者可能会导致演员移动得相当不可预测。这就是为什么 UnrealScript 只允许在精确地一个“协同程序”中使用此类函数。 - Kylotan
即使每个角色只有一个协程,这仍然不足以确保逻辑在语言级别上始终正常工作:如果您的协程说:“拔剑;等待1秒钟;用剑攻击对手”,那么如果由于另一个玩家使您解除武装而导致剑消失会发生什么?特别是,如果您的语言通过使用剑的局部变量来实现该协程,而该变量现在引用空对象,那么怎么办?如果您正在以正常方式使用通用语言,则很难考虑所有这些情况。 - Kylotan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接