惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog
V
Vulnerabilities – Threatpost
Apple Machine Learning Research
Apple Machine Learning Research
V
V2EX
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
Latest news
Latest news
博客园 - 三生石上(FineUI控件)
美团技术团队
aimingoo的专栏
aimingoo的专栏
Google Online Security Blog
Google Online Security Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threatpost
Y
Y Combinator Blog
T
Tailwind CSS Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
A
Arctic Wolf
C
Cyber Attacks, Cyber Crime and Cyber Security
小众软件
小众软件
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
Tenable Blog
W
WeLiveSecurity
L
LINUX DO - 热门话题
D
Docker
Cyberwarzone
Cyberwarzone
量子位
A
About on SuperTechFans
The Last Watchdog
The Last Watchdog
雷峰网
雷峰网
C
CERT Recently Published Vulnerability Notes
P
Palo Alto Networks Blog
The Hacker News
The Hacker News
Blog — PlanetScale
Blog — PlanetScale
P
Proofpoint News Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
F
Full Disclosure
The Cloudflare Blog
T
The Blog of Author Tim Ferriss
T
The Exploit Database - CXSecurity.com
Engineering at Meta
Engineering at Meta
O
OpenAI News
Hacker News - Newest:
Hacker News - Newest: "LLM"
Scott Helme
Scott Helme
IT之家
IT之家
S
Secure Thoughts
MongoDB | Blog
MongoDB | Blog
L
Lohrmann on Cybersecurity
博客园 - 司徒正美
Google DeepMind News
Google DeepMind News

碎言

不懂就别瞎掰掰:【程序员的常识】写的什么玩意? AI 辅助编程下的程序设计与代码编写 使用 Next.js 和 Tailwind CSS 构建可编辑和删除的 ToDo 待办事项应用 探索编程新境界:MarsCode 助你一臂之力 使用 Next.js 和 Tailwind CSS 搭建静态图片展示站点并部署到 Vercel AI 辅助编程:免费工具的优缺点及官网一览 GitHub push更新总是失败,写个python脚本解决 把博客从GitHub迁移到到了vercel ComfyUI 和 Flux.1 安装与使用教程 Flux.1 入门必知:硬件、环境、模型 Git项目的子文件夹中的内容无法同步到远程仓库的解决方法 blender流体Fluid使用中没有流体、流体穿模等一些问题的解决方法 博客聚合网站:积薪,竟然关闭了! blender日常使用中的一些技巧 虽然只有我自己在用,但还是更新了碎言博客的源代码, 与其在迷茫中困惑,不如在努力中前进 差点忘了我还有一个博客... 秧歌、博客和AI 终于熬到了新手上路 好久没有更新博客了。。。 老妈大腿骨骨折,最近一直在医院护理 Hello, September! Ubuntu开机自动启动Docker容器运行WordPress Docker 简单快速安装部署WordPress Docker下安装MySQL 加碘盐能防核辐射的话,还怕什么核战争? shields.io 一个简洁、一致、清晰的徽章 Ubuntu下使用root登录ssh的设置 GitHub Actions 构建、部署 Next.js项目 我又用回了"IE"--edge ubuntu-22-live-server 设置固定IP Next项目部署GitHub pages的一些坑 整理了一个下午的收藏夹...... 短暂而忙碌的暑假即将谢幕 GitHub Actions 是干什么用的? 放弃Next.js重构博客:经验、教训与未来技术探索 关于Google LCP 问题:超过了2.5秒 验证已通过 仅用了几个小时就使用next.js重构博客的所有功能 关于使用Next.js重构博客构思的记录 使用Tailwind css和JavaScript实现一个简单的返回顶部功能 罗技K380 F1-F12 功能键锁定 PHILIPS猛禽SPK8404机械键盘手感真是超赞! 局域网的DNS设置192.168.0.1与8.8.8.8,114.114.114.114有什么区别? 史诗结合的浪漫:《长安三万里》 聊 聊 友 情 链 接 1767个中文博客的数据洞察:中文博主都喜欢干啥? python requests 设置读取超时时间 八月回顾:博客之路,一路前行 Docker基础学习笔记和常用命令汇总 vim常用命令整理 Ubuntu 22 安装Docker过程记录 使用python结合feedparser模块编写自己的私人RSS订阅程序 博客大数据分析:38880条博文揭示博主最爱写什么? Windows下国产杀毒软件还有选择吗?火绒安全可以一试。 export exports module.exports 这三个命令的区别是什么如何使用? 时代的变迁:从56K拨号上网到1G光纤的回忆 理工计算机专业准大学生如何选配笔记本? 使用JavaScript编写自己的私人RSS订阅程序 如何使用CSS为博客标题设置彩虹颜色? 最终还是决定使用传统的网线来解决千兆速率问题。 1300M USB双频2.4+5G无线网卡的一些常识和使用技巧 Python Gui新框架:NiceGui之初体验 pycharm 正在关闭项目(closing project)无法关闭退出程序的解决方法! 速度与优雅:博客性能优化之旅 久了,博客主题就会一直做减法 七月的焦虑 在react中使用复制插件clipboard.js react中如把HTML代码转成字符串进行渲染? react useEffect()中使用lodash库中的防抖debounce react useEffect() 为什么在开发模式的时候会执行两次? 七年之痒!别了我曾深爱的MacBookPro 前端React+Material UI开发环境的快速搭建 一键更新Github部署的静态博客 编程新手学习过程中如何更好的提问? Typed.js 一款有意思的网页打字机效果库 解决错误 ERROR [WinError 10049] 在其上下文中,该请求的地址无效。 Stable Diffusion 相关教程与模型的记录 Stable Diffusion ControlNet openpose 报错无法使用 在react或vue中使用localStorage存储数据 使用React+Vite+TailwindCSS+daisyUI打造一个简单的ToDos 碎言静态博客 聊聊Python中的装饰器 npm更换源与nrm安装与配置 pip临时修改下载源 Vue快速学习笔记(9)Vuetify配置及简单使用 Vue快速学习笔记(8)webpack4.X配置Vue开发打包环境 Vue快速学习笔记(5)使用路由vue-router(1) Vue快速学习笔记(4)Vue组件中的数据传递 Vue快速学习笔记(3)Vue的组件 Vue快速学习笔记(2)Vue中的监听与计算属性 Vue快速学习笔记(1)Vue实例基础 基于Django构建的小学生英文单词背写检测考试系统。 如何在Python程序中使用配置文件.ini(ConfigParser) 写在我的博客PV10万时:聊聊编程与画画 python3函数学习笔记简单总结 PythonINI配置文件读写的简单方法 Python open()函数文件打开、读、写操作总结 Python os.path模块中一些常用方法的整理总结 Python 采集数据三步曲之[Requests + re.py模块进行数据采集] Python 采集数据三步曲之[Requests开源协议的HTTP 库]
在 Godot 4.4 中构建灵活的有限状态机 (FSM)
J.sky · 2025-11-14 · via 碎言

为什么要使用状态机?

在游戏开发中,角色的行为(如站立、行走、跳跃、攻击)通常需要复杂的逻辑来控制。如果不使用状态机,代码会充斥着大量的 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