从零开始的 Godot 之旅 --- EP9:有限状态机(一)
上一节中我们实现了角色待机和行走的动画,并且配合键盘输入的监听,让角色能在两个动画间切换。本节我们将继续完善角色功能,实现攻击系统,并引入有限状态机这个重要的设计模式来优化我们的代码结构。
实现角色攻击功能
在上一节的课后作业中,我们需要完成攻击动画的制作,并将攻击绑定到J键。现在我们先来实现这个功能,这将为我们后续学习状态机打下基础。
制作攻击动画
先来看下图片,可以发现攻击的动画只有4帧,而行走和待机有6帧。

所以我们创建一个attack_down动画,从第36帧开始,每0.1秒切换一帧,持续0.3秒;这样我们就完成一个向下的攻击动画。

小技巧 :当我们的素材是连续的,并且我们需要固定间隔插入关键帧时,我们可以在打开动画编辑器的前提下,在第一帧时直接点击
Frame后面的插入关键帧按钮,这样就会自动插入下一帧到固定间隔。这个技巧可以大大提高动画制作的效率。

完成所有方向的攻击动画
现在我们创建完了所有方向的攻击动画:
- attack_down:向下攻击动画
- attack_side:横向攻击动画(左右共用)
- attack_up:向上攻击动画

配置输入映射
在开始编写脚本之前,我们需要先配置输入映射。进入项目 → 项目设置 → 输入映射,添加一个名为attack的输入动作,并绑定到J键。

不要忘记在项目中配置输入映射,否则无法检测到按键输入。
定义攻击规则
在实现攻击功能之前,我们先确定几个规则:
- 触发条件:当角色处于待机状态或移动状态时,按下J键都可以触发攻击动画
- 攻击持续时间:玩家的攻击需要持续0.3秒(与动画时长匹配)
- 状态限制:在攻击的0.3秒内,玩家无法移动,也无法切换到待机状态
这些规则确保了攻击动作的完整性和游戏体验的流畅性。
实现攻击逻辑
为了满足上面的需求,我们先定义几个成员变量:
GDScript
# 攻击持续时长,与动画时长匹配,代表角色攻击需要的时间
var attack_duration: float = 0.3
# 攻击计时器,用来跟踪攻击用了多久
var attack_timer: float = 0.0
# 是否正在攻击
var is_attacking: bool = false
1. 攻击触发逻辑
接着我们在_physics_process中添加攻击判断。攻击开始后,我们需要重置攻击计时器,并且将攻击状态改成true:
GDScript
if !is_attacking and Input.is_action_just_pressed("attack"):
# 只有不处于攻击状态并且按下攻击键时,才进入攻击逻辑
velocity = Vector2.ZERO
is_attacking = true
attack_timer = 0.0
# 其他攻击的逻辑处理
2. 攻击计时器逻辑
接着我们继续添加攻击计时器逻辑:
我们已经知道
_physics_process会在物理帧回调中被调用,它的参数delta是与上一帧的间隔。那么这就是一个很好的计时器,我们只需要每帧加上这个间隔,就能准确跟踪攻击的持续时间。
最后我们还需要在动画播放结束后,将is_attacking设置为false,这样我们就可以在动画播放结束后,继续移动了:
GDScript
if is_attacking:
# 正在攻击,计时并检查是否攻击结束
attack_timer += delta
if attack_timer >= attack_duration:
# 攻击结束,更新状态
attack_timer = 0.0
is_attacking = false
测试攻击功能
至此我们已经完成了角色攻击的功能。运行游戏,按下J键,可以看到角色成功执行攻击动画:

代码结构的隐患
到现在我们已经完成了角色待机、移动、攻击等功能,看起来一切都很顺利。但是,后续我们还要实现受到攻击、死亡、翻滚、拾取等等功能。试想一下我们的代码要怎么写?
是不是全部都写在player.gd的_physics_process中?
问题示例
如果按照当前的方式继续扩展,我们的代码可能会变成这样,陷入混乱的if-else中:
yaml
func _physics_process(delta: float) -> void:
# 移动逻辑
if is_moving:
move_and_slide()
if 更多判断:
更多逻辑
# 攻击逻辑
if is_attacking:
attack()
# 受到攻击逻辑
if is_hit:
hit()
# 死亡逻辑
if is_dead:
die()
# 翻滚逻辑
if is_rolling:
roll()
# 拾取逻辑
if is_picking:
pick()
......
问题分析
这样我们的代码就会变得非常混乱,难以维护。主要问题包括:
- 逻辑耦合:所有状态的逻辑都混在一起,难以分离
- 可维护性差:修改一个状态可能影响其他状态
- 可扩展性差:添加新状态需要修改大量代码
- 可读性差:大量的if-else嵌套让代码难以理解
所以我们需要一种更好的方式来管理我们的代码,这就是有限状态机要解决的问题。
有限状态机
有限状态机(Finite State Machine,简称FSM)是一种强大的设计模式,它能够优雅地解决我们上面遇到的问题。
什么是有限状态机?
有限状态机是一种设计模式,它将对象的状态分为有限个状态,每个状态处理自己的逻辑,并且可以按照预定义的规则互相转换,从而实现对象的行为。
状态机的核心思想
对于我们的玩家角色来说,它同一时间只会处于一种状态,处于每种状态时玩家会执行对应的动作,每种状态之间切换都有固定的规则。就如同下图所示:

状态机的优势
如果我们将每个状态的逻辑都写在对应的脚本中,并且让一个管理员(状态机)来管理状态的切换,那么我们就成功地:
- 消除大量if-else:每个状态的逻辑独立管理,不再需要复杂的条件判断
- 提高代码可维护性:修改一个状态不会影响其他状态
- 增强代码可扩展性:添加新状态只需要创建新的状态类
- 提升代码可读性:代码结构清晰,逻辑一目了然
有限状态机的架构设计
要想实现一个有限状态机,我们至少需要以下几个核心组件:
- 状态机(StateMachine):负责维护当前状态,控制状态的切换,调用状态的逻辑
- 状态(State):每个状态都有自己的逻辑,负责处理自己的状态行为
- 状态转换条件(StateTransition):负责维护状态转换的规则和条件
类示意图如下:

核心组件说明
状态机(StateMachine)类
- 维护当前状态(
current_state)、状态集合(states)、状态转换条件(transitions)等 - 对外提供添加、删除状态、添加状态转换条件等方法
- 每帧都会:
- 调用当前状态的
update(physics_update)方法 - 调用自动状态转换方法,检查是否需要转换状态
- 调用当前状态的
- 每当需要转换状态时,状态机会调用当前状态的
exit()方法,调用新状态的enter()方法,并且将状态切换为新状态
状态类(State)
- 该类是状态的抽象基类,所有具体状态都应继承此类
- 每个状态都有自己的逻辑,负责处理自己的状态行为
- 状态类需要实现以下方法:
enter(params):状态进入时调用exit():状态退出时调用physics_update(delta):状态物理更新时调用update(delta):状态逻辑更新时调用handle_input(event):处理输入事件name:状态名称,用于调试
状态转换条件类(StateTransition)
- 该类是状态转换条件类,定义状态间的转换规则
- 每当我们有一个状态转换的条件,我们就需要实例一个状态转换条件类
- 支持优先级设置,当多个转换条件同时满足时,选择优先级最高的
具体实现代码
下面我们来看看具体的实现代码:
状态基类
GDScript
# State.gd
# 状态基类,所有具体状态都应继承此类
# @author chenrui
# @date 2025-08-31
class_name State
extends RefCounted
# 状态机引用
var state_machine: StateMachine
# 状态名称
var name: String
# 进入状态时调用
# @param _previous_state 前一个状态名称
func enter(_previous_state: String = "") -> void:
pass
# 退出状态时调用
# @param _next_state 下一个状态名称
func exit(_next_state: String = "") -> void:
pass
# 每帧更新
# @param _delta 帧间隔时间
func update(_delta: float) -> void:
pass
# 物理帧更新
# @param _delta 物理帧间隔时间
func physics_update(_delta: float) -> void:
pass
# 处理输入事件
# @param _event 输入事件
func handle_input(_event: InputEvent) -> void:
pass
# 获取状态机的拥有者(通常是角色节点)
# @return 状态机的拥有者节点
func get_owner() -> Node:
return state_machine.owner if state_machine else null
状态转换条件基类
GDScript
# StateTransition.gd
# 状态转换条件管理器,定义状态间的转换规则
# @author chenrui
# @date 2025-08-31
class_name StateTransition
extends RefCounted
# 源状态名称("*" 表示任意状态)
var from_state: String
# 目标状态名称
var to_state: String
# 转换条件函数
var condition: Callable
# 转换优先级(数值越大优先级越高)
var priority: int = 0
# 构造函数
# @param _from_state 源状态名称
# @param _to_state 目标状态名称
# @param _condition 转换条件函数
# @param _priority 转换优先级
func _init(
_from_state: String, _to_state: String, _condition: Callable, _priority: int = 0
) -> void:
from_state = _from_state
to_state = _to_state
self.condition = _condition
priority = _priority
# 检查转换条件是否满足
# @param current_state 当前状态名称
# @return 是否可以转换
func can_transition(current_state: String) -> bool:
# 检查源状态是否匹配("*" 匹配任意状态)
if from_state != "*" and from_state != current_state:
return false
# 检查转换条件
if condition.is_valid():
return condition.call()
return false
状态机
GDScript
# StateMachine.gd
# 核心状态机管理器,负责状态的切换、更新和生命周期管理
# @author chenrui
# @date 2025-08-31
class_name StateMachine
extends Node
# 信号定义
signal state_changed(previous_state: String, new_state: String)
signal state_entered(state_name: String)
signal state_exited(state_name: String)
# 当前状态
var current_state: State
# 所有注册的状态
var states: Dictionary = {}
# 状态转换规则
var transitions: Array[StateTransition] = []
# 是否启用状态机
var enabled: bool = true
# 自动加载基础路径(用户可自定义)
var auto_load_base_path: String = ""
func _init(custom_name: String = "StateMachine") -> void:
name = custom_name
# 初始化
func _ready() -> void:
# 设置为单线程处理模式,确保状态切换的原子性
set_process_mode(Node.PROCESS_MODE_INHERIT)
# 每帧更新
func _process(delta: float) -> void:
if not enabled or current_state == null:
return
current_state.update(delta)
_check_transitions()
# 物理帧更新
func _physics_process(delta: float) -> void:
if not enabled or current_state == null:
return
current_state.physics_update(delta)
# 处理输入事件
func _input(event: InputEvent) -> void:
if not enabled or current_state == null:
return
current_state.handle_input(event)
# 添加状态
# @param state_name 状态名称
# @param state 状态实例
func add_state(state_name: String, state: State) -> void:
states[state_name] = state
state.state_machine = self
state.name = state_name
# 移除状态
# @param state_name 状态名称
func remove_state(state_name: String) -> void:
if state_name in states:
states.erase(state_name)
# 添加状态转换规则
# @param transition 转换规则
func add_transition(transition: StateTransition) -> void:
transitions.append(transition)
# 启动状态机
# @param initial_state 初始状态名称
func start(initial_state: String) -> void:
if initial_state in states:
_change_state(initial_state)
else:
print("StateMachine: 初始状态不存在: " + initial_state)
# 手动切换到指定状态
# @param state_name 目标状态名称
# @param force 是否强制切换(忽略转换规则)
func transition_to(state_name: String, force: bool = false) -> bool:
if not force:
# 检查是否有有效的转换规则
var can_transition: bool = false
var current_name: String = current_state.name if current_state else ""
for transition: StateTransition in transitions:
if transition.can_transition(current_name) and transition.to_state == state_name:
can_transition = true
break
if not can_transition:
print("StateMachine: 无有效转换规则从 " + current_name + " 到 " + state_name)
return false
return _change_state(state_name)
# 自动加载状态
# @param object_name 对象名称
func auto_load_states(object_name: String) -> void:
# 确定加载路径
var base_path: String = auto_load_base_path
var loaded_states: Dictionary = StateLoader.load_states(object_name, base_path)
for state_name: String in loaded_states.keys():
add_state(state_name, loaded_states[state_name])
# 获取当前状态名称
# @return 当前状态名称
func get_current_state_name() -> String:
return current_state.name if current_state else ""
# 检查是否处于指定状态
# @param state_name 状态名称
# @return 是否处于指定状态
func is_in_state(state_name: String) -> bool:
return current_state != null and current_state.name == state_name
# 暂停状态机
func pause() -> void:
enabled = false
# 恢复状态机
func resume() -> void:
enabled = true
# 内部:执行状态切换
# @param state_name 目标状态名称
# @return 是否切换成功
func _change_state(state_name: String) -> bool:
if not state_name in states:
print("StateMachine: 状态不存在: " + state_name)
return false
var previous_state_name: String = ""
# 退出当前状态
if current_state != null:
previous_state_name = current_state.name
current_state.exit(state_name)
emit_signal("state_exited", previous_state_name)
# 进入新状态
current_state = states[state_name]
current_state.enter(previous_state_name)
# 发送信号
emit_signal("state_entered", state_name)
emit_signal("state_changed", previous_state_name, state_name)
print("StateMachine: 状态切换: " + previous_state_name + " -> " + state_name)
return true
# 内部:检查自动状态转换
func _check_transitions() -> void:
if current_state == null:
return
var current_name: String = current_state.name
var best_transition: StateTransition = null
var highest_priority: int = -999999
# 查找优先级最高的可执行转换
for transition: StateTransition in transitions:
if transition.can_transition(current_name) and transition.priority > highest_priority:
best_transition = transition
highest_priority = transition.priority
# 执行转换
if best_transition != null:
transition_to(best_transition.to_state, true)
本节小结
在本节中,我们首先完成了角色攻击功能的实现,然后发现了当前代码结构存在的问题,最后引入了有限状态机这个重要的设计模式来解决这些问题。
主要收获
- 攻击系统实现:成功实现了角色的攻击动画和攻击逻辑,包括攻击触发、计时和状态管理
- 问题识别:发现了当前代码结构在扩展性、可维护性方面的不足
- 设计模式学习:引入了有限状态机这一重要的设计模式,理解了其核心思想和架构设计
- 代码架构:学习了状态机的三大核心组件:状态机、状态类和状态转换条件类
关键知识点回顾
- 攻击系统:通过计时器管理攻击持续时间,确保攻击动作的完整性
- 有限状态机:一种设计模式,将对象的行为分解为有限个状态,每个状态独立管理自己的逻辑
- 状态转换:通过状态转换条件类管理状态之间的转换规则,支持优先级机制
- 代码组织:通过状态机模式,实现代码的解耦和模块化,提高可维护性和可扩展性
下一步计划
下一节我们将在我们的项目中实际使用有限状态机,并且用状态机改造我们玩家的脚本。这将让我们的代码结构更加清晰,也为后续添加更多功能(如受击、死亡、翻滚等)打下坚实的基础。
敬请期待!