翻译:Panda3D Manual/V. Programming with Panda/O. Finite State Machines

有限状态机(Finite State Machines
“有限状态机”是来自计算机科学的一个概念,用来表示包含有限个不同状态的系统,以及从一种状态转换到另一种状态的机制。
Panda3D 有限状态机,或简称FSM,由一个Python类实现。要定义一个新的FSM,应该从FSM类派生出一个Python类,并通过类的方法来定义状态,这些 方法定义了进入或离开某种状态时的FSM行为。然后你可以根据需要请求FSM从一种状态转换成另一种状态。
你可能在老的Panda3D代码里发现有人使用ClassicFSM类。ClassicFSM是FSM类早期的一个实现,现在已不再使用,也不再提供关于它的文档。我们建议你使用新的FSM类,接下来将具体介绍它。
 
FSM 介绍
在Panda3D 中,FSM经常用在游戏代码中,以便当游戏状态转换时,自动进行逻辑清理。例如,假设你正在开发一款游戏,大多数时间游戏的主角都在行走,但偶尔也会跳进 水里游泳。当他在地面行走时,我们需要给他赋予行走的动画和音效,以及某种游戏特性。当他游泳时,应该运行另外的动画、音效和游戏特性(当然我们只是在举 例):
Walk state
  • Should be playing "walk" animation
  • Should hear footsteps sound effect
  • Collision detection with doors should be active
Swim state
  • Should be playing "swim" animation
  • Should hear underwater sound effect
  • Should have fog on camera
  • Should have an air timer running
因此,当主角从行走变成游泳时,我们需要停止脚步声、关闭门的碰撞检测,并开始运行“游泳”动画,播放水下音效,打开镜头雾效以及开始憋气计时。
当然你可以人工来做这些转换,但使用一个FSM更为快捷。在这个简单的模型里,你可以定义一个只有“行走”和“游泳”2种状态的FSM。如下图所示:
Walk
←→
Swim
 
为了让Panda3D实现这个FSM,应该从FSM.FSM派生出一个新类,在该类里定义4个方法:enterWalk()、exitWalk()、enterSwim()和exitSwim()。代码如下:
from direct.fsm import FSM
 
class AvatarFSM(FSM.FSM):
    def __init__(self):#optional because FSM already defines __init__
        #if you do write your own, you *must* call the base __init__ :
        FSM.FSM.__init__(self,'avatarFSM')
        ##do your init code here
 
    def enterWalk(self):
        avatar.loop('walk')
        footstepsSound.play()
        enableDoorCollisions()
       
    def exitWalk(self):
        avatar.stop()
        footstepsSound.stop()
        disableDoorCollisions()
 
    def enterSwim(self):
        avatar.loop('swim')
        underwaterSound.play()
        render.setFog(underwaterFog)
        startAirTimer()
       
    def exitSwim(self):
        avatar.stop()
        underwaterSound.stop()
        render.clearFog()
        stopAirTimer()
 
myfsm = AvatarFSM()
记住上面只是个假设的例子,为了帮助我们了解FSM类。
注意,每个enter方法激活某种状态下的所有行为——这是关键部分——对应的exit方法关闭或撤销由enter方法打开的一切行为。也就是说,任何时候FSM离开某个状态,都可以确信所有进入该状态时开始的活动将完全终止。
从行走状态转换到游泳状态,我们只需调用:
myfsm.request('Swim')
这个FSM示例非常简单,很快你就会发现实际所需的状态远不止2个。例如,当主角从行走状态转换到游泳状态或反过来时,你可能要运行一段转换动画,而这可以独立成一个状态。主角长时间待在水下时可能要来一段“溺水”的动画,这又是另一个状态。此时,状态转换图变成:
Walk2Swim
 
 
Walk
 
Swim
 
Drowning
Swim2Walk
 
 
 
在现实中,可想而知我们需要很多很多个状态。所以,使用FSM类管理状态变换能够大大减轻我们的负担。 如果由人脑来处理的所有清理代码,很快你就会傻眼了。
 
简单的FSM 使用
从direct.fsm.FSM.FSM类(一般导入成FSM.FSM或FSM)派生一个新的Python类可以得到一个Panda3D有限状态机,我们在其中定义enter和exit方法。
FSM 的状态由一个名字字符串表示,名字不能包含空格或标点。Panda3D规定,状态名必须以大写字母开头。一个FSM在某一时刻只能处在一种状态下。 fsm.state存储当前状态的名字。从一种状态转换到另一种状态时,FSM首先调用exitOldState(),接着调用 enterNewState(),这里的OldState和NewState分别是前后2个状态的名字。转换进行时,技术上讲FSM不处在这2种状态的任 一种,fsm.state的值为None——但你可以从fsm.oldState和fsm.newState中分别得到新、旧状态的名字。
定 义一个FSM状态,你只需在类中定义一个enterStateName()或exitStateName()方法,StateName指代你要定义的状态 的名字。enterStateName()方法将完成进入新状态的所有动作,相应的exitStateName()方法撤销enterStateName ()中的所有动作,此时世界将回到空白状态。
一个FSM启动或完成的状态称为“Off”。FSM一被创建就进入“Off” 状态,当被销毁时(调用fsm.cleanup()),自动回到“Off” 状态。
调用fsm.request('StateName')显式地请求FSM进入一个新状态,'StateName'为状态的名字。
enterStateName 方法的参数
一 般情况,enterStateName()和exitStateName()方法都不带参数(除了self)。但假如你的FSM在进入某个状态前需要另外 一些信息,你可以为enterStateName方法设定参数;调用request()时这些参数将跟在状态名字后边:
from direct.fsm import FSM
 
class AvatarFSM(FSM.FSM):
    def enterWalk(self, speed, doorMask):
        avatar.setPlayRate(speed, 'walk')
        avatar.loop('walk')
        footstepsSound.play()
        enableDoorCollisions(doorMask)
       
    def exitWalk(self):
        avatar.stop()
        footstepsSound.stop()
        disableDoorCollisions()
 
myfsm = AvatarFSM()
myfsm.request('Walk', 1.0, BitMask32.bit(2))
注意,exitStateName方法通常不带参数。
允许和禁止状态转换
默认情况下,每个状态都是可以转换的:调用fsm.request('StateName')都会成功,FSM进入新的状态。你可以禁止某些不希望发生的转换,使FSM变得更健壮。
例如,前文所举的简单FSM例子,状态图如下:
Walk2Swim
 
 
Walk
 
Swim
 
Drowning
Swim2Walk
 
 
 
图中,箭头表示合法的转换。可以从'Walk' 到 'Walk2Swim',但却不能从'Walk' 到 'Swim2Walk'。如果当前状态为'Walk',你请求FSM转到'Swim2Walk'就会造成一个bug。你可以让FSM抛出一个异常,帮助你找bug。
为 实现这种机制,你可以在FSM的__init__()方法存储self.defaultTransitions,这是允许转换的一个map,该map的每 个key都是一个状态名字;key的value是一个该状态下所有允许的转换的列表。defaultTransitions中没有列出的转换视为无效。例 如:
class AvatarFSM(FSM.FSM):
    def __init__(self):
        FSM.FSM.__init__(self)
        self.defaultTransitions = {
            'Walk' : [ 'Walk2Swim' ],
            'Walk2Swim' : [ 'Swim' ],
            'Swim' : [ 'Swim2Walk', 'Drowning' ],
            'Swim2Walk' : [ 'Walk' ],
             'Drowning' : [ ],
            }
如果你不给self.defaultTransitions指派任何东西,那么所有的状态转换都是合法的。但一旦像上面一样分配了一个map,当你请求一个map中不存在的转换时将触发FSM.RequestDenied异常。
 
FSM 的输入
FSM的另一个主要用途就是作为AI状态的抽象。为此,你应该给FSM提供一个“输入”字符串,由FSM决定转换到哪个状态,而不是显式地指定目标状态的名字。
考虑下面这个FSM状态图:
↷ straight
 
↶ straight
North
← left
East
↓ left
 
↑ left
West
→ left
South
↺ straight
 
↻ straight
上图箭头旁边的文字表示传给FSM的“输入”字符串,箭头方向代表该输入对应的状态转换。
在 这个例子里,我们编了一个简单的FSM来决定角色的方向,它应该输入 “left”或“straight”,然后根据前一个状态的方向,转换到另一个方向状态。如果我们从朝北的状态要求“left”,FSM将转换到朝西的状 态。相反,如果我们从朝南要求“left”,FSM将转换到朝东状态。在任何状态下请求“straight”,FSM将保持在当前方向。
为了在Panda3D中实现这个机制,我们为每个状态定义一个 filter 函数,目的是根据接收到的输入决定应该转换到哪个状态。
我 们通过定义名为filterStateName()的Python方法来创建filter函数,其中StateName为应用此filter函数的FSM 状态的名字。filterStateName方法接受2个参数,一个字符串和一个参数元组(tuple)(参数包括传给fsm.request()的可选 参数,通常为一个空元组)。filter函数应该返回要转换成的那个状态的名字。如果那个转换被禁止,filter函数既可以返回一个None,也可以产 生一个异常。例如:
class CompassDir(FSM.FSM):
    def filterNorth(self, request, args):
        if request == 'straight':
            return 'North'
        elif request == 'left':
            return 'West'
        else:
            return None
 
    def filterWest(self, request, args):
        if request == 'straight':
            return 'West'
        elif request == 'left':
            return 'South'
        else:
            return None
 
    def filterSouth(self, request, args):
        if request == 'straight':
            return 'South'
        elif request == 'left':
            return 'East'
        else:
            return None
 
    def filterEast(self, request, args):
        if request == 'straight':
            return 'East'
        elif request == 'left':
            return 'North'
        else:
            return None
注意,为方便起见,输入字符串应该以小写字符开头,与状态名字相反,后者以大写字母开头。以此来区分直接请求一个状态,或者给FSM提供某个输入字符。跟以前一样,我们调用request()来传递输入:
myfsm.request('left')
myfsm.request('left')
myfsm.request('straight')
myfsm.request('left')
如果FSM原来是朝北状态,执行以上命令后它将变成朝东状态。
defaultFilter 方法
虽 然定义一系列单个的filter方法给我们最大的灵活度,但很多FSM并不需要这么详细的控制。为此,我们只需定义一个defaultFilter方法。 当某个状态没有定义filterStateName()方法时,FSM将调用defaultFilter()方法。可以把通用的处理逻辑放到该方法里。
例如对上面那个FSM,我们只需使用一个defaultFilter方法和一个查找表:
class CompassDir(FSM.FSM):
    nextState = {
        ('North', 'straight') : 'North',
        ('North', 'left') : 'West',
        ('West', 'straight') : 'West',
        ('West', 'left') : 'South',
        ('South', 'straight') : 'South',
        ('South', 'left') : 'East',
        ('East', 'straight') : 'East',
        ('East', 'left') : 'North',
        }
 
    def defaultFilter(self, request, args):
        key = (self.state, request)
        return self.nextState.get(key)
FSM基类定义了一个defaultFilter()方法实现默认的FSM转换规则(如果没有定义self.defaultTransitions,允许所有的direct-to-state(大写开头)直接转换请求;或者,忽略输入(小写开头)请求)。
在 实际应用中,可以混合使用defaultFilter方法和自定义的filter方法。当某个状态的用户定义filter方法不存在时才调用 defaultFilter。如果已经有了filterStateName方法,发生状态转换请求时立刻调用该方法;它可以满足任何用户处理需要(也可以 在其中调用defaultFilter方法)。
 
一些FSM 高级应用
request vs. demand
前 面讲过,一般我们请求FSM转换状态不是调用fsm.request('NewState', arg1, arg2, ...)就是fsm.request('inputString', arg1, arg2, ...),其中arg1, arg2, ...表示传给目标状态enter函数(或filter函数)的参数。根据filter函数,request()可能成功也可能失败。如果成功,它将返回 ('NewState', arg1, arg2)元组,指出已经转换到的新状态。如果失败,它只返回None(如果filter函数定义为不抛出异常)。
如果请求一个状 态转换失败了,可能你把它看成条件错误,也可能你更想让程序马上停下来。在这种情况下,你应该换用fsm.demand(),它的语法和request ()一样,区别是它失败时不返回None,通常将产生一个异常。demand()没有返回值,如果它返回,就证明转换被接受了。
FSM.AlreadyInTransition
一个FSM通常只处在一种状态下,除了在两种状态转换过程中(当它正在调用前一个状态的exitStateName方法,接着调用新状态的enterStateName方法时)。在此过程中,FSM被视为不属于哪个状态,此时fsm.state值为None。
在状态转换过程中,调用fsm.request()请求一个新状态是非法的。如果你试着这样做,FSM将会产生一个FSM.AlreadyInTransition异常。这个错误很常见,很多人的exitStateName清理代码有副作用,会引发状态转换。
但我们有一个更简单的解决办法:换成fsm.demand()。不像request(),demand()在FSM处于转换中也可以调用。这时,FSM将请求放入队列中,一旦完全进入新状态就立刻处理这个请求。
forceTransition()
Panda 还提供一个fsm.forceTransition()方法,与demand()相同的是它既不会失败也没有返回值,不同的是它完全绕过filter函 数。因此,应该给它一个大写开头的状态名字(和可选参数),不要给它小写开头的输入字符串。即使转换被禁止,FSM也会转换到那个状态。所以, forceTransition()的用武之地在当我们需要跳到一个跟当前状态没有什么关联的状态时(例如,发生异常时进行紧急处理)。但是,任何情况下 都不要滥用forceTransition(),首先考虑demand()。如果你调用forceTransition()的次数很多,只能证明你的 filter函数(或者defaultTransitions)写得很差,把许多合理的状态转换都给禁止了。
过滤可选参数(Filtering the optional arguments
filterStateName方法接受2个参数:一个字符串和一个元组,元组包含传给request(或demand)方法的参数。它返回应该转换到的那个状态的名字,或返回None,表示不允许转换。
然 而,filter函数也可以返回一个元组。如果返回元组,应该是这样的形式('StateName', arg1, arg2, ...),其中arg1, arg2, ...表示应该传给enterStateName方法的可选参数。通常它们是传给filterStateName方法的同一组参数(这种情况下,你可以通 过Python语法('StateName',) + args返回元组)。
但返回的参数不必跟传入的一样。filter函数可以对参数进行检查、修改和重排,或者制造一个全新的参数集合。因此,filter函数不仅可以过滤状态转换,也可以过滤转换请求连带的数据。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值