参考
本节参考自GDQuest的《Godot中的有限状态机》一文。
有关状态机的解释也可以参考:https://gpp.tkchu.me/
场景结构

# Player节点的代码 -- 提供基础的控制参数和全局变量extends KinematicBody2Dclass_name Playerexport var speed = 60.0 # 速度 像素/秒export var gravity = 5000 # 重力加速度export var jump_force = 3000.0 # 单次跳跃高度var velocity = Vector2.ZERO # 速度向量
状态转换示意图


class_name StateMachineextends Nodesignal changed(state_name)export var initial_state := NodePath() # 通过属性面板设置初始状态,平台跳跃游戏的玩家初始在空中,所以指定Aironready 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): # 不存在对应名称的子节点,也就是不存在对应的状态returnstate.exit() # 调用旧状态的退出state = get_node(target_state_name) # 赋值和记录新的状态state.enter(msg) # 调用新状态进入方法,并传递消息$"../Label".text = target_state_name # 这一句自己加的,用于显示状态名称emit_signal("changed", state.name) # 触发信号,表明状态已经切换完成
状态基类State
# 状态基类,供具体的状态继承和重写class_name Stateextends Nodevar 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 PlayerStateextends Statevar player: Playerfunc _ready() -> void:yield(owner, "ready") # 等待根节点Player就绪player = owner as Player # 将根节点保存为变量assert(player != null)
通过让具体的状态继承PlayerState,而不是State。可以将owner改为player,并且此时脚本编辑器中也支持代码提示了。语义上也更加清晰了。
另外之所以不直接写入State,是因为后续可能会给其他比如“敌人”也设定状态机,这是,State就无法复用了。
具体的状态
# 状态:静止extends PlayerStatefunc enter(_msg := {}) -> void:player.velocity = Vector2.ZEROfunc 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 PlayerStatefunc enter(_msg := {}) -> void:player.velocity = Vector2.ZEROfunc update(delta: float) -> void:# 左右移动player.velocity.x = Input.get_axis("ui_left","ui_right") * player.speedplayer.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 PlayerStatefunc enter(_msg := {}) -> void:player.velocity = Vector2.ZERO# 执行跳跃if _msg.has("do_jump"):print(player.jump_force)player.velocity.y = -player.jump_forceplayer.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.speedplayer.velocity = player.move_and_slide(player.velocity,Vector2.UP)# 实现下落player.velocity.y += player.gravity * deltaplayer.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")
