State machines and state modes

The state machine

The earliest contact with the word state machine comes from the compilation principle of learning, in the lexical analysis, through the finite state machine for word recognition. The state machine is defined inside as a mathematical model, a quintuple.

Screenshot from Wikipedia

For Android developers, perhaps the most familiar is the MediaPlayer state machine diagram

A state machine generally has four elements

  1. Status: Indicates the current state of the state machine
  2. Condition: a condition (in keystroke systems, the value of the key) that triggers an action or state transition.
  3. Action: An action that is performed after a condition is met (that is, in response to tasks, usually one action for one method in code)
  4. Sub-state: the next state to be migrated after the action is completed

Above the blue circle represents all the state is a state machine, each method is representative of an action, and action of conditions on the android system is usually a response, finger touching the screen if the arrow points to represent the current state machine is in a state of now, after the action to perform, the state machine arriving time.

The state pattern

State model of the word is not strange for most developers, from 23 common design patterns of behavior in class a design pattern, as a class in the presence of large amounts of if statements to state judgment, often should consider when state model, the relationship between the state machine and the state pattern is that the state machine is a theory, is to guide, is a mathematical model, State mode is a programming language to the realization of state machine, is a concrete application.

Problems with not using state mode

Recently, I’ve been trying to write some small games in my spare time. Using state machines and state modes in games is a basic operation, because without state modes, it would be impossible to write maintainable code in the face of complex character state changes in the game. Here’s an example of the Godot engine’s GDScript: In the Player class, the person jumps first by pressing the key

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

At this time, we have the demand, in the jump process if press the key again, then the two jump, what to do? Add a flag bit 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
Copy the code

What if I press the “up” button again when I’m in the second jump state? What if the mission can glide and add a flag bit 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
Copy the code

Do you see the problem? As the number of states increases, the number of state bits increases, which is a nightmare to maintain, and changes to the _physics_process method every time, which violates an important object oriented principle, the Open closed principle,

Refactor using state mode

First let’s convert the above example to a state machine

You can see that there are four states, which are standing state, jump state, secondary jump state and glide state. The state switching condition is as simple as pressing the up key. Here we define a PlayerState base class, PlayerStandingState, PlayerJumpingState, PlayerSecondJumpingState, and PlayerSwingState are all subclasses of it.

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"): PlayerSwingState extends PlayerState; PlayerSwingState extends PlayerState; func _init(player:Player).(player): pass func _physics_process(delta:float): swing()Copy the code

At this point, you can see that the Player class holds all of the references to the state class, and has a cur_state that marks the current state. All of the status bits have been removed. Each subclass state represents the current state, because the second hop state can only be accessed from the jump state, and Player can only be in one state at a time. So we remove the is_jumping parameter, in The PlayerJumpingState, we determine if the up button was clicked, and if so, we switch the state to PlayerSecondJumpingState, so that the Player’s behavior is all encapsulated in the state, The state switch caused by the condition change is also maintained by the respective state. After that, such as adding such as sliding shovel and three-segment jump, it is good to add the state as long as it conforms to the open-close principle.

Concurrent state machine

Now consider another problem, that is, we for the Player to add weapons systems, such as we have a pistol, assault rifles and rocket-propelled grenades, we’d like to fly wherever he is jumping, standing, or what state, can launch weapons, so the question becomes, now we need to add additional state, such as stand design pistol shooting, standing submachine gun, standing launch rocket launchers, There are jump fired pistols, jump fired submachine guns…. The whole state representation exploded exponentially, and the reason for this problem is that we crammed two states — what the Player was doing and what the Player was carrying — into a single state machine. To simulate all possible combinations, we need to write pairs of states. The solution to this problem is also obvious: set up two separate state machines.

Class_name Player extends Character: Func _physics_process(delta:float): func _physics_process(delta:float): cur_state._physics_process(delta) weapon_state._physics_process(delta)Copy the code

Each state machine behaves differently in response to the inputs and is independent of the others, thus avoiding the problem of state explosion due to combinations of states.

Layered state machine

For certain conditions, multiple states may jump to the same state, so to handle the same state switch, it is not necessary to write one in each state subclass, but in the base class, a common structure called a hierarchical state machine. A state can have a state superclass (making itself a substate). When an event occurs, if the child state does not process it, it travels down the inheritance chain to the superclass of the state and then processes it. In other words, it is like overwriting methods in inheritance.

conclusion

State machine whether in game development or in Android development, especially in audio and video development are widely used, is a need to master the knowledge points

Follow my official account: ‘Old arsenic on skateboard’