游戏中的状态机



 

基本上所有的软件都是有限状态机(finite-state machine,FSM)。什么是FSM呢?它是一个有向图,由一组节点和一组相应的转移函数组成。这句话是在写作这篇文章前刚从书上抄来的。通俗一点讲,它是一个事件驱动系统的模型,这个模型由有限数目的状态,若干输入和状态与状态之间转换的规则组成。在某一时刻,有一个或一组状态是FSM的当前状态,FSM接收输入事件并根据转换规则,将当前状态转为新的状态。正是由于这三个元素的组合,使得FSM具备了自己的行为特点。在游戏开发中,FSM被用来实现人工智能的决策过程,控制游戏对象的行为。

最简单的状态机

可能上面的解释还是有些抽象,绝大多数的文章会举用“门”或“锁”的例子来说明什么是状态机,我想还是举一个新鲜的例子吧:目前我们正在进行的一个项目中,有一个叫做“小贩”的NPC,它会在指定的区域里行走,有时候停留,当行走到区域的边界所时,会自己转向(图1)。我们把状态机的转换规则函数列出表格(图2),并根据列出的规则写出是这个简单状态机的Python实现:

游戏中的状态机_第1张图片

 

图1

 
图2
(TS_STOP,TS_WALK,TS_TURN)=range(1,4)
 
class FSM:
    #初始化
    def __init__(self):
        self.__curState=TS_STOP
        ... 
        pass
    def next(self,input):
        if self.__curState == TS_STOP:
            #检测是否超过停留时间
            if CheckStopTimeOut():
                self.__curState == TS_WALK
 
        elif self.__curState == TS_WALK:
            #检测是否移动出指定区域
            if CheckPosOutOfRect():
                #转向
                self.TurnBack()
                self.__curState = TS_TURN
            else:
                if IsTimeToStop():
                    self.__curState = TS_WALK
                else:
                #继续移动
                self.MoveNextPos()
 
        elif self.__curState == TS_TURN:
            self.__curState = TS_WALK

 

OO一点的状态机

显然,如果你继续将上面的代码写完整的话,它一定能很好的工作。但我似乎已经闻到了传说中“坏代码”的味道,上面长长的条件判断语句,会随着状态的增多变得更长。每增加一个状态,你就需要在长长的条件判断语句中小心查找和修改。当这样的条件语句增长到需要多个人合作完成时,那当导致严重的维护与调试方面的问题。另外,对于编译型语言而言,长长的switch语句形式上简化了if…then…else…的逻辑,事实上它还是会被拆解为if…then…else…的形式,而这种形式是难以优化的。一个具有N个状态的FSM要查找一个正确的状态,平均需要进行N/2次if…then…else…的判断。

在上面的状态机中,其实是让这个对象在不同的状态中表现出了不同的行为特征,那不同的行为特征之间除了有类似的形式化的接口之外,基本上没有任何的联系。那我们很自然地想到将接口上提,而将实现子类化。

首先构造一个State类用来存在某一个状态,再构造一个新的FSM类用来管理多个State。(图3)

 

图3

class State:

    def __init__(self,name):

        self.name = name

 

class StateMachine:

    pass

 

接下来的工作就是将那长长的if…then…else…分拆出来。每个状态只关心会使自己发生状态迁移的几个变量,并且只了解它后继的一个或几个状态。

 

(TS_STOP,TS_WALK,TS_TURN)=range(1,4)

 

class State:

    def __init__(self,state):

        self.state = state

 

class StopState(State):

    def __init__(self):

        State.__init__(self,TS_STOP)

    def next(self,input_event):

        if CheckStopTimeOut():

            return WalkState()

        else:

            return self

 

class WalkState(State):

    def __init__(self):

        State.__init__(self,TS_STOP)

    def next(self,input_event):

        if CheckPosOutOfRect():

            self.TurnBack()

            return TurnState()

        else:

            if IsTimeToStop():

                self.__curState = TS_WALK

            else:

                self.MoveNextPos()

            return self

 

class TurnState(State):

    def __init__(self):

        State.__init__(self,TS_STOP)

    def next(self,input_event):

        sleep()

        return WalkState()

 

class FSM:

    def __init__(self):

        self.__curState = WalkState()

 

    def next(self,input_event):

        self.__curState = self.__curState.next()

 

当我们需要添加了一种新的状态时,不需要去修改长长的条件判断语句了,只需要构造一个新的状态类,修改它的前序和后序状态类就可以了。对于任何一个状态的特有行为,都是独立的,不会混杂在其它状态的代码里。原为决定状态转移逻辑的那个长长的条件语句不见了,而是被分布在State的子类之间。另一方面,从设计的角度看,原先对当前状态的标识,是FSM内部的一个自有变量,状态与状态之间的转换也仅仅是表现为对自有变量的赋值,如果这个自有变量衍生为变量数组时,那极易出现FSM内部状态不一致的情况,而State的引入可以使得这样的情况变得相当简单。对于新版本的FSM来说,状态与状态之间的转换也成了原子化的操作,不需要兼顾多个变量的赋值。

在实际应用中,还有一种组织状态转换规则的方式,就是基于表的状态转换规则,这种实现方法中,将原来的条件判断语句改变成一个表。表的索引是可能的状态组合项,表的值域是状态的转换逻辑。在进行状态转化时,由FSM进行查表,并决定转换到哪个新的状态。这样的形式看似是对前一种方式的退化,但这种方式确实会带来意外的好处:你可以使用数据而不是用代码来描述转换的法则。不过相对于OO版本的FSM,这样的方式更注意的是转换的形式,而不是转换的逻辑。

 

游戏中的状态机_第2张图片

图4

关于状态机更详细的解说请查阅[Gamma94]里给出的那个State模式(图4),灵巧的利用现在OO语言的多态特性,同一实体在不同时刻表现出来的行为特征分离到不同的子类的实例中。除了OO之外,还有很多的编程技巧和语言特性支持你来完成这样的分离,比如动态语言可以在运行时动态改变对象的类的方法的实现。还有原始的函数指针,在灵活性和效率上也是一个很好的平衡,如果你能正确的处理它的话,可以做出很棒的设计。

游戏中的状态机

在MMORPG中,状态机被广范地用来实现NPC和玩家对象。在游戏开发中,面临着很多复杂的状态机应用情景。有很多的时候,当你的状态机的状态繁多,转换逻辑过于复杂时,你应该尝试使用多个状态机来组合完成你的功能,此时对游戏逻辑深刻的理解,良好的需要分析,老道的设计经验都是不可或缺的。

首先是并行的状态机。这样设计的初衷是这样的,当状态复杂到一定程序时,即便是使用上面OO的状态机,形式化的代码也让我们不胜其烦。不过你会发现,很多的状态并不是互斥的,相当多的状态是由几个基本的状态相互组合而形成的没有实际意义的离散式。此时,我们做出这样的决择,仅仅将互斥的状态放到同一个状态机中(当然还需要加上逻辑的因素),这样原先的状态机被拆分成独立的两个、三个或者更多个可以独立运行或者与其它状态机并行运行的状态机。纵向看,形成了对一个描述对象多层次的状态描述。这样的分层,带来的好处远远不止这些,在后面我们还会看到很多。

 

图5

 

 

 

图6

依然看我们正在进行那个休闲类MMORPG.。玩家始终存在于几种主要的状态中(图5)。

l         在正常状态下玩家可以进行正常的游戏操作,比如:行走、交易、施放魔法、交谈等等。

l         石化状态时,玩家不能进行任何的操作,只有石化时间到、自动使用或被别人使用了反石化魔法时才会恢复到正常状态。石化状态还会影响其它状态层的转换。

l         破产状态,顾名思义,当玩家的资产为负时就进入了破产状态,此时玩家不需要支付任何费用,还可以向经过他的其它玩家乞讨。

l         离线状态,当玩家不在线对自己的角色进行操作时,角色就进入了离线状态。

这些是玩家存在的主要状态,直接影响到玩家对游戏的理解,所这个层次我们需要独立的一个状态机来处理。

游戏中的状态机_第3张图片 

图7

另外玩家还需要在地图里行走。这样的行走可能是徒步的,也可能是驾驶交通工具的。玩家的行走不但具有方向,还会有主动与被动之分:比如玩家被别人玩家绑架到某处,这样的移动就属于被动的行走方式;或者被其它玩家催眠,跟着别的玩家行走。而这些状态在游戏逻辑和游戏表现上都有着截然不同的处理方式。

同样的玩家角色的操作状态也是一个可控的状态机,绝大部分游戏的玩法都是由一组有限操作,不断迭代组成的。游戏的可玩性只是在于:游戏情节的演进让玩家忽略了他所正在进行的迭代操作的枯燥。这就是为什么连扫雷那样的游戏都会有人沉迷其中的道理。 我们的游戏中设定了很多的操作类型:基本行动操作、背包操作、交易操作、聊天操作等。

目前我们至少有了三个层次的状态机组合在运行。为了让这个三层次的状态机能协作运行,我们需要提供一个适合的管理者,来协调多个状态机的关系。当然逻辑上最显而易见的管理者就是角色本身(图8),不过角色本身本不是各种状态的真正观察者。而各种观察者所关心的状态层次也不尽相同。如果这几个层次都是各自独立的运行相互没有耦合的话,这部分的内容应该到此为止了,而现实的情况是各个层次之前有着频繁的交互。常见的交互主要有同步和跨层阻碍(Cross-Layer Blocking)。

 

图8

同步,是指当一个状态层发生状态转换时,逻辑上要求另外层也进行相应的转换而不是由状态机本身按照自己的转换规则进行转换。例如玩家当玩家破产后,主状态发生的变化,同样在在行走层和操作层都产生了一些相应的变化。玩家的行走被限定了速度,并不得驾驶交通工具,而且只能在指定的区域活动。操作层,很多的操作被取消,但增加了乞讨操作。所以当主状态变化时,这两个状态层次都要进行同步的变化。这一部分有些麻烦,你必须在设计时从逻辑上就解耦这部分各层之前的协同关系。如果你试图例举出来所有可能的情况,那之前的设计鲜得就多余了,因为你又引入了一个复杂的“简单”状态机。基于消息的状态机通讯是一个可取的办法,主状态机在进入石化状态时,向其它层发送一个主状态变更的消息。由其它层的状态机自觉处理。

跨层阻碍应用的情景更多,例如,玩家在玩成石化的动画后,在一定时间内就不能再进行其它状态层次的转换操作。这时,可以在所有的超类中加入一个控制变量来实现。被个状态层进行状态转换时,加上对这个变量的判断,如果变量为阻碍则不进行状态的转换。而跨层次的阻碍可以通过调用共同的超类接口实现。更加复杂一点的情形是,当一个状态层被多个状态层阻碍时。比如,有两个玩家对同一玩家先后施加了石化魔法,那么,当每一个玩家的魔法失效后,被施加魔法的玩家还应该处于石化状态。因为第二个玩家的魔法依然存在。此时,应该在状态层类的阻碍操作中加入引用计数。

class StateLayer:

    ...

    def Lock(self):

        self.__lockcount += 1

 

    def UnLock(self):

        assert(self.__lockcount > 0)

        self.__lockcount -= 1

 

    def CanTransition(targetState):

        if self.__lockcount > 0 :

            return False

        ...

 

与并行状态机相仿的,还有“串行”的状态机系统。最易理解的是一个由若干个状态机组成的队列。在这样的队列中,只有头部的状态机才处在活动状态。当第一个状态机运行到一个终态状态时,会发消息给队列,将执行权交给第二个状态机。我们回到开头时的那个“小贩”NPC,现在我需要他完成一个复杂一点的任务:

l         到地图左上角

l         喊话一分钟

l         到地图右上角

l         驻留五分钟

l         到地图右下角

l         坐下上休息五分钟

l         到地图左下角

上面的任务,只是示例性的,显得有些生涩,不过我们重点来关心它的实现。你可以对转换逻辑做足功夫,然后将这一系列动作写到一个状态机中去。这样一定是可行的!但是第二天,策划告诉你,他明天夜里做了一个梦,觉得把“小贩”动作序列的顺序重新排列一下……

这里你就可以用到上面的串行状态机。将上面的一系列动作,分解为若干个状态机,放到一个队列中,顺序激活最前面的一个状态机。这样原本复杂的状态组合,分解成了若干简单状态机的组合。面对策划的要求,你只要改变一下入队的顺序就可以了。如果“小贩”的动作是循环的,那执行到终态的状态机将重置后重新入队。另外,你可以方便的复用这些成形的小状态机,甚至以数据驱动的方式,改变“小贩”的行为动作,定制出若干“中贩”、“大贩”。

另外一个常见的应用,就是星际中指定战斗单元行走路线的方式:按住Ctrl键,点击鼠标右键。松开后,战斗单元会依次前进到每一个你指定过的地方。这样的实现就来自于一个动态创建的状态机队列,每一个状态机指示一个简单的Source到Dest的过程,到达中间目标地时,当前的状态机便会被删除,并激活下一个状态机。

下面的内容太过简单,但我是为了下一篇文章做一点铺垫,我想谈谈关于客户端和服务器之间状态机的同步问题。经过良好的设计,状态机的代码是与外围代码无关的。当你对你的游戏世界做了完整的仿真后,你所取得的状态机是可以在客户端和服务器之间进行复用的,只需要进行一些小小的修改。

l         你最好将所有的状态标识放到一个编号序列中去,哪怕这些状态处于不同层次的状态机当中的。也就是说,你最好为每一个状态指定一个独一无二的编号。这样当你在客户端和服务器之间发送同步消息时会方便很多。

l         绝大多数的状态转换动作被分布到了客户端和服务器合作完成。基本上都是按照这样的模式进行:客户端请求状态转换→服务器验证→服务器更新→服务器广播→客户端同步转换。

结束语

状态机是一个被充分验证简单、高效的AI逻辑实现的方式,贯穿在游戏的策划、脚本、编码等全过程。上面介绍的一些常用手法也是成熟和惯用的办法。所以掌握状态机的类型结构和常用模式,是每一个游戏开发人员的必修科。

你可能感兴趣的:(游戏,服务器,input,语言,任务,交通)