参考
本节参考自GDQuest的《Godot中的有限状态机》一文。
有关状态机的解释也可以参考:https://gpp.tkchu.me/
场景结构
# Player节点的代码 -- 提供基础的控制参数和全局变量
extends KinematicBody2D
class_name Player
export var speed = 60.0 # 速度 像素/秒
export var gravity = 5000 # 重力加速度
export var jump_force = 3000.0 # 单次跳跃高度
var velocity = Vector2.ZERO # 速度向量
状态转换示意图
class_name StateMachine
extends Node
signal changed(state_name)
export var initial_state := NodePath() # 通过属性面板设置初始状态,平台跳跃游戏的玩家初始在空中,所以指定Air
onready var state: State = get_node(initial_state) # 当前状态(节点)
func _ready() -> void:
# 因为StateMachine是它的owner也就是根节点Player的子节点
# 所以StateMachine先就绪,Player要等到所有节点就绪后才就绪
yield(owner, "ready") # 等待根节点就绪
# 遍历StateMachine的子节点,将各个子节点代码中的state_machine设置为自己
for child in get_children():
child.state_machine = self as StateMachine
# 调用状态的进入函数
state.enter()
# 输入、帧处理、物理帧处理统一调用当前状态中的对应函数
func _unhandled_input(event: InputEvent) -> void:
state.handle_input(event)
func _process(delta: float) -> void:
state.update(delta)
func _physics_process(delta: float) -> void:
state.physics_update(delta)
# 改变状态
func change_to(target_state_name: String, msg: Dictionary = {}) -> void:
if not has_node(target_state_name): # 不存在对应名称的子节点,也就是不存在对应的状态
return
state.exit() # 调用旧状态的退出
state = get_node(target_state_name) # 赋值和记录新的状态
state.enter(msg) # 调用新状态进入方法,并传递消息
$"../Label".text = target_state_name # 这一句自己加的,用于显示状态名称
emit_signal("changed", state.name) # 触发信号,表明状态已经切换完成
状态基类State
# 状态基类,供具体的状态继承和重写
class_name State
extends Node
var state_machine = null # 状态机对象,由StateMachine.gd内部初始化时自动赋值给具体的状态
# 进入状态
func enter(_msg := {}) -> void:
pass
# _unhandled_input -- 事件处理
func handle_input(_event: InputEvent) -> void:
pass
# _process -- 帧处理
func update(_delta: float) -> void:
pass
# _physics_process -- 物理帧处理
func physics_update(_delta: float) -> void:
pass
# 退出状态
func exit() -> void:
pass
中间状态类PlayerState
# 中间类,用于方便在状态中将owner写成Player,并获得代码提示
class_name PlayerState
extends State
var player: Player
func _ready() -> void:
yield(owner, "ready") # 等待根节点Player就绪
player = owner as Player # 将根节点保存为变量
assert(player != null)
通过让具体的状态继承PlayerState,而不是State。可以将owner改为player,并且此时脚本编辑器中也支持代码提示了。语义上也更加清晰了。
另外之所以不直接写入State,是因为后续可能会给其他比如“敌人”也设定状态机,这是,State就无法复用了。
具体的状态
# 状态:静止
extends PlayerState
func enter(_msg := {}) -> void:
player.velocity = Vector2.ZERO
func update(delta: float) -> void:
# 跳跃
if Input.is_action_just_pressed("ui_accept"):
state_machine.change_to("Air", {do_jump = true})
# 跑步
elif Input.is_action_pressed("ui_left") or Input.is_action_pressed("ui_right"):
state_machine.change_to("Run")
# 状态:在移动
extends PlayerState
func enter(_msg := {}) -> void:
player.velocity = Vector2.ZERO
func update(delta: float) -> void:
# 左右移动
player.velocity.x = Input.get_axis("ui_left","ui_right") * player.speed
player.velocity = player.move_and_slide(player.velocity,Vector2.UP)
# 跳跃
if Input.is_action_just_pressed("ui_accept"):
state_machine.change_to("Air", {do_jump = true})
# 静止
elif is_equal_approx(player.dir.x, 0.0):
state_machine.change_to("Idle")
# 状态:在空中
extends PlayerState
func enter(_msg := {}) -> void:
player.velocity = Vector2.ZERO
# 执行跳跃
if _msg.has("do_jump"):
print(player.jump_force)
player.velocity.y = -player.jump_force
player.velocity = player.move_and_slide(player.velocity,Vector2.UP)
func update(delta: float) -> void:
player.velocity = Vector2.ZERO
# 左右移动
player.velocity.x = Input.get_axis("ui_left","ui_right") * player.speed
player.velocity = player.move_and_slide(player.velocity,Vector2.UP)
# 实现下落
player.velocity.y += player.gravity * delta
player.velocity = player.move_and_slide(player.velocity,Vector2.UP)
# 在地面
if player.is_on_floor():
if is_equal_approx(player.velocity.x, 0.0):
state_machine.change_to("Idle")
else:
state_machine.change_to("Run")