Initial commit: Vertical Partition Streaming plugin (UE 5.7)
Bounded vertical route partition / streaming for handcrafted spiral climb maps (Only Up / Only Climb). Not World Partition, not Level Instances as core arch. - Z-cell segmentation + optional XY subcells, editor builder + commandlet - NonDestructive visibility backend + streaming-level backend - Offline HLOD proxies: MergedMesh / SimplifiedProxy (ProxyLOD) - Universal Level Instance + Packed Level Actor + HISM support (flatten+merge) - Vertical-aware runtime streamer (full/preload/HLOD/unload, hysteresis, velocity prediction, fall-risk), WP-style debug viz + vp.* console - Safe-by-default: only static decor streams; sky/lights/gameplay stay persistent Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,114 @@
|
||||
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||
#include "VerticalPartitionDebugComponent.h"
|
||||
#include "VerticalPartitionRuntimeSubsystem.h"
|
||||
#include "VerticalPartitionStatics.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
static TAutoConsoleVariable<int32> CVarVPDebug(
|
||||
TEXT("vp.Debug"), 0,
|
||||
TEXT("Master Vertical Partition debug toggle (gates DebugDraw + DebugOverlay)."), ECVF_Default);
|
||||
|
||||
static TAutoConsoleVariable<int32> CVarVPDebugDraw(
|
||||
TEXT("vp.DebugDraw"), 1,
|
||||
TEXT("Draw 3D coloured cell boxes / ids / counts (requires vp.Debug 1)."), ECVF_Default);
|
||||
|
||||
static TAutoConsoleVariable<int32> CVarVPDebugOverlay(
|
||||
TEXT("vp.DebugOverlay"), 1,
|
||||
TEXT("Draw the on-screen runtime stats overlay (requires vp.Debug 1)."), ECVF_Default);
|
||||
|
||||
UVerticalPartitionDebugComponent::UVerticalPartitionDebugComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.bStartWithTickEnabled = true;
|
||||
}
|
||||
|
||||
UVerticalPartitionRuntimeSubsystem* UVerticalPartitionDebugComponent::GetSub() const
|
||||
{
|
||||
return UVerticalPartitionRuntimeSubsystem::Get(this);
|
||||
}
|
||||
|
||||
void UVerticalPartitionDebugComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
if (CVarVPDebug.GetValueOnGameThread() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UVerticalPartitionRuntimeSubsystem* Sub = GetSub();
|
||||
if (!Sub || !Sub->IsReady())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (CVarVPDebugDraw.GetValueOnGameThread() != 0)
|
||||
{
|
||||
DrawDebugCells(Sub);
|
||||
}
|
||||
if (CVarVPDebugOverlay.GetValueOnGameThread() != 0)
|
||||
{
|
||||
DrawOverlay(Sub);
|
||||
}
|
||||
}
|
||||
|
||||
void UVerticalPartitionDebugComponent::DrawDebugCells(UVerticalPartitionRuntimeSubsystem* Sub) const
|
||||
{
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
UWorld* World = GetWorld();
|
||||
if (!World || !Sub)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Sub->GetCellRuntimes())
|
||||
{
|
||||
const FVerticalCellRuntime& C = Pair.Value;
|
||||
if (!C.Bounds.IsValid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Purple overrides the state colour when a cell is force-locked.
|
||||
FColor Color = C.bForced
|
||||
? FColor(180, 60, 220)
|
||||
: UVerticalPartitionStatics::GetStateColor(C.State).ToFColor(true);
|
||||
|
||||
const FVector Center = C.Bounds.GetCenter();
|
||||
const FVector Extent = C.Bounds.GetExtent();
|
||||
DrawDebugBox(World, Center, Extent, Color, false, -1.f, 0, 4.f);
|
||||
|
||||
const FString Label = FString::Printf(TEXT("%s [%s] actors:%d"),
|
||||
*C.Id.ToString(),
|
||||
C.bForced ? TEXT("LOCK") : *UEnum::GetDisplayValueAsText(C.State).ToString(),
|
||||
C.ResolvedActors.Num());
|
||||
DrawDebugString(World, Center + FVector(0, 0, Extent.Z * 0.5f), Label, nullptr, Color, 0.f, true);
|
||||
}
|
||||
#endif // ENABLE_DRAW_DEBUG
|
||||
}
|
||||
|
||||
void UVerticalPartitionDebugComponent::DrawOverlay(UVerticalPartitionRuntimeSubsystem* Sub) const
|
||||
{
|
||||
if (!GEngine || !Sub)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FVerticalRuntimeStats& S = Sub->GetStats();
|
||||
const int32 Key = 0x5650; // 'VP'
|
||||
auto Line = [&](int32 Slot, const FColor& Col, const FString& Text)
|
||||
{
|
||||
GEngine->AddOnScreenDebugMessage(Key + Slot, 0.f, Col, Text);
|
||||
};
|
||||
|
||||
Line(0, FColor::Cyan, TEXT("==== Vertical Partition ===="));
|
||||
Line(1, FColor::White, FString::Printf(TEXT("Player Z cell: %d"), S.PlayerCellZ));
|
||||
Line(2, FColor::Green, FString::Printf(TEXT("Full cells: %d"), S.FullCellsLoaded));
|
||||
Line(3, FColor::Blue, FString::Printf(TEXT("HLOD cells: %d"), S.HLODCellsVisible));
|
||||
Line(4, FColor::Silver, FString::Printf(TEXT("Unloaded: %d"), S.CellsUnloaded));
|
||||
Line(5, FColor::Yellow, FString::Printf(TEXT("Loading/async: %d / %d"), S.CellsLoading, S.ActiveAsyncLoads));
|
||||
Line(6, FColor::White, FString::Printf(TEXT("Est. memory: %.1f MB"), S.EstimatedMemoryMB));
|
||||
Line(7, FColor::White, FString::Printf(TEXT("Update time: %.2f ms"), S.LastUpdateTimeMS));
|
||||
Line(8, S.HitchesOverThreshold > 0 ? FColor::Red : FColor::White,
|
||||
FString::Printf(TEXT("Hitches: %d"), S.HitchesOverThreshold));
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||
#include "VerticalPartitionDescriptor.h"
|
||||
|
||||
int32 UVerticalPartitionDescriptor::FindCellIndex(const FVerticalCellId& Id) const
|
||||
{
|
||||
for (int32 i = 0; i < Cells.Num(); ++i)
|
||||
{
|
||||
if (Cells[i].CellId == Id)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
const FVerticalCellDescriptor* UVerticalPartitionDescriptor::FindCell(const FVerticalCellId& Id) const
|
||||
{
|
||||
const int32 Index = FindCellIndex(Id);
|
||||
return Cells.IsValidIndex(Index) ? &Cells[Index] : nullptr;
|
||||
}
|
||||
|
||||
bool UVerticalPartitionDescriptor::HasFullPackages() const
|
||||
{
|
||||
for (const FVerticalCellDescriptor& Cell : Cells)
|
||||
{
|
||||
if (Cell.FullCellPackage.IsValid())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
189
Source/VerticalPartition/Private/VerticalPartitionManager.cpp
Normal file
189
Source/VerticalPartition/Private/VerticalPartitionManager.cpp
Normal file
@ -0,0 +1,189 @@
|
||||
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||
#include "VerticalPartitionManager.h"
|
||||
#include "VerticalPartitionDebugComponent.h"
|
||||
#include "VerticalPartitionRuntimeSubsystem.h"
|
||||
#include "VerticalPartitionDescriptor.h"
|
||||
#include "VerticalPartitionVolume.h"
|
||||
#include "VerticalPartitionStatics.h"
|
||||
#include "VerticalPartitionLog.h"
|
||||
#include "EngineUtils.h" // TActorIterator
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Camera/PlayerCameraManager.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
AVerticalPartitionManager::AVerticalPartitionManager()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
PrimaryActorTick.bStartWithTickEnabled = true;
|
||||
|
||||
DebugComponent = CreateDefaultSubobject<UVerticalPartitionDebugComponent>(TEXT("DebugComponent"));
|
||||
}
|
||||
|
||||
void AVerticalPartitionManager::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
UWorld* World = GetWorld();
|
||||
|
||||
// Resolve a volume if none was assigned.
|
||||
if (!Volume && World)
|
||||
{
|
||||
for (TActorIterator<AVerticalPartitionVolume> It(World); It; ++It)
|
||||
{
|
||||
Volume = *It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the descriptor: explicit -> volume's -> none.
|
||||
UVerticalPartitionDescriptor* Desc = Descriptor.LoadSynchronous();
|
||||
if (!Desc && Volume)
|
||||
{
|
||||
Desc = Volume->Descriptor.LoadSynchronous();
|
||||
}
|
||||
|
||||
if (!Desc)
|
||||
{
|
||||
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] Manager found no descriptor. Build the partition first. Streaming disabled."));
|
||||
SetActorTickEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const FBox Bounds = Volume ? Volume->GetVolumeBounds() : Desc->VolumeBounds;
|
||||
|
||||
if (UVerticalPartitionRuntimeSubsystem* Sub = UVerticalPartitionRuntimeSubsystem::Get(this))
|
||||
{
|
||||
Sub->RegisterDescriptor(Desc, Bounds);
|
||||
}
|
||||
|
||||
bSnapNextUpdate = true;
|
||||
}
|
||||
|
||||
void AVerticalPartitionManager::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
if (UVerticalPartitionRuntimeSubsystem* Sub = UVerticalPartitionRuntimeSubsystem::Get(this))
|
||||
{
|
||||
Sub->UnregisterDescriptor();
|
||||
}
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
APawn* AVerticalPartitionManager::ResolveTrackedPawn()
|
||||
{
|
||||
if (TrackedPawn.IsValid())
|
||||
{
|
||||
return TrackedPawn.Get();
|
||||
}
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (APlayerController* PC = World->GetFirstPlayerController())
|
||||
{
|
||||
return PC->GetPawn();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool AVerticalPartitionManager::BuildContext(FVerticalStreamingContext& OutCtx, float DeltaSeconds, bool& bOutTeleport)
|
||||
{
|
||||
bOutTeleport = false;
|
||||
|
||||
APawn* Pawn = ResolveTrackedPawn();
|
||||
UVerticalPartitionRuntimeSubsystem* Sub = UVerticalPartitionRuntimeSubsystem::Get(this);
|
||||
if (!Pawn || !Sub || !Sub->IsReady())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FVerticalPartitionSettings& S = Sub->GetSettings();
|
||||
const FBox Bounds = Sub->GetVolumeBounds();
|
||||
const FVector Pos = Pawn->GetActorLocation();
|
||||
|
||||
// Velocity (smoothed) + teleport detection.
|
||||
if (bHavePrevPos && DeltaSeconds > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
const FVector RawVel = (Pos - PrevTrackedPos) / DeltaSeconds;
|
||||
SmoothedVelocity = FMath::Lerp(SmoothedVelocity, RawVel, 0.5f);
|
||||
|
||||
const float Jump = FMath::Abs(Pos.Z - PrevTrackedPos.Z);
|
||||
if (Jump > S.CellHeight * 3.f)
|
||||
{
|
||||
bOutTeleport = true;
|
||||
SmoothedVelocity = FVector::ZeroVector;
|
||||
}
|
||||
}
|
||||
PrevTrackedPos = Pos;
|
||||
bHavePrevPos = true;
|
||||
|
||||
OutCtx.PlayerWorldZ = Pos.Z;
|
||||
OutCtx.PlayerVelocityZ = SmoothedVelocity.Z;
|
||||
OutCtx.PlayerCellZ = UVerticalPartitionStatics::GetCellZFromWorldZ(Pos.Z, Bounds.Min.Z, S.CellHeight);
|
||||
OutCtx.bUseXY = S.bUseXYSubCells;
|
||||
if (S.bUseXYSubCells)
|
||||
{
|
||||
OutCtx.PlayerCellX = UVerticalPartitionStatics::GetCellXFromWorldX(Pos.X, Bounds.Min.X, S.XYCellSize);
|
||||
OutCtx.PlayerCellY = UVerticalPartitionStatics::GetCellYFromWorldY(Pos.Y, Bounds.Min.Y, S.XYCellSize);
|
||||
}
|
||||
|
||||
if (const APlayerController* PC = Cast<APlayerController>(Pawn->GetController()))
|
||||
{
|
||||
if (PC->PlayerCameraManager)
|
||||
{
|
||||
OutCtx.CameraPitchDeg = PC->PlayerCameraManager->GetCameraRotation().Pitch;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AVerticalPartitionManager::Tick(float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
|
||||
UVerticalPartitionRuntimeSubsystem* Sub = UVerticalPartitionRuntimeSubsystem::Get(this);
|
||||
if (!Sub || !Sub->IsReady())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateAccumulator += DeltaSeconds;
|
||||
const float Interval = FMath::Max(0.01f, Sub->GetSettings().UpdateInterval);
|
||||
if (!bSnapNextUpdate && UpdateAccumulator < Interval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float Elapsed = UpdateAccumulator;
|
||||
UpdateAccumulator = 0.f;
|
||||
|
||||
FVerticalStreamingContext Ctx;
|
||||
bool bTeleport = false;
|
||||
if (!BuildContext(Ctx, Elapsed, bTeleport))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const bool bSnap = bSnapNextUpdate || bTeleport;
|
||||
bSnapNextUpdate = false;
|
||||
|
||||
Sub->UpdateStreaming(Ctx, Elapsed, bSnap);
|
||||
}
|
||||
|
||||
void AVerticalPartitionManager::TeleportToCell(int32 Z)
|
||||
{
|
||||
APawn* Pawn = ResolveTrackedPawn();
|
||||
UVerticalPartitionRuntimeSubsystem* Sub = UVerticalPartitionRuntimeSubsystem::Get(this);
|
||||
if (!Pawn || !Sub || !Sub->IsReady())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FVerticalPartitionSettings& S = Sub->GetSettings();
|
||||
const FBox Bounds = Sub->GetVolumeBounds();
|
||||
const FVector Center = Bounds.GetCenter();
|
||||
const float TargetZ = Bounds.Min.Z + (Z + 0.5f) * S.CellHeight;
|
||||
|
||||
Pawn->TeleportTo(FVector(Center.X, Center.Y, TargetZ), Pawn->GetActorRotation());
|
||||
bSnapNextUpdate = true;
|
||||
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Teleported tracked pawn to cell Z=%d (worldZ=%.0f)"), Z, TargetZ);
|
||||
}
|
||||
179
Source/VerticalPartition/Private/VerticalPartitionModule.cpp
Normal file
179
Source/VerticalPartition/Private/VerticalPartitionModule.cpp
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||
//
|
||||
// Runtime module impl + the vp.* console command surface. Build/editor commands
|
||||
// route through FVerticalPartitionEditorBridge (editor builds only); runtime
|
||||
// commands talk to the world's UVerticalPartitionRuntimeSubsystem / manager.
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "Engine/World.h"
|
||||
#include "VerticalPartitionLog.h"
|
||||
#include "VerticalPartitionTypes.h"
|
||||
#include "VerticalPartitionVolume.h"
|
||||
#include "VerticalPartitionManager.h"
|
||||
#include "VerticalPartitionRuntimeSubsystem.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "Editor.h"
|
||||
#include "Selection.h"
|
||||
#endif
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogVerticalPartition);
|
||||
|
||||
namespace VPConsole
|
||||
{
|
||||
static AVerticalPartitionVolume* FindVolume(UWorld* World)
|
||||
{
|
||||
if (!World) { return nullptr; }
|
||||
#if WITH_EDITOR
|
||||
// Prefer an editor-selected volume when available.
|
||||
if (GEditor)
|
||||
{
|
||||
if (USelection* Sel = GEditor->GetSelectedActors())
|
||||
{
|
||||
for (FSelectionIterator It(*Sel); It; ++It)
|
||||
{
|
||||
if (AVerticalPartitionVolume* V = Cast<AVerticalPartitionVolume>(*It))
|
||||
{
|
||||
return V;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
for (TActorIterator<AVerticalPartitionVolume> It(World); It; ++It)
|
||||
{
|
||||
return *It;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static UVerticalPartitionRuntimeSubsystem* GetSub(UWorld* World)
|
||||
{
|
||||
return World ? World->GetSubsystem<UVerticalPartitionRuntimeSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
static AVerticalPartitionManager* FindManager(UWorld* World)
|
||||
{
|
||||
if (!World) { return nullptr; }
|
||||
for (TActorIterator<AVerticalPartitionManager> It(World); It; ++It)
|
||||
{
|
||||
return *It;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void RouteEditor(UWorld* World, EVerticalPartitionAction Action, const TCHAR* Name)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
if (AVerticalPartitionVolume* V = FindVolume(World))
|
||||
{
|
||||
FVerticalPartitionEditorBridge::Execute(V, Action);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] %s: no AVerticalPartitionVolume in the level."), Name);
|
||||
}
|
||||
#else
|
||||
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] %s is an editor-only command."), Name);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
class FVerticalPartitionModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override
|
||||
{
|
||||
using namespace VPConsole;
|
||||
IConsoleManager& CM = IConsoleManager::Get();
|
||||
auto Add = [this, &CM](const TCHAR* Name, const TCHAR* Help, FConsoleCommandWithWorldAndArgsDelegate Del)
|
||||
{
|
||||
Commands.Add(CM.RegisterConsoleCommand(Name, Help, Del, ECVF_Default));
|
||||
};
|
||||
|
||||
// ---- runtime: reporting ----
|
||||
Add(TEXT("vp.DumpState"), TEXT("Dump runtime streaming stats."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { S->DumpRuntimeStats(); } }));
|
||||
Add(TEXT("vp.DumpCells"), TEXT("Dump every cell's state."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { S->DumpCells(); } }));
|
||||
Add(TEXT("vp.DumpWarnings"), TEXT("Dump build warnings/errors from the descriptor."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { S->DumpWarnings(); } }));
|
||||
Add(TEXT("vp.ProfileStreaming"), TEXT("Log a one-shot streaming profile snapshot."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { S->DumpRuntimeStats(); S->DumpCells(); } }));
|
||||
|
||||
// ---- runtime: forcing / overrides ----
|
||||
Add(TEXT("vp.ForceCellFull"), TEXT("vp.ForceCellFull <Z> - pin a cell to Full."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->ForceCellFull(FCString::Atoi(*A[0])); } } }));
|
||||
Add(TEXT("vp.ForceCellHLOD"), TEXT("vp.ForceCellHLOD <Z> - pin a cell to HLOD."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->ForceCellHLOD(FCString::Atoi(*A[0])); } } }));
|
||||
Add(TEXT("vp.UnforceCell"), TEXT("vp.UnforceCell <Z> - release a forced cell."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->UnforceCell(FCString::Atoi(*A[0])); } } }));
|
||||
Add(TEXT("vp.UnforceAll"), TEXT("Release all forced cells and ShowOnly mode."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { S->UnforceAll(); } }));
|
||||
Add(TEXT("vp.ShowOnlyCell"), TEXT("vp.ShowOnlyCell <Z> - Full only this cell, unload the rest."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->ShowOnlyCell(FCString::Atoi(*A[0])); } } }));
|
||||
Add(TEXT("vp.SetFullRange"), TEXT("vp.SetFullRange <Below> <Above>."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { if (A.Num() > 1) { S->SetFullRange(FCString::Atoi(*A[0]), FCString::Atoi(*A[1])); } } }));
|
||||
Add(TEXT("vp.SetHLODRange"), TEXT("vp.SetHLODRange <Below> <Above>."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { if (A.Num() > 1) { S->SetHLODRange(FCString::Atoi(*A[0]), FCString::Atoi(*A[1])); } } }));
|
||||
Add(TEXT("vp.SetCellHeight"), TEXT("vp.SetCellHeight <cm> (affects prediction only at runtime)."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->SetCellHeight(FCString::Atof(*A[0])); } } }));
|
||||
Add(TEXT("vp.TeleportToCell"), TEXT("vp.TeleportToCell <Z> - move the tracked pawn to a cell."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||
{ if (auto* M = FindManager(W)) { if (A.Num() > 0) { M->TeleportToCell(FCString::Atoi(*A[0])); } } }));
|
||||
|
||||
// ---- editor/build (routed via the bridge) ----
|
||||
Add(TEXT("vp.Analyze"), TEXT("Analyze the current level (editor)."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ RouteEditor(W, EVerticalPartitionAction::Analyze, TEXT("vp.Analyze")); }));
|
||||
Add(TEXT("vp.BuildFromSelectedVolume"), TEXT("Build using the selected/first volume (editor)."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ RouteEditor(W, EVerticalPartitionAction::Build, TEXT("vp.BuildFromSelectedVolume")); }));
|
||||
Add(TEXT("vp.Validate"), TEXT("Validate cells against the level (editor)."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ RouteEditor(W, EVerticalPartitionAction::Validate, TEXT("vp.Validate")); }));
|
||||
Add(TEXT("vp.Rebuild"), TEXT("Rebuild changed cells (editor)."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ RouteEditor(W, EVerticalPartitionAction::RebuildChangedCells, TEXT("vp.Rebuild")); }));
|
||||
Add(TEXT("vp.RebuildHLOD"), TEXT("Rebuild HLOD proxies (editor)."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ RouteEditor(W, EVerticalPartitionAction::RebuildHLOD, TEXT("vp.RebuildHLOD")); }));
|
||||
Add(TEXT("vp.PreviewChunks"), TEXT("Draw coloured cell boxes in the editor viewport (editor)."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ RouteEditor(W, EVerticalPartitionAction::PreviewChunks, TEXT("vp.PreviewChunks")); }));
|
||||
Add(TEXT("vp.PreviewHLOD"), TEXT("Spawn baked HLOD proxy meshes in the editor viewport (editor)."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ RouteEditor(W, EVerticalPartitionAction::SpawnHLODPreview, TEXT("vp.PreviewHLOD")); }));
|
||||
Add(TEXT("vp.PreviewClear"), TEXT("Clear preview boxes + spawned HLOD preview actors (editor)."),
|
||||
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||
{ RouteEditor(W, EVerticalPartitionAction::ClearPreview, TEXT("vp.PreviewClear")); }));
|
||||
}
|
||||
|
||||
virtual void ShutdownModule() override
|
||||
{
|
||||
IConsoleManager& CM = IConsoleManager::Get();
|
||||
for (IConsoleCommand* Cmd : Commands)
|
||||
{
|
||||
if (Cmd) { CM.UnregisterConsoleObject(Cmd); }
|
||||
}
|
||||
Commands.Reset();
|
||||
}
|
||||
|
||||
private:
|
||||
TArray<IConsoleCommand*> Commands;
|
||||
};
|
||||
|
||||
IMPLEMENT_MODULE(FVerticalPartitionModule, VerticalPartition);
|
||||
@ -0,0 +1,632 @@
|
||||
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||
#include "VerticalPartitionRuntimeSubsystem.h"
|
||||
#include "VerticalPartitionDescriptor.h"
|
||||
#include "VerticalPartitionSettings.h"
|
||||
#include "VerticalPartitionLog.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Level.h"
|
||||
#include "Engine/LevelStreaming.h"
|
||||
#include "Engine/LevelStreamingDynamic.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Engine/StaticMeshActor.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
|
||||
UVerticalPartitionRuntimeSubsystem* UVerticalPartitionRuntimeSubsystem::Get(const UObject* WorldContextObject)
|
||||
{
|
||||
if (!WorldContextObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (const UWorld* World = WorldContextObject->GetWorld())
|
||||
{
|
||||
return World->GetSubsystem<UVerticalPartitionRuntimeSubsystem>();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::Deinitialize()
|
||||
{
|
||||
UnregisterDescriptor();
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::RegisterDescriptor(UVerticalPartitionDescriptor* InDescriptor, const FBox& InVolumeBounds)
|
||||
{
|
||||
UnregisterDescriptor();
|
||||
|
||||
Descriptor = InDescriptor;
|
||||
if (!Descriptor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EffectiveSettings = Descriptor->Settings;
|
||||
VolumeBounds = InVolumeBounds.IsValid ? InVolumeBounds : Descriptor->VolumeBounds;
|
||||
|
||||
Cells.Reset();
|
||||
Cells.Reserve(Descriptor->Cells.Num());
|
||||
|
||||
for (int32 i = 0; i < Descriptor->Cells.Num(); ++i)
|
||||
{
|
||||
const FVerticalCellDescriptor& D = Descriptor->Cells[i];
|
||||
|
||||
FVerticalCellRuntime RT;
|
||||
RT.Id = D.CellId;
|
||||
RT.DescriptorIndex = i;
|
||||
RT.Bounds = D.Bounds;
|
||||
RT.EstimatedMemoryMB = D.EstimatedMemoryMB;
|
||||
RT.bUsesStreamingBackend = D.FullCellPackage.IsValid();
|
||||
|
||||
if (!RT.bUsesStreamingBackend)
|
||||
{
|
||||
// NonDestructive: the actors are already in the persistent level. Resolve
|
||||
// them once and start from "Full" (they are currently visible).
|
||||
RT.ResolvedActors.Reserve(D.SourceActors.Num());
|
||||
for (const FSoftObjectPath& Path : D.SourceActors)
|
||||
{
|
||||
if (AActor* Actor = Cast<AActor>(Path.ResolveObject()))
|
||||
{
|
||||
RT.ResolvedActors.Add(Actor);
|
||||
}
|
||||
}
|
||||
RT.State = EVerticalCellState::Full;
|
||||
}
|
||||
else
|
||||
{
|
||||
RT.State = EVerticalCellState::Unloaded;
|
||||
}
|
||||
|
||||
Cells.Add(RT.Id, MoveTemp(RT));
|
||||
}
|
||||
|
||||
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Registered descriptor '%s' with %d cells (%s backend)."),
|
||||
*Descriptor->GetName(), Cells.Num(), Descriptor->HasFullPackages() ? TEXT("streaming") : TEXT("visibility"));
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::UnregisterDescriptor()
|
||||
{
|
||||
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||
{
|
||||
FVerticalCellRuntime& C = Pair.Value;
|
||||
// Restore persistent actors so the level looks normal after PIE/unregister.
|
||||
if (!C.bUsesStreamingBackend)
|
||||
{
|
||||
SetActorGroupActive(C, true, true, true);
|
||||
}
|
||||
if (C.ProxyActor.IsValid())
|
||||
{
|
||||
C.ProxyActor->Destroy();
|
||||
}
|
||||
if (C.StreamingLevel.IsValid())
|
||||
{
|
||||
C.StreamingLevel->SetShouldBeVisible(false);
|
||||
C.StreamingLevel->SetShouldBeLoaded(false);
|
||||
}
|
||||
}
|
||||
Cells.Reset();
|
||||
Descriptor = nullptr;
|
||||
Stats = FVerticalRuntimeStats();
|
||||
bShowOnlyMode = false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main update
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::UpdateStreaming(const FVerticalStreamingContext& Ctx, float /*DeltaSeconds*/, bool bSnap)
|
||||
{
|
||||
if (!Descriptor || Cells.Num() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const double T0 = FPlatformTime::Seconds();
|
||||
Stats.PlayerCellZ = Ctx.PlayerCellZ;
|
||||
|
||||
// 1) Desired state for every cell.
|
||||
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||
{
|
||||
FVerticalCellRuntime& C = Pair.Value;
|
||||
PollStreaming(C); // refresh State from in-flight async ops first
|
||||
|
||||
if (C.bForced)
|
||||
{
|
||||
C.Desired = C.ForcedState;
|
||||
}
|
||||
else if (bShowOnlyMode)
|
||||
{
|
||||
C.Desired = (C.Id.Z == ShowOnlyZ) ? EVerticalCellDesiredState::Full : EVerticalCellDesiredState::Unloaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
C.Desired = UVerticalPartitionStatics::GetDesiredStateForCell(C.Id, Ctx, EffectiveSettings, C.State);
|
||||
}
|
||||
|
||||
C.bPreloadRequested = (C.Desired == EVerticalCellDesiredState::HLOD)
|
||||
&& UVerticalPartitionStatics::ShouldPreloadFull(C.Id, Ctx, EffectiveSettings);
|
||||
}
|
||||
|
||||
// 2) Enforce budgets: keep the nearest MaxFullCells as Full, nearest
|
||||
// MaxVisibleHLODCells as HLOD, demote the rest.
|
||||
{
|
||||
TArray<FVerticalCellRuntime*> Ordered;
|
||||
Ordered.Reserve(Cells.Num());
|
||||
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||
{
|
||||
Ordered.Add(&Pair.Value);
|
||||
}
|
||||
const int32 PlayerZ = Ctx.PlayerCellZ;
|
||||
Ordered.Sort([PlayerZ](const FVerticalCellRuntime& A, const FVerticalCellRuntime& B)
|
||||
{
|
||||
return FMath::Abs(A.Id.Z - PlayerZ) < FMath::Abs(B.Id.Z - PlayerZ);
|
||||
});
|
||||
|
||||
int32 FullCount = 0;
|
||||
int32 HlodCount = 0;
|
||||
const int32 MaxFull = FMath::Max(0, EffectiveSettings.MaxFullCells);
|
||||
const int32 MaxHlod = FMath::Max(0, EffectiveSettings.MaxVisibleHLODCells);
|
||||
for (FVerticalCellRuntime* C : Ordered)
|
||||
{
|
||||
if (C->bForced) { continue; } // forced cells ignore budgets
|
||||
if (C->Desired == EVerticalCellDesiredState::Full)
|
||||
{
|
||||
if (FullCount < MaxFull) { ++FullCount; }
|
||||
else { C->Desired = EVerticalCellDesiredState::HLOD; }
|
||||
}
|
||||
if (C->Desired == EVerticalCellDesiredState::HLOD)
|
||||
{
|
||||
if (HlodCount < MaxHlod) { ++HlodCount; }
|
||||
else { C->Desired = EVerticalCellDesiredState::Unloaded; }
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Apply transitions, nearest first, within the async budget.
|
||||
int32 LoadBudget = bSnap ? MAX_int32 : FMath::Max(1, EffectiveSettings.MaxAsyncLoadsPerFrame);
|
||||
int32 UnloadBudget = bSnap ? MAX_int32 : FMath::Max(1, EffectiveSettings.MaxAsyncUnloadsPerFrame);
|
||||
for (FVerticalCellRuntime* C : Ordered)
|
||||
{
|
||||
ApplyDesiredState(*C, bSnap, LoadBudget, UnloadBudget);
|
||||
}
|
||||
}
|
||||
|
||||
RebuildStatsFromCells();
|
||||
Stats.LastUpdateTimeMS = (float)((FPlatformTime::Seconds() - T0) * 1000.0);
|
||||
Stats.LastStreamingTimeMS = Stats.LastUpdateTimeMS;
|
||||
|
||||
if (const UVerticalPartitionDeveloperSettings* Project = GetDefault<UVerticalPartitionDeveloperSettings>())
|
||||
{
|
||||
if (Stats.LastUpdateTimeMS > Project->HitchThresholdMS)
|
||||
{
|
||||
++Stats.HitchesOverThreshold;
|
||||
UE_LOG(LogVerticalPartition, Verbose, TEXT("[VP] Streaming update hitch: %.2f ms"), Stats.LastUpdateTimeMS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::ApplyDesiredState(FVerticalCellRuntime& C, bool bSnap, int32& LoadBudget, int32& UnloadBudget)
|
||||
{
|
||||
switch (C.Desired)
|
||||
{
|
||||
case EVerticalCellDesiredState::Full:
|
||||
if (C.bUsesStreamingBackend)
|
||||
{
|
||||
if (C.State != EVerticalCellState::Full)
|
||||
{
|
||||
if (LoadBudget > 0)
|
||||
{
|
||||
LoadCellFullAsync(C, /*bVisible*/true);
|
||||
--LoadBudget;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
HideProxy(C);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetActorGroupActive(C, /*bVisible*/true, /*bCollision*/true, /*bTick*/true);
|
||||
HideProxy(C);
|
||||
C.State = EVerticalCellState::Full;
|
||||
}
|
||||
break;
|
||||
|
||||
case EVerticalCellDesiredState::HLOD:
|
||||
ShowCellHLOD(C);
|
||||
if (C.bUsesStreamingBackend)
|
||||
{
|
||||
if (C.bPreloadRequested)
|
||||
{
|
||||
if (C.State != EVerticalCellState::HLOD && C.State != EVerticalCellState::PreloadingFull && LoadBudget > 0)
|
||||
{
|
||||
LoadCellFullAsync(C, /*bVisible*/false); // resident, hidden
|
||||
--LoadBudget;
|
||||
}
|
||||
}
|
||||
else if (C.StreamingLevel.IsValid() && UnloadBudget > 0)
|
||||
{
|
||||
C.StreamingLevel->SetShouldBeVisible(false);
|
||||
C.StreamingLevel->SetShouldBeLoaded(false); // free full data while showing proxy
|
||||
--UnloadBudget;
|
||||
}
|
||||
if (C.State == EVerticalCellState::Full || C.State == EVerticalCellState::Unloaded)
|
||||
{
|
||||
C.State = EVerticalCellState::HLOD;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetActorGroupActive(C, /*bVisible*/false, /*bCollision*/false, /*bTick*/false);
|
||||
C.State = EVerticalCellState::HLOD;
|
||||
}
|
||||
break;
|
||||
|
||||
case EVerticalCellDesiredState::Unloaded:
|
||||
default:
|
||||
if (C.bUsesStreamingBackend)
|
||||
{
|
||||
if (C.State != EVerticalCellState::Unloaded && UnloadBudget > 0)
|
||||
{
|
||||
UnloadCell(C);
|
||||
--UnloadBudget;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UnloadCell(C);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::LoadCellFullAsync(FVerticalCellRuntime& C, bool bVisible)
|
||||
{
|
||||
ULevelStreaming* L = EnsureStreamingLevel(C);
|
||||
if (!L)
|
||||
{
|
||||
C.State = EVerticalCellState::Error;
|
||||
return;
|
||||
}
|
||||
L->SetShouldBeLoaded(true);
|
||||
L->SetShouldBeVisible(bVisible);
|
||||
C.State = bVisible ? EVerticalCellState::LoadingFull : EVerticalCellState::PreloadingFull;
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::ShowCellHLOD(FVerticalCellRuntime& C)
|
||||
{
|
||||
if (AStaticMeshActor* Proxy = EnsureProxy(C))
|
||||
{
|
||||
Proxy->SetActorHiddenInGame(false);
|
||||
}
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::UnloadCell(FVerticalCellRuntime& C)
|
||||
{
|
||||
if (C.bUsesStreamingBackend)
|
||||
{
|
||||
if (C.StreamingLevel.IsValid())
|
||||
{
|
||||
C.StreamingLevel->SetShouldBeVisible(false);
|
||||
C.StreamingLevel->SetShouldBeLoaded(false);
|
||||
C.State = EVerticalCellState::Unloading;
|
||||
}
|
||||
else
|
||||
{
|
||||
C.State = EVerticalCellState::Unloaded;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetActorGroupActive(C, /*bVisible*/false, /*bCollision*/false, /*bTick*/false);
|
||||
C.State = EVerticalCellState::Unloaded;
|
||||
}
|
||||
HideProxy(C);
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::PollStreaming(FVerticalCellRuntime& C)
|
||||
{
|
||||
if (!C.bUsesStreamingBackend || !C.StreamingLevel.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
ULevelStreaming* L = C.StreamingLevel.Get();
|
||||
const ELevelStreamingState St = L->GetLevelStreamingState();
|
||||
const bool bVisible = (St == ELevelStreamingState::LoadedVisible);
|
||||
const bool bLoaded = bVisible
|
||||
|| St == ELevelStreamingState::LoadedNotVisible
|
||||
|| St == ELevelStreamingState::MakingVisible
|
||||
|| St == ELevelStreamingState::MakingInvisible;
|
||||
|
||||
if (St == ELevelStreamingState::FailedToLoad)
|
||||
{
|
||||
C.State = EVerticalCellState::Error;
|
||||
return;
|
||||
}
|
||||
|
||||
if (C.Desired == EVerticalCellDesiredState::Full)
|
||||
{
|
||||
if (bVisible) { C.State = EVerticalCellState::Full; HideProxy(C); }
|
||||
else { C.State = EVerticalCellState::LoadingFull; }
|
||||
}
|
||||
else if (C.Desired == EVerticalCellDesiredState::HLOD)
|
||||
{
|
||||
C.State = (C.bPreloadRequested && !bLoaded) ? EVerticalCellState::PreloadingFull : EVerticalCellState::HLOD;
|
||||
}
|
||||
else
|
||||
{
|
||||
C.State = bLoaded ? EVerticalCellState::Unloading : EVerticalCellState::Unloaded;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Backend helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::SetActorGroupActive(FVerticalCellRuntime& C, bool bVisible, bool bCollision, bool bTick)
|
||||
{
|
||||
for (const TWeakObjectPtr<AActor>& Weak : C.ResolvedActors)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Actor->SetActorHiddenInGame(!bVisible);
|
||||
Actor->SetActorEnableCollision(bCollision);
|
||||
Actor->SetActorTickEnabled(bTick);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AStaticMeshActor* UVerticalPartitionRuntimeSubsystem::EnsureProxy(FVerticalCellRuntime& C)
|
||||
{
|
||||
if (C.ProxyActor.IsValid())
|
||||
{
|
||||
return C.ProxyActor.Get();
|
||||
}
|
||||
if (!Descriptor || !Descriptor->Cells.IsValidIndex(C.DescriptorIndex))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const FVerticalCellDescriptor& D = Descriptor->Cells[C.DescriptorIndex];
|
||||
if (!D.HLODProxyAsset.IsValid())
|
||||
{
|
||||
if (!C.bProxyMissingWarned)
|
||||
{
|
||||
UE_LOG(LogVerticalPartition, Verbose, TEXT("[VP] Cell %s has no HLOD proxy; far cell will simply hide."), *C.Id.ToString());
|
||||
C.bProxyMissingWarned = true;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UStaticMesh* Mesh = Cast<UStaticMesh>(D.HLODProxyAsset.TryLoad());
|
||||
UWorld* World = GetWorld();
|
||||
if (!Mesh || !World)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FActorSpawnParameters Params;
|
||||
Params.ObjectFlags |= RF_Transient;
|
||||
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
// Proxy meshes are baked with bPivotPointAtZero (world-space verts), so the actor
|
||||
// goes at the world origin and the geometry lands exactly over the source actors.
|
||||
AStaticMeshActor* Actor = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator, Params);
|
||||
if (!Actor)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (UStaticMeshComponent* Comp = Actor->GetStaticMeshComponent())
|
||||
{
|
||||
Comp->SetMobility(EComponentMobility::Movable);
|
||||
Comp->SetStaticMesh(Mesh);
|
||||
Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
Comp->SetCanEverAffectNavigation(false);
|
||||
}
|
||||
Actor->SetActorHiddenInGame(true);
|
||||
C.ProxyActor = Actor;
|
||||
return Actor;
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::HideProxy(FVerticalCellRuntime& C)
|
||||
{
|
||||
if (C.ProxyActor.IsValid())
|
||||
{
|
||||
C.ProxyActor->SetActorHiddenInGame(true);
|
||||
}
|
||||
}
|
||||
|
||||
ULevelStreaming* UVerticalPartitionRuntimeSubsystem::EnsureStreamingLevel(FVerticalCellRuntime& C)
|
||||
{
|
||||
if (C.StreamingLevel.IsValid())
|
||||
{
|
||||
return C.StreamingLevel.Get();
|
||||
}
|
||||
UWorld* World = GetWorld();
|
||||
if (!World || !Descriptor || !Descriptor->Cells.IsValidIndex(C.DescriptorIndex))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const FVerticalCellDescriptor& D = Descriptor->Cells[C.DescriptorIndex];
|
||||
const FString PackageName = D.FullCellPackage.GetLongPackageName();
|
||||
if (PackageName.IsEmpty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const FName PackageFName(*PackageName);
|
||||
|
||||
for (ULevelStreaming* Existing : World->GetStreamingLevels())
|
||||
{
|
||||
if (Existing && Existing->GetWorldAssetPackageFName() == PackageFName)
|
||||
{
|
||||
C.StreamingLevel = Existing;
|
||||
return Existing;
|
||||
}
|
||||
}
|
||||
|
||||
ULevelStreamingDynamic* NewStream = NewObject<ULevelStreamingDynamic>(World, ULevelStreamingDynamic::StaticClass(), NAME_None, RF_Transient);
|
||||
NewStream->SetWorldAssetByPackageName(PackageFName);
|
||||
NewStream->SetShouldBeLoaded(false);
|
||||
NewStream->SetShouldBeVisible(false);
|
||||
NewStream->bShouldBlockOnLoad = false;
|
||||
World->AddStreamingLevel(NewStream);
|
||||
C.StreamingLevel = NewStream;
|
||||
return NewStream;
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::RebuildStatsFromCells()
|
||||
{
|
||||
Stats.FullCellsLoaded = 0;
|
||||
Stats.HLODCellsVisible = 0;
|
||||
Stats.CellsUnloaded = 0;
|
||||
Stats.CellsLoading = 0;
|
||||
Stats.ActiveAsyncLoads = 0;
|
||||
Stats.EstimatedMemoryMB = 0.f;
|
||||
|
||||
for (const TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||
{
|
||||
const FVerticalCellRuntime& C = Pair.Value;
|
||||
switch (C.State)
|
||||
{
|
||||
case EVerticalCellState::Full:
|
||||
++Stats.FullCellsLoaded;
|
||||
Stats.EstimatedMemoryMB += C.EstimatedMemoryMB;
|
||||
break;
|
||||
case EVerticalCellState::HLOD:
|
||||
++Stats.HLODCellsVisible;
|
||||
break;
|
||||
case EVerticalCellState::LoadingFull:
|
||||
case EVerticalCellState::LoadingHLOD:
|
||||
case EVerticalCellState::PreloadingFull:
|
||||
++Stats.CellsLoading;
|
||||
++Stats.ActiveAsyncLoads;
|
||||
Stats.EstimatedMemoryMB += C.EstimatedMemoryMB; // resident while preloading
|
||||
break;
|
||||
case EVerticalCellState::Unloading:
|
||||
++Stats.CellsLoading;
|
||||
break;
|
||||
case EVerticalCellState::Unloaded:
|
||||
default:
|
||||
++Stats.CellsUnloaded;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FVerticalCellRuntime* UVerticalPartitionRuntimeSubsystem::FindCellByZ(int32 Z)
|
||||
{
|
||||
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||
{
|
||||
if (Pair.Value.Id.Z == Z)
|
||||
{
|
||||
return &Pair.Value;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Live overrides + debug forcing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::SetFullRange(int32 Below, int32 Above)
|
||||
{
|
||||
EffectiveSettings.FullLoadCellsBelow = FMath::Max(0, Below);
|
||||
EffectiveSettings.FullLoadCellsAbove = FMath::Max(0, Above);
|
||||
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Full range set to below=%d above=%d"), Below, Above);
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::SetHLODRange(int32 Below, int32 Above)
|
||||
{
|
||||
EffectiveSettings.HLODLoadCellsBelow = FMath::Max(0, Below);
|
||||
EffectiveSettings.HLODLoadCellsAbove = FMath::Max(0, Above);
|
||||
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] HLOD range set to below=%d above=%d"), Below, Above);
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::SetCellHeight(float NewHeight)
|
||||
{
|
||||
EffectiveSettings.CellHeight = FMath::Max(1.f, NewHeight);
|
||||
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] CellHeight changed at runtime to %.0f. Note: cell boundaries are baked in the descriptor; this only affects velocity prediction. Rebuild to re-segment."), NewHeight);
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::ForceCellFull(int32 Z)
|
||||
{
|
||||
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||
{
|
||||
if (Pair.Value.Id.Z == Z) { Pair.Value.bForced = true; Pair.Value.ForcedState = EVerticalCellDesiredState::Full; }
|
||||
}
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::ForceCellHLOD(int32 Z)
|
||||
{
|
||||
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||
{
|
||||
if (Pair.Value.Id.Z == Z) { Pair.Value.bForced = true; Pair.Value.ForcedState = EVerticalCellDesiredState::HLOD; }
|
||||
}
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::UnforceCell(int32 Z)
|
||||
{
|
||||
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||
{
|
||||
if (Pair.Value.Id.Z == Z) { Pair.Value.bForced = false; }
|
||||
}
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::UnforceAll()
|
||||
{
|
||||
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||
{
|
||||
Pair.Value.bForced = false;
|
||||
}
|
||||
bShowOnlyMode = false;
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::ShowOnlyCell(int32 Z)
|
||||
{
|
||||
bShowOnlyMode = true;
|
||||
ShowOnlyZ = Z;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reporting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::DumpRuntimeStats() const
|
||||
{
|
||||
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] ==== Runtime Stats ===="));
|
||||
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] PlayerCellZ=%d Full=%d HLOD=%d Unloaded=%d Loading=%d Async=%d Mem~%.1fMB Update=%.2fms Hitches=%d"),
|
||||
Stats.PlayerCellZ, Stats.FullCellsLoaded, Stats.HLODCellsVisible, Stats.CellsUnloaded,
|
||||
Stats.CellsLoading, Stats.ActiveAsyncLoads, Stats.EstimatedMemoryMB, Stats.LastUpdateTimeMS, Stats.HitchesOverThreshold);
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::DumpCells() const
|
||||
{
|
||||
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] ==== Cells (%d) ===="), Cells.Num());
|
||||
for (const TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||
{
|
||||
const FVerticalCellRuntime& C = Pair.Value;
|
||||
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] %s state=%d desired=%d actors=%d backend=%s forced=%d"),
|
||||
*C.Id.ToString(), (int32)C.State, (int32)C.Desired, C.ResolvedActors.Num(),
|
||||
C.bUsesStreamingBackend ? TEXT("stream") : TEXT("vis"), C.bForced ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
void UVerticalPartitionRuntimeSubsystem::DumpWarnings() const
|
||||
{
|
||||
if (!Descriptor)
|
||||
{
|
||||
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] No descriptor registered."));
|
||||
return;
|
||||
}
|
||||
const FVerticalPartitionBuildReport& R = Descriptor->LastReport;
|
||||
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] ==== Build Warnings (%d) / Errors (%d) ===="), R.Warnings.Num(), R.Errors.Num());
|
||||
for (const FVerticalPartitionWarning& W : R.Warnings)
|
||||
{
|
||||
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] WARN %s :: %s"),
|
||||
*UVerticalPartitionStatics::WarningTypeToString(W.Type), *W.Message);
|
||||
}
|
||||
for (const FVerticalPartitionError& E : R.Errors)
|
||||
{
|
||||
UE_LOG(LogVerticalPartition, Error, TEXT("[VP] ERR %s :: %s"),
|
||||
*UVerticalPartitionStatics::WarningTypeToString(E.Type), *E.Message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||
#include "VerticalPartitionSettings.h"
|
||||
|
||||
UVerticalPartitionDeveloperSettings::UVerticalPartitionDeveloperSettings()
|
||||
{
|
||||
// DefaultSettings keeps the struct defaults declared in VerticalPartitionTypes.h.
|
||||
}
|
||||
|
||||
const UVerticalPartitionDeveloperSettings* UVerticalPartitionDeveloperSettings::Get()
|
||||
{
|
||||
return GetDefault<UVerticalPartitionDeveloperSettings>();
|
||||
}
|
||||
257
Source/VerticalPartition/Private/VerticalPartitionStatics.cpp
Normal file
257
Source/VerticalPartition/Private/VerticalPartitionStatics.cpp
Normal file
@ -0,0 +1,257 @@
|
||||
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||
#include "VerticalPartitionStatics.h"
|
||||
|
||||
int32 UVerticalPartitionStatics::GetCellZFromWorldZ(float WorldZ, float VolumeMinZ, float CellHeight)
|
||||
{
|
||||
if (CellHeight <= KINDA_SMALL_NUMBER)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return FMath::FloorToInt((WorldZ - VolumeMinZ) / CellHeight);
|
||||
}
|
||||
|
||||
int32 UVerticalPartitionStatics::GetCellXFromWorldX(float WorldX, float VolumeMinX, float XYCellSize)
|
||||
{
|
||||
if (XYCellSize <= KINDA_SMALL_NUMBER)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return FMath::FloorToInt((WorldX - VolumeMinX) / XYCellSize);
|
||||
}
|
||||
|
||||
int32 UVerticalPartitionStatics::GetCellYFromWorldY(float WorldY, float VolumeMinY, float XYCellSize)
|
||||
{
|
||||
if (XYCellSize <= KINDA_SMALL_NUMBER)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return FMath::FloorToInt((WorldY - VolumeMinY) / XYCellSize);
|
||||
}
|
||||
|
||||
int32 UVerticalPartitionStatics::GetNumZCells(const FBox& VolumeBounds, const FVerticalPartitionSettings& S)
|
||||
{
|
||||
if (!VolumeBounds.IsValid || S.CellHeight <= KINDA_SMALL_NUMBER)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
const float SizeZ = VolumeBounds.Max.Z - VolumeBounds.Min.Z;
|
||||
return FMath::Max(1, FMath::CeilToInt(SizeZ / S.CellHeight));
|
||||
}
|
||||
|
||||
FVerticalCellId UVerticalPartitionStatics::GetCellIdFromPoint(const FVector& Point, const FBox& VolumeBounds, const FVerticalPartitionSettings& S)
|
||||
{
|
||||
FVerticalCellId Id;
|
||||
Id.Z = GetCellZFromWorldZ(Point.Z, VolumeBounds.Min.Z, S.CellHeight);
|
||||
if (S.bUseXYSubCells)
|
||||
{
|
||||
Id.X = GetCellXFromWorldX(Point.X, VolumeBounds.Min.X, S.XYCellSize);
|
||||
Id.Y = GetCellYFromWorldY(Point.Y, VolumeBounds.Min.Y, S.XYCellSize);
|
||||
}
|
||||
|
||||
if (S.bClampToVolumeBounds && VolumeBounds.IsValid)
|
||||
{
|
||||
const int32 NumZ = GetNumZCells(VolumeBounds, S);
|
||||
Id.Z = FMath::Clamp(Id.Z, 0, NumZ - 1);
|
||||
if (S.bUseXYSubCells && S.XYCellSize > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
const int32 NumX = FMath::Max(1, FMath::CeilToInt((VolumeBounds.Max.X - VolumeBounds.Min.X) / S.XYCellSize));
|
||||
const int32 NumY = FMath::Max(1, FMath::CeilToInt((VolumeBounds.Max.Y - VolumeBounds.Min.Y) / S.XYCellSize));
|
||||
Id.X = FMath::Clamp(Id.X, 0, NumX - 1);
|
||||
Id.Y = FMath::Clamp(Id.Y, 0, NumY - 1);
|
||||
}
|
||||
}
|
||||
return Id;
|
||||
}
|
||||
|
||||
FVerticalCellId UVerticalPartitionStatics::GetCellIdFromBounds(const FBox& ActorBounds, const FBox& VolumeBounds, const FVerticalPartitionSettings& S, bool& bOutCrossesMultiple)
|
||||
{
|
||||
bOutCrossesMultiple = false;
|
||||
if (!ActorBounds.IsValid)
|
||||
{
|
||||
return FVerticalCellId();
|
||||
}
|
||||
|
||||
// Dominant cell = the cell containing the bounds centre (deterministic).
|
||||
const FVerticalCellId Id = GetCellIdFromPoint(ActorBounds.GetCenter(), VolumeBounds, S);
|
||||
|
||||
// Does the box span more than one cell on any partitioned axis?
|
||||
const int32 MinZ = GetCellZFromWorldZ(ActorBounds.Min.Z, VolumeBounds.Min.Z, S.CellHeight);
|
||||
const int32 MaxZ = GetCellZFromWorldZ(ActorBounds.Max.Z, VolumeBounds.Min.Z, S.CellHeight);
|
||||
bOutCrossesMultiple = (MinZ != MaxZ);
|
||||
|
||||
if (S.bUseXYSubCells && !bOutCrossesMultiple)
|
||||
{
|
||||
const int32 MinX = GetCellXFromWorldX(ActorBounds.Min.X, VolumeBounds.Min.X, S.XYCellSize);
|
||||
const int32 MaxX = GetCellXFromWorldX(ActorBounds.Max.X, VolumeBounds.Min.X, S.XYCellSize);
|
||||
const int32 MinY = GetCellYFromWorldY(ActorBounds.Min.Y, VolumeBounds.Min.Y, S.XYCellSize);
|
||||
const int32 MaxY = GetCellYFromWorldY(ActorBounds.Max.Y, VolumeBounds.Min.Y, S.XYCellSize);
|
||||
bOutCrossesMultiple = (MinX != MaxX) || (MinY != MaxY);
|
||||
}
|
||||
|
||||
return Id;
|
||||
}
|
||||
|
||||
FBox UVerticalPartitionStatics::GetCellBounds(const FVerticalCellId& Cell, const FBox& VolumeBounds, const FVerticalPartitionSettings& S)
|
||||
{
|
||||
FVector Min = VolumeBounds.Min;
|
||||
FVector Max = VolumeBounds.Max;
|
||||
|
||||
Min.Z = VolumeBounds.Min.Z + Cell.Z * S.CellHeight;
|
||||
Max.Z = Min.Z + S.CellHeight;
|
||||
|
||||
if (S.bUseXYSubCells && S.XYCellSize > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
Min.X = VolumeBounds.Min.X + Cell.X * S.XYCellSize;
|
||||
Max.X = Min.X + S.XYCellSize;
|
||||
Min.Y = VolumeBounds.Min.Y + Cell.Y * S.XYCellSize;
|
||||
Max.Y = Min.Y + S.XYCellSize;
|
||||
}
|
||||
|
||||
FBox Result(Min, Max);
|
||||
if (S.bClampToVolumeBounds && VolumeBounds.IsValid)
|
||||
{
|
||||
Result = Result.Overlap(VolumeBounds);
|
||||
if (!Result.IsValid)
|
||||
{
|
||||
Result = FBox(Min, Max); // degenerate clamp: keep nominal bounds
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
// Recompute the predicted player Z cell (shared by desired-state + preload).
|
||||
static int32 EffectivePlayerZ(const FVerticalStreamingContext& Ctx, const FVerticalPartitionSettings& S)
|
||||
{
|
||||
int32 Eff = Ctx.PlayerCellZ;
|
||||
if (S.bUsePlayerVelocityPrediction && S.CellHeight > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
Eff += FMath::RoundToInt((Ctx.PlayerVelocityZ * S.VelocityPredictionSeconds) / S.CellHeight);
|
||||
}
|
||||
return Eff;
|
||||
}
|
||||
|
||||
EVerticalCellDesiredState UVerticalPartitionStatics::GetDesiredStateForCell(
|
||||
const FVerticalCellId& Cell, const FVerticalStreamingContext& Ctx,
|
||||
const FVerticalPartitionSettings& S, EVerticalCellState CurrentState)
|
||||
{
|
||||
const int32 EffPlayerZ = EffectivePlayerZ(Ctx, S);
|
||||
const int32 Dz = Cell.Z - EffPlayerZ; // +above the player
|
||||
|
||||
int32 FullAbove = S.FullLoadCellsAbove;
|
||||
int32 FullBelow = S.FullLoadCellsBelow;
|
||||
int32 HlodAbove = S.HLODLoadCellsAbove;
|
||||
int32 HlodBelow = S.HLODLoadCellsBelow;
|
||||
|
||||
// Bias the reach upward: in a climbing map the route is above you.
|
||||
if (S.bPrioritizeAbovePlayer)
|
||||
{
|
||||
HlodAbove += 1;
|
||||
}
|
||||
// Look up -> extend upward; look down -> extend downward.
|
||||
if (S.bUseCameraDirectionPriority)
|
||||
{
|
||||
if (Ctx.CameraPitchDeg > 15.f) { FullAbove += 1; HlodAbove += 1; }
|
||||
else if (Ctx.CameraPitchDeg < -15.f) { HlodBelow += 1; }
|
||||
}
|
||||
// You can always fall back down: keep the cells below loaded out to the unload edge.
|
||||
if (S.bKeepBelowPlayerLoadedOnFallRisk)
|
||||
{
|
||||
HlodBelow = FMath::Max(HlodBelow, S.UnloadCellsBelow);
|
||||
}
|
||||
|
||||
const int32 Hys = FMath::Max(0, S.HysteresisCells);
|
||||
const bool bCurFullish = (CurrentState == EVerticalCellState::Full
|
||||
|| CurrentState == EVerticalCellState::LoadingFull
|
||||
|| CurrentState == EVerticalCellState::PreloadingFull);
|
||||
const bool bCurHlodish = (CurrentState == EVerticalCellState::HLOD
|
||||
|| CurrentState == EVerticalCellState::LoadingHLOD);
|
||||
|
||||
EVerticalCellDesiredState Result;
|
||||
if (Dz >= -FullBelow && Dz <= FullAbove)
|
||||
{
|
||||
Result = EVerticalCellDesiredState::Full;
|
||||
}
|
||||
else if (bCurFullish && Dz >= -(FullBelow + Hys) && Dz <= (FullAbove + Hys))
|
||||
{
|
||||
Result = EVerticalCellDesiredState::Full; // sticky: don't drop a loaded cell at the edge
|
||||
}
|
||||
else if (Dz >= -HlodBelow && Dz <= HlodAbove)
|
||||
{
|
||||
Result = EVerticalCellDesiredState::HLOD;
|
||||
}
|
||||
else if (bCurHlodish && Dz >= -(HlodBelow + Hys) && Dz <= (HlodAbove + Hys))
|
||||
{
|
||||
Result = EVerticalCellDesiredState::HLOD;
|
||||
}
|
||||
else
|
||||
{
|
||||
Result = EVerticalCellDesiredState::Unloaded;
|
||||
}
|
||||
|
||||
// XY refinement (optional): far columns never go full and drop out past 2 rings.
|
||||
if (Ctx.bUseXY)
|
||||
{
|
||||
const int32 XYDist = FMath::Max(FMath::Abs(Cell.X - Ctx.PlayerCellX), FMath::Abs(Cell.Y - Ctx.PlayerCellY));
|
||||
if (XYDist > 2)
|
||||
{
|
||||
Result = EVerticalCellDesiredState::Unloaded;
|
||||
}
|
||||
else if (XYDist > 1 && Result == EVerticalCellDesiredState::Full)
|
||||
{
|
||||
Result = EVerticalCellDesiredState::HLOD;
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool UVerticalPartitionStatics::ShouldPreloadFull(const FVerticalCellId& Cell, const FVerticalStreamingContext& Ctx, const FVerticalPartitionSettings& S)
|
||||
{
|
||||
const int32 EffPlayerZ = EffectivePlayerZ(Ctx, S);
|
||||
const int32 Dz = Cell.Z - EffPlayerZ;
|
||||
const bool bAbovePreload = (Dz > S.FullLoadCellsAbove && Dz <= S.PreloadCellsAbove);
|
||||
const bool bBelowPreload = (Dz < -S.FullLoadCellsBelow && Dz >= -S.PreloadCellsBelow);
|
||||
return bAbovePreload || bBelowPreload;
|
||||
}
|
||||
|
||||
FLinearColor UVerticalPartitionStatics::GetStateColor(EVerticalCellState State)
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case EVerticalCellState::Full: return FLinearColor(0.10f, 0.85f, 0.15f); // green
|
||||
case EVerticalCellState::HLOD: return FLinearColor(0.15f, 0.45f, 0.95f); // blue
|
||||
case EVerticalCellState::LoadingHLOD:
|
||||
case EVerticalCellState::LoadingFull:
|
||||
case EVerticalCellState::PreloadingFull:
|
||||
case EVerticalCellState::Unloading: return FLinearColor(0.95f, 0.85f, 0.10f); // yellow
|
||||
case EVerticalCellState::Error: return FLinearColor(0.95f, 0.10f, 0.10f); // red
|
||||
case EVerticalCellState::Unloaded:
|
||||
default: return FLinearColor(0.35f, 0.35f, 0.35f); // gray
|
||||
}
|
||||
}
|
||||
|
||||
FString UVerticalPartitionStatics::WarningTypeToString(EVerticalWarningType Type)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case EVerticalWarningType::ActorCrossesMultipleCells: return TEXT("Actor crosses multiple cells");
|
||||
case EVerticalWarningType::ActorTooLarge: return TEXT("Actor too large");
|
||||
case EVerticalWarningType::ActorSimulatesPhysics: return TEXT("Actor simulates physics");
|
||||
case EVerticalWarningType::ActorHasBlueprintTick: return TEXT("Actor has Blueprint tick");
|
||||
case EVerticalWarningType::ActorDynamicMobility: return TEXT("Actor has dynamic mobility");
|
||||
case EVerticalWarningType::ActorHasExternalReferences:return TEXT("Actor has external references");
|
||||
case EVerticalWarningType::ActorAttachedAcrossCells: return TEXT("Attached children in another cell");
|
||||
case EVerticalWarningType::ActorDependsOnOtherCell: return TEXT("Depends on actor in another cell");
|
||||
case EVerticalWarningType::ActorNeedsManualReview: return TEXT("Gameplay tag requires manual review");
|
||||
case EVerticalWarningType::ActorHasAudioLightNav: return TEXT("Contains audio/light/nav components");
|
||||
case EVerticalWarningType::ActorNotStreamSafe: return TEXT("Actor not safe for streaming");
|
||||
case EVerticalWarningType::ActorHasUnsavedChanges: return TEXT("Actor has unsaved changes");
|
||||
case EVerticalWarningType::CellTooManyActors: return TEXT("Cell has too many actors");
|
||||
case EVerticalWarningType::CellTooManyDrawCalls: return TEXT("Cell has too many draw calls");
|
||||
case EVerticalWarningType::HLODGenerationFailed: return TEXT("HLOD generation failed");
|
||||
case EVerticalWarningType::PackageSaveFailed: return TEXT("Package save failed");
|
||||
case EVerticalWarningType::NoVolume: return TEXT("No partition volume");
|
||||
case EVerticalWarningType::DescriptorOutOfDate: return TEXT("Descriptor out of date");
|
||||
default: return TEXT("None");
|
||||
}
|
||||
}
|
||||
71
Source/VerticalPartition/Private/VerticalPartitionVolume.cpp
Normal file
71
Source/VerticalPartition/Private/VerticalPartitionVolume.cpp
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||
#include "VerticalPartitionVolume.h"
|
||||
#include "VerticalPartitionSettings.h"
|
||||
#include "VerticalPartitionLog.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
TFunction<void(AVerticalPartitionVolume*, EVerticalPartitionAction)> FVerticalPartitionEditorBridge::Handler;
|
||||
#endif
|
||||
|
||||
AVerticalPartitionVolume::AVerticalPartitionVolume()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
Box = CreateDefaultSubobject<UBoxComponent>(TEXT("Bounds"));
|
||||
SetRootComponent(Box);
|
||||
Box->SetBoxExtent(FVector(5000.f, 5000.f, 25000.f));
|
||||
Box->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
Box->SetCollisionProfileName(TEXT("NoCollision"));
|
||||
Box->SetGenerateOverlapEvents(false);
|
||||
Box->ShapeColor = FColor(80, 200, 255);
|
||||
Box->bDrawOnlyIfSelected = false;
|
||||
Box->SetHiddenInGame(true);
|
||||
|
||||
// Seed per-volume settings from project defaults.
|
||||
if (const UVerticalPartitionDeveloperSettings* Project = UVerticalPartitionDeveloperSettings::Get())
|
||||
{
|
||||
Settings = Project->DefaultSettings;
|
||||
}
|
||||
}
|
||||
|
||||
FBox AVerticalPartitionVolume::GetVolumeBounds() const
|
||||
{
|
||||
if (!Box)
|
||||
{
|
||||
return FBox(ForceInit);
|
||||
}
|
||||
const FVector Extent = Box->GetScaledBoxExtent();
|
||||
const FVector Origin = Box->GetComponentLocation();
|
||||
return FBox(Origin - Extent, Origin + Extent);
|
||||
}
|
||||
|
||||
void AVerticalPartitionVolume::OnConstruction(const FTransform& Transform)
|
||||
{
|
||||
Super::OnConstruction(Transform);
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void AVerticalPartitionVolume::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||
{
|
||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||
// Keep preview drawing fresh when ranges/segmentation change.
|
||||
if (Box)
|
||||
{
|
||||
Box->MarkRenderStateDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void AVerticalPartitionVolume::AnalyzeCurrentLevel() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::Analyze); }
|
||||
void AVerticalPartitionVolume::BuildVerticalPartition(){ FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::Build); }
|
||||
void AVerticalPartitionVolume::RebuildChangedCells() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::RebuildChangedCells); }
|
||||
void AVerticalPartitionVolume::RebuildHLOD() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::RebuildHLOD); }
|
||||
void AVerticalPartitionVolume::ValidateCells() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::Validate); }
|
||||
void AVerticalPartitionVolume::PreviewChunks() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::PreviewChunks); }
|
||||
void AVerticalPartitionVolume::SpawnHLODPreview() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::SpawnHLODPreview); }
|
||||
void AVerticalPartitionVolume::ClearPreview() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::ClearPreview); }
|
||||
void AVerticalPartitionVolume::CommitConversion() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::Commit); }
|
||||
void AVerticalPartitionVolume::RestoreFromBackup() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::RestoreBackup); }
|
||||
void AVerticalPartitionVolume::ClearGeneratedData() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::ClearGenerated); }
|
||||
void AVerticalPartitionVolume::DumpReport() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::DumpReport); }
|
||||
#endif
|
||||
Reference in New Issue
Block a user