51testing 发表于 2007-12-13 10:20:45

游戏人工智能演示(1)追逐

逻辑 FPS

接触过游戏编程的朋友都知道在每一帧刷新图形界面的时候执行一次逻辑代码显然是行不通的。比如在一个《俄罗斯方块》游戏中,程序设定掉落中的每10帧下落一个像素点的话,那每在配置好的机器上方块可能瞬间就已经掉到最低端,玩家根本反应不过来。这时我们可以引入逻辑FPS,即设定1秒内运行逻辑的次数,如30次。这时只要机器能够一秒执行30帧逻辑,那方块肯定掉落了3个像素,从而同步了不同的机器。如果一台很老旧的机器,不能达到 30 FPS呢?那就要使用另一套解决方案了,暂时我们还用不着,就先不讨论这个。

在 aidemo 中, 逻辑 FPS 设定为 30,这在当前主流机器上运行已有的 aidemo 是肯定可以达到的。逻辑 FPS 实现在 fps.py 文件中,如下:
class FixFPS(object):
       def __init__(self, fps):
            self.fps = fps
            self.span = 1.0 / fps
            self.bgn = time()


       def GetFps(self):
            return self.fps


       def NeedUpdate(self):
            end = time()
            span = end - self.bgn
            if span > self.span:
                     self.fps = int(1.0 // span)
                     self.bgn = end
                     return True
            return False
FixFPS.NeedUpdate() 方法用在这里:
class MyApp(GameApp):
       def Logic(self):
            if not logic_fps.NeedUpdate():
                     return
            game.Logic()
这样就能保证游戏逻辑的执行频率了。
dc_defend

如果我们用 WIN32 或者 MFC 开发过 GDI 或 GDI+ 程序,我们一定对 SetObject()、GetOjbect()之类的函数非常熟悉。为了不“污染” DC ,我们需要在配置 DC 时保存 DC 原有的设置,然后在使用完 DC 后再恢复过来。使用 wxWidgets 的时候也是如此,使用 wxPython 也不例外,不过借助 Python 的 decorator 语法,我们可以有效地提升编程体验:1)不用再写那么多 Set/Get 方法;2)提升执行效率。利用 decorator 编写的 dc_defend:
def dc_defend(getters, setters):
       def func_decorator(func):
            def defender(self, obj, *a, **k):
                     values =
                     ret = func(self, obj, *a, **k)
                     for setter, arg in zip(setters, values):
                            setter(obj, arg)
                     return ret
            return defender
       return func_decorator
很简单的带参数的 decorator 实现,对 decorator 不了解的朋友可以去看一下 Python Manual,里面有详解。dc_defend,顾名思义就是“保护DC”的了,它的用法示例:
       @dc_defend( \
            (wx.BufferedPaintDC.GetTextForeground,), \
            (wx.BufferedPaintDC.SetTextForeground,))
       def DrawFps(self, dc):
            dc.SetTextForeground(wx.RED)
            dc.DrawText('FPS: %d/%d'%(self.fps.GetFps(), logic_fps.GetFps()), 10, 10)
因为在 DrawFps() 函数里调用了DC.SetTextForeground() 方法来设置字体颜色,所以在 dc_defend 的参数里传入 Get/Set 字体颜色的函数,由 dc_defend 自动在调用 DrawFps() 之前保存字体颜色的原有配置,并在调用之后恢复。
引入 Game类

在 demo_1 中,我们要实现一个垂直俯视角度的游戏,游戏很简单:只有一个不停地追逐鼠标的 Ball(怪物的抽象)。demo 虽小,但已经有一个实体(圆球)要管理,还要处理鼠标输入(以获得圆球追逐的目标),要绘制实体,还要执行实体的逻辑。这么多功能,如果不独立出来,就很难保证正交的设计了。所以有必要引入Game类:
class Game(object):
       def __init__(self):
            # … 在这里初始化游戏,如实例化 Ball
       def Draw(self, dc):
            # … 在这里绘制 Ball
       def Logic(self):
            # … 在这里执行 Ball 的逻辑(趋近鼠标)
       def OnMotion(self, evt):
            # … 在这里告诉 Ball 鼠标位置已经改变
具体的Game 类实现请参见 demo_1 目录下的 game.py 文件。

显而易见的,App 需要知道 Game 的实例以在执行游戏逻辑的时候调用 Game.Logic() 以执行游戏逻辑,而 Frame 也需要知道 Game 的实例以委托 Game.Draw() 绘制游戏界面。因为整个游戏是如此简单,所以 Game 的实例完全可以定义为全局变量,demo.py 的内容大体如下:
game = Game()    # 全局的 Game 实例
class MyFrame(GameFrame):
       def OnMotion(self, evt):
            game.OnMotion(evt)      # 传递鼠标消息
       def DoPaint(self, dc):
            game.Draw(dc)      # 绘制游戏
class MyApp(GameApp):
       def Logic(self):
            if not logic_fps.NeedUpdate():
                     return
            game.Logic() # 执行游戏逻辑
实体与动体

按面向对象的编程思想,每一件事物都有一个对应的类。aidemo 的实现是相当面向对象的,所以 aidemo 中在游戏中可见与不可见事物都是一个实体(Entity),而部分在游戏中会发生空间位置移动的称为动体(Motile)。所有的实体都有拥有游戏中唯一ID。关于实体和动体的实现,请见 entity.py 文件。
追逐

要实现追逐,有三个要点:
1) 需要一个动体来执行追逐的动作
2) 这个动体能够判断是否已经追上目标
3) 这个动体必须能够觉察目标的位置变化
class Ball(Motile):
       def __init__(self, *args, **kw):
            super(Ball, self).__init__(*args, **kw)
            self.SetBoxes(((collision.InCircle, (12,)),))


       def Logic(self):
            self.MoveToTarget()


       def MoveToTarget(self):
            if self.IsArrivedTarget():
                     return
            # 计算自己与目标之间的距离
            dist = self.target - self.pos
            # 归一化
            dist.Normalize()
            # 向目标前进
            self.pos += vector2d.Vector2D( \
                     dist.x * self.velocity.x, \
                     dist.y * self.velocity.y)
上图是“不停地追逐鼠标的Ball”的主要实现代码。其中 MoveToTarget() 对应着三个要点中的第一点。 IsArrivedTarget() 是父类 Motile 实现的方法,用以判断是否已经追上目标(第二点):
class Motile(Entity):
       def IsArrivedTarget(self):
            for box, args in self.boxes:
                     if box(self.pos, self.target, *args):
                            return True
            return False
在IsArrivedTarget()是引用到的 self.boxes 来自 Ball.__init__() 的这一行调用:
            self.SetBoxes(((collision.InCircle, (12,)),))
可见所谓的 boxes (包围盒)其实是一个回调函数及其部分参数(以后会用函数式编程理念来重构的J)。
现在要完成的是第三个要点:这个动体必须能够觉察目标的位置变化。这个任务交给 Game 来完成,请参见上两节“引入 Game 类”里的 Game 示意代码。实现代码如下:
class Game(object):
       def OnMotion(self, evt):
            self.ball.SetTarget( \
                     Vector2D(float(evt.m_x), float(evt.m_y)))
完成这些代码,我们的 demo 就就成了。
截图
http://aidemo.googlecode.com/files/demo_1.PNG
预告

在游戏中,我们可以看到无数怪物、NPC甚至小虫小鱼不停地按照一定的路线循环运动,这就是有着 PathFollow 能力的智能体,接下来,让我们来实现它们。敬请期待。
页: [1]
查看完整版本: 游戏人工智能演示(1)追逐