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:
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);
|
||||
}
|
||||
Reference in New Issue
Block a user