有限状态机 Finite State Machine

一个对象拥有有限的几种状态,比如角色的状态可以是,待机(idle),移动(walk),跳跃(jump)。

我们需要控制角色在这些状态之间进行转换,比如按下左右方向键,角色就要从代理状态变成移动状态,这时我们松开方向键,角色就会停下,从移动状态变成待机状态。

有限状态机的目的是隔离处理不同状态的代码,降低整体代码的复杂度。如果没有有限状态机,我们可能要写出大量嵌套的if/else条件去实现不同状态之间的变换关系,通过有限状态机,我们可以把不同状态的处理条件放在一起,可以降低问题调试和后期维护的难度。


/----\ --按下方向键--> /----\
|idle|                 |walk|
\----/ <--松开方向键-- \----/

/----\ --按下跳跃键--> /----\
|idle|                 |jump|
\----/ <--落地-------- \----/

/----\ --按下跳跃键--> /----\
|walk|                 |jump|
\----/ <--落地-------- \----/

          

有限状态机的思路是,定义出基础状态,然后通过StateMachine实现在不同状态之间转换。在Godot中可以继承Node,实现StateMachine。

StateMachine


extends Node
class_name StateMachine

# 当前状态
var current_state: Object

# 所有状态
var states = {}

# 物理帧中,直接执行当前状态
func _physics_process(delta: float) -> void:
  if current_state:
    current_state._physics_process(delta)

# 转换状态
func transit(state_name):
  if not current_state:
    print("unexists state : ", state_name)
    return
  set_current_state(state_name)

# 设置当前状态
func set_current_state(state_name):
  if not states.has(state_name):
    print("unexists state : ", state_name)
    return
  current_state = states[state_name]
  if not current_state:
    print("unexists state : ", state_name)
    return
  current_state.enter()

# 添加并初始化状态
func set_current_state(state):
  state.fsm = self
  state._ready()
  states[state.name] = state

下一步准备三个状态

StateIdle


extends Node
class_name StateIdle

var fsm: StateMachine

func _ready():
  name = "idle"

func _physics_process(delta: float) -> void:

  var character_body_2d: CharacterBody2D = fsm.get_node("..")
  var animation_player: AnimationPlayer = fsm.get_node("../AnimationPlayer")

  # 判断跳跃键
  if Input.is_action_just_pressed("ui_accept"):
    fsm.transit("jump")
    return

  # 左右按键
  var input_direction = Input.get_axis("ui_left", "ui_right")

  if not is_zero_approx(input_direction):
    # 移动
    fsm.transit("walk")
    return

func enter():
  print("Hello from : ", name)

  var character_body_2d: CharacterBody2D = fsm.get_node("..")
  var animation_player: AnimationPlayer = fsm.get_node("../AnimationPlayer")

  character_body_2d.velocity = Vector2.ZERO
  animation_player.play("idle")

StateWalk


extends Node
class_name StateWalk

const SPEED := 200.0

var fsm: StateMachine

func _ready():
  name = "walk"

func _physics_process(delta: float) -> void:
  move()

  var character_body_2d: CharacterBody2D = fsm.get_node("..")
  var animation_player: AnimationPlayer = fsm.get_node("../AnimationPlayer")

  # 判断跳跃键
  if Input.is_action_just_pressed("ui_accept"):
    fsm.transit("jump")
    return

  # 左右按键
  var input_direction = Input.get_axis("ui_left", "ui_right")

  if not is_zero_approx(input_direction):
    # 如果按键,就设置速度
    character_body_2d.velocity.x = SPEED * input_direction
  else:
    # 如果没按键,就减速
    character_body_2d.velocity.x = character_body_2d.move_forward(character_body_2d.velocity.x, 0, SPEED)

  if is_zero_approx(character_body_2d.velocity.x):
    # 如果速度为零,就变成待机状态
    fsm.transit("idle")
    return
  else:
    character_body_2d.move_and_slide()

func enter():
  print("Hello from : ", name)

  var character_body_2d: CharacterBody2D = fsm.get_node("..")
  var animation_player: AnimationPlayer = fsm.get_node("../AnimationPlayer")

  # 初始化速度
  var input_direction = Input.get_axis("ui_left", "ui_right")
  character_body_2d.velocity.x = SPEED * input_direction

  # 播放动画
  animattion_player.play("walk")

StateJump


extends Node
class_name StateIdle

const JUMP_SPEED := -400.0

var fsm: StateMachine

func _ready():
  name = "jump"

func _physics_process(delta: float) -> void:

  var character_body_2d: CharacterBody2D = fsm.get_node("..")
  var animation_player: AnimationPlayer = fsm.get_node("../AnimationPlayer")

  # 重力
  character_body_2d.velocity.y += character_body_2d.get_gravity() * delta
  character_body_2d.move_and_slide()

  if character_body_2d.is_on_floor():
    # 根据水平速度判断是进入待机还是移动状态
    if is_zero_approx(character_body_2d.velocity.x):
      fsm.transit("idle")
    else:
      fsm.transit("walk")

func enter():
  print("Hello from : ", name)

  var character_body_2d: CharacterBody2D = fsm.get_node("..")
  var animation_player: AnimationPlayer = fsm.get_node("../AnimationPlayer")

  # 起跳初速度
  character_body_2d.velocity.y = JUMP_SPEED
  animation_player.play("jump")

为角色准备场景树

  • CharacterBody2D
    • CollectionShape2D
    • Sprite2D
    • AnimationPlayer
    • StateMachine

为CharacterBody2D绑定脚本,在初始化_ready()中,把三个状态注册到StateMachine中,并设置初始状态为idle。然后状态机就可以根据用户输入转换角色的状态了。


func _ready():
  state_machine.register_state(StateIdle.new())
  state_machine.register_state(StateWalk.new())
  state_machine.register_state(StateJump.new())
  state_machine.set_current_state("idle")

从实例代码可以看出,我们把待机,移动,跳跃三个状态分成了三个脚本,每个脚本只需要关注自己状态下的逻辑,比如待机时判断用户按键,可以转换为移动或跳跃状态。在跳跃状态,角色悬空不会相应按键输入,直到落地才会根据是否存在横向速度判断进入待机还是移动状态。