为什么要使用状态机?
在游戏开发中,角色的行为(如站立、行走、跳跃、攻击)通常需要复杂的逻辑来控制。如果不使用状态机,代码会充斥着大量的 if/else 或 match 语句,导致代码难以阅读、维护和扩展,这就是所谓的“意大利面条式代码”。例如下边的代码:
# constants.gd (假设我们有一个全局脚本或内部枚举来定义状态)
enum PlayerState {
IDLE,
MOVE,
JUMP,
ATTACK
}
# Player.gd 脚本 (所有逻辑集中于此)
extends CharacterBody2D
# 运动和重力常量...
const SPEED = 300.0
const JUMP_VELOCITY = -500.0
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
# 追踪当前状态的核心变量
var current_state: int = PlayerState.IDLE
# 追踪攻击是否完成
var is_attack_finished: bool = true
# 追踪动画播放器
@onready var animated_sprite = $AnimatedSprite2D
func _physics_process(delta):
# 1. 应用重力 (这部分是通用的,但需要注意JUMP状态下的特殊处理)
if not is_on_floor():
velocity.y += gravity * delta
# 2. 处理攻击完成后的状态清理 (相当于 Attack.gd 的 _exit 逻辑)
if current_state == PlayerState.ATTACK and is_attack_finished:
# 攻击完成后,根据是否还在移动来决定回到 IDLE 还是 MOVE
var input_direction = Input.get_axis("move_left", "move_right")
if input_direction != 0:
current_state = PlayerState.MOVE
# 播放动画 "Move" (重复的动画逻辑)
else:
current_state = PlayerState.IDLE
# 播放动画 "Idle" (重复的动画逻辑)
is_attack_finished = false # 重置,避免频繁切换 (这里逻辑可能更复杂)
# --- 主状态判断逻辑 (巨大的 if/else 块) ---
# === IDLE 状态逻辑 (对应 Idle.gd) ===
if current_state == PlayerState.IDLE:
# 动画播放逻辑 (相当于 _enter)
animated_sprite.play("Idle")
# 检查转换条件
if Input.is_action_just_pressed("attack"):
current_state = PlayerState.ATTACK
# 转换后的 _enter 逻辑
animated_sprite.play("Attack")
is_attack_finished = false
elif Input.is_action_just_pressed("jump") and is_on_floor():
current_state = PlayerState.JUMP
# 转换后的 _enter 逻辑
animated_sprite.play("Jump")
velocity.y = JUMP_VELOCITY
elif Input.get_axis("move_left", "move_right") != 0:
current_state = PlayerState.MOVE
# 转换后的 _enter 逻辑
animated_sprite.play("Move")
# === MOVE 状态逻辑 (对应 Move.gd) ===
elif current_state == PlayerState.MOVE:
# 动画播放逻辑 (相当于 _enter)
animated_sprite.play("Move")
# 1. 移动处理 (_physics_update)
var direction = Input.get_axis("move_left", "move_right")
velocity.x = direction * SPEED
# 2. 检查转换条件
if Input.is_action_just_pressed("attack"):
current_state = PlayerState.ATTACK
animated_sprite.play("Attack")
is_attack_finished = false
elif Input.is_action_just_pressed("jump") and is_on_floor():
current_state = PlayerState.JUMP
animated_sprite.play("Jump")
velocity.y = JUMP_VELOCITY
elif direction == 0:
current_state = PlayerState.IDLE
# 转换后的 _enter 逻辑 (可以省略动画播放,因为 IDLE 状态会处理)
# === JUMP 状态逻辑 (对应 Jump.gd) ===
elif current_state == PlayerState.JUMP:
# JUMP 状态下的移动处理
var direction = Input.get_axis("move_left", "move_right")
velocity.x = direction * SPEED * 0.7 # 跳跃时速度减慢
# 检查转换条件 (落地)
if is_on_floor():
var input_direction = Input.get_axis("move_left", "move_right")
if input_direction != 0:
current_state = PlayerState.MOVE
else:
current_state = PlayerState.IDLE
# JUMP 状态的 _exit 逻辑被省略了,因为它和新状态的 _enter 混合在了一起
# 检查转换条件 (空中攻击)
if Input.is_action_just_pressed("attack"):
current_state = PlayerState.ATTACK
# ... (重复的 Attack 转换逻辑)
# === ATTACK 状态逻辑 (对应 Attack.gd) ===
elif current_state == PlayerState.ATTACK:
# ATTACK 状态下不能接收移动或跳跃输入
velocity.x = 0
# 攻击动画的自动完成逻辑 (需要一个信号或计时器来设置 is_attack_finished = true)
# 假设这里有一个处理攻击动画进度的逻辑
pass # 等待 is_attack_finished 变为 true
# 3. 最终移动
move_and_slide()
有限状态机(FSM)提供了一种优雅的解决方案。它将角色的每一个行为定义为一个状态(State),并清晰地定义了状态之间的转换(Transition)规则。本教程将基于 godotfsmtest 项目的代码结构,向您展示如何在 Godot 4.4 中实现一个结构清晰、基于类的 FSM。
项目结构

前期准备:Godot4.4 以上的版本,熟悉 GDScript的基本语法、类、类的继承和信号量(Signal)的使用,熟悉场景中的角色添加以及动画的添加,如果这些内容有不熟悉的,查看下边的代码可能会有些迷糊。
状态机的含义简单来说就控制动画来展示是角色所处的状态,例如空闲、行走、跳跃和攻击等。场景中的AnimatedSprite2D定义了角色的状态动画。

状态机类 State
创建State基类,后续的状态都会继承这个State,通过状态机和信号量来管理当前的状态,例如状态之间的切换。
代码如下:
# State.gd
class_name State
extends Node
# 信号:用于通知状态机进行状态转换
signal transitioned(state_name, new_state_name)
# 对拥有这个状态机的角色的引用(例如:Player)
var actor: CharacterBody2D
var anim_sprite: AnimatedSprite2D
# 1. 状态被进入时调用
func enter() -> void:
pass
# 2. 状态被退出时调用
func exit() -> void:
pass
# 3. 在 _process (每帧) 中调用
func update(delta: float) -> void:
pass
# 4. 在 _physics_process (物理帧) 中调用
func physics_update(delta: float) -> void:
pass
状态机类
State : 状态机基类
方法

信号

状态机管理器
状态机管理器负责管理当前状态、存储所有状态,以及处理状态间的转换。代码如下:
# StateMachine.gd
class_name StateMachine extends Node
# 导出变量:允许在编辑器中设置初始状态
@export var initial_state_name: State
var states: Dictionary = {}
@onready var current_state: State = null
@export var actor: CharacterBody2D # 引用拥有者
@export var anim_sprite: AnimatedSprite2D #引用动画
func _ready() -> void:
# 收集所有子节点(即所有具体状态)
for child in get_children():
if child is State:
var state = child as State
child.anim_sprite = self.anim_sprite
# 将状态添加到字典中,键为节点名
states[state.name] = state
state.actor = actor # 传递角色引用
state.transitioned.connect(on_state_transition)
# 检查并进入初始状态
if initial_state_name:
change_state(initial_state_name)
else:
printerr("Error: Initial state not set or not found.")
# 转换状态的核心函数
func change_state(new_state: State) -> void:
# 1. 退出旧状态
if current_state:
current_state.exit()
# 2. 设置新状态
current_state = new_state
# 3. 进入新状态
current_state.enter()
# 信号处理函数
func on_state_transition(state_name: String, new_state_name: String) -> void:
# 确保是当前状态发出的转换请求
if current_state and current_state.name == state_name:
change_state(states[new_state_name])
func _process(delta: float) -> void:
if current_state:
current_state.update(delta)
func _physics_process(delta: float) -> void:
if current_state:
current_state.physics_update(delta)
在角色中集成状态机管理器
# Player.gd (附加到 CharacterBody2D 节点)
class_name Player
extends CharacterBody2D
# 引用状态机节点
@onready var state_machine: StateMachine = $StateMachine
@onready var anim_sprite: AnimatedSprite2D = $AnimatedSprite2D
func _ready() -> void:
# 将自身的引用传递给状态机,以便状态可以控制角色
state_machine.actor = self
state_machine._ready() # 确保状态机初始化
# 注意:现在 _physics_process 由状态机和当前状态来管理,
# Player 脚本本身不再直接处理复杂的运动逻辑。
func _physics_process(delta: float) -> void:
state_machine._physics_process(delta)
func _process(delta: float) -> void:
state_machine._process(delta)
绑定状态机管理器,以及动画节点等相关的节点。
创建具体的状态
Idle 角色的空闲状态,
# Idle.gd (附加到 Idle 节点, 继承 State.gd)
class_name Idle
extends State
#新增:定义跳跃速度常量
const JUMP_VELOCITY = -450.0 # 负值表示向上运动
# Godot 4 默认重力加速度(方便计算)
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
func enter() -> void:
# 在进入空闲状态时,确保移动速度为0
actor.velocity.x = 0
# 播放 Idle 动画 (假设 actor 有一个 AnimationPlayer 节点)
anim_sprite.play("idle")
func physics_update(delta: float) -> void:
var direction = Input.get_axis("move_left", "move_right")
# 1. 如果有移动输入,转换到 Move 状态
if direction != 0:
transitioned.emit(name, "Move")
return
# 2. 如果有跳跃输入,转换到 Jump 状态
if Input.is_action_just_pressed("jump"):
# ⚠️ 关键修正:在转换之前,赋予角色向上的速度
actor.velocity.y = JUMP_VELOCITY
transitioned.emit(name, "Jump")
return
# 3. 攻击
if Input.is_action_just_pressed("attack"):
transitioned.emit(name,"Attack")
return
# 3. 在 Idle 状态下保持重力和移动
# 仅施加重力(如果角色站在一个活动的平台上,或者检测到不再贴地)
if not actor.is_on_floor():
# 这里应该转换到 Fall 状态,但如果只是施加重力,确保使用正确的重力值
actor.velocity.y += gravity * delta # 使用常量 gravity 更准确
# 注意:如果角色还在 Idle 状态且 velocity.y > 0,它应该转换到 Fall 状态
actor.move_and_slide()
enter()函数中触发当前状态的相关的动画、声音等。
在转态进行时进行判断,transitioned.emit(name, "Jump")发出信号进行状态的切换。
总结
通过 FSM,我们成功地将复杂的角色逻辑解耦:
Player.gd 只需要知道它有一个状态机。 StateMachine.gd 负责切换状态。 Idle.gd, Move.gd 等状态脚本只负责各自状态下的具体行为和转换检查。
完整的案例代码仓库: