关于 ue-world-level-streaming
ue-world-level-streaming 是面向 UE 5.8 的世界构成与运行时关卡管理开发技能,帮助开发者选择和实现流送方案。内容涵盖 World Partition、数据层、子关卡及地图切换。
主要功能
- 01UE 5.8 流送 API 指引
- 02World Partition 与流送源
- 03运行时数据层状态控制
- 04关卡实例与打包 Actor
- 05子关卡加载与可见性
- 06地图切换与 HLOD
适用场景
- 01为大型开放世界流送单元格
- 02用数据层切换可选内容
- 03在运行时加载可复用关卡片段
- 04管理区域可见性或地图切换
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ quodsoler/ unreal-engine-skills --skill ue-world-level-streaming安装后在 Claude Code 中输入/ue-world-level-streaming确认已加载。
附带文件references/streaming-patterns.md
详细介绍
UE 世界与关卡流送
目标引擎:UE 5.8。以下 API 已依据 5.8 版头文件验证;旧版形式列于“已弃用——请勿使用”下。
本技能介绍运行时 UWorld 的组成与切换方式:World Partition 网格、数据层、HLOD、关卡实例、传统子关卡流送、流送体积和地图旅行。这里的所有内容都位于 Engine 模块(Engine/Source/Runtime/Engine),因此在 PublicDependencyModuleNames 中添加 Core、CoreUObject 和 Engine 即可。唯一的例外是 Level Streaming Persistence 插件,它会添加 LevelStreamingPersistence 模块。
如需查看各游戏类型(开放世界、中心辐射式、程序化、章节式、体积驱动、专用服务器)的端到端配置,请参阅流送模式。
上下文
如果 .agents/ue-project-context.md 存在,请阅读该文件(模块名称、约定、已启用插件、GAS/网络设置)。如果文件不存在,也请继续。
根据请求和代码库确定相关领域。只有在两种合理解读会导致不同代码时才提问。
| 请求涉及…… | 请转到 |
|---|---|
| 在 World Partition 和子关卡之间进行选择 | 选择流送模型 |
| 网格流送、流送源、阻塞加载、CVar | World Partition 运行时 |
| 在分区世界内启用或禁用内容 | 数据层 |
| 在编辑器中放置的可复用关卡块、打包 Actor | 关卡实例 |
LoadStreamLevel、ULevelStreaming、体积、运行时实例化 | 子关卡流送 |
OpenLevel、ServerTravel、ClientTravel、无缝旅行 | 关卡旅行 |
| 已流送出的单元格的远距离代理 | HLOD |
| 在流送周期中保留 Actor 变更 | 关卡流送持久化 |
| 每个 World 的管理器、必须在旅行后保留的状态 | World 子系统与跨关卡状态 |
选择流送模型
| 情况 | 使用 | 原因 |
|---|---|---|
| 一张大型连续地图 | World Partition 网格 | 自动划分空间单元格,无需手动管理子关卡 |
| 分区地图中的可选内容 | 数据层 | 通过 EDataLayerRuntimeState 切换,无需旅行 |
| 在编辑器中多次放置同一关卡资源 | ALevelInstance / APackedLevelActor | 作为一个整体进行编辑,并打包为实例化网格 |
| 在运行时多次生成同一关卡包 | ULevelStreamingDynamic::LoadLevelInstance | 为每个实例创建名称唯一的包 |
| 非分区地图中由手工制作的离散区域 | ULevelStreaming 子关卡 | 对每个区域进行显式加载和可见性控制 |
| 非分区地图中由距离驱动的室内区域 | ALevelStreamingVolume | 根据摄像机位置驱动,无需游戏逻辑代码 |
| 完全不同的地图 | OpenLevel / ServerTravel | 拆除并重建 World |
World Partition 和子关卡流送不能同时用于同一个持久关卡。UWorld::IsPartitionedWorld()(Engine/World.h:2968)可报告正在使用哪一种;AWorldSettings::GetWorldPartition()(GameFramework/WorldSettings.h:837)会返回分区对象(WorldPartition 成员受保护)。
World Partition 运行时
UWorldPartition(WorldPartition/WorldPartition.h)负责管理运行时哈希,将 Actor 划分为单元格。UWorldPartitionSubsystem 是每个 World 的入口点,也是一个 UTickableWorldSubsystem。
#include "WorldPartition/WorldPartition.h"
#include "WorldPartition/WorldPartitionSubsystem.h"
UWorldPartitionSubsystem* Subsystem = GetWorld()->GetSubsystem<UWorldPartitionSubsystem>();
if (Subsystem && Subsystem->IsAllStreamingCompleted())
{
// Every cell has finished loading, adding to or removing from the world.
}
Subsystem->ForEachWorldPartition([](UWorldPartition* Partition)
{
return Partition->IsStreamingEnabled(); // return false to stop iterating
});
UWorldPartitionRuntimeCell 是哈希生成的单元。要查询任意点的就绪状态,请使用查询源,而不是轮询 Actor:
#include "WorldPartition/WorldPartitionRuntimeCell.h"
#include "WorldPartition/WorldPartitionStreamingSource.h"
TArray<FWorldPartitionStreamingQuerySource> QuerySources;
QuerySources.Emplace(TargetLocation); // FVector
const bool bReady = Subsystem->IsStreamingCompleted(
EWorldPartitionRuntimeCellState::Activated, QuerySources, /*bExactState=*/false);
EWorldPartitionRuntimeCellState 的状态为 Unloaded、Loaded 和 Activated(WorldPartitionRuntimeCell.h:202)。
流式传输源
任何对象都可以通过实现 IWorldPartitionStreamingSourceProvider(位于 WorldPartitionStreamingSource.h 中的普通接口结构体)并向子系统注册来驱动流式传输。对于常见的“由此 Actor 拉入单元格”场景,请改为添加 UWorldPartitionStreamingSourceComponent——它已经实现了该提供程序并会自行注册。
需要重写的两个虚函数是 virtual bool GetStreamingSources(TArray<FWorldPartitionStreamingSource>& StreamingSources) const 和 virtual const UObject* GetStreamingSourceOwner() const。在 BeginPlay 中调用 UWorldPartitionSubsystem::RegisterStreamingSourceProvider(this) 进行注册,并在 EndPlay 中调用 UnregisterStreamingSourceProvider(this) 释放注册。完整的 Actor 实现请参见流式传输模式。
EStreamingSourceTargetState:Loaded(已载入内存,但未添加到世界中)或Activated。EStreamingSourcePriority:Highest、High、Normal、Low、Lowest、Default(等同于Normal)。TargetGrids和EStreamingSourceTargetBehavior(Include/Exclude)可将源限制在指定的运行时网格中。bBlockOnSlowLoading可让源在流式传输落后时使用引擎的阻塞路径。对于外观效果源,请保持其为 false。UWorldPartitionSubsystem::GetStreamingSources(const UWorldPartition*, TArray<FWorldPartitionStreamingSource>&)可读取当前已注册的所有流式传输源。
运行时 CVar
| CVar | 效果 |
|---|---|
wp.Runtime.EnableServerStreaming | 在专用服务器和监听服务器上启用服务器端单元格流式传输 |
wp.Runtime.EnableServerStreamingOut | 允许服务器将单元格流出 |
wp.Runtime.BlockOnSlowStreaming | 当阻塞源落后时阻塞游戏线程 |
wp.Runtime.MaxLoadingStreamingCells | 限制并发载入的单元格数量 |
wp.Runtime.OverrideRuntimeLoadingRange | 覆盖网格的载入范围以进行测试 |
wp.Runtime.ToggleDrawRuntimeHash2D | 在屏幕上绘制单元格网格 |
wp.Runtime.DumpStreamingSources | 记录每个已注册的流式传输源 |
wp.Runtime.HLOD | 在运行时启用和禁用 HLOD 代理 |
数据层
Actor 通过 AActor::DataLayerAssets(TArray<TSoftObjectPtr<UDataLayerAsset>>,GameFramework/Actor.h:1123)引用数据层。UDataLayerAsset 是创作时使用的资源,UDataLayerInstance 是其在各个世界中的实例,而 UDataLayerManager 是运行时访问器,可从分区世界内的任何对象访问。
// MyDataLayerController.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyDataLayerController.generated.h"
class UDataLayerAsset;
UCLASS()
class MYGAME_API AMyDataLayerController : public AActor
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "MyGame|Streaming")
void SetDungeonActive(bool bActive);
UFUNCTION(BlueprintCallable, Category = "MyGame|Streaming")
bool IsDungeonActivated() const;
protected:
UPROPERTY(EditAnywhere, Category = "MyGame|Streaming")
TObjectPtr<const UDataLayerAsset> DungeonLayer;
};
// MyDataLayerController.cpp
#include "MyDataLayerController.h"
#include "WorldPartition/DataLayer/DataLayerAsset.h"
#include "WorldPartition/DataLayer/DataLayerInstance.h"
#include "WorldPartition/DataLayer/DataLayerManager.h"
void AMyDataLayerController::SetDungeonActive(bool bActive)
{
UDataLayerManager* Manager = UDataLayerManager::GetDataLayerManager(this);
if (!Manager || !DungeonLayer)
{
return;
}
const EDataLayerRuntimeState NewState =
bActive ? EDataLayerRuntimeState::Activated : EDataLayerRuntimeState::Unloaded;
Manager->SetDataLayerRuntimeState(DungeonLayer, NewState, /*bInIsRecursive=*/true);
}
bool AMyDataLayerController::IsDungeonActivated() const
{
UDataLayerManager* Manager = UDataLayerManager::GetDataLayerManager(this);
if (!Manager || !DungeonLayer)
{
return false;
}
const UDataLayerInstance* Instance = Manager->GetDataLayerInstanceFromAsset(DungeonLayer);
return Instance && Manager->GetDataLayerInstanceRuntimeState(Instance) == EDataLayerRuntimeState::Activated;
}
EDataLayerRuntimeState(DataLayerInstance.h:23):Unloaded、Loaded(已载入内存但不可见——可用于预热)和 Activated(已载入且可见)。
管理器的其他入口:SetDataLayerInstanceRuntimeState(const UDataLayerInstance*, EDataLayerRuntimeState, bool bInIsRecursive)、GetDataLayerInstanceEffectiveRuntimeState、GetDataLayerInstances()、GetDataLayerInstanceFromName(const FName&),以及 BlueprintAssignable 委托 OnDataLayerInstanceRuntimeStateChanged。遍历每个实例:
Manager->ForEachDataLayerInstance([](UDataLayerInstance* Instance)
{
return Instance->IsRuntime(); // return false to stop early
});
**权限:**没有加载筛选器的运行时数据层只会响应服务器上进行的状态更改,产生的状态会进行复制。客户端专用层必须在客户端更改,服务器专用层必须在服务器更改。旧的世界子系统访问器仍作为已弃用的兼容接口存在——请参见已弃用——请勿使用;所有操作都应通过 UDataLayerManager 进行。
关卡实例
ALevelInstance(LevelInstance/LevelInstanceActor.h)将 .umap 作为可复用区块放置,并实现 ILevelInstanceInterface。ULevelInstanceSubsystem 是用于追踪这些实例的 UWorldSubsystem。
#include "Engine/World.h"
#include "LevelInstance/LevelInstanceActor.h"
#include "LevelInstance/LevelInstanceSubsystem.h"
void AMyLevelInstanceDriver::ShowRoom(ALevelInstance* RoomInstance)
{
if (!RoomInstance)
{
return;
}
RoomInstance->LoadLevelInstance(); // queued; loads on the subsystem's next streaming update, so IsLoaded() is false this frame
ULevelInstanceSubsystem* Subsystem = GetWorld()->GetSubsystem<ULevelInstanceSubsystem>();
if (Subsystem && Subsystem->IsLoaded(RoomInstance))
{
Subsystem->ForEachActorInLevelInstance(RoomInstance, [](AActor* LevelActor)
{
LevelActor->SetActorTickEnabled(true);
return true;
});
}
}
ILevelInstanceInterface运行时成员:LoadLevelInstance()、UnloadLevelInstance()、IsLoaded()、GetLoadedLevel()、GetLevelStreaming()、GetWorldAsset()、GetLevelInstanceID()、IsInitiallyVisible()。ULevelInstanceSubsystem运行时成员:GetLevelInstance(const FLevelInstanceID&)、GetOwningLevelInstance(const ULevel*)、RequestLoadLevelInstance(ILevelInstanceInterface*, bool bUpdate)、RequestUnloadLevelInstance(ILevelInstanceInterface*)、IsLoaded、IsLoading、ForEachActorInLevelInstance、GetLevelInstanceLevel。编辑入口仅限WITH_EDITOR。ELevelInstanceRuntimeBehavior(LevelInstanceTypes.h:56)决定实例如何进入运行时:Partitioned(演员移入主分区网格;显示为“嵌入式”)或LevelStreaming(使用独立的流送关卡;显示为“独立”)。APackedLevelActor(PackedLevelActor/PackedLevelActor.h)派生自ALevelInstance,并将静态网格体烘焙到实例化网格体组件中。它报告ELevelInstanceRuntimeBehavior::None,且在运行时不加载关卡。
子关卡流送
ULevelStreaming 状态
ELevelStreamingState(Engine/LevelStreaming.h:110)包括 Removed、Unloaded、FailedToLoad、Loading、LoadedNotVisible、MakingVisible、LoadedVisible、MakingInvisible。使用 GetLevelStreamingState() 读取。请求的目标状态由 ELevelStreamingTargetState 指定(Unloaded、UnloadedAndRemoved、LoadedNotVisible、LoadedVisible)。
#include "Engine/LevelStreaming.h"
#include "Kismet/GameplayStatics.h"
ULevelStreaming* Streaming = UGameplayStatics::GetStreamingLevel(this, FName("/Game/Levels/L_Zone_A"));
if (Streaming)
{
Streaming->SetShouldBeLoaded(true);
Streaming->SetShouldBeVisible(true);
const bool bReady = Streaming->IsLevelLoaded() && Streaming->IsLevelVisible();
ULevel* Loaded = Streaming->GetLoadedLevel();
}
四个 BlueprintAssignable 委托均不带参数:OnLevelLoaded、OnLevelUnloaded、OnLevelShown、OnLevelHidden。使用 AddDynamic 绑定到无参数的 UFUNCTION()。
使用 SetIsRequestingUnloadAndRemoval(true) 可完全移除流送关卡对象。切勿直接修改 UWorld::StreamingLevels——请使用 AddStreamingLevel、AddStreamingLevels、RemoveStreamingLevel、RemoveStreamingLevels 和 UpdateStreamingLevelShouldBeConsidered(Engine/World.h:1073-1109),这些函数会维护 StreamingLevelsToConsider。
延迟加载与卸载
UGameplayStatics::LoadStreamLevel(const UObject* WorldContextObject, FName LevelName, bool bMakeVisibleAfterLoad, bool bShouldBlockOnLoad, FLatentActionInfo LatentInfo) 和 UnloadStreamLevel(const UObject* WorldContextObject, FName LevelName, FLatentActionInfo LatentInfo, bool bShouldBlockOnUnload) 会通过 FLatentActionInfo::ExecutionFunction 恢复执行,该函数通过反射系统按名称解析。因此,回调必须是在头文件的类体内声明的 UFUNCTION()。
// MyStreamingActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyStreamingActor.generated.h"
UCLASS()
class MYGAME_API AMyStreamingActor : public AActor
{
GENERATED_BODY()
public:
void StreamInRoom(FName LevelName);
void StreamOutRoom(FName LevelName);
// UFUNCTION on the declaration is mandatory: FLatentActionInfo::ExecutionFunction
// is looked up by name on CallbackTarget's UClass.
UFUNCTION()
void OnRoomLoaded();
UFUNCTION()
void OnRoomUnloaded();
protected:
bool bRoomReady = false;
};
// MyStreamingActor.cpp
#include "MyStreamingActor.h"
#include "Engine/LatentActionManager.h"
#include "Kismet/GameplayStatics.h"
void AMyStreamingActor::StreamInRoom(FName LevelName)
{
FLatentActionInfo LatentInfo;
LatentInfo.CallbackTarget = this;
LatentInfo.ExecutionFunction = FName("OnRoomLoaded");
LatentInfo.Linkage = 0;
LatentInfo.UUID = 1; // unique per pending latent action on this object
UGameplayStatics::LoadStreamLevel(this, LevelName,
/*bMakeVisibleAfterLoad=*/true, /*bShouldBlockOnLoad=*/false, LatentInfo);
}
void AMyStreamingActor::StreamOutRoom(FName LevelName)
{
FLatentActionInfo LatentInfo;
LatentInfo.CallbackTarget = this;
LatentInfo.ExecutionFunction = FName("OnRoomUnloaded");
LatentInfo.Linkage = 0;
LatentInfo.UUID = 2;
UGameplayStatics::UnloadStreamLevel(this, LevelName, LatentInfo, /*bShouldBlockOnUnload=*/false);
}
void AMyStreamingActor::OnRoomLoaded()
{
bRoomReady = true;
}
void AMyStreamingActor::OnRoomUnloaded()
{
bRoomReady = false;
}
LoadStreamLevelBySoftObjectPtr 和 UnloadStreamLevelBySoftObjectPtr 使用 TSoftObjectPtr<UWorld> 替代 FName,是适用于打包的形式。UGameplayStatics::FlushLevelStreaming(const UObject*) 会阻塞,直到待处理工作完成;UWorld::FlushLevelStreaming(EFlushLevelStreamingType) 则提供更细粒度的控制(None、Full、Visibility)。
运行时关卡实例化
ULevelStreamingDynamic 会以不同的变换重复加载同一个包。优先使用参数结构体重载,因为按名称调用的重载无法表达完整的 FTransform。
#include "Engine/LevelStreamingDynamic.h"
#include "Engine/World.h"
ULevelStreamingDynamic* AMyDungeonGenerator::SpawnRoom(const FTransform& RoomTransform, const FString& InstanceName)
{
ULevelStreamingDynamic::FLoadLevelInstanceParams Params(
GetWorld(), TEXT("/Game/Levels/L_Room_Corridor"), RoomTransform);
Params.OptionalLevelNameOverride = &InstanceName; // identical on server and clients
Params.bInitiallyVisible = true;
bool bSuccess = false;
ULevelStreamingDynamic* Level = ULevelStreamingDynamic::LoadLevelInstance(Params, bSuccess);
return bSuccess ? Level : nullptr;
}
要拆除实例,请依次调用 SetShouldBeVisible(false)、SetShouldBeLoaded(false),然后调用 SetIsRequestingUnloadAndRemoval(true)。跟踪自身实例并绑定 OnLevelShown 的生成器,参见流送模式。
FLoadLevelInstanceParams 的成员:World、LongPackageName、LevelTransform、OptionalLevelNameOverride(const FString*)、OptionalLevelStreamingClass、bLoadAsTempPackage、bInitiallyVisible、bAllowReuseExitingLevelStreaming、EditorPathOwner、LevelStreamingCreatedCallback。
向 Blueprint 暴露的重载为 LoadLevelInstance(UObject* WorldContextObject, FString LevelName, FVector Location, FRotator Rotation, bool& bOutSuccess, const FString& OptionalLevelNameOverride, TSubclassOf<ULevelStreamingDynamic> OptionalLevelStreamingClass, bool bLoadAsTempPackage) 和 LoadLevelInstanceBySoftObjectPtr;后者用 TSoftObjectPtr<UWorld> 替代 LevelName,并使用相同的尾部参数。仅限 C++ 的 BySoftObjectPtr 重载使用 FTransform(Engine/LevelStreamingDynamic.h:94)。
流送体积
ALevelStreamingVolume 根据摄像机位置驱动子关卡。EStreamingVolumeUsage 的取值:SVB_Loading、SVB_LoadingAndVisibility、SVB_VisibilityBlockingOnLoad、SVB_BlockingOnLoad、SVB_LoadingNotVisible。在体积上设置 StreamingUsage;StreamingLevelNames 列出它影响的关卡,而流送关卡的 EditorStreamingVolumes 数组则是编辑器端的关联。将 ULevelStreaming::bDisableDistanceStreaming = true,可使关卡不再受体积控制;设置 MinTimeBetweenVolumeUnloadRequests,可减轻体积边界处的闪烁。在分区世界中,流送体积不起作用。
关卡跳转
| 调用 | 头文件 | 效果 |
|---|---|---|
UGameplayStatics::OpenLevel(const UObject*, FName LevelName, bool bAbsolute, FString Options) | Kismet/GameplayStatics.h:340 | 拆除世界;客户端断开连接 |
UGameplayStatics::OpenLevelBySoftObjectPtr(const UObject*, TSoftObjectPtr<UWorld>, bool, FString) | Kismet/GameplayStatics.h:350 | 相同效果,但使用对打包安全的引用 |
UWorld::ServerTravel(const FString& InURL, bool bAbsolute, bool bShouldSkipGameNotify) | Engine/World.h:4231 | 服务器跳转,客户端跟随 |
APlayerController::ClientTravel(const FString& URL, ETravelType, bool bSeamless, FGuid) | GameFramework/PlayerController.h:1413 | 由客户端发起;TRAVEL_Absolute、TRAVEL_Partial、TRAVEL_Relative |
UWorld::SeamlessTravel(const FString& InURL, bool bAbsolute) | Engine/World.h:4243 | 后台切换,保持连接 |
UGameplayStatics::GetCurrentLevelName(const UObject*, bool bRemovePrefixString) | Kismet/GameplayStatics.h:358 | 当前地图名称 |
无缝旅行
- 在 GameMode 构造函数中设置
bUseSeamlessTravel = true。它已经是AGameModeBase(GameModeBase.h:579)上的UPROPERTY,不要重新声明。 - 设置过渡地图。如果不设置,引擎会经过一个空的虚拟世界进行过渡(
World.cpp:8367)。在 PIE 中,除非设置net.AllowPIESeamlessTravel=1(GameModeBase.cpp:506),否则无缝旅行会回退为硬旅行。
[/Script/EngineSettings.GameMapsSettings]
TransitionMap=/Game/Maps/L_Transition.L_Transition
- 重写持久化钩子。以下签名原样摘自
GameFramework/GameModeBase.h:239-268:
// MyGameMode.cpp
void AMyGameMode::GetSeamlessTravelActorList(bool bToTransition, TArray<AActor*>& ActorList)
{
Super::GetSeamlessTravelActorList(bToTransition, ActorList);
if (MyPersistentManager)
{
ActorList.Add(MyPersistentManager); // called for both legs (to transition, then to destination); add on both
}
}
void AMyGameMode::PostSeamlessTravel()
{
Super::PostSeamlessTravel();
}
void AMyGameMode::HandleSeamlessTravelPlayer(AController*& C)
{
Super::HandleSeamlessTravelPlayer(C);
}
APlayerController::GetSeamlessTravelActorList(bool bToEntry, TArray<AActor*>& ActorList) 是客户端对应的接口。APlayerController::SeamlessTravelTo(APlayerController* NewPC) / SeamlessTravelFrom(APlayerController* OldPC) 和 APlayerState::SeamlessTravelTo(APlayerState* NewPlayerState) 会将每位玩家的状态复制到新对象上。UWorld::IsInSeamlessTravel() 和 UWorld::SetSeamlessTravelMidpointPause(bool bNowPaused) 用于控制中点;UWorld::OnSeamlessTravelStart 和 UWorld::OnSeamlessTravelTransition 是供观察者使用的静态多播委托。
旅行期间的复制行为(通道拆除、Actor 重新创建、网络相关性)属于 ue-networking-replication;流式关卡卸载时,它会使用钩子 UNetDriver::NotifyActorLevelUnloaded(AActor*)。
HLOD
HLOD 会为已流出的单元生成替代代理。UHLODLayer(WorldPartition/HLOD/HLODLayer.h)用于配置层级;网格位置由运行时分区的设置决定,而非该层级。哈希和重建记录由 UHLODRebuildPolicy 和 UHLODRebuildPolicyData 处理;构建器设置(UHLODBuilderSettings)则实现 ComputeHLODHash(FHLODHashBuilder&)。
运行时,UWorldPartitionHLODRuntimeSubsystem(一个 UWorldSubsystem)会将代理跟踪为 IWorldPartitionHLODObject,而非 Actor:RegisterHLODObject、UnregisterHLODObject、GetHLODObjectsForCell(const UWorldPartitionRuntimeCell*)、OnHLODObjectRegisteredEvent()、OnHLODObjectUnregisteredEvent()、IsHLODEnabled()、IsWarmupEnabled()。AWorldPartitionHLOD 和 AWorldPartitionCustomHLOD(WorldPartition/HLOD/CustomHLODActor.h)都实现了该接口;构建期间,AWorldPartitionHLODSourceCellPlaceholder 则充当源单元的占位对象。
发布前请构建 HLOD。没有它们,加载范围之外的一切都会直接消失。
关卡流式持久化
关卡流式持久化插件(5.8 中为实验性功能)会将属性值、已销毁 Actor 记录和重生数据附加到流式关卡,以便关卡流出后再次流入时保留更改。在 Build.cs 中添加 LevelStreamingPersistence 模块。
ULevelStreamingPersistenceManager 是一个 UWorldSubsystem。可供蓝图调用的接口:SerializeTo(TArray<uint8>& OutPayload, bool bForceUpdate)、InitializeFrom(const TArray<uint8>& InPayload)、EjectPlacedActor(AActor*)、RecreateActorInLevel(AActor*, ULevel* NewOwningLevel)、RecreateActorInPersistentLevel(AActor*)。模板访问器 SetPropertyValue、TrySetPropertyValue 和 GetPropertyValue 可通过对象路径和属性名称读写单个持久化属性。哪些属性会持久化,由 ULevelStreamingPersistenceSettings 声明;它是一个配置项为 Engine 的 UDeveloperSettings。
每张地图调用一次 InitializeFrom,并且应尽早调用——可从自定义的 UWorldSubsystem::Initialize 中调用——这样属性值会在 Actor 开始运行前生效。它生成的负载由 USaveGame 保存;请参阅 ue-serialization-savegames。
世界子系统与跨关卡状态
UWorldSubsystem(Subsystems/WorldSubsystem.h)在每个 UWorld 中创建一次,并随其销毁,包括关卡切换时——它适合用来管理流式加载或跟踪区域。完整的子系统类型表见 ue-cpp-foundations。
可重写的钩子(原文摘自 Subsystems/WorldSubsystem.h:34-66):virtual bool ShouldCreateSubsystem(UObject* Outer) const、virtual void PostInitialize()、virtual void OnWorldBeginPlay(UWorld& InWorld)、virtual void OnWorldEndPlay(UWorld& InWorld)、virtual void OnWorldComponentsUpdated(UWorld& World)、virtual void PreDeinitialize()、virtual bool DoesSupportWorldType(const EWorldType::Type WorldType) const。使用 GetWorld()->GetSubsystem<UMyStreamingManager>() 获取它。一个已实现的区域管理器子系统示例见流式加载模式。
如需逐帧执行工作,请派生自 UTickableWorldSubsystem,并实现 Tick(float DeltaTime) 和 GetStatId() const(纯虚函数)。其 Initialize 和 Deinitialize 重写函数负责启用和停用 Tick,因此务必在两者中调用 Super::。
| 机制 | 生命周期 | 用途 |
|---|---|---|
UGameInstance | 整个应用会话 | 跨地图的玩家和会话数据 |
UGameInstanceSubsystem | 整个应用会话 | 生命周期长于所有世界的服务 |
| 无缝旅行 Actor 列表 | 仅限过渡期间 | 实际跨越关卡的 Actor |
UWorldSubsystem | 单个世界 | 世界范围内的缓存;旅行前推送到 UGameInstance |
USaveGame | 磁盘 | 进度数据——见 ue-serialization-savegames |
已弃用 — 请勿使用
| 不要使用 | 在 5.8 中使用 | 来源 |
|---|---|---|
AActor::DataLayers, FActorDataLayer | AActor::DataLayerAssets | GameFramework/Actor.h:1115 中的 UE_DEPRECATED(5.8) |
UDataLayerSubsystem | UDataLayerManager | WorldPartition/DataLayer/DataLayerSubsystem.h:37 中的 UE_DEPRECATED(5.3) |
AWorldDataLayers::OverwriteDataLayerRuntimeStates | UDataLayerManager::SetDataLayerRuntimeState | WorldPartition/DataLayer/WorldDataLayers.h:135 中的 UE_DEPRECATED(5.8) |
UDataLayerInstance::GetDataLayerFName | UObject::GetFName | WorldPartition/DataLayer/DataLayerInstance.h:190 中的 UE_DEPRECATED(5.8) |
在编辑器外使用 UDataLayerManager::GetDataLayerInstanceNames | 在运行时传递资源或实例指针 | WorldPartition/DataLayer/DataLayerManager.h:121 中的 UE_DEPRECATED(5.8) |
ULevelStreaming::ECurrentState, GetCurrentState() | ELevelStreamingState, GetLevelStreamingState() | Engine/LevelStreaming.h:367 中的 UE_DEPRECATED(5.2) |
UWorldPartitionLevelStreamingDynamic::LoadInEditor / UnloadFromEditor | 无受支持的替代方案 | WorldPartition/WorldPartitionLevelStreamingDynamic.h:63 中的 UE_DEPRECATED(5.7) |
UHLODBuilderSettings::GetCRC() | ComputeHLODHash(FHLODHashBuilder&) | WorldPartition/HLOD/HLODBuilder.h:43 中的 UE_DEPRECATED(5.7) |
UWorldPartitionHLODSourceActors::GetHLODHash() | ComputeHLODHash(FHLODHashBuilder&) | WorldPartition/HLOD/HLODSourceActors.h:27 中的 UE_DEPRECATED(5.7) |
AWorldPartitionHLOD::GetHLODHash / ComputeHLODHash / SetHLODHash | 使用 UHLODRebuildPolicyData 的 GetHLODRebuildPolicyDataSet / SetHLODRebuildPolicyDataSet | WorldPartition/HLOD/HLODActor.h:146 中的 UE_DEPRECATED(5.8) |
UHLODLayer::GetCellSize / GetLoadingRange / IsSpatiallyLoaded | 运行时分区设置 | WorldPartition/HLOD/HLODLayer.h:82 中的 UE_DEPRECATED(5.7) |
UWorldPartitionHLODRuntimeSubsystem::RegisterHLODActor / GetHLODActorsForCell / GetNumOutdatedHLODActors | RegisterHLODObject / GetHLODObjectsForCell / GetNumOutdatedHLODObjects | WorldPartition/HLOD/HLODRuntimeSubsystem.h:77 中的 UE_DEPRECATED(5.6) |
AWorldPartitionCustomHLODPlaceholder | AWorldPartitionHLODSourceCellPlaceholder | WorldPartition/HLOD/CustomHLODPlaceholderActor.h:9 中的 UE_DEPRECATED(5.8) |
UWorldPartitionStreamingSourceComponent::TargetGrid / TargetHLODLayers | 使用 TargetBehavior 的 TargetGrids | Components/WorldPartitionStreamingSourceComponent.h:63 中的 DeprecatedProperty |
常见错误
在类外定义上方使用 UFUNCTION()。 在 .cpp 中将潜伏回调写成 UFUNCTION() void AMyActor::OnRoomLoaded() {},UHT 无法识别,因此 FLatentActionInfo::ExecutionFunction 永远无法解析,后续流程也不会触发。请在头文件的类定义中声明它。
重复使用同一个 FLatentActionInfo::UUID。 同一个 CallbackTarget 上的两个待处理操作如果共用一个 UUID,就会发生冲突,其中一个会被丢弃。请为每个调用点指定独立的常量。
在加载画面之外执行阻塞加载。 bShouldBlockOnLoad = true、FlushLevelStreaming 和 wp.Runtime.BlockOnSlowStreaming 都会阻塞游戏线程。在服务器上,它们会阻塞所有客户端。
在客户端设置数据层状态。 未设置加载筛选器的运行时数据层只会响应服务器上的设置;随后该状态会进行复制。在客户端调用时,看起来就像什么也没发生。
多人游戏中的动态关卡名称不一致。 ULevelStreamingDynamic::LoadLevelInstance 会为每个进程生成唯一的包名。如果服务器和客户端没有将 OptionalLevelNameOverride 设置为相同字符串,实例就会属于不同的包,彼此无法对应。
跨关卡使用硬引用。 卸载时,关卡包中的所有对象都会被标记为垃圾(LevelStreamingGCHelper.cpp:201-208),因此从持久关卡 Actor 指向流式加载关卡的 UPROPERTY() 指针会被置空(非 UPROPERTY 原始指针则会悬空),关卡重新流入时也不会重新指向对象。跨关卡引用请使用 TSoftObjectPtr 或 TWeakObjectPtr。
期望流送体积在 World Partition 中生效。 它们不起作用。请改用数据层或流送源。
以为专用服务器的流送方式与客户端相同。 除非启用,否则服务器单元格流送处于关闭状态;服务器只会使用已注册的流送源。排查服务器上“缺失”的 Actor 前,请检查 UWorldPartition::IsServerStreamingEnabled()。
直接修改 UWorld::StreamingLevels。 这会绕过 StreamingLevelsToConsider,导致关卡永远不会被评估。请使用 AddStreamingLevel / RemoveStreamingLevel 系列函数。
在 UTickableWorldSubsystem 中跳过 Super::Initialize / Super::Deinitialize。 这些调用负责启用和禁用 Tick。
相关技能
ue-cpp-foundations— 完整的子系统类型表、TObjectPtr、TSoftObjectPtr、GC 和对象生命周期规则。ue-networking-replication— 跨旅行的复制、网络相关性、DOREPLIFETIME、专用服务器权限。ue-gameplay-framework— GameMode 和 GameState 的旅行回调、登录流程、UGameInstance的角色。ue-data-assets-tables—UDataAsset(UDataLayerAsset的基类)、异步资源加载、Asset Manager。ue-procedural-generation— 用于填充分区关卡的 PCG 驱动内容。ue-serialization-savegames—USaveGame,将关卡流送持久化数据写入磁盘。ue-actor-component-architecture— 流送源 Actor 和世界管理器的组件设计。ue-game-features— Game Feature 插件、GameFeatureAction 和模块化组件管理器ue-sequencer-cinematics— Level Sequences、播放、电影摄影机和 Movie Render Graph