第四章:内存与加载优化
内存和加载优化,说白了就是让游戏跑得更顺、加载更快。我见过太多项目,画面挺漂亮,一进场景就卡成幻灯片,或者加载等得让人想睡觉。这些问题,十有八九出在内存管理和资源加载策略上。
4.1 内存Profiler:找到吃内存的元凶
做内存优化,第一步不是瞎猜,而是用工具看。Unreal 自带的 Memory Profiler 就是你的好帮手。
4.1.1 怎么打开 Memory Profiler
在编辑器里,打开 Session Frontend(窗口 → 开发者工具 → Session Frontend),切到 Profiler 标签页。连上你的游戏进程,点一下「Memory」按钮,就开始记录了。
我个人习惯是跑一段典型的游戏流程——比如进主城、打一波怪、再切地图。这样能抓到内存波动的全貌。
4.1.2 看懂那些数据
Profiler 会给你一堆数字,别慌。重点关注这几个:
- Total Physical:实际占用的物理内存。这个超了,游戏就崩。
- Asset Registry:资源注册表。我遇到过项目里这个飙到 200MB+,一查发现是美术导入了上千个没用的贴图。
- RHI:渲染相关的内存。贴图、材质、Mesh 都在这里。
- GC:垃圾回收占用的临时内存。
关键指标:如果 Total Physical 接近平台限制(比如 PS4 的 5GB),你就该紧张了。
4.1.3 定位内存泄漏
内存泄漏怎么找?很简单。在 Profiler 里反复执行同一个操作(比如打开关闭背包),看内存是不是只涨不跌。如果是,恭喜你,找到泄漏点了。
我曾经排查过一个项目,每次打开地图界面内存涨 50MB,关掉也不降。最后发现是 UMG 里绑定了大量动态材质实例,没及时释放。
小技巧:用 obj list 命令在控制台看当前所有 UObject 实例。按数量排序,最多的那个往往就是问题所在。
4.2 资源加载策略:别一股脑全塞进来
很多新手喜欢在关卡里把所有资源都拖进去,结果加载慢、内存爆。资源加载,讲究的是「按需加载,用完就扔」。
4.2.1 同步加载 vs 异步加载
同步加载(LoadObject、StaticLoadObject)会卡主线程。你想想看,玩家正跑着呢,突然卡一下,体验多差。
异步加载(FStreamableManager、AsyncLoad)才是正道。它不会阻塞主线程,加载完了回调通知你。
// 异步加载示例
FStreamableManager& StreamableManager = UAssetManager::Get().GetStreamableManager();
TSoftObjectPtr<UTexture2D> TexturePtr = ...;
StreamableManager.RequestAsyncLoad(
TexturePtr.ToSoftObjectPath(),
FStreamableDelegate::CreateLambda([this]() {
// 加载完成后的处理
MyTexture = TexturePtr.Get();
})
);
4.2.2 软引用 vs 硬引用
硬引用(UPROPERTY() 直接指向资源)会让资源在加载时就跟着一起加载。软引用(TSoftObjectPtr)只存路径,不强制加载。
我建议:能用软引用就别用硬引用。尤其是那些只在特定条件下才用到的资源,比如某个 Boss 的专属特效。
| 引用类型 | 加载时机 | 内存占用 | 适用场景 |
|---|---|---|---|
| 硬引用 | 随父对象一起加载 | 始终占用 | 必须常驻的资源(如 UI 字体) |
| 软引用 | 手动触发加载 | 按需占用 | 非关键资源(如过场动画) |
4.2.3 资源包(Pak)策略
把资源打成 Pak 文件,可以按需下载和加载。我做过一个项目,把新手村、主城、副本分别打成独立的 Pak。玩家进哪个区域,就只加载对应的 Pak。
这样做的效果很明显:初始包体从 2GB 降到了 500MB,加载时间从 40 秒缩到了 8 秒。
注意:Pak 文件太多也会增加 IO 开销。一般建议控制在 10-20 个以内,每个 Pak 不要小于 50MB。
4.3 Level Streaming:让世界无缝加载
开放世界游戏不可能一次性加载整个地图。Level Streaming 就是用来做这个的——把世界切成小块,玩家走到哪就加载哪。
4.3.1 设置 Level Streaming
在 World Settings 里,把关卡分成 Persistent Level(常驻)和 Streaming Levels(动态加载)。每个 Streaming Level 可以设置加载范围(Distance Based)或触发条件(Volume Based)。
我个人习惯用 Distance Based,配合 Level Streaming Distance 参数调优。比如一个 1km x 1km 的地图,每个 Streaming Level 覆盖 200m 范围,玩家走到 300m 外就开始预加载。
4.3.2 避免加载卡顿
Level Streaming 最怕的就是加载瞬间卡一下。原因通常是:
- 单个 Level 太大(超过 100MB)
- 加载时触发了大量蓝图逻辑
- 磁盘 IO 跟不上
解决办法:
- 把大 Level 拆成更小的块
- 用
OnLevelLoaded事件延迟执行复杂逻辑 - 考虑用
Async Loading Thread分担 IO 压力
// 延迟执行加载后的逻辑
void AMyGameMode::OnLevelLoaded()
{
// 不要在这里直接 Spawn 大量 Actor
// 用定时器延迟几帧
FTimerHandle Handle;
GetWorld()->GetTimerManager().SetTimer(Handle, this, &AMyGameMode::SpawnActors, 0.1f, false);
}
4.3.3 卸载策略
加载重要,卸载同样重要。不用的 Level 要及时卸载,否则内存会慢慢涨上去。
我建议设置 Unload Distance 比 Load Distance 大 1.5 倍。比如加载距离 300m,卸载距离就设 450m。这样玩家来回走动时,不会频繁加载卸载。
经验之谈:用 Level Streaming Volume 配合触发器,可以实现更精细的控制。比如玩家进入某个山洞,才加载洞内的 Level。
4.4 GC 优化:别让垃圾回收拖后腿
Unreal 的垃圾回收(GC)是个好东西,但用不好就是灾难。我见过 GC 触发时游戏直接卡 1 秒的案例。
4.4.1 GC 是怎么工作的
简单说,GC 会遍历所有 UObject,标记还在用的,清理没用的。这个过程是单线程的,所以会卡主线程。
GC 触发条件:
- 每 60 秒自动触发一次
- 手动调用
ForceGarbageCollection - 内存压力过大时自动触发
4.4.2 减少 GC 开销
核心思路就一条:减少需要 GC 的对象数量。
具体做法:
- 用对象池代替频繁创建销毁 Actor
- 避免在 Tick 里创建 UObject
- 用
AddToRoot保护常驻对象不被 GC - 合理设置
GC MaxObjectsPerFrame(默认 250,可以适当调低)
// 对象池示例
class FObjectPool
{
TArray<AActor*> Pool;
AActor* GetFromPool()
{
if (Pool.Num() > 0)
{
AActor* Actor = Pool.Pop();
Actor->SetActorHiddenInGame(false);
Actor->SetActorEnableCollision(true);
return Actor;
}
// 没有可用对象才创建
return GetWorld()->SpawnActor<AActor>(...);
}
void ReturnToPool(AActor* Actor)
{
Actor->SetActorHiddenInGame(true);
Actor->SetActorEnableCollision(false);
Pool.Add(Actor);
}
};
4.4.3 手动控制 GC 时机
有时候自动 GC 的时机不对。比如玩家正在打 Boss,突然来一次 GC,画面卡一下,体验极差。
我建议在关键战斗前手动触发一次 GC:
// 进入战斗前清理内存
void AMyGameMode::OnBossFightStart()
{
// 强制 GC,确保战斗时不会突然触发
GetWorld()->ForceGarbageCollection(true);
// 然后才开始加载 Boss 相关资源
LoadBossAssets();
}
警告:不要频繁调用 ForceGarbageCollection。每次 GC 都有开销,频繁触发反而更卡。一般只在场景切换或关键节点调用。
4.5 综合优化案例
最后分享一个我实际做过的优化案例。一个开放世界 RPG,主城有 200+ NPC,每个 NPC 都有自己的对话 UI、寻路网格、动画蓝图。
问题:进主城加载 30 秒,内存占用 4.2GB(平台限制 4GB),GC 每 30 秒卡一次。
优化方案:
- 把 NPC 分成 5 个 Streaming Level,按区域加载
- 对话 UI 用软引用,玩家靠近 NPC 时才异步加载
- NPC 动画蓝图用对象池,复用同一个蓝图实例
- 设置 GC 间隔为 120 秒,在玩家出城时手动触发
结果:加载时间降到 8 秒,内存占用 3.1GB,GC 卡顿完全消失。
核心原则:内存优化不是一锤子买卖,而是贯穿整个开发周期的持续工作。每个资源、每个加载策略、每个 GC 设置,都值得你反复推敲。
嗯,这一章的内容就到这里。内存和加载优化,说白了就是「少加载、晚加载、及时卸载」。记住这九个字,你的游戏性能不会差到哪去。