mcpField
U
ue-world-level-streaming

ue-world-level-streaming

github.com/quodsoler开发4191.2k 次安装4 周前更新
代码仓库

关于 ue-world-level-streaming

ue-world-level-streaming 是面向 UE 5.8 的世界构成与运行时关卡管理开发技能,帮助开发者选择和实现流送方案。内容涵盖 World Partition、数据层、子关卡及地图切换。

主要功能

  1. 01UE 5.8 流送 API 指引
  2. 02World Partition 与流送源
  3. 03运行时数据层状态控制
  4. 04关卡实例与打包 Actor
  5. 05子关卡加载与可见性
  6. 06地图切换与 HLOD

适用场景

  1. 01为大型开放世界流送单元格
  2. 02用数据层切换可选内容
  3. 03在运行时加载可复用关卡片段
  4. 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 和子关卡之间进行选择选择流送模型
网格流送、流送源、阻塞加载、CVarWorld 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当前地图名称
无缝旅行
  1. 在 GameMode 构造函数中设置 bUseSeamlessTravel = true。它已经是 AGameModeBase(GameModeBase.h:579)上的 UPROPERTY,不要重新声明。
  2. 设置过渡地图。如果不设置,引擎会经过一个空的虚拟世界进行过渡(World.cpp:8367)。在 PIE 中,除非设置 net.AllowPIESeamlessTravel=1(GameModeBase.cpp:506),否则无缝旅行会回退为硬旅行。
[/Script/EngineSettings.GameMapsSettings]
TransitionMap=/Game/Maps/L_Transition.L_Transition
  1. 重写持久化钩子。以下签名原样摘自 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, FActorDataLayerAActor::DataLayerAssetsGameFramework/Actor.h:1115 中的 UE_DEPRECATED(5.8)
UDataLayerSubsystemUDataLayerManagerWorldPartition/DataLayer/DataLayerSubsystem.h:37 中的 UE_DEPRECATED(5.3)
AWorldDataLayers::OverwriteDataLayerRuntimeStatesUDataLayerManager::SetDataLayerRuntimeStateWorldPartition/DataLayer/WorldDataLayers.h:135 中的 UE_DEPRECATED(5.8)
UDataLayerInstance::GetDataLayerFNameUObject::GetFNameWorldPartition/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 / SetHLODRebuildPolicyDataSetWorldPartition/HLOD/HLODActor.h:146 中的 UE_DEPRECATED(5.8)
UHLODLayer::GetCellSize / GetLoadingRange / IsSpatiallyLoaded运行时分区设置WorldPartition/HLOD/HLODLayer.h:82 中的 UE_DEPRECATED(5.7)
UWorldPartitionHLODRuntimeSubsystem::RegisterHLODActor / GetHLODActorsForCell / GetNumOutdatedHLODActorsRegisterHLODObject / GetHLODObjectsForCell / GetNumOutdatedHLODObjectsWorldPartition/HLOD/HLODRuntimeSubsystem.h:77 中的 UE_DEPRECATED(5.6)
AWorldPartitionCustomHLODPlaceholderAWorldPartitionHLODSourceCellPlaceholderWorldPartition/HLOD/CustomHLODPlaceholderActor.h:9 中的 UE_DEPRECATED(5.8)
UWorldPartitionStreamingSourceComponent::TargetGrid / TargetHLODLayers使用 TargetBehavior 的 TargetGridsComponents/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

评论

支持 Markdown