这次我们来看一个不是工具、也不是插件的开发效率主题:Godot 项目里的脚本命名、类型检查与性能优化。很多人在 Godot 里能很快跑出 Demo,但项目一旦膨胀到几十个场景、上百个脚本,改一个变量名要在全局搜索上半天,帧率掉下去也不知道是脚本慢、物理慢还是渲染慢。这类问题不是功能开发问题,而是工程化问题,而且越早处理,后面节省的时间越多。
先给结论:如果你希望一个项目半年后还能改得动、上线后跑得顺,最优先做的三件事是——统一命名规范和目录分层;给 GDScript 写静态类型标注;用性能分析器先定位瓶颈再动手优化。这三件事不需要换引擎,不需要引入额外框架,从今天写第一个脚本就能开始。
本文会通过可复制的 GDScript 片段、命名规范对照表和问题排查清单,把这套方法完整拆解。内容以 Godot 4.x 为主,还在用 Godot 3.x 的朋友,需要根据版本调整少量 API,整体思路完全通用。
1. 核心能力速览
| 方向 | 解决什么问题 | 关键手段 | 上手成本 | 是否依赖硬件 |
|---|---|---|---|---|
| 脚本命名规范 | 可读性差、重构困难、多人协作冲突 | PascalCase 类名、snake_case 变量、信号命名、目录分层 | 低 | 否 |
| 类型检查 | 编辑器补全不准、运行期报错、Variant 开销 | 静态类型标注、class_name、@export 类型、as / is 类型转换 | 低到中 | 否 |
| 性能优化 | 帧率不稳定、加载慢、内存高、移动端卡顿 | 节点引用缓存、对象池、绘制合批、物理层优化、状态机 | 中 | 中 |
从这张表能看出来,脚本命名和类型检查不依赖贴图、模型和 GPU,任何 Godot 项目都能立刻开始。性能优化则需要一个明确的“基准”来判断效果,不能靠感觉优化。后面的内容会把这三部分分别展开,同时覆盖手游性能优化、2D 人物走路模糊、状态机等具体开发场景。
2. 适用场景与使用边界
这套方法适合以下几类开发者:
- 用 Godot 做 2D 或 3D 中小型项目的开发者,项目规模从几十个场景开始增长。
- 正在从“一个人写全部代码”过渡到“多人协作”的团队。
- 想在上线前对手游、桌面游戏做一轮性能和可维护性体检的开发者。
- 已经被 GDScript 动态语言的“灵活性”坑过的人,比如局部变量拼错、运行时才发现节点类型不对。
不太适合的情况也需要说清楚:
- 如果你的诉求是“不改游戏代码,只靠系统批处理或关闭服务来提升 Godot 性能”,这不是本文讨论范围。Godot 游戏的性能瓶颈几乎不会在系统服务层面,关闭后台服务大概率治标不治本,还可能影响开发环境。
- 如果你完全不想接触 GDScript 的静态类型,第 4 章可以暂时跳过,但你将失去编辑器补全、提前报错和一部分热循环性能收益。
- 如果项目已经进入发布前最后阶段,临时大规模改名和补类型标注可能引入回归,更稳妥的做法是先挑热点脚本改,而不是全盘重构。
合规方面也提醒一句:Godot 引擎本身使用 MIT 协议开源,商用友好,但项目内的美术、音频、字体资源仍需确认授权。如果游戏涉及联网服务、用户数据,还需要遵守隐私和内容合规规范。
3. 脚本命名规范:从文件名到信号,统一一套约束
命名规范最容易被忽略,但它是整个项目可维护性的地基。命名不统一时,全局搜索、编辑器补全和代码评审的效率都会下降。这里的规则不是“个人审美”,而是一套可以落地的工程约束。
3.1 文件名、类名与 class_name
在 Godot 4.x 中,GDScript 文件名建议使用 PascalCase,并让class_name与文件名保持一致。例如Player.gd内部写class_name Player。这样其他脚本可以直接用Player作为类型名来声明变量、构造实例,而不是靠字符串路径去查。
# Player.gd class_name Player extends CharacterBody2D有了class_name之后,在同一个项目的任意脚本里都可以写:
var player: Player var new_player := Player.new()编辑器和编译器能识别Player类型,补全和报错都会更可靠。如果不用class_name,你只能写成var player = get_node("Player"),类型是Node而不是Player,后续访问自定义属性时容易触发运行时错误。
3.2 变量、函数、常量与私有成员
Godot 官方 GDScript 风格指南推荐:
| 内容 | 推荐格式 | 示例 |
|---|---|---|
| 文件名 | PascalCase | Player.gd |
| class_name | PascalCase | class_name Player |
| 变量、函数 | snake_case | attack_speed、take_damage() |
| 常量 | 大写蛇形 | MAX_HEALTH |
| 枚举类型 | PascalCase | enum MoveState |
| 枚举成员 | 大写蛇形 | IDLE、RUNNING |
| 私有成员 | 下划线前缀 | _velocity、_update_state() |
私有成员加下划线前缀有实际作用:看到_开头就能判断这个变量只在当前脚本内部使用,外部脚本不应该直接访问。这在多人协作中非常有用。
下面是一段命名完整的示例脚本:
# Player.gd class_name Player extends CharacterBody2D signal health_changed(current: int, max_value: int) signal died enum MoveState { IDLE, RUNNING, ATTACKING } const MAX_HEALTH: int = 100 const MOVE_SPEED: float = 180.0 @export var player_name: String = "Player" @onready var sprite: Sprite2D = $Sprite2D @onready var attack_timer: Timer = $AttackTimer var _health: int = MAX_HEALTH var _current_state: MoveState = MoveState.IDLE func take_damage(amount: int) -> void: if amount <= 0: return _health = maxi(_health - amount, 0) health_changed.emit(_health, MAX_HEALTH) if _health == 0: died.emit()这段代码里,文件名Player.gd和class_name一致;信号表达“事件已经发生”,比如health_changed而不是on_health;常量全大写;私有状态用下划线。整个脚本读起来不需要额外注释,就能判断每个成员的可见范围和职责。
3.3 信号命名
Godot 中信号是节点之间解耦通信的主要方式。信号命名最好用“事件已发生”的过去时或状态变化描述:
- 推荐:
health_changed、died、attack_finished - 不推荐:
on_health、check_hp、update_timer
原因很简单:信号是通知外部“某件事发生了”,不是请求外部“去检查某件事”。命令式命名会让连接信号的人误以为需要自己调用逻辑。信号回调函数可以在其他脚本里命名为_on_player_health_changed,这样信号和回调的关系一目了然。
3.4 目录结构与场景组织
脚本命名之外,目录结构也需要统一。实际项目里常见的混乱是:场景、脚本、贴图全部堆在res://根目录。项目规模小的时候没问题,一旦资源变多,加载路径、查找文件、git 合并都会变得痛苦。
一个可以复用的目录分层建议:
res:// ├── scenes/ │ ├── player/ │ ├── enemy/ │ └── ui/ ├── scripts/ │ ├── player/ │ ├── enemy/ │ └── ui/ ├── assets/ │ ├── textures/ │ ├── audio/ │ └── fonts/ ├── autoload/ │ ├── GameManager.gd │ └── SaveManager.gd └── tests/原则是:场景、脚本、贴图按功能模块分组,而不是按资源类型全堆在一起。autoload单例单独放一个目录,避免和普通场景逻辑混在一起。这样迁移一个功能模块时,只需要调整它自己的目录。
3.5 命名规范带来的直接收益
统一命名后,最直接的收益是全局搜索成本降低。比如你要查找“玩家受到伤害”的整个链路,只需要搜索take_damage和health_changed。如果命名混乱,take_damage、getHurt、hurt_me、on_damage同时存在,搜索结果会非常分散,改逻辑时很容易漏掉一处。
另一个隐性收益是 git diff 更清晰。命名一致的代码,多人修改同一文件时冲突概率更低,代码评审也能更快判断变量作用域。
4. 类型检查:让 GDScript 从“动态甜”走向“静态稳”
GDScript 是动态语言,动态类型带来了快速原型开发的便利,但也带来了两个问题:编辑器补全不准确,运行时错误出现得晚。更重要的是,在热循环中大量使用无类型变量,会产生不必要的运行时开销。这一章讲的类型检查,就是同时解决可维护性和性能的手段。
4.1 为什么类型检查能同时提升效率和性能
第一,编辑器补全更准确。当你把变量声明为var player: Player时,在代码编辑器中输入player.,Godot 会自动列出Player脚本里的方法和属性,不需要记忆属性名。
第二,错误提前到编辑器阶段。类型不匹配、访问不存在的属性,很多问题在写代码时就会出现警告甚至报错,而不是等到运行到那一帧才崩溃。
第三,运行期开销更小。GDScript 中不标注类型的变量会被当作 Variant 处理,运行时需要做额外的类型判断和转换。在_process、_physics_process、信号回调这类每帧执行多次的代码里,Variant 开销会被放大。类型标注能让运行方式更直接,虽然不是“一劳永逸”,但几乎没有副作用。
4.2 最基本的静态类型写法
最基础的做法是给变量、函数参数、返回值都加上类型:
var speed: float = 1.0 var max_count: int = 10 var display_name: String = "Player" func take_damage(amount: int) -> void: pass func get_current_health() -> int: return _health如果你不确定类型,可以用:=让编辑器推断,但前提是右侧表达式有明确类型:
var direction := Vector2.RIGHT # 推断为 Vector2 var node := get_node("Sprite2D") # 推断为 Node,实际还需要优化:=推断对局部变量很友好,但节点获取场景最好单独处理,因为get_node()默认返回Node,需要手动缩小类型范围。
4.3 节点获取与类型转换
Godot 4.x 的独特节点访问方式%配合唯一名称可以简化节点路径,但类型依然需要声明。例如场景树里有一个AttackTimer节点,可以这样写:
@onready var attack_timer: Timer = %AttackTimer@onready保证节点就绪后再赋值,%符号按唯一名称查找节点,类型声明为Timer,之后使用attack_timer.start()就不会出现“Node 没有 start 方法”的问题。
处理物理碰撞信号时,信号参数常常是基础类型Node2D,需要转成具体类型才能访问自定义属性:
func _on_hitbox_body_entered(body: Node2D) -> void: var enemy := body as Enemy if enemy == null: return enemy.take_damage(10)用as做类型转换,如果类型不匹配返回null,再加一个空值判断。这样可以避免在运行时出现“调用方法但对象类型不对”的隐性错误。
4.4 数组、字典与泛型类型标注
Godot 4.x 支持对数组声明元素类型:
var enemies: Array[Enemy] = [] var hitboxes: Array[Area2D] = []Array[Enemy]会在运行时检查元素类型,插入错误类型会直接报错。这在维护一份敌人列表时非常有价值,能减少很多“我以为它是 Enemy,结果塞进了其他对象”的问题。
字典类型参数在不同 4.x 版本中支持程度不同,如果你使用的版本还不支持完整的泛型字典,至少写成带注释的普通字典,并确保 key 使用StringName而不是String来减少内存重复。
4.5 警告控制与严格模式
Godot 编辑器可以在项目设置里配置 GDScript 警告等级。对于UNSAFE_*系列的警告,例如不安全的属性访问、不安全的类型转换,可以把它们从“警告”提升为“错误”。项目设置路径通常是:
Project Settings -> Debug -> GDScript如果你明确知道某处访问是安全的,而编辑器无法识别类型,可以使用警告忽略注解:
@warning_ignore("UNSAFE_PROPERTY_ACCESS") var value = some_dynamic_object.attribute但不是所有地方都建议忽略警告。更推荐的做法是先用as把动态对象压成具体类型,让代码在类型层面自然安全。把警告当错误,能让问题在最早阶段暴露,而不是上线后在玩家设备上暴露。
4.6 类型检查的使用边界
类型检查不是越多越好。对于高频调用、对外接口、节点引用、信号回调这些关键位置,类型标注价值最大。但对于临时变量、工具脚本、原型验证代码,不需要强迫自己补全每一个类型,否则写代码的节奏会被打断。
此外,如果使用了第三方插件、热更新方案或高度动态的反射调用,静态类型无法覆盖所有场景。这时候要保持局部动态能力,但在模块边界上做一次类型校验,比如函数入口处判断参数类型是否合法,而不是放任类型错误在内部蔓延。
5. 性能优化:先定位瓶颈,再动手优化
性能优化最忌讳拍脑袋。看到帧率低就认为一定是脚本问题,看到卡顿就猜是渲染问题,往往改了一圈没有效果。Godot 自带性能分析工具,第一步应该是把瓶颈从“猜测”变成“数据”。
5.1 性能监视器与 Profiler 是第一步
打开方式非常直接:在编辑器里运行游戏,然后在菜单栏打开调试 -> 性能分析器。性能分析器会按线程显示不同类型的时间消耗,包括脚本、渲染、物理、导航等。开启录制后,跑一段真实游戏流程,比如打一次 Boss 战、在 NPC 密集区走一圈,然后停下录制,重点看:
- 脚本总耗时和所有函数的调用耗时排序
- 渲染耗时是否始终处于高位
- 物理耗时是否异常
- 帧率图像是否存在明显掉帧区间
至少跑三次,取稳定结果。单次数据没有代表性,第一次进入场景时可能有资源加载峰值,会干扰判断。
5.2 节点引用的缓存
最常踩的性能坑之一是在_process中调用get_node()或find_child()。每次调用都涉及场景树查找,即使结果相同,也会产生查询开销。正确做法是使用@onready在节点就绪时缓存:
# 不推荐:每帧查找字符串节点 func _process(delta: float) -> void: get_node("Label").text = str(get_tree().get_nodes_in_group("enemies").size()) # 推荐:缓存节点引用 @onready var enemy_label: Label = %EnemyLabel func _process(delta: float) -> void: enemy_label.text = str(get_tree().get_nodes_in_group("enemies").size())进一步优化是减少每帧调用get_nodes_in_group()。组查询每次都会重新收集节点。更高效的做法是维护一个敌人计数,在敌人进入组和退出组时更新:
func _on_enemy_spawned(enemy: Enemy) -> void: _enemy_count += 1 enemy.died.connect(_on_enemy_died) func _on_enemy_died() -> void: _enemy_count -= 1这种“缓存 + 事件驱动”的方式不仅减少了每帧查询开销,还让逻辑更清晰。
5.3 对象池:减少高频创建和销毁
子弹、粒子、敌人尸体、掉落物,这些对象如果每帧都在生成和销毁,内存分配压力会直接体现在帧率上。对象池的思路很简单:预先创建一批实例,用的时候从池里取,不用的时候放回池里,而不是销毁。
下面是一个简化的子弹对象池:
class_name BulletPool const BulletScene: PackedScene = preload("res://scenes/bullet.tscn") var _available: Array[Node2D] = [] var _active: Array[Node2D] = [] func acquire() -> Node2D: var bullet: Node2D if _available.is_empty(): bullet = BulletScene.instantiate() else: bullet = _available.pop_back() _active.append(bullet) return bullet func release(bullet: Node2D) -> void: _active.erase(bullet) bullet.get_parent().remove_child(bullet) bullet.visible = false _available.append(bullet)使用时,在玩家射击时调用acquire()取出子弹,子弹命中或飞出屏幕后调用release()放回池中。对象池的收益在高频生成场景下最明显。如果某个对象每分钟只生成几次,就不需要强行做池,简单的实例化和释放即可,过度设计反而增加维护成本。
5.4 渲染与合批优化
当你发现性能分析器里渲染耗时占比很高时,就要检查场景中的绘制元素。常见的优化手段包括:
- 使用图集,把多个小贴图合并到一张大图上,减少材质切换。
- 控制
CanvasItem节点数量,比如大量粒子效果不要创建几百个独立的Sprite2D,可以考虑使用 GPUParticles2D 或自定义_draw()绘制。 - 减少实时阴影。2D 中的
Light2D、3D 中的动态灯光数量都会显著影响渲染开销。 - 3D 场景中尽量使用静态光照和烘焙光照,减少实时光源数量。
- 避免每一帧修改材质参数,材质参数变化会导致引擎难以合并绘制批次。
渲染优化的核心是减少“绘制批次”和“状态切换”,而不是单纯减少贴图数量。
5.5 物理系统优化
Godot 的物理系统在物体数量暴涨时会成为瓶颈。物理优化有几个常见方向:
- 合理设置碰撞层和掩码,不要让没有交互需求的物体互相碰撞检测。
- 静态墙体、地面使用
StaticBody2D,不要用CharacterBody2D或RigidBody2D做静态物体。 - 减少物理查询次数。
PhysicsDirectSpaceState2D的相交查询如果每帧多次调用,会消耗明显。 - 避免在
_physics_process中创建和释放物理体。物理体的生命周期最好在事件中处理,而不是物理步进中。
不要轻易修改默认物理帧率。对大多数 2D 平台游戏来说,默认物理频率已经够用。调得太高会浪费 CPU,调得太低会导致碰撞抖动。
5.6 用状态机代替大段分支
热词里经常出现“状态机”。很多人不理解状态机为什么是性能优化手段,其实它有两个作用:第一减少无效逻辑,第二提高可维护性。
一个常见的反面写法是,在_process里判断一大堆布尔值:
if _is_attacking: # 攻击逻辑 elif _is_running: if _is_jumping: # 跑跳逻辑 elif _is_crouching: # 蹲跑逻辑这种写法的问题在于,每个_process都要检查所有状态,哪怕当前状态只有一个。状态机把这些分支拆成一个个独立状态对象,每一帧只执行当前状态自己的update()。
class_name State extends RefCounted var _actor: Node2D func setup(actor: Node2D) -> void: _actor = actor func enter() -> void: pass func update(delta: float) -> void: pass func exit() -> void: pass然后在状态管理器中维护当前状态:
func change_state(new_state: State) -> void: if _current_state: _current_state.exit() _current_state = new_state _current_state.enter()这样主循环里只需要一行_current_state.update(delta),不再需要几十个布尔分支。状态机本身不是银弹,但它能显著降低“每帧全量判断”的开销,也方便在每个状态内单独做帧率热点分析。
5.7 2D 人物走路模糊的排查
Godot 中“2D 人物走路模糊”是很常见的问题,尤其像素风项目。模糊原因通常不是单一的,按顺序排查:
第一,检查纹理过滤。放大角色贴图后如果边缘发虚,尝试把项目默认纹理过滤从线性(Linear)改为最近邻(Nearest)。这个设置会影响所有 CanvasItem。
第二,检查相机平滑。如果给Camera2D开了position_smoothing,相机在移动时会做插值,角色在屏幕上可能出现轻微拖影或模糊感。像素游戏建议关闭平滑,或者把平滑时间调得非常低。
第三,检查子像素渲染。如果角色坐标不是整数,贴图会落在像素之间,产生模糊。可以在角色移动后对坐标做取整,或者调整项目的像素对齐设置。
第四,检查分辨率缩放。低分辨率画面放大到高分辨率屏幕时,如果拉伸过滤不对,整个画面都会显得糊。需要统一项目的stretch模式和纹理过滤策略。
这些问题的根本原因都是“渲染位置和纹理采样不一致”,排查时先让角色和镜头都静止,看贴图是否清晰;运动时才模糊,就重点查相机和坐标插值。
5.8 数据序列化与多线程
热词里出现了 JSON.stringify 前端性能优化。Godot 项目同样会用到JSON.stringify和JSON.parse_string,比如保存存档、上传分数、做关卡编辑器。如果序列化的数据达到数 MB,并且在主线程同步执行,界面会卡顿。
解决方案是把序列化和文件读写放到后台线程。Godot 4.x 提供了WorkerThreadPool,可以快速提交任务:
# 注意:传入线程的字典建议是副本,避免主线程同时修改 WorkerThreadPool.add_task(_save_data.bind(path, data.duplicate())) func _save_data(path: String, data: Dictionary) -> void: var json_text := JSON.stringify(data) var file := FileAccess.open(path, FileAccess.WRITE) if file: file.store_string(json_text) file.close()多线程的核心原则是:不要在线程里访问Node、不要修改场景树。线程只负责纯数据和文件操作,结果通过信号或主线程轮询返回。
5.9 移动端与手游性能优化要点
手游性能优化和桌面端不完全一样。PC 上 60 帧流畅,低端 Android 设备可能只有 20 帧。常见的手游优化方向:
- 控制渲染分辨率。高分辨率视口在移动端功耗大,可以降低缩放比例或使用动态分辨率。
- 使用纹理压缩。移动端导出时选择适合 GPU 的压缩格式,能明显减少内存和带宽占用。
- 控制同时加载的资源数量。通过
ResourceLoader异步加载,避免场景切换时一次性加载过多资源。 - 减少高分辨率实时特效。屏幕后处理、实时阴影、模糊效果在移动端的开销远高于桌面端。
- 真机测试。PC Profiler 数据只能做参考,移动端功耗和发热会直接影响帧率,必须依赖真机测量。
6. 可落地的性能优化工作流
既然讲性能优化,就需要一套可以重复执行的检查流程,而不是遇到问题临时找原因。
第一步,建立基准场景清单。列出主菜单、战斗场景、NPC 密集区、Boss 战等核心场景,每个场景定义一段固定操作,比如“进入场景后 30 秒内移动并释放三次技能”。
第二步,在 PC 上用性能分析器记录数据。记录脚本耗时、渲染耗时、物理耗时和帧率曲线。
第三步,按数据排序改脚本。先处理 TOP 5 耗时函数,优先做类型标注、节点缓存、减少重复查询。每次只改一个瓶颈,改完立刻复测。
第四步,处理渲染和物理瓶颈。根据分析结果选择图集、合批、减少实时灯光、优化碰撞层。
第五步,在目标真机上复测。手机游戏至少准备一台低配置真机,按同样流程记录帧率。
下面的表格可以当作团队内的优化登记表:
| 阶段 | 关注指标 | 使用工具 | 判断标准 |
|---|---|---|---|
| 1 | 脚本耗时占比 | Profiler | 找到 TOP 5 耗时函数 |
| 2 | 渲染耗时 | Profiler | 是否有明显渲染峰值 |
| 3 | 物理耗时 | Profiler | 物理步进是否超时 |
| 4 | 内存占用 | 系统监视器 | 是否持续上涨不回收 |
| 5 | 真机帧率 | 真机性能工具 | 低配设备是否保持目标帧率 |
优化工作是迭代的。一次只改一个变量,改完马上跑基准场景,而不是一口气改十个地方后再一次性测试。
7. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 角色走路模糊 | 纹理过滤、相机插值、子像素渲染 | 静止和运动时分别观察 | 调整 Nearest 纹理过滤、关闭相机平滑、坐标取整 |
| 帧率低但不知瓶颈在哪 | 没有使用性能分析器 | 打开调试 -> 性能分析器 | 按脚本/渲染/物理耗时排序定位 |
| 加了类型标注后报一堆错 | 节点是动态获取,编译器不确定类型 | 查看报错信息 | 使用as缩小类型或使用@warning_ignore |
| 敌人一多帧率骤降 | 节点数量、物理碰撞、绘制批次过多 | Profiler 对比数量变化 | 对象池、碰撞层、图集合批 |
| 场景切换卡顿 | 同步加载大量资源 | 查看加载耗时 | 使用 ResourceLoader 异步加载 |
| 使用 JSON.stringify 卡顿 | 大数据量在主线程同步序列化 | 函数耗时分析 | 使用 WorkerThreadPool 后台序列化 |
| 真机帧率不稳定 | 渲染设置超出设备能力 | 真机 Profiler | 降低分辨率、压缩纹理、减少后处理 |
| 项目打开很慢 | 大量资源重导入、复杂 autoload | 查看编辑器日志 | 控制导入资源、简化启动流程 |
遇到问题时,先记录现象,再复现,最后定位。直接改代码而不看数据,效率最低。
8. 最佳实践与使用建议
建立一个团队级的最小规范,不用覆盖所有细节,但至少明确:文件名与class_name对应、变量函数用 snake_case、信号用过去时、私有成员加下划线、目录按功能分组。这样新人入职后能快速进入状态。
类型标注优先覆盖以下位置:
@export变量@onready节点引用- 函数参数和返回值
- 信号回调参数
- 高频循环中的局部变量
性能优化前先建基准,优化后复测对比。不要用“感觉变流畅了”替代数据记录。
不要依赖系统级批处理来优化游戏性能。关闭后台服务、改电源计划这些措施,对 Godot 项目的脚本、渲染、物理瓶颈没有帮助,反而可能引入环境差异。把时间花在项目内部,收益会高得多。
涉及资源合规时,确认美术、音频、字体资源的授权范围。商用项目更要保留授权记录。
发布或商用前,要按真机数据复核真实场景。PC 上流畅不代表手机流畅,开发机上流畅不代表玩家设备流畅。
9. 总结与下一步
如果只能从这篇文章里带走一件事情,我希望是:先统一命名、补上类型、再用 Profiler 定位性能问题,而不是先写一堆“优化脚本”去碰运气。
最先应该做的是挑一个你正在维护的 Godot 项目,找出项目里最大的 10 个脚本,把命名规范补起来,给@export、@onready和函数参数补上类型标注,然后跑一次性能分析器,记下耗时 TOP 10 的函数。
最容易踩的坑是在_process里反复get_node()、每帧创建对象、同时改多个优化点后无法判断哪个有效。
后续可以继续扩展的方向包括:把状态机引入现有的敌人 AI、为高频弹幕类游戏接入对象池、对移动端做一轮完整的纹理压缩和真机性能测试。每一步都带来增量收益,但这些收益的前提,是你能用数据看到它们。