在 Godot 4.4 中构建灵活的有限状态机 (FSM)

J.sky
Godot
Godot教程
2025/11/14

为什么要使用状态机?

在游戏开发中,角色的行为(如站立、行走、跳跃、攻击)通常需要复杂的逻辑来控制。如果不使用状态机,代码会充斥着大量的 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 等状态脚本只负责各自状态下的具体行为和转换检查。

完整的案例代码仓库:

案例代码仓库 github

案例代码仓库 国内镜像gitee

本文为原创文章,遵循: CC BY-NC-SA 4.0版权协议,转载请附上原文出处链接和本声明。

英雄请留步!对我博客最大的鼓励来自于你的评论!欢迎留言!