移动/游戏
在 UE 中做开放世界/关卡流式加载与关卡切换时…
当需要为 UE 项目实现大地图流式加载、子关卡动态加载/卸载、运行时关卡实例化或多人在关卡间切换时,按此流程选型与落地。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 UE 中做开放世界/关卡流式加载与关通用
解决思路
- 先阅读 `.agents/ue-project-context.md`,确认引擎版本(World Partition 仅 UE5,子关卡流送 UE4/UE5 通用)、构建目标(专用服务器无渲染驱动流送,必须服务端安全)、世界大小、是否多人(无缝旅行与每玩家流送半径)。
- 在给出方案前先确认:世界大小与类型(开放世界/离散关卡/枢纽辐射)、是否多人及是否需要每玩家流送半径、流送控制方式(代码显式控制还是邻近驱动)、关卡旅行类型(非无缝/无缝/不旅行)、跨关卡需保留的持久数据(玩家状态、背包、会话状态)。
- World Partition 启用:通过 Level 菜单 **World -> World Partition -> Convert Level**;启用后所有 Actor 由 WP 网格管理,不能再有传统子关卡;协作编辑使用 One File Per Actor (OFPA),每个 Actor 存为 `__ExternalActors__` 下的独立 `.uasset`。
- 运行时数据层:用 `UDataLayerManager::GetDataLayerManager(GetWorld())` 获取管理器,通过 `SetDataLayerRuntimeState(DungeonLayer, EDataLayerRuntimeState::Activated/Unloaded)` 在运行时加载/卸载数据层;状态有 `Unloaded`(未加载不可见)、`Loaded`(已加载不可见,可预热)、`Activated`(已加载且可见)。
- 流送源:每个 PlayerController 默认是流送源;自定义源(过场相机、AI 导演)实现 `IWorldPartitionStreamingSourceProvider`。
- HLOD:在 World Partition 编辑器中配置 HLOD 层,发布前执行 **Build -> Build World Partition HLODs**;没有 HLOD 时超出流送半径的内容会直接消失。
- 将子关卡转换为 World Partition:使用 **Tools -> World Partition -> Convert Level**,Actor 迁入持久关卡由 WP 管理;转换前审计跨关卡引用,对已转换 Actor 的硬引用会失效。
- World Partition 多人:每个 PlayerController 作为可配置半径的流送源;服务端按服务端源流送,客户端通过 `AServerStreamingLevelsVisibility` 接收可见性更新;专用服务器上渲染驱动流送不适用,仅由服务端源驱动;流送半径在 WP 编辑器 UI 中按分区配置(`URuntimePartition` 上的 `LoadingRange`),不是通过 ini。
- 子关卡流送状态机(`LevelStreaming.h`):`Removed -> Unloaded -> Loading -> LoadedNotVisible -> MakingVisible -> LoadedVisible -> MakingInvisible -> LoadedNotVisible`,失败进入 `FailedToLoad`(查日志,关卡资源缺失或损坏);用 `StreamingLevel->GetLevelStreamingState()` 查询并按 `ELevelStreamingState` 分支处理。
- 异步流送用 `UGameplayStatics::LoadStreamLevel` / `UnloadStreamLevel`,构造 `FLatentActionInfo`(设置 `CallbackTarget`、`ExecutionFunction`、`Linkage`、`UUID`),`bMakeVisibleAfterLoad=true`、`bShouldBlockOnLoad=false` 保持异步;卸载时 `bShouldBlockOnUnload=false`;打包安全优先用 `LoadStreamLevelBySoftObjectPtr`。
- 运行时关卡实例用 `ULevelStreamingDynamic::LoadLevelInstance` 在同一关卡包的不同变换处多次加载(程序化地牢、模块化建筑、实例化房间);成功后绑定 `OnLevelShown`/`OnLevelHidden` 委托,卸载时调用 `SetShouldBeLoaded(false)`、`SetShouldBeVisible(false)`、`SetIsRequestingUnloadAndRemoval(true)`。
- 联机时用 `ULevelStreamingDynamic::FLoadLevelInstanceParams` 并设置 `OptionalLevelNameOverride`(服务端与所有客户端使用同一 FString 名称),否则每个进程自动生成唯一名称导致跨连接不匹配;可设 `bInitiallyVisible`。
- 绑定委托:`OnLevelLoaded`、`OnLevelUnloaded`、`OnLevelShown`、`OnLevelHidden` 均为 `BlueprintAssignable`,用 `AddDynamic` 绑定。
- 流送体积:用 `ALevelStreamingVolume` 根据玩家相机进出体积自动控制子关卡加载;`EStreamingVolumeUsage` 取值 `SVB_Loading`(加载不可见)、`SVB_LoadingAndVisibility`(加载并可见,最常用)、`SVB_VisibilityBlockingOnLoad`(进入时强制阻塞加载)、`SVB_BlockingOnLoad`(阻塞关联关卡加载)、`SVB_LoadingNotVisible`(加载保持不可见,预热);体积通过子关卡的 `EditorStreamingVolumes` 数组关联;要纯代码控制时设 `ULevelStreaming::bDisableDistanceStreaming = true`。
- 手动可见性控制:遍历 `GetWorld()->GetStreamingLevels()`,按 `GetWorldAssetPackageFName()` 匹配后调用 `SetShouldBeLoaded(true)` / `SetShouldBeVisible(true)`;需要强制完成时用 `UGameplayStatics::FlushLevelStreaming(this)`(阻塞,慎用)。
- Level Instance:用 `ALevelInstance` 在编辑器中把关卡作为可复用块放置,内部 Actor 作为整体编辑;性能优化用右键 **Pack Level Actor** 将实例网格合并为单个静态网格;UE5.1+ 支持每个实例覆盖单个 Actor 属性(材质、游戏数值),覆盖值在 cook 时烘焙进打包关卡数据。
- 非无缝旅行:用 `UGameplayStatics::OpenLevel(this, FName("/Game/Maps/MainMenu"), true)`,可附加选项字符串如 `?Difficulty=Hard`;打包安全用 `OpenLevelBySoftObjectPtr`;多人由服务端调用 `GetWorld()->ServerTravel(TEXT("/Game/Maps/Level02?listen"), false)`,所有客户端跟随。
- 无缝旅行:`UWorld::SeamlessTravel(InURL, bAbsolute)` 通过过渡(中点)地图后台加载目标地图,客户端保持连接;用 `IsInSeamlessTravel()` 查询、`SetSeamlessTravelMidpointPause(true)` 在中点暂停以预加载。
- 无缝旅行配置:在 `AGameModeBase` 构造函数中设 `bUseSeamlessTravel = true`(该变量已在基类声明,不要重复声明);在 `DefaultEngine.ini` 的 `[/Script/Engine.GameMapsSettings]` 下设 `TransitionMap=/Game/Maps/Transition`;重写 `GetSeamlessTravelActorList`(`bToTransition=false` 表示正前往目标地图,把需存活的 Actor 加入 `ActorList`)、`PostSeamlessTravel`(目标地图加载后重新初始化系统)、`HandleSeamlessTravelPlayer`(重新附身玩家并恢复状态)。
- 客户端发起的旅行:在 PlayerController 中调用 `APlayerController::ClientTravel(URL, ETravelType::TRAVEL_Absolute)`。
- 每世界单例逻辑用 `UWorldSubsystem`(随世界销毁,包括关卡旅行时);重写 `PostInitialize`、`OnWorldBeginPlay`、`PreDeinitialize`、`ShouldCreateSubsystem`;用 `GetWorld()->GetSubsystem<UMyStreamingManager>()` 访问。
- 需要每帧更新(距离检测、区域检测)时继承 `UTickableWorldSubsystem`,必须调用 `Super::Initialize` 和 `Super::Deinitialize` 以启用/禁用 tick,并实现 `GetStatId` 返回 `RETURN_QUICK_DECLARE_CYCLE_STAT`。
- 跨关卡持久数据选型:`UGameInstance`(整个应用会话,跨关卡玩家状态/会话配置)、`UGameInstanceSubsystem`(整个应用会话的服务)、无缝旅行 Actor 列表(仅过渡期,物理跨越的 GameState/管理器)、`USaveGame` + `SaveGameToSlot`(磁盘持久,长期存档与进度)、`UWorldSubsystem`(每世界缓存,旅行清除前在 `Deinitialize()` 中把数据推给 `UGameInstance`)。
- 避免常见错误:不要对多个子关卡设 `bShouldBlockOnLoad = true`(造成卡顿,仅在加载画面后阻塞);避免流送体积重叠导致反复卸载/重载,用 `MinTimeBetweenVolumeUnloadRequests` 加冷却;`bUseSeamlessTravel` 为 true 但未设过渡地图会静默回退为非无缝;跨关卡边界一律用 `TSoftObjectPtr`/`TSoftClassPtr` 而非硬引用;`LoadLevelInstance` 联机时必须提供 `OptionalLevelNameOverride`;专用服务器上必须显式添加服务端流送源;不要直接修改 `UWorld::StreamingLevels`,改用 `AddStreamingLevels`、`AddUniqueStreamingLevels`、`RemoveStreamingLevels`;`UTickableWorldSubsystem` 不要忘记调用 `Super::Initialize`/`Super::Deinitialize`。
适用边界 · 注意事项
- 项目使用 UE4 且不需要开放世界时,不要引入 World Partition(仅 UE5 可用),应使用传统子关卡流送。
- 专用服务器上不要依赖渲染驱动的流送(如流送体积、相机邻近),必须显式添加服务端流送源。
- 不要在游戏运行中频繁调用 `FlushLevelStreaming` 或大量阻塞加载,除非处于加载画面之后。