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>
190 lines
5.1 KiB
C++
190 lines
5.1 KiB
C++
// 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);
|
|
}
|