加载卡顿通常不是 Godot 慢,而是策略太随意
Godot 项目早期经常直接 preload() 场景,或者在按钮点击时 load() 一个资源。原型阶段这样没问题,资源少、场景小、设备强,感受不到明显成本。项目做大后,主城切战斗、打开商城、进入角色预览都会加载大量 PackedScene、贴图、材质、音频和脚本。如果加载策略没有统一设计,玩家看到的就是黑屏、卡顿、按钮无响应。
Godot 的资源系统给了不少选择:preload 编译期绑定,load 同步加载,ResourceLoader 可以做线程加载,PackedScene 可以实例化场景,Resource 可以复用配置。问题不是选哪个 API,而是你是否知道资源什么时候必须在内存里,什么时候可以后台准备,什么时候用完必须释放。
flowchart TD
A[玩法/页面请求] --> B[资源请求描述]
B --> C{资源是否已缓存?}
C -->|是| D[返回 Resource/PackedScene]
C -->|否| E{是否阻塞关键路径?}
E -->|是| F[显示加载页并高优先级加载]
E -->|否| G[后台异步加载]
F --> H[实例化/绑定]
G --> H
H --> I[引用计数/作用域管理]
I --> J[释放或保留缓存]
preload 适合稳定依赖,不适合一切
preload() 的好处是路径在脚本加载时就被解析,运行时访问快,打包也更明确。它适合脚本强依赖的小资源,比如一个角色脚本固定要用的状态图标、一个组件固定实例化的子场景。缺点是它会把资源生命周期绑到脚本上。脚本常驻时,资源也可能常驻。
如果把大型关卡、高清贴图、所有怪物场景都写成 preload,启动或进入某个主脚本时就会提前占用内存。你以为是在优化加载,实际可能让首屏更慢、内存更高。尤其移动端,过度 preload 会让低端设备更早触发系统回收或崩溃。
团队可以约定:小型、稳定、必需的资源允许 preload;大型、可选、跨页面的资源走资源服务;活动资源、DLC、语言包、皮肤预览绝不散落 preload。这个约定比事后查内存泄漏更有效。
PackedScene 是蓝图,不是实例
Godot 的 .tscn 加载后得到 PackedScene,调用 instantiate() 才得到 Node 实例。很多性能问题发生在实例化阶段,而不是文件读取阶段。一个复杂场景里可能有大量子节点、脚本 _ready()、材质初始化、碰撞体和动画树。你提前加载 PackedScene,但进入战斗时一次性实例化 50 个敌人,仍然会卡。
所以资源预热要分两层:加载资源和预实例化对象。投射物、伤害数字、怪物小兵这类高频对象,可以用对象池提前实例化一部分。大型 Boss 可以提前加载 PackedScene,进入演出前再实例化,并把重逻辑延后到激活时。
实例化后也要注意 _ready()。不要在 _ready() 里做重型查询、同步读取文件、扫描整个场景树。_ready() 应该只完成节点绑定和轻量初始化,重型准备交给显式 setup(),由上层控制时机。
异步加载要配合过渡体验
Godot 的线程加载能减少主线程卡顿,但不能自动让体验变好。玩家需要知道正在发生什么:进度、当前阶段、是否可取消、失败后怎么办。场景路由器应该把异步加载和过渡 UI 结合起来,而不是业务页面自己开线程。
进入战斗可以分阶段:先加载战斗场景 PackedScene,再加载角色和怪物资源,再预实例化对象池,再切换显示。每个阶段可以更新进度。进度不一定精确到百分比,但要避免长时间停在同一个数字。
异步加载完成后,最终实例化和加入场景树仍要回到主线程。这里可能产生短卡顿。可以把实例化分帧处理,或者在加载页遮罩下做。关键是不要让玩家在可操作状态里遇到突然掉帧。
缓存要有作用域
资源缓存不能只有一个全局字典。不同资源有不同作用域:全局常驻、当前场景、当前活动、当前预览页面、临时过渡。作用域结束时,缓存应该释放引用,让 Godot 能回收资源。
例如 UI 通用图标可以常驻,当前关卡怪物资源属于关卡作用域,商城试穿的皮肤属于预览作用域。玩家关闭商城后,预览皮肤可以延迟几秒释放,避免快速重开又加载,但不能永久留着。
缓存释放要可观察。调试面板里至少显示每个作用域的资源数量、估算大小、引用原因。Godot 的引用计数有时会因为节点仍持有 Resource 而无法释放,调试信息能帮助定位。
失败路径要设计
资源加载会失败:文件缺失、热更新未完成、哈希不对、设备内存不足、路径配置错误。客户端不能只在控制台报错。关键资源失败时,加载页要显示可重试或修复;非关键资源失败时,要有占位资源和上报。
Godot 项目里路径字符串很容易写错。建议资源请求不要散落字符串,而是通过资源表或生成的常量访问。构建前扫描引用,发现缺失路径直接失败。运行时再发现缺资源,成本已经太高。
小结
Godot 的资源加载要从“API 调用”提升到“生命周期管理”。preload 用于稳定小依赖,PackedScene 加载和实例化分开考虑,异步加载接入场景路由和过渡 UI,缓存按作用域释放,失败路径可恢复。这样项目变大后,加载体验仍然可控。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
我会在项目里给资源服务加一条日志:每次加载记录请求者、路径、资源类型、耗时、是否命中缓存和所属作用域。几天后你会很清楚哪些页面在乱加载,哪些资源应该预热,哪些缓存从来没有释放。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。