一个对象拥有有限的几种状态,比如角色的状态可以是,待机(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绑定脚本,在初始化_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")
从实例代码可以看出,我们把待机,移动,跳跃三个状态分成了三个脚本,每个脚本只需要关注自己状态下的逻辑,比如待机时判断用户按键,可以转换为移动或跳跃状态。在跳跃状态,角色悬空不会相应按键输入,直到落地才会根据是否存在横向速度判断进入待机还是移动状态。