在游戏中看状态机与状态模式

1,777 阅读6分钟

状态机与状态模式

状态机

最早接触状态机这个词来自编译原理的学习,在词法分析中,通过有限状态机来进行单词识别。状态机在里面被定义为一个数学模型,一个五元组。

截屏2021-07-26 上午9.19.57.png 截图来自维基百科

对于Android开发者,可能最为熟知的就是MediaPlayer的状态机图了

mediaplayerstate.jpg

状态机一般有四个要素

  1. 现态:状态机当前所处的状态
  2. 条件:触发动作或者状态迁移的条件(在按键系统中,就是指按键的值)
  3. 动作:条件满足后执行的动作(也就是响应各个任务,在代码中往往一个动作对应一个方法)
  4. 次态:动作完成后要迁移的下个状态

上图中蓝色圆圈代表的就是一个状态机的所有状态,每个方法代表的就是一个动作,而出发动作的条件在android系统上往往就是手指触摸屏幕的响应,箭头的指向代表了如果当前状态机处于现在的状态,当动作执行后,状态机到达的次态。

状态模式

状态模式这个词对于大部分开发者来说并不陌生,来自23中常见设计模式之中的行为类设计模式的一种,当一个类中存在大量的if语句用来进行状态的判断时,这时往往就应该考虑状态模式了,状态机与状态模式的关系就是,状态机是理论,是指导,是数学模型,状态模式是编程语言对状态机的实现,是具体应用。

不使用状态模式带来的问题

最近在业余时间在尝试写一些小游戏,在游戏中使用状态机和状态模式是一个基本操作,因为如果不使用状态模式,面对游戏中复杂的人物的状态变化,想编写可维护的代码那无异于天方夜谭。这里以Godot引擎的GDScript举例来说:在Player的类中首先按上键人物进行跳跃

class_name Player extends Character
func _physics_process(delta: float):
    if Input.is_action_pressed("ui_up"):
        jump()

这时我们有了需求,在跳跃过程中如果再按上键,则进行二段跳,怎么办?加一个标志位is_jumping

class_name Player extends Character
func _physics_process(delta: float):
    if Input.is_action_pressed("ui_up"):
        if is_jumping:
            jump_second()
        else:
            jump()
            is_jumping=true

如过在二段跳状态时再按上键,任务可以滑翔怎么办,再加个标志位is_second_jumping

class_name Player extends Character
func _physics_process(delta: float):
    if Input.is_action_pressed("ui_up"):
        if is_second_jumping:
            swing()
        elif is_jumping:
            jump_second()
            is_second_jumping=true
        else:
            jump()
            is_jumping=true

发现问题所在了吗?随着状态的增加,状态位也不停的增加,后续维护起来面对大量的状态位,那简直就是噩梦,而且每次都要修改_physics_process方法,这些都违反了面向对象的一个重要原则,开闭原则,

使用状态模式重构

首先我们将上面的举例转换为状态机

blog1.jpg

可以看到有四种状态,分别是站立状态,跳跃状态,二级跳状态以及滑翔状态。状态切换条件也是很简单,就是按上键。这里我们定义一个PlayerState基类,PlayerStandingState,PlayerJumpingState,PlayerSecondJumpingState和PlayerSwingState都是它的子类。

class_name Player extends Character:
    var cur_state:PlayerState
    var standing_state:PlayerStandingState
    var jumping_state:PlayerJumpingState
    var second_jumping_state:PlayerSecondJumpingState
    var swing_state:PlayerSwingState
    func _init():
        cur_state=PlayerStandingState(self)
        jumping_state=PlayerJumpingState(self)
        second_jumping_state=PlayerSecondJumpingState(self)
        swing_state=PlayerSwingState(self)
        
    func _physics_process(delta:float):
        cur_state._physics_process(delta)
​
​
class PlayerState:
    var player:Player
    func _init(player:Player):
        self.player=player
    func _physics_process(delta:float):
        pass
        
class PlayerStandingState extends PlayerState:
    func _init(player:Player).(player):
        pass
    func _physics_process(delta:float):
        player.set_image("standing")
        if Input.is_action_pressed("ui_up"):    //切换到跳跃状态
            player.cur_state=player.jumping_state
        
 class PlayerJumpingState extends PlayerState:
    func _init(player:Player).(player):
        pass
    func _physics_process(delta:float):
        jump()
        if Input.is_action_pressed("ui_up"):   //切换为二级跳跃状态
            player.cur_state=player.second_jumping_state
 
  class PlayerSecondJumpingState extends PlayerState:
    func _init(player:Player).(player):
        pass
    func _physics_process(delta:float):
        jump_second()
        if Input.is_action_pressed("ui_up"):   //切换为滑翔状态
            player.cur_state=player.swing_state
    
    class PlayerSwingState extends PlayerState:
    func _init(player:Player).(player):
        pass
    func _physics_process(delta:float):
        swing()
        
        
    

这个时候可以看到,Player类会持有所有状态类的引用,并有一个cur_state用来标记当前的状态,之前所有状态位标志都被移除掉了,每个子类状态代表着当前的状态,因为到二段跳状态只能从跳跃状态进入,而player每次只能处于一个状态,所以我们移除了is_jumping参数,在PlayerJumpingState判断是否点击了向上按键,如果点击了就切换状态为PlayerSecondJumpingState,这样Player的行为全部都封装在了状态里,由条件变化引发的状态切换也由各自状态去维护,之后比如添加例如什么滑铲、三段跳,只要添加状态就好了,符合开闭原则。

并发状态机

现在考虑另一个问题,就是我们为Player添加武器系统,比如我们有手枪,冲锋枪和火箭筒,我们想无论他是跳跃、站立、飞翔还是什么状态,都能发射武器,那么问题就来了,现在我们需要添加额外的状态,比如站立设计手枪、站立射击冲锋枪,站立发射火箭筒,还有跳跃发射手枪,跳跃发射冲锋枪....这样的话整个状态呈现指数类暴增,而出现这种问题的原因就是我们将两个状态——Player正在做什么以及Player所携带的东西——塞进了一个单一的状态机中。为了模拟所有可能的组合,我们需要编写成对的状态。这个问题的解决方案也很明显:分别设立两个独立的状态机。

class_name Player extends Character:
    var cur_state:PlayerState
    var weapon_state:WeaponState //武器状态
    
func _physics_process(delta:float):
        cur_state._physics_process(delta)     
        weapon_state._physics_process(delta)

每个状态机相应输入,做出不同的行为,并且独立于其他状态机,这样就能避免因为状态之间的组合带来的状态爆炸的问题。

分层状态机

对于某种条件,多个状态可能跳转到相同的状态,那么为了处理这种相同的状态切换,就没必要在每个状态子类里再写一个了,可以在基类里进行编写,这是一种被称为分层状态机的常见结构。一个状态可以有一个状态超类(使自己成为一个子状态)。当一个事件发生时,如果子状态没有处理它,它会顺着继承链到达状态的超类然后进行处理。换句话说,它就像继承中方法的重写一样。

总结

状态机无论在游戏开发还是在Android开发,尤其是音视频开发中都有广泛应用,是一个需要着重掌握的知识点

关注我的公众号:‘滑板上的老砒霜’