Files
vertical-partition-system-p…/Source/VerticalPartition/Private/VerticalPartitionManager.cpp
Bonchellon 04ad6d3f25 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>
2026-06-30 17:42:41 +03:00

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);
}