使用 Python 进行游戏脚本编程

使用 Python 进行游戏脚本编程

作者

Bruce Dawson
Humongous 娱乐公司
http://www.cygnus-software.com/papers/

介绍

使用脚本语言可以更加快速地开发游戏逻辑,而不必担心由于 C++ 程序员的粗心大意所造成的后果。使用已有的脚本语言可以节省开发新型自定义语言的时间和开销,并且这些语言通常要比自己创造的语言更加的强大。

Python 对于游戏脚本语言来说是一种不错的选择,它很强大,容易嵌入使用,能够无缝地使用 C/C++ 进行扩展,包含很多脚本语言所具有的高级特性,并且它可以用来实现自动化过程[TR1: automating production]。另外,关于 Python 的书籍、开发工具 和 库 很丰富,使得我们很容易从其他开发者那里受益。

下来就谈一谈我们在 Humongous 娱乐公司将 Python 集成进新游戏引擎的一些经验。说明我们选择 Python 的原因、获得的收益、遇到的问题,以及我们是怎样解决它们的。

为什么要使用脚本语言

C++ 是一种强大的语言,并且是 C 语言的巨大改进,但它并不是完成所有任务的最佳选择。C++ 非常强调运行时性能 [Stroustrup94],譬如,假如一个语言特性使得程序跑起来变慢,那么这个特性便不会加入 C++ 语言中。C++ 程序员也因此背负了很多的限制和烦恼。

这里列出一些限制,C++ 程序员经常遭遇这些事情但很少注意它们的存在:

  • 手工管理内存:C++ 程序员的大量时间都花在考虑调用 delete 的适当时机。
  • 链接过程:C++ 模块(在编译时或加载时)链接在一起,因此在运行时,无需进行的函数地址的解析。这提高了运行时的性能,但是却使 编辑/测试 周期变长了。
  • 缺乏自省能力 [TR2: introspection]:C++ 有自己的方式知道一个类中包含哪些成员,但是这种方式需要编写过多的加载和存储对象的代码,而在一些脚本语言中这只需调用一个内建函数就可以完成。

C++ 是静态的,而脚本语言是动态的。简单地说,C++ 的程序运行地很快,但是脚本语言能让你编码更快。

所以,C++ 应该只用在你希望优化运行时性能的地方。现在计算机的运行速度都足够快,对于大多数代码来说性能都不是问题。如果你用 C++ 开发那些用脚本语言也能实现的程序,那么你是在错误的事情上进行优化。

SCUMM 的问题

Humongous 公司已经使用 SCUMM (Script Creation Utility for Maniac Mansion) 创造了 50 多个游戏。SCUMM 是一个强大的 冒险游戏 开发语言,但是它有一些局限性。SCUMM 是十多年前写的,它缺少一些现代语言的特性。

尽管 SCUMM 有持续的补丁和维护,它也没有办法像其它语言一样健壮和有完备的功能了。

为什么选择 Python

我们有过创造一种新型的、现代的 私有语言的想法,但最终明智地放弃了这种想法。我们的职责是在做游戏,而不语言。

我们在每年花费大量开销维护一套私有工具的情况下,确实希望使用一种已有的脚本语言而不是重新创造一种。使用已有语言更快地投入工作,花费更少的开销,并且通常情况下要比我们创造的好,并且以后会发展地更好,即使我们不用它工作。

一旦我们决定要使用已有的脚本语言,就需要从中选择一种。我们需要一种支持 面向对象编程,并且能嵌入到我们游戏中的语言,而且它不存在任何技术和许可授权上的问题。

我们考虑了 Lua [Lua01] 和 Python [Python02],这两种语言已经被应用在某些游戏中了。

Lua 较小,更加容易嵌入到应用程序中,并且有一些很棒的语言结构。但是,那时我们发觉 Lua 的文档有些粗略,这大概是因为 Lua 是比 Python 更新的语言。

Python 比 Lua 有更多的扩展模块,更多的参考书籍,并且 stackless Python [Tismer01] 很适合为对象 AI 创建微线程[TR3: micro-threads]。最后我们没有选择 Python 的 stackless 版本,但开始用 Python 写自动生成脚本,这给了我们继续使用 Python 的动力。当了解了 Python 后,我们喜欢上了它的语法,最后选择了它。

在我们决定之后,这两种语言都发生了改进:Lua 已经变成 stackless,而 Python 有了生成器,这个能提供一些相似的功能。现在任何一种都是安全的选择。

谁在游戏中使用了 Python

Python 已经被使用在很多游戏中,包括:

还有很多其它的游戏,只是我们很难确认,例如至少有一个 PS2 游戏使用了 Python。

同时 Python 也至少用在两个游戏引擎中:

一个生成脚本示例

下面是一段 Python 代码示例,它是一个递归生成所有 VC++ 工作区的简单生成脚本。它只有以下几行:

  1. import os   
  2.   
  3. def BuildAllWalker(unused, dirname, nameList):   
  4.     for entry in nameList:   
  5.         if entry.endswith(".dsw"):   
  6.             workspaceName = os.path.join(dirname, entry)   
  7.             resultFile = os.popen("msdev %s /make all" % workspaceName)   
  8.             resultLines = resultFile.readlines()   
  9.             for line in resultLines:   
  10.                 print line   
  11.   
  12. if __name__ == '__main__':   
  13.     os.path.walk(os.curdir, BuildAllWalker, None)  
import os

def BuildAllWalker(unused, dirname, nameList):
    for entry in nameList:
        if entry.endswith(".dsw"):
            workspaceName = os.path.join(dirname, entry)
            resultFile = os.popen("msdev %s /make all" % workspaceName)
            resultLines = resultFile.readlines()
            for line in resultLines:
                print line

if __name__ == '__main__':
    os.path.walk(os.curdir, BuildAllWalker, None)

加上更多的代码,可以让这个脚本 [Dawson02] 分析输出结果,然后给团队中的每个人发送一份结果报告邮件。不像某些其它脚本语言,上面代码有很好的可读性。使用 Python 来写生成脚本和游戏脚本将会省却很多学习的时间。

这个生成脚本示例也显示了一些对 Python 新手很头疼的问题。Python 的流程控制由缩进指明,而不使用 begin/end 声明或大括号。

我用了很短的时间来适应这种规则,最后我发现这种规则很有效。我曾经不止一次讨论过 C/C++ 中的大括号应该写在哪里,我想 Python 程序员有更高的工作效率,因为他们不用花费时间争论 K&R 及其它缩进风格[TR4: indenting style] 的事情。因为代码块由缩进定义,编写时便不会出现任何不符合 Python 编译器规则的缩进(因为那样的话,程序就会出错)。

要注意的是,当你混用 TAB 和空格进行缩进时,可能出现问题。大多数程序员使用宽度为 3 个或 4 个空格的 TAB 缩进,但是在 Python 编译器内部却使用 8 个空格的缩进,混合使用 TAB 和空格可能导致语法错误。如果你完全地使用空格或 TAB 进行缩进,并且使用一个能够提示混用空格、TAB 缩进警告的 IDE,那么便没有什么问题。

游戏脚本示例

下面的示例是我们的第一个 Python/C++ 游戏中的一些 Python 代码。这些代码是 Python 正在执行的一个主循环,它调用了其它的模块,这些模块甚至可以用其它语言编写:

  1. try:   
  2.     # Update the input   
  3.     g_InputManager.DispatchInput()   
  4.   
  5.     # Tick the menu code   
  6.     g_MenuManager.Tick()   
  7.   
  8.     # Tick the scene code   
  9.     g_SceneManager.Tick()   
  10.     g_SceneManager.SetScene()   
  11.   
  12. except:   
  13.     import traceback   
  14.     traceback.print_exc(None,sys.stderr)   
  15.     g_Clock.Stop()   
  16. #-endtry  
try:
    # Update the input
    g_InputManager.DispatchInput()

    # Tick the menu code
    g_MenuManager.Tick()

    # Tick the scene code
    g_SceneManager.Tick()
    g_SceneManager.SetScene()

except:
    import traceback
    traceback.print_exc(None,sys.stderr)
    g_Clock.Stop()
#-endtry

因此,我们的游戏由 Python 启动,并在需要时调用 C++ 程序。

它是如何工作的

Python 程序由模块组成,当在一个源文件中使用另一个源文件中定义的函数时,需要导入那个文件。例如,gameai.py 有一个 UpdateAI 函数,那么在其它 Python 源文件中可以这样调用它:

  1. import gameai   
  2. gameai.UpdateAI()  
import gameai
gameai.UpdateAI()

游戏程序员能够想到的一个很棒的事情是,如果 UpdateAI() 跑起来很慢,那么可以用 C++ 来重写它。为了做到这点,在 gameai.py 中的函数和类型需要用 C++ 实现,并且在 Python 中注册为原先的模块名。之后,使用者能够继续导入并使用 gameai 模块,而不需要任何更改。

因此,Python 模块能够帮你简单地用 Python 搭建你的整个游戏框架,而在适当的地方用 C++ 代码实现。

粘合代码 (Glue Code)

如果你自己手工编写让 C++ 代码和 Python 协同工作的粘合代码,那将是一件枯燥繁琐的事情 [TR5: glue code]。一个能够产生粘合代码的系统框架是很重要的。

Swig, Boost, CXX 等 [Abrahams01] 能帮你产生代码,更方便地将 Python 和 C++ 粘合起来。还有 Fubi[Bilas01],它是一个通用的框架,可以将 C++ 的函数和类映射到一种脚本语言中。

早期,大多数这些粘合代码框架都依靠分析 C++ 头文件工作。因此,它们受到暴露的 C++ 头文件的限制,并且一些框架不支持从 C++ 类派生出 Python 类。后来,这些框架都有所改进,所以现在还是值得考虑的。

而我们决定做一个自己的方案,它可以根据类的 IDL 描述或导出函数来生成粘合代码。它的代码叫做 Yaga,是一个递归命名法,表示 Yaga is A Game Architecture。

一个典型的 Yaga IDL 代码如下:

module yagagraphics
{
    struct Rect
    {
        int32_t x;
        int32_t y;
        int32_t width;
        int32_t height;
    };
}

它可以生成以下粘合代码,还有其它一些代码:

  1. static int YAGAPYCALL Rect_init(   
  2.         YagaPyStruct<Rect>* self,   
  3.         PyObject* args,   
  4.         PyObject* kwds)   
  5. {   
  6.     if (!PyArg_ParseTuple(args, "|llll",   
  7.             &self->structData.x,   
  8.             &self->structData.y,   
  9.             &self->structData.width,   
  10.             &self->structData.height))   
  11.     {   
  12.         return -1;   
  13.     }   
  14.   
  15.     return 0;   
  16. }  
static int YAGAPYCALL Rect_init(
        YagaPyStruct<Rect>* self,
        PyObject* args,
        PyObject* kwds)
{
    if (!PyArg_ParseTuple(args, "|llll",
            &self->structData.x,
            &self->structData.y,
            &self->structData.width,
            &self->structData.height))
    {
        return -1;
    }

    return 0;
}

使用这个框架可以很简单地导出类和函数,从 C++ 类派生 Python 类,将 C++ 的数组和 vector 映射为 Python 的序列类型,以及更多的事。

内存分配

Python 之中任何东西都是对象,对象被分配内存。因为所有的对象都有引用计数,所有你不用担心释放内存。但是,如果你是在编写游戏,尤其是控制台游戏(译注:指次时代及专用游戏机平台游戏),你必需要明白这些内存从何处分配而来,以及分配过程会产生内存碎片的严重性。

为了控制这个性能问题,你需要隔离 Python,使其有自己的内存分配场。你需要重定向所有的内存分配操作到一个自定义的分配器上,它从一个固定大小的分配场中分配内存。只要你预留足够大小的缓冲区,大于最大的 Python 历史分配额度(原文:leave enough of a buffer above the maximum Python memory footprint),应该就能避免内存碎片问题。

另一个内存问题是没有释放的块。这通常在 Python 中不是问题,因为每个对象都有引用计数,当变量离开作用域或者被显式删除,其引用计数就会减一,当计数为 0 时,对象就被释放,对象生命结束。

试想这样情况,一个被忘记的变量,它关联了一串其它的对象,这时就会阻碍这些对象的释放,所以你应该对清理对象保持警惕。然而,更糟糕的事情是循环引用问题,例如:对象 A 包含对象 B,但是对象 B 有一个回调指针指向对象 A,那么这两个对象永远都不会被删除。Python 的开发者们意识到这个问题,在最近的 Python 版本中加入了一个垃圾收集器,它搜寻无法访问到达的对象,并将其全部清除。

垃圾收集器对于游戏是很糟的,因为无法预知它们的运行时间,并且可能运行很长时间,使得画面的帧率降低。因此,游戏程序中需要禁用垃圾收集器,这个做起来很简单,随后在每个游戏关卡后显式地调用它。垃圾收集器同时也能告诉你 有多少无法访问到达的对象仍然在分配中,这个可以帮助你跟踪循环引用的情况,之后你可以手工地解决它们,这相当于 Python 的内存泄露检查。

性能

如果你用 Python 做一些繁重的浮点计算工作,和 C++ 的性能相比会很让人失望。Python 是一个慢语言,每个对象引用都意味着进行哈希表查询,每个函数调用也一样。这根本不能和 C++ 的性能相提并论,后者的变量位置和函数调用地址在编译时就决定了。

但这并不意味着 Python 不适合做游戏编程,而是你需要在适当的地点用它。如果拿字符串操作或 C++ STL 的 set 和 map 类型操作做对比,那么 Python 代码也许会做地更快。Python 的字符串操作函数是用 C 写的,并且 Python 的引用计数对象模型能够避免一些 C++ string 类的字符串复制过程。set 和 map 的大多数操作的复杂度是 O(log n),而对于 Python 的哈希表复杂度则是 O(1)。

你一定想,最好不要用 Python 写 场景图形遍历 或 BSP 冲突检测代码。但是如果你用 C++ 写它们,而后又导出到 Python 中使用,那么你就可以更快地编写 AI 代码。

请牢记 90/10 原则,这意味着对于 90% 的代码,你不必过多操心它们的运行时性能,而代码的明确表达力和编码的效率才是关键。

控制台游戏

内存和性能问题在控制台游戏平台上尤其重要。当不存在虚拟内存可以让你漫不经心做内存分配的时候,保证在独立的内存分配场中分配 Python 内存就显得格外重要。同时,也要更明智地使用垃圾收集器 (as is using the garbage collector wisely)。

控制台平台没有键盘、鼠标和多显示器,所以在控制台平台上运行 Python 调试器用起来很不方便。远程调试是关键,它能让你知道 Python 代码的运行过程。很幸运,使用免费的 HapDebugger[Josephson02] 可以很容易建立远程调试环境。

Python 使用 C 编写,并且已经被移植到多种编译环境和平台下,包括 PDA。因此,在某个控制台游戏平台下 Python 可能已经有了很充分的发展。

Python 会花费掉一小部分和控制台游戏无关的内存,但是在新一代游戏平台上可以不用担心这个,它们最小都有 24M 内存。

法律问题

推向一种新的语言对于我们公司来说是个重大的决定,我觉得在进行之前,它定是受到了公司律师们的祝福。

律师懂得法律,但他们通常不太懂编程。大多数程序员在引入开源代码前都不会咨询公司的律师,当你确实问他们时,他们会认为你正在问一些奇怪且偏僻的事情。他们的立即反应是,认为那是有风险、没有保证的计划。

如果你和一个擅长知识产权的律师长谈,他会一直向你灌输“使用开源软件会让你焦头烂额”的思想。有一些案例指明,在“免费发布”的源码中包含专利或有版权的内容时,有严重的法律问题隐患。当你从商业软件供应商那里得到授权代码时,他们会保护你免受法律责任,但对于开源软件没有人能给予授权许可 (with open source software there is no one to license it from)。

然而,开源社区对知识产权法律总是很警惕。例如 JPEG 已经从它们的开发库中移除了 LZW 算法代码以避免专利问题 [IJG]。负责的程序员会关心授权许可问题,并且通常对 GPL 和 LPGL[FSF01] 以及他们的区别很熟悉。

将开源代码引入商业产品存在很多风险。这些风险应严肃对待,但不应该阻止对开源代码的使用。有很多开源的开发库使用在游戏开发中,Python 实在没什么理由不被使用。

缺点

多语言开发增加了额外的复杂层次。同时调试两种语言的代码很困难,而且必须花费时间维护绑定两种语言的粘合代码。

类似 Python 的动态语言没有编译时类型检查。这种情况初看让人惊恐,但它的实际意味着,相比 C++ 你会遇到各式各样不同的运行时错误,通常它们都很容易解决。

不同类型的换行符

UNIX (LF)、Mac OS (CR) 和 Windows (CR LF) 对待文本文件中一行的结束有不同的约定,这实在很糟。

Windows 上的 C/C++ 库(译注:指 Windows API 和 VC 运行时库)会做换行符转换,所以 UNIX 文件能够在 Windows 上读取,可以将 Windows 文件像 UNIX 文件一样的操作。UNIX 和 Macintosh 文本文件之间的共同点更少,只能依靠假定某个平台上的文件都只是这个平台上曾经创建的,这个假设进行转换。这个假设在当今的网络环境下站不住脚,Python 也深受其害。直到现在,在 Windows 下写的 Python 代码可能无法在 Macintosh 下编译,反之亦然。

这个问题的解决方法是,在运行 Python 代码前,将 Python 源文件通过一个文件过滤器(可以用 Python 开发?)执行,另一种方法是以编译后的字节码形式发布 Python 代码。但是,这两种办法都有缺点。最理想的是在计算机工业中标准化文本文件格式,或者让所有的文件 IO 库实现读取任意类型文本文件的能力。

这个问题在苹果的 OS X 上更加有趣,换行符由运行程序的模式而定,你可以运行 UNIX 或 Macintosh 两种模式程序。这会在一个系统下出现两种不同的换行符,甚至不用重启。

Python 的 Macintosh 版本最近修正了这个问题,在打开文件时检查换行符并对每个文件进行调整。将所有的换行符都规定为 UNIX 类型是一种可行的方法,它在所有平台下都能工作,但是还是要留心这个问题。

调试器问题

很多 Python 程序员认为自动化测试和打印语句是他们唯一需要的调试工具,而使用调试器会影响编码的产能。或许这对他们来说的确如此,但我已经习惯于进行源码级调试,并且不会轻易放弃它。

PythonWin 是一个在 Windows 下的 Python 调试器兼 IDE(奇特吧?)。它是免费的,有一些不错的功能,但也有一些缺点,如:只能在 Windows 下运行,无法调试有自身消息循环的 Python 程序。

在 Humongous 娱乐公司,我们为 Macintosh 和 Windows 开发游戏,同时也涉及控制台游戏的开发。我们需要一种能工作在所有三个平台上的调试器,而最好的方案就是使用远程调试器。Python 的架构使得编写它的调试器很容易,再加上其它一些免费组件,我们开发出了自己的 Python 调试器,我觉得它的效果比 PythonWin 好,并且具有远程调试功能。被调试的客户端需要运行一些额外代码。调试接口是 socket 上的 ASCII 文本,另外,我们还没考虑将调试器客户端移植到更多其它平台的问题。

因为我们希望集中精力开发游戏本身,而不是语言工具,所以决定再次借用开源的力量。我们在 Python 社区发布了 HAP 调试器 (Humongous Addition to Python),将其作为一个开源项目[Josephson02]。这是一个回馈社区的好机会,并且我们也从维护这个调试工具的事务中解放出来。

我们还没有解决的问题是调试器的性能问题。大多数编译式语言实现调试断点的方法是,将常规指令替换为导致 CPU 异常的指令,如 x86 处理器的 int 3 中断。这让程序可以全速执行,直到触发中断点。Python 不支持从异常处恢复执行,所以不能使用断点异常的方法。Python 调试器处理断点的方法是 单步检查代码,即不停地在问自己“这一行有没有断点?”

这个性能影响的后果可能很严重。我们现在减小此影响的方法是,保证开发机器要比目标机器快得多。还有,将所有重量级计算用 C++ 扩展实现,这样即使 Python 代码拖慢了调试器,也不至于让整个游戏速度太慢。这是一个可以解决的问题,只是 Python 的主要开发者还没考虑过。

代码安全和游戏作弊

C++ 程序员有时开玩笑说,删除注释和缩短变量名可以优化代码。然而,在 Python 中确实如此。

Python 代码在运行时被编译成字节码,并缓存起来以备后续运行,所以删除注释的方法不会起到优化程序的效果,但是缩短变量名则是另外一回事。大多数脚本语言都是在运行时通过名字定位变量的,这也是脚本语言强大的原因之一,因为它可以突破很多由 C++ 编译时绑定造成的限制。然而,这也意味着变量名会一直伴随着代码而存在(译注:C/C++ 等传统编译式语言则不同,经优化编译后的 C/C++ 程序中没有变量名而只有地址的概念)。

游戏程序中包含语义清晰 (scatological) 的变量名,会被人当做笑谈。更严重的问题是,如果在多人游戏中使用 Python 脚本,作弊者反编译 Python 程序后会得到完整的变量和函数名,这比起通过反编译 C++ 程序来破解游戏要更简单。

Python 的优点

Python 编程很有趣。Python 易于学习,有更高的生产效率,并且促使你使用另一种思维编程。学习 Python 编程让我成为更好的 C++ 程序员。

快乐的程序员有更高的学习效率和生产效率,他们倾向创造更好的游戏。Humongous 公司中使用 Python 开发游戏的团队,在整个公司中拥有最高的工作士气。

Python 游戏编程系统(译注:应指开发工具、框架、类库等)具有很高的生产效率,而且它们仍然在发展之中。因为采用了它们,我们节省了很多资金。(原文:Productivity is higher with the Python game programming system, even though development is still being done on it. It is already clear that we will save a lot of money from this switch.)

用户界面的开发,在 C++ 中可能花费较长的时间,而在 Python 中可以使用一些新意的方式进行实现。通常使用文本文件定义 GUI 元素的位置和关联图形资源,进而定义菜单。在 C++ 中会使用硬编码的函数和控件对象,挂钩 GUI 元素;而在 Python 中,可将函数及对象名放入文本文件中,并在运行时扫描它们。Python 的动态和内省特性 (introspective) 使得做起这些事来很自然。(译注:C++ 也可使用读取文本配置方式,自动生成菜单,只是用 Python 的反射特性做起来更自然)

很多起先我们担忧的 Python 语言限制问题都已成为过去。Python 的开发者们对该语言进行持续地改进,有时他们就像一直在满足我们对 Python 特性需求的渴望一样。

游戏存档和读档

C++ 程序员要花费很多时间解决脚本语言中不会出现的困难问题。例如,用 C++ 进行游戏状态的存储和读取就是一个麻烦问题,经常要编写大量的代码。而且这种方法通常会导致,存档只能和特定版本的游戏程序配合工作。而在 Python 中,使用 cPickle 模块可以很方便的解决此问题,它可以存储和读取任何复杂的数据结构。

下面例子中声明了一个对象 mainObject,通常它是一个用户自定义类对象,包含各种需要存储的状态的句柄,但为简单起见,这里只把它做成一个列表。最初该列表包含数字 0 和一个字符串,然后将列表的第一个元素赋值为另外一个列表。这个过程可以继续下去,让 mainObject 包含任意复杂嵌套层次的对象,包括循环引用。

  1. mainObject = [0"A string"]   
  2. mainObject[0] = ["a string"0.2341012341234123412341234]  
mainObject = [0, "A string"]
mainObject[0] = ["a string", 0.234, 10, 12341234123412341234]

接下来保存着这个 mainObject,这需要两行代码。一行导入 cPickle 模块,另一行打开一个文件,将对象保存为二进制格式。在开发时,保存为文本格式很有用,只需省略掉 dump() 的最后一个参数即可。

  1. import cPickle   
  2. cPickle.dump(mainObject, open("game.sav""wb"), 1)  
import cPickle
cPickle.dump(mainObject, open("game.sav", "wb"), 1)

然后是装载文件数据,这同样需要两行代码。一行导入 cPickle 模块,另一行重建 mainObject 对象,以及包含的子对象、列表、成员变量等。第三行打印出 mainObject 对象,可以看出已经正确地恢复了嵌套的列表。

  1. import cPickle   
  2. mainObject = cPickle.load(open("game.sav"))   
  3. print mainObject   
  4. [['a string'0.234000000000000011012341234123412341234L], 'A string']  
import cPickle
mainObject = cPickle.load(open("game.sav"))
print mainObject
[['a string', 0.23400000000000001, 10, 12341234123412341234L], 'A string']

这个 Python 特性在 C++ 基本功能中不存在。

生成器:游戏 AI 的微线程

微线程将对象状态信息放到局部变量中(这是恰当的位置),从而极大简化 AI 和对象更新代码 [Carter01]。可以使用汇编语言的技巧将微线程放进 C++ 中,但是那样很凌乱。在最近版本的 Python 中,微线程内建于语言之中。现在使用微线程会工作地很好。

在 Python 中它们叫做生成器 (generator),使用它们编写函数,函数产生某个结果后,控制返回到主程序。主程序稍后可以重新唤醒它们,并从中断处继续运行,并保持原来的局部变量值。下面的示例代码展示创建一个对象,并移动它们穿过屏幕。这个简单例子并不能从微线程/生成器中得到实际的好处,它只是基本展示它们怎样用来简化 AI 和对象更新代码。

  1. # 告诉编译器 generator 和关键字 yield 可用   
  2. from __future__ import generators   
  3.   
  4. # 一个类,定义角色的行为   
  5. class MovingObject:   
  6.   
  7.     # 构造函数中创建一个 generator 函数的句柄,并把它保存在私有成员变量中   
  8.     def __init__(self):   
  9.         self.__genHandle = self.__UpdateFunction()   
  10.   
  11.     # 每个栈帧调用一次 Update(),它恢复运行更新函数 __UpdateFunction()   
  12.     # 原文:Update() is called once per frame and tells the update function to resume.   
  13.     def Update(self):   
  14.         return self.__genHandle.next()   
  15.   
  16.     # 每个栈帧通过 Update() 恢复运行私有的对象更新函数 __UpdateFunction()   
  17.     # 该函数保存所有的状态到局部变量中,如果需要的话,其中一些可以存储到成员变量中   
  18.     # 原文:The private object update function is resumed once per frame by Update().   
  19.     # It stores all of its state in local variables. Some of it could be stored in   
  20.     # member variables if desired.   
  21.     def __UpdateFunction(self):   
  22.         x, y = 00  
  23.         dx = 0.25  
  24.         dy = 0.75  
  25.         while 1:    # 死循环   
  26.             # 打印当前的位置   
  27.             print x, y   
  28.   
  29.             # 返回当前的位置,主程序中需要它   
  30.             yield (x, y)   
  31.   
  32.             # 函数从这里恢复运行,更新位置   
  33.             x += dx   
  34.             y += dy   
  35.   
  36.             # 更新速度,会加速运动   
  37.             dx += 0.1  
  38.             dy += 0.5  
  39.   
  40. # 创建一个带 generator 的 AI 对象   
  41. character = MovingObject()   
  42.   
  43. # 首次运行 generator,局部变量 x, y 初始化、打印,然后控制返回到这里   
  44. character.Update()   
  45.   
  46. # 接着再次运行 generator 若干次   
  47. # 角色移动、打印它的位置,然后控制返回,重复若干次   
  48. for x in range(10):   
  49.     character.Update()  
# 告诉编译器 generator 和关键字 yield 可用
from __future__ import generators

# 一个类,定义角色的行为
class MovingObject:

    # 构造函数中创建一个 generator 函数的句柄,并把它保存在私有成员变量中
    def __init__(self):
        self.__genHandle = self.__UpdateFunction()

    # 每个栈帧调用一次 Update(),它恢复运行更新函数 __UpdateFunction()
    # 原文:Update() is called once per frame and tells the update function to resume.
    def Update(self):
        return self.__genHandle.next()

    # 每个栈帧通过 Update() 恢复运行私有的对象更新函数 __UpdateFunction()
    # 该函数保存所有的状态到局部变量中,如果需要的话,其中一些可以存储到成员变量中
    # 原文:The private object update function is resumed once per frame by Update().
    # It stores all of its state in local variables. Some of it could be stored in
    # member variables if desired.
    def __UpdateFunction(self):
        x, y = 0, 0
        dx = 0.25
        dy = 0.75
        while 1:    # 死循环
            # 打印当前的位置
            print x, y

            # 返回当前的位置,主程序中需要它
            yield (x, y)

            # 函数从这里恢复运行,更新位置
            x += dx
            y += dy

            # 更新速度,会加速运动
            dx += 0.1
            dy += 0.5

# 创建一个带 generator 的 AI 对象
character = MovingObject()

# 首次运行 generator,局部变量 x, y 初始化、打印,然后控制返回到这里
character.Update()

# 接着再次运行 generator 若干次
# 角色移动、打印它的位置,然后控制返回,重复若干次
for x in range(10):
    character.Update()

即使你不使用生成器,在 Python 中实现 AI 更新方法也比用 C++ 更干净。因为如果你的某部分 AI 代码需要一些额外的临时状态时,Python 可以将它加入到对象中,然后在不需要时删除它。而 C++ 因其静态特点,不能在运行时加入新的成员变量,这使你的对象在任何时候都必须包含所需的所有状态。

开始使用 Python

如果你开始使用 Python,第一件事是访问 Python 的官方网站 http://www.python.org/ 下载你的平台上的 Python 版本。

Python 文档在 http://www.python.org/doc/current/download.html,也有编译的 HTML 版本 (CHM) 更便于检索。

Windows 开发者可以使用 PythonWin 和各种 Win32 扩展 http://users.bigpond.net.au/mhammond/win32all-142.exe

调试器 HapDebugger https://sourceforge.net/projects/hapdebugger/

你可以下载并编译 Python 源码,构建自己的 Debug 和 Release 版 Python。Python 2.2 源码下载 ftp://ftp.python.org/pub/python/2.2/Python-2.2.tgz

最后,你可以阅读关于手工创建 Python 扩展的细节 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66509,然后选择一种粘合代码包来帮你做这些事情,参考http://www.boost.org/libs/python/doc/comparisons.html

参考

关于作者

Bruce 是 Humongous 娱乐公司的技术主管,他从事其他人无闲去做的挑战性工作,并充满乐趣。同时他兼职在 DigiPen 进行教学。在进入 Humongous 娱乐公司之前,他在 Cavedog 娱乐公司的多个产品组工作。

Bruce 在 Avid 技术公司的 Elastic Reality 分公司工作多年,编写特效处理软件和视频编辑插件,在这之前他在 EA 加拿大公司工作,并在公司更名为 Distinctive 软件的时候回国。他在那里编写了他的第一个电脑游戏,基于 Commodore Amiga 平台,并在作品的 2/3 已上色后回国。(原文:back when thirty-two colours was state of the art)

Bruce 在 GDC 2001 上发表了一篇名为 "What Happened to my Colours?!?"(《我的颜色究竟怎么了》),讨论 NTSC 制式上的奇怪问题。

他现在从事 Python 脚本编程,用其自动化完成工作任务,因此他现在有足够的时间摆弄他的游戏机了,那是在一次扑克中赢得的。

Bruce 和他的妻子及孩子住在西雅图近郊。在华盛顿,他告诉同事,所有的程序员都应该学会变魔术、单轮车和走钢丝。

[END]


翻译注释


原文链接: http://blog.csdn.net/breakerzy/article/details/7240159
标签: <无>
已标记关键词 清除标记
相关推荐
中文名: Python高级编程 作者: (法)莱德译者: 姚军 夏海轮 王秀丽 资源格式: PDF 版本: 扫描版 出版社: 人民邮电出版社书号: 9787115217035发行时间: 2010年01月01日 地区: 大陆 语言: 简体中文 简介: 内容简介   本书通过大量的实例,介绍了Python语言的最佳实践和敏捷开发方法,并涉及整个软件生命周期的高级主题,诸如持续集成、版本控制系统、包的发行和分发、开发模式、文档编写等。本书首先介绍如何设置最优的开发环境,然后以Python敏捷开发方法为线索,阐述如何将已被验证的面向对象原则应用到设计中。这些内容为开发人员和项目管理人员提供了整个软件工程中的许多高级概念以及专家级的建议,其中有些内容的意义甚至超出了Python语言本身。   本书针对具备一定Python基础并希望通过在项目中应用最佳实践和新的开发技术来提升自己的Python开发人员。 目录: 第1章 准备工作 1.1 安装Python 1.1.1 Python实现版本 1.1.2 在Linux环境下安装 1.1.3 在Windows环境下安装 1.1.4 在Mac OS X环境下安装 1.2 Python命令行 1.2.1 定制交互式命令行 1.2.2 iPython:增强型命令行 1.3 安装setuptools 1.3.1 工作原理 1.3.2 使用EasyInstall安装setuptools 1.3.3 将MinGW整合到distutils中 1.4 工作环境 1.4.1 使用文本编辑器与辅助工具的组合 1.4.2 使用集成开发环境 1.5 小结 第2章 语法最佳实践——低于类级 2.1 列表推导 2.2 迭代器和生成器 2.2.1 生成器 2.2.2 协同程序 2.2.3 生成器表达式 2.2.4 itertools模块 2.3 装饰器 2.3.1 如何编写装饰器 2.3.2 参数检查 2.3.3 缓存 2.3.4 代理 2.3.5 上下文提供者 2.4 with和contextlib 2.4.1 contextlib模块 2.4.2 上下文实例 2.5 小结 第3章 语法最佳实践——类级 3.1 子类化内建类型 3.2 访问超类中的方法 3.2.1 理解Python的方法解析顺序 3.2.2 super的缺陷 3.3 最佳实践 3.4 描述符和属性 3.4.1 描述符 3.4.2 属性 3.5 槽 3.6 元编程 3.6.1 __new__方法 3.6.2 __metaclass__方法 3.7 小结 第4章 选择好的名称 4.1 PEP 8和命名最佳实践 4.2 命名风格 4.2.1 变量 4.2.2 函数和方法 4.2.3 属性 4.2.4 类 4.2.5 模块和包 4.3 命名指南 4.3.1 使用“has”或“is”前缀命名布尔元素 4.3.2 用复数形式命名序列元素 4.3.3 用显式的名称命名字典 4.3.4 避免通用名称 4.3.5 避免现有名称 4.4 参数最佳实践 4.4.1 根据迭代设计构建参数 4.4.2 信任参数和测试 4.4.3 小心使用*args和**kw魔法参数 4.5 类名 4.6 模块和包名称 4.7 使用API 4.7.1 跟踪冗长 4.7.2 构建命名空间树 4.7.3 分解代码 4.7.4 使用Egg 4.7.5 使用deprecation过程 4.8 有用的工具 4.8.1 Pylint 4.8.2 CloneDigger 4.9 小结 第5章 编写一个包 第6章 编写一个应用程序 第7章 使用zc.buildout 第8章 代码管理 第9章 生命周期管理 第10章 编写项目文档 第11章 测试驱动开发 第12章 优化:通用原则和剖析技术 第13章 优化:解决方案 第14章 有用的设计模式
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页