今天不聊新功能,聊一件迟早会撞上的事:性能。

原型阶段没人管性能,可一旦你要把 Demo 放到 Steam 上给陌生人玩,玩家的机器从十年前的笔记本到 Steam Deck 什么都有。掉帧不会给你解释的机会,它直接变成差评。好消息是:性能问题 90% 不是「引擎太慢」,而是「我们没测量就乱改」。Godot 官方文档在这件事上说得非常直白——Always use profiling and timing to guide your efforts.

今日速览

  • 性能问题分三类:每帧都慢(持续低帧)、偶发卡顿(spike)、加载慢(非游戏时段)。三类要用三种手段治,别混为一谈。
  • 第一步永远是开 Profiler,不是改代码。Godot 内置 Profiler 要手动启停,因为它本身也吃性能。
  • 60 fps 的预算是 16.6 ms/帧。先把这个数字贴在墙上,再看每一栏占了多少。
  • 2D 掉帧的两大元凶极其稳定:draw call 太多(没合图、没批处理)和 每帧 new 对象/查节点。
  • 优化是循环不是一次性动作:测量 → 改最大的那个 → 再测量。改完不复测,等于没改。

核心学习内容

一、把「卡」翻译成数字

官方文档把测量手段列得很清楚:手动计时、内置 Profiler、外部 CPU/GPU 分析器、以及关掉垂直同步看真实帧率。对独立开发者来说,前两个就够用。

手动计时代码可以直接抄:

var t0 := Time.get_ticks_usec()
update_enemies()
var t1 := Time.get_ticks_usec()
print("update_enemies() took %d us" % (t1 - t0))

文档特别提醒:单次测量不准,因为计时器精度有限、CPU 调度也会抖。跑 1000 次取平均才有意义。

而 Profiler 的价值在于「一张按耗时排序的榜单」。你看的不该是绝对值,而是比例:如果 _physics_process 占了 60% 的帧时间,那其他所有优化都是在剩下的 40% 里做微调。

二、帧预算:先分配,再实现

不要问「这帧为什么慢」,要问「这帧的 16.6 ms 花在哪了」。一个可用的 2D 预算分配:

项目预算说明
脚本逻辑(AI/生成/UI)≤ 5 ms超过就是算法或调用频率问题
物理(碰撞体数量)≤ 3 ms上千个 Area2D 就该合并或降频
渲染提交≤ 4 msdraw call / 合图 / 批处理
音频 + 其他≤ 1 ms通常不是瓶颈
余量~3 ms留给 Steam Deck 与后台程序

关键动作:把物理和渲染降频。敌人 AI 不需要每秒跑 60 次,10 Hz 足够;屏幕外的敌人用 VisibleOnScreenNotifier2D 关掉逻辑;冷却计时用 Timer 节点,不要在每个 _process 里 if cooldown > 0: cooldown -= delta。

三、三个最稳的「立刻见效」改法

  1. 缓存节点引用。get_node() 在 _process 里每帧调用是新手最常见的浪费,用 @onready var sprite := $Sprite2D 或导出引用。
  2. 对象池。子弹、伤害数字、粒子这类「高频生灭」的对象,反复 instantiate() + queue_free() 会带来内存分配与 GC 抖动,表现为随机 spike。池化后 spike 基本消失。
  3. 合图 + 少材质。Godot 2D 靠批处理减少 draw call,而批处理的前提是相邻对象用同一张图集、同一材质。一张 4096×4096 的图集,往往比 200 张散图快好几倍。

还有一个「不写代码」的优化:把 _process 里只为了 UI 刷新的逻辑改成信号驱动。数值变了才更新,没变就不做事——这是最便宜的优化。

🎯 DeepSeek 提示词

提示词 1:性能瓶颈诊断(把 Profiler 数据交给 AI 排序)

适用场景:Profiler 里一堆函数名,你不知道先动哪个。

你是 Godot 4 性能优化专家。我会给你一份 Profiler 结果和关键脚本,请只做诊断,不要重写整个项目。

【输入】
1. 目标帧率与实测帧率:
2. 目标平台(PC / Steam Deck / 低端笔记本):
3. Profiler Top 10(函数名 + 占比 + 调用次数):
4. 相关脚本代码:
5. 场景结构简述(节点数量、碰撞体数量、是否用图集):

【任务】
- 判断瓶颈类型:每帧持续慢 / 偶发 spike / 加载慢,并说明判断依据。
- 列出最多 3 个最可能的原因,按「预计收益 ÷ 改动成本」排序。
- 每个原因给出:证据(引用了哪一行/哪个数字)、具体改法(可粘贴的 GDScript 片段)、预期收益量级(高/中/低)、副作用。
- 明确指出哪些「看起来慢但其实不该动」的地方,避免我浪费时间。

【约束】
- 不要建议换引擎、换语言、上 C# 这类大改。
- 不要给泛泛的「减少对象数量」,必须落到具体节点或函数。
- 如果信息不足以下结论,先列出你需要我补测的 2-3 个数据,不要猜。

【输出格式】
## 结论(一句话)
## 瓶颈排序(表格:原因 / 证据 / 改法 / 收益 / 风险)
## 建议先做的第一步(≤10 行代码)
## 需要我补测的数据

提示词 2:把高频生灭对象重构为对象池

适用场景:子弹、敌人、飘字、特效导致随机卡顿。

你是 Godot 4 重构专家。请把下面这段「实例化 + 销毁」的代码改造成对象池模式。

【输入】
- 原代码(完整粘贴):
- 对象类型(2D 节点 / 场景):
- 预估同时在场上限:
- 是否需要预热(游戏开始时预生成):

【任务】
1. 先指出原代码在性能上的具体问题(点名到函数级)。
2. 给出一个独立的 ObjectPool 脚本(GDScript,Godot 4.x 语法),要求:
   - 支持预热、取出、归还、自动扩容上限;
   - 归还时重置状态(可见性、位置、速度、信号连接);
   - 用数组/队列管理,不用 Dictionary 做热点查找。
3. 给出调用方改造后的代码,标注「原来这里是什么,现在改成什么」。
4. 说明这个改法在什么情况下反而更慢(例如对象极少时),以及阈值建议。

【约束】
- 只用 Godot 内置 API,不引入插件。
- 变量与函数命名用英文,注释用中文。
- 不要省略错误处理;池空且达到上限时必须有明确行为。

【输出格式】
1. 问题诊断(列表)
2. ObjectPool.gd(完整代码块)
3. 调用方 diff 说明
4. 适用边界与阈值

提示词 3:写一个开发期性能守卫 HUD

适用场景:你不想每次都开 Profiler,但想知道「什么时候开始变慢」。

你是 Godot 4 工具脚本开发者。请写一个只在 debug 构建下启用的性能守卫(Performance Guard)。

【需求】
- 显示:FPS、本帧耗时(ms)、物理耗时、当前 draw call 数、场上活跃对象数。
- 当连续 N 帧超过阈值时,在屏幕上打一条黄色警告,并把当帧的 Top 3 耗时来源打印到控制台。
- 阈值、N、是否显示都通过 @export 暴露,便于在编辑器里调。
- 逻辑本身开销要极低:每 0.5 秒采样一次即可,不要每帧拼字符串。

【约束】
- GDScript,Godot 4.x。
- 使用 Performance.get_monitor(...) 提供的指标,先列出你用了哪些 monitor 及其含义。
- 只在 OS.is_debug_build() 为真时运行,发布版自动禁用。
- 代码可直接挂到 CanvasLayer 下的 Label 上使用。

【输出格式】
1. 使用的 Performance monitor 清单(名称 + 含义)
2. 完整脚本(一个文件)
3. 场景挂载步骤(3 步以内)
4. 如何根据读数判断「该优化逻辑还是该优化渲染」

学习心得

做 Steam 游戏最怕的不是「性能不够」,而是「不知道哪里不够」。玩家反馈「有点卡」,你只能瞎猜;但如果你手上有帧预算表和一份 Profiler 榜单,这句话就变成了可执行的任务清单。

这也正好是 DeepSeek 最适合介入的位置:它不擅长替你感受手感,但非常擅长在你给出数据后做归因和排序。所以我的用法是——我负责测量和提供上下文,DeepSeek 负责给方案和代码,我负责验证收益。三者缺一,优化就会退化成「凭感觉改代码」。

另外一个现实提醒:别在原型期优化。先把游戏做完、能跑通、有人愿意玩,再拿这篇里的流程过一遍。官方文档那句「premature optimization is the root of all evil」不是鸡汤,是省下来的开发时间。

明日关注

  • 在自己的 Demo 上跑一次内置 Profiler,记录 Top 5 耗时函数,存档对比。
  • 找出项目里所有在 _process 中调用 get_node() 的地方,改成 @onready 缓存。
  • 给子弹/特效加对象池,用「连续击杀 200 个敌人」压测前后帧时间差。

本文由自动化学习流水线每日生成,坚持即学即记。