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:
Bonchellon
2026-06-30 17:42:41 +03:00
commit 04ad6d3f25
28 changed files with 4361 additions and 0 deletions

View File

@ -0,0 +1,31 @@
// Copyright IHY. Vertical Partition Streaming plugin.
//
// World-Partition-style debug: coloured cell boxes, ids, actor counts, problem
// actor highlights, and an on-screen runtime overlay. Driven by the vp.Debug /
// vp.DebugDraw / vp.DebugOverlay console variables (and the volume's debug flags).
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "VerticalPartitionDebugComponent.generated.h"
class UVerticalPartitionRuntimeSubsystem;
UCLASS(ClassGroup=(VerticalPartition), meta=(BlueprintSpawnableComponent))
class VERTICALPARTITION_API UVerticalPartitionDebugComponent : public UActorComponent
{
GENERATED_BODY()
public:
UVerticalPartitionDebugComponent();
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/** 3D coloured cell boxes + ids + counts (the spec's DrawDebugCells). */
void DrawDebugCells(UVerticalPartitionRuntimeSubsystem* Sub) const;
/** On-screen text block of FVerticalRuntimeStats. */
void DrawOverlay(UVerticalPartitionRuntimeSubsystem* Sub) const;
private:
UVerticalPartitionRuntimeSubsystem* GetSub() const;
};

View File

@ -0,0 +1,50 @@
// Copyright IHY. Vertical Partition Streaming plugin.
//
// The build artifact: a data asset describing every generated cell plus a snapshot
// of the settings and volume bounds the build was run with. The runtime streamer
// consumes this; the editor builder produces it.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "VerticalPartitionTypes.h"
#include "VerticalPartitionDescriptor.generated.h"
UCLASS(BlueprintType)
class VERTICALPARTITION_API UVerticalPartitionDescriptor : public UDataAsset
{
GENERATED_BODY()
public:
/** Map this descriptor was built from (for rebuild / out-of-date checks). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FSoftObjectPath SourceLevel;
/** Settings snapshot used at build time (runtime ranges may be overridden live). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
FVerticalPartitionSettings Settings;
/** World bounds of the partition volume at build time. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FBox VolumeBounds = FBox(ForceInit);
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
EVerticalConversionMode BuiltMode = EVerticalConversionMode::NonDestructive;
/** Content hash of the source actors at build time (for incremental rebuild). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int64 SourceContentHash = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
TArray<FVerticalCellDescriptor> Cells;
/** Last build report (kept for the UI / Dump Report). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FVerticalPartitionBuildReport LastReport;
/** Find a cell descriptor by id. Returns nullptr if absent. */
const FVerticalCellDescriptor* FindCell(const FVerticalCellId& Id) const;
int32 FindCellIndex(const FVerticalCellId& Id) const;
bool HasFullPackages() const;
};

View File

@ -0,0 +1,6 @@
// Copyright IHY. Vertical Partition Streaming plugin.
#pragma once
#include "CoreMinimal.h"
VERTICALPARTITION_API DECLARE_LOG_CATEGORY_EXTERN(LogVerticalPartition, Log, All);

View File

@ -0,0 +1,62 @@
// Copyright IHY. Vertical Partition Streaming plugin.
//
// In-world coordinator. One per streamed level (auto-spawned by the volume at
// BeginPlay, or placed manually). Determines the tracked player's current cell,
// drives the runtime subsystem at the configured interval, and hosts the debug
// component. Keep this lightweight: the heavy lifting is in the subsystem.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "VerticalPartitionTypes.h"
#include "VerticalPartitionManager.generated.h"
class UVerticalPartitionDescriptor;
class UVerticalPartitionDebugComponent;
class AVerticalPartitionVolume;
UCLASS()
class VERTICALPARTITION_API AVerticalPartitionManager : public AActor
{
GENERATED_BODY()
public:
AVerticalPartitionManager();
/** Descriptor to stream. If unset, the manager searches the level for a volume. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
TSoftObjectPtr<UVerticalPartitionDescriptor> Descriptor;
/** Optional explicit volume (otherwise the first one found is used). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
TObjectPtr<AVerticalPartitionVolume> Volume;
/** Pawn whose position drives streaming. Defaults to local player pawn each tick. */
UPROPERTY(BlueprintReadWrite, Category="VerticalPartition")
TWeakObjectPtr<APawn> TrackedPawn;
UPROPERTY(VisibleAnywhere, Category="VerticalPartition")
TObjectPtr<UVerticalPartitionDebugComponent> DebugComponent;
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void Tick(float DeltaSeconds) override;
/** Snap streaming to the tracked pawn immediately (checkpoint / teleport). */
UFUNCTION(BlueprintCallable, Category="VerticalPartition")
void RequestSnap() { bSnapNextUpdate = true; }
/** Teleport the tracked pawn to the centre of a Z cell (debug). */
UFUNCTION(BlueprintCallable, Category="VerticalPartition")
void TeleportToCell(int32 Z);
private:
float UpdateAccumulator = 0.f;
bool bSnapNextUpdate = true; // snap on the first update
bool bHavePrevPos = false;
FVector PrevTrackedPos = FVector::ZeroVector;
FVector SmoothedVelocity = FVector::ZeroVector;
APawn* ResolveTrackedPawn();
bool BuildContext(struct FVerticalStreamingContext& OutCtx, float DeltaSeconds, bool& bOutTeleport);
};

View File

@ -0,0 +1,119 @@
// Copyright IHY. Vertical Partition Streaming plugin.
//
// The runtime streaming engine. World-scoped, holds the per-cell state machine,
// the async load queue, the proxy actors and the live statistics. Driven each
// update tick by AVerticalPartitionManager (subsystems are not ticked directly).
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "VerticalPartitionTypes.h"
#include "VerticalPartitionStatics.h"
#include "VerticalPartitionRuntimeSubsystem.generated.h"
class UVerticalPartitionDescriptor;
class ULevelStreaming;
class AStaticMeshActor;
/** Per-cell live runtime record. Plain struct (C++-only, same-module access). */
struct FVerticalCellRuntime
{
FVerticalCellId Id;
int32 DescriptorIndex = INDEX_NONE;
FBox Bounds = FBox(ForceInit);
EVerticalCellState State = EVerticalCellState::Unloaded;
EVerticalCellDesiredState Desired = EVerticalCellDesiredState::Unloaded;
/** Backend B (DestructiveCommit): the streaming sub-level for the full actors. */
TWeakObjectPtr<ULevelStreaming> StreamingLevel;
/** Backend A (NonDestructive): the original persistent-level actors of this cell. */
TArray<TWeakObjectPtr<AActor>> ResolvedActors;
/** Shown while the cell is HLOD. */
TWeakObjectPtr<AStaticMeshActor> ProxyActor;
bool bUsesStreamingBackend = false; // true => backend B
bool bPreloadRequested = false;
bool bProxyMissingWarned = false;
/** Debug lock: when set, Desired is pinned to ForcedState. */
bool bForced = false;
EVerticalCellDesiredState ForcedState = EVerticalCellDesiredState::Full;
float EstimatedMemoryMB = 0.f;
double LastChangeTime = 0.0;
};
UCLASS()
class VERTICALPARTITION_API UVerticalPartitionRuntimeSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
static UVerticalPartitionRuntimeSubsystem* Get(const UObject* WorldContextObject);
virtual void Deinitialize() override;
// ---- lifecycle (called by the manager) ----
void RegisterDescriptor(UVerticalPartitionDescriptor* InDescriptor, const FBox& InVolumeBounds);
void UnregisterDescriptor();
bool IsReady() const { return Descriptor != nullptr; }
/** Main entry point. Recomputes desired states and steps the machine within the
* per-update async budget. bSnap forces immediate convergence (teleport/checkpoint). */
void UpdateStreaming(const FVerticalStreamingContext& Ctx, float DeltaSeconds, bool bSnap = false);
// ---- live range overrides (console) ----
FVerticalPartitionSettings& GetMutableSettings() { return EffectiveSettings; }
const FVerticalPartitionSettings& GetSettings() const { return EffectiveSettings; }
void SetFullRange(int32 Below, int32 Above);
void SetHLODRange(int32 Below, int32 Above);
void SetCellHeight(float NewHeight);
// ---- debug forcing ----
void ForceCellFull(int32 Z);
void ForceCellHLOD(int32 Z);
void UnforceCell(int32 Z);
void UnforceAll();
void ShowOnlyCell(int32 Z); // force one Full, everything else Unloaded
// ---- introspection ----
const FVerticalRuntimeStats& GetStats() const { return Stats; }
const TMap<FVerticalCellId, FVerticalCellRuntime>& GetCellRuntimes() const { return Cells; }
const FBox& GetVolumeBounds() const { return VolumeBounds; }
UVerticalPartitionDescriptor* GetDescriptor() const { return Descriptor; }
int32 GetPlayerCellZ() const { return Stats.PlayerCellZ; }
// ---- reporting (console) ----
void DumpRuntimeStats() const;
void DumpCells() const;
void DumpWarnings() const;
private:
UPROPERTY(Transient)
TObjectPtr<UVerticalPartitionDescriptor> Descriptor = nullptr;
FVerticalPartitionSettings EffectiveSettings;
FBox VolumeBounds = FBox(ForceInit);
TMap<FVerticalCellId, FVerticalCellRuntime> Cells;
FVerticalRuntimeStats Stats;
bool bShowOnlyMode = false;
int32 ShowOnlyZ = 0;
// ---- state-machine helpers (the spec's key runtime functions) ----
void ApplyDesiredState(FVerticalCellRuntime& Cell, bool bSnap, int32& LoadBudget, int32& UnloadBudget);
void LoadCellFullAsync(FVerticalCellRuntime& Cell, bool bVisible);
void ShowCellHLOD(FVerticalCellRuntime& Cell);
void UnloadCell(FVerticalCellRuntime& Cell);
void PollStreaming(FVerticalCellRuntime& Cell);
void SetActorGroupActive(FVerticalCellRuntime& Cell, bool bVisible, bool bCollision, bool bTick);
AStaticMeshActor* EnsureProxy(FVerticalCellRuntime& Cell);
void HideProxy(FVerticalCellRuntime& Cell);
ULevelStreaming* EnsureStreamingLevel(FVerticalCellRuntime& Cell);
void RebuildStatsFromCells();
FVerticalCellRuntime* FindCellByZ(int32 Z); // first cell at this Z (XY=0 path)
};

View File

@ -0,0 +1,36 @@
// Copyright IHY. Vertical Partition Streaming plugin.
//
// Project-level defaults + global debug toggles. Appears under
// Project Settings -> Plugins -> Vertical Partition. Per-volume overrides live on
// AVerticalPartitionVolume; this provides the defaults a freshly placed volume gets.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "VerticalPartitionTypes.h"
#include "VerticalPartitionSettings.generated.h"
UCLASS(Config=Game, DefaultConfig, meta=(DisplayName="Vertical Partition"))
class VERTICALPARTITION_API UVerticalPartitionDeveloperSettings : public UDeveloperSettings
{
GENERATED_BODY()
public:
UVerticalPartitionDeveloperSettings();
virtual FName GetCategoryName() const override { return FName(TEXT("Plugins")); }
/** Defaults applied to a newly placed AVerticalPartitionVolume. */
UPROPERTY(Config, EditAnywhere, Category="Defaults")
FVerticalPartitionSettings DefaultSettings;
/** Master debug switch mirrored by the vp.Debug console variable. */
UPROPERTY(Config, EditAnywhere, Category="Debug")
bool bDebugEnabledByDefault = false;
/** Frame time (ms) above which a streaming update is logged as a hitch. */
UPROPERTY(Config, EditAnywhere, Category="Debug", meta=(ClampMin="1.0", Units="ms"))
float HitchThresholdMS = 8.0f;
static const UVerticalPartitionDeveloperSettings* Get();
};

View File

@ -0,0 +1,71 @@
// Copyright IHY. Vertical Partition Streaming plugin.
//
// Pure cell math shared by the editor builder and the runtime streamer. No state.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "VerticalPartitionTypes.h"
#include "VerticalPartitionStatics.generated.h"
/** Context the desired-state policy needs beyond the raw cell index. */
struct FVerticalStreamingContext
{
int32 PlayerCellZ = 0;
int32 PlayerCellX = 0;
int32 PlayerCellY = 0;
float PlayerVelocityZ = 0.f; // cm/s, +up
float PlayerWorldZ = 0.f; // cm, exact (for hysteresis at boundaries)
float CameraPitchDeg = 0.f; // +looking up
bool bUseXY = false;
};
UCLASS()
class VERTICALPARTITION_API UVerticalPartitionStatics : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/** Z index of a world Z (cm) relative to the volume floor. */
UFUNCTION(BlueprintPure, Category="VerticalPartition")
static int32 GetCellZFromWorldZ(float WorldZ, float VolumeMinZ, float CellHeight);
UFUNCTION(BlueprintPure, Category="VerticalPartition")
static int32 GetCellXFromWorldX(float WorldX, float VolumeMinX, float XYCellSize);
UFUNCTION(BlueprintPure, Category="VerticalPartition")
static int32 GetCellYFromWorldY(float WorldY, float VolumeMinY, float XYCellSize);
/** Cell id for a single point (actor origin). */
static FVerticalCellId GetCellIdFromPoint(const FVector& Point, const FBox& VolumeBounds, const FVerticalPartitionSettings& S);
/** Cell id for a bounds box. Uses the dominant-overlap cell (center of the
* clamped intersection) so a box straddling a boundary lands deterministically.
* bOutCrossesMultiple reports whether the box spans more than one cell. */
static FVerticalCellId GetCellIdFromBounds(const FBox& ActorBounds, const FBox& VolumeBounds, const FVerticalPartitionSettings& S, bool& bOutCrossesMultiple);
/** World-space bounds of a cell, clamped to the volume when requested. */
static FBox GetCellBounds(const FVerticalCellId& Cell, const FBox& VolumeBounds, const FVerticalPartitionSettings& S);
/** Number of Z cells the volume spans (>=1). */
static int32 GetNumZCells(const FBox& VolumeBounds, const FVerticalPartitionSettings& S);
/** THE policy. Maps a cell relative to the player to its desired representation,
* honouring full/preload/HLOD/unload bands, hysteresis, velocity prediction,
* fall-risk-below, camera direction and the above-player priority bias.
* CurrentState is the cell's live state and is what makes hysteresis sticky. */
static EVerticalCellDesiredState GetDesiredStateForCell(
const FVerticalCellId& Cell,
const FVerticalStreamingContext& Ctx,
const FVerticalPartitionSettings& S,
EVerticalCellState CurrentState = EVerticalCellState::Unloaded);
/** True when a cell sits in the preload band (load full data, keep it hidden). */
static bool ShouldPreloadFull(const FVerticalCellId& Cell, const FVerticalStreamingContext& Ctx, const FVerticalPartitionSettings& S);
/** Human-readable colour for a runtime state (debug draw / overlay). */
UFUNCTION(BlueprintPure, Category="VerticalPartition")
static FLinearColor GetStateColor(EVerticalCellState State);
UFUNCTION(BlueprintPure, Category="VerticalPartition")
static FString WarningTypeToString(EVerticalWarningType Type);
};

View File

@ -0,0 +1,486 @@
// Copyright IHY. Vertical Partition Streaming plugin.
//
// Shared data contract for the whole plugin: cell identity, states, the tunable
// settings block, descriptors, and build/runtime report structs.
//
// This is a BOUNDED VERTICAL ROUTE partition system for a hand-crafted spiral
// climbing map. It is NOT an infinite open-world (World Partition) system and it
// does NOT use Level Instances as its core architecture.
#pragma once
#include "CoreMinimal.h"
#include "VerticalPartitionTypes.generated.h"
// ---------------------------------------------------------------------------
// Cell identity
// ---------------------------------------------------------------------------
/** Integer grid coordinate of a cell. Z is the primary (vertical) axis; X/Y are
* only used when bUseXYSubCells is enabled. */
USTRUCT(BlueprintType)
struct FVerticalCellId
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
int32 X = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
int32 Y = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
int32 Z = 0;
FVerticalCellId() = default;
FVerticalCellId(int32 InX, int32 InY, int32 InZ) : X(InX), Y(InY), Z(InZ) {}
bool operator==(const FVerticalCellId& Other) const
{
return X == Other.X && Y == Other.Y && Z == Other.Z;
}
bool operator!=(const FVerticalCellId& Other) const { return !(*this == Other); }
FString ToString() const { return FString::Printf(TEXT("X%d_Y%d_Z%d"), X, Y, Z); }
/** Stable, sortable label used for generated package names ("Cell_Z0010" / "Cell_X0_Y0_Z0010"). */
FString ToLabel(bool bWithXY) const
{
return bWithXY
? FString::Printf(TEXT("Cell_X%d_Y%d_Z%04d"), X, Y, Z)
: FString::Printf(TEXT("Cell_Z%04d"), Z);
}
friend uint32 GetTypeHash(const FVerticalCellId& C)
{
return HashCombine(HashCombine(::GetTypeHash(C.X), ::GetTypeHash(C.Y)), ::GetTypeHash(C.Z));
}
};
// ---------------------------------------------------------------------------
// State machine enums
// ---------------------------------------------------------------------------
/** Live runtime state of a cell inside the streaming state machine. */
UENUM(BlueprintType)
enum class EVerticalCellState : uint8
{
Unloaded,
LoadingHLOD,
HLOD,
PreloadingFull,
LoadingFull,
Full,
Unloading,
Error
};
/** The representation the streamer *wants* a cell to be in this update. */
UENUM(BlueprintType)
enum class EVerticalCellDesiredState : uint8
{
Unloaded,
HLOD,
Full
};
/** How Build/Convert mutates the source level. */
UENUM(BlueprintType)
enum class EVerticalConversionMode : uint8
{
/** Source level untouched. Cells stream by toggling the existing in-level actors
* (visibility/collision/tick) + showing proxies. Safe, no memory savings. */
NonDestructive,
/** Actors are baked out into per-cell streaming sub-level packages and removed
* from the persistent level. True memory streaming. A backup is taken first. */
DestructiveCommit
};
/** What to do with an actor that does not fit cleanly into a single cell. */
UENUM(BlueprintType)
enum class ELargeActorPolicy : uint8
{
/** Leave it in the persistent (always-loaded) level. */
KeepInPersistentLevel,
/** Assign it to the cell that owns the largest fraction of its bounds. */
AssignToDominantCell,
/** Keep it only as part of HLOD proxies, never as a full streamed actor. */
DuplicateAsHLODOnly,
/** Emit a warning — splitting a single actor across cells is not supported. */
SplitNotSupportedWarning,
/** Drop it from partitioning entirely. */
ExcludeFromPartition
};
/** How a cell's HLOD proxy is baked from its static geometry. */
UENUM(BlueprintType)
enum class EVerticalHLODMethod : uint8
{
/** One merged static mesh, original materials preserved (exact silhouette, heavier).
* HISM/ISM instances are flattened into the single mesh. */
MergedMesh,
/** Proxy LOD: simplified geometry + a single baked atlas material (cheapest, true HLOD).
* Requires the ProxyLODPlugin to be enabled; falls back to MergedMesh otherwise. */
SimplifiedProxy
};
UENUM(BlueprintType)
enum class EVerticalIssueSeverity : uint8
{
Info,
Warning,
Error
};
/** Classifies an analysis/validation finding so UI can group and filter them. */
UENUM(BlueprintType)
enum class EVerticalWarningType : uint8
{
None,
ActorCrossesMultipleCells,
ActorTooLarge,
ActorSimulatesPhysics,
ActorHasBlueprintTick,
ActorDynamicMobility,
ActorHasExternalReferences,
ActorAttachedAcrossCells,
ActorDependsOnOtherCell,
ActorNeedsManualReview,
ActorHasAudioLightNav,
ActorNotStreamSafe,
ActorHasUnsavedChanges,
CellTooManyActors,
CellTooManyDrawCalls,
HLODGenerationFailed,
PackageSaveFailed,
NoVolume,
DescriptorOutOfDate
};
/** Editor action dispatched from the volume's detail-panel buttons / console. */
UENUM()
enum class EVerticalPartitionAction : uint8
{
Analyze,
Build,
RebuildChangedCells,
RebuildHLOD,
Validate,
PreviewChunks,
ClearGenerated,
Commit,
RestoreBackup,
DumpReport,
SpawnHLODPreview,
ClearPreview
};
// ---------------------------------------------------------------------------
// Tunable settings (one flat, category-grouped block reused everywhere)
// ---------------------------------------------------------------------------
/** Everything tunable about segmentation, runtime streaming ranges, conversion
* and debug. Embedded in AVerticalPartitionVolume (per-volume) and snapshotted
* into UVerticalPartitionDescriptor at build time. Defaults are provided by
* UVerticalPartitionDeveloperSettings (project settings). */
USTRUCT(BlueprintType)
struct FVerticalPartitionSettings
{
GENERATED_BODY()
// ===== Segmentation =====
/** Height (cm) of one Z cell. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation", meta=(ClampMin="100.0", Units="cm"))
float CellHeight = 5000.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
bool bUseXYSubCells = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation", meta=(EditCondition="bUseXYSubCells", ClampMin="100.0", Units="cm"))
float XYCellSize = 10000.f;
/** Snap the generated grid to the volume bounds (no cells outside the volume). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
bool bClampToVolumeBounds = true;
/** Only actors whose origin/bounds fall inside the volume are partitioned. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
bool bIncludeOnlyActorsInsideVolume = true;
/** Assign by full actor bounds (true) or by actor origin point (false). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
bool bUseActorBoundsInsteadOfOrigin = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
ELargeActorPolicy LargeActorPolicy = ELargeActorPolicy::AssignToDominantCell;
/** An actor is "large" when its Z extent exceeds this many cell heights. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation", meta=(ClampMin="1.0"))
float LargeActorCellSpan = 1.5f;
/** SAFETY (recommended ON): only stream pure STATIC DECOR. Actors with lights,
* sky atmosphere / Ultra Dynamic Sky, sky-light, fog, post-process, audio,
* particles, reflection captures, decals, physics, replication, movable mobility,
* and Level Instances are kept in the persistent level and are NEVER hidden by
* streaming. Turn OFF only if every actor inside the volume is safe to hide. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
bool bStreamOnlyStaticDecor = true;
/** Treat Level Instances / Packed Level Actors as streamable units: their static
* geometry (incl. HISM/ISM, flattened recursively) is baked into per-cell HLOD and
* their static child actors are hidden/shown by streaming. Lights / gameplay inside
* an LI stay persistent. Off => LIs are left fully persistent. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
bool bStreamLevelInstances = true;
// ===== Runtime streaming ranges (in cells, measured from the player cell) =====
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Full", meta=(ClampMin="0"))
int32 FullLoadCellsBelow = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Full", meta=(ClampMin="0"))
int32 FullLoadCellsAbove = 1;
/** Begin async-loading full data (kept resident, not yet shown) this many cells out. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Preload", meta=(ClampMin="0"))
int32 PreloadCellsBelow = 2;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Preload", meta=(ClampMin="0"))
int32 PreloadCellsAbove = 3;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|HLOD", meta=(ClampMin="0"))
int32 HLODLoadCellsBelow = 4;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|HLOD", meta=(ClampMin="0"))
int32 HLODLoadCellsAbove = 5;
/** Beyond this many cells the cell is fully unloaded. Should be >= HLOD range. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Unload", meta=(ClampMin="0"))
int32 UnloadCellsBelow = 5;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Unload", meta=(ClampMin="0"))
int32 UnloadCellsAbove = 6;
/** Z distance (cm) the player must move past a boundary before a downgrade fires. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Hysteresis", meta=(ClampMin="0.0", Units="cm"))
float HysteresisDistanceZ = 500.f;
/** Extra cells kept at their current (higher) state to avoid thrash near edges. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Hysteresis", meta=(ClampMin="0"))
int32 HysteresisCells = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Budget", meta=(ClampMin="0.0", Units="s"))
float UpdateInterval = 0.2f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Budget", meta=(ClampMin="1"))
int32 MaxAsyncLoadsPerFrame = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Budget", meta=(ClampMin="1"))
int32 MaxAsyncUnloadsPerFrame = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Budget", meta=(ClampMin="0"))
int32 MaxVisibleHLODCells = 24;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Budget", meta=(ClampMin="0"))
int32 MaxFullCells = 8;
// ===== Priority / prediction =====
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Priority")
bool bPrioritizeAbovePlayer = true;
/** Keep cells below the player loaded longer (the player can always fall back down). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Priority")
bool bKeepBelowPlayerLoadedOnFallRisk = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Priority")
bool bUseCameraDirectionPriority = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Priority")
bool bUsePlayerVelocityPrediction = true;
/** How many seconds of velocity to extrapolate the player position for prediction. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Priority", meta=(ClampMin="0.0", Units="s"))
float VelocityPredictionSeconds = 0.75f;
// ===== Conversion =====
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Conversion")
EVerticalConversionMode ConversionMode = EVerticalConversionMode::NonDestructive;
/** Content root for generated cell maps / proxy meshes / descriptor. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Conversion")
FString GeneratedPackageRoot = TEXT("/Game/VPGenerated");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Conversion")
bool bCreateBackup = true;
/** When set, only actors of these classes are considered for partitioning. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Conversion")
TArray<TSoftClassPtr<AActor>> IncludeOnlyClasses;
/** Actors of these classes are always excluded from partitioning. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Conversion")
TArray<TSoftClassPtr<AActor>> ExcludeClasses;
// ===== HLOD =====
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HLOD")
bool bGenerateHLODProxies = true;
/** Only static-mesh actors below this draw-call-ish triangle budget are merged. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HLOD", meta=(ClampMin="0"))
int32 HLODMaxSourceActorsPerCell = 4096;
/** MergedMesh (default, exact) or SimplifiedProxy (cheaper, needs ProxyLODPlugin). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HLOD")
EVerticalHLODMethod HLODMethod = EVerticalHLODMethod::MergedMesh;
/** Target screen size (px) for SimplifiedProxy generation. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HLOD", meta=(ClampMin="1", ClampMax="1200", EditCondition="HLODMethod==EVerticalHLODMethod::SimplifiedProxy"))
int32 ProxyScreenSize = 300;
// ===== Debug =====
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
bool bDrawVolumeBounds = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
bool bDrawZSlices = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
bool bDrawXYCells = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
bool bDrawCellIds = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
bool bDrawActorCounts = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
bool bDrawProblemActors = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
bool bShowRuntimeOverlay = true;
};
// ---------------------------------------------------------------------------
// Descriptor data (serialized in UVerticalPartitionDescriptor)
// ---------------------------------------------------------------------------
/** One generated cell: bounds, the package that holds its full actors, its proxy
* asset, and the source actors that fed it. */
USTRUCT(BlueprintType)
struct FVerticalCellDescriptor
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FVerticalCellId CellId;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FBox Bounds = FBox(ForceInit);
/** Streaming sub-level package for the full representation (DestructiveCommit only). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FSoftObjectPath FullCellPackage;
/** Offline-generated proxy static mesh shown while the cell is in HLOD range. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FSoftObjectPath HLODProxyAsset;
/** Soft paths to the original actors assigned to this cell (NonDestructive backend). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
TArray<FSoftObjectPath> SourceActors;
/** Actors in *other* cells that this cell hard-references (cross-cell dependencies). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
TArray<FSoftObjectPath> Dependencies;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 ActorCount = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 StaticMeshCount = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 BlueprintActorCount = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 LightCount = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 DynamicActorCount = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
float EstimatedMemoryMB = 0.f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
bool bHasWarnings = false;
};
/** A single analysis / validation finding. */
USTRUCT(BlueprintType)
struct FVerticalPartitionWarning
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
EVerticalWarningType Type = EVerticalWarningType::None;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
EVerticalIssueSeverity Severity = EVerticalIssueSeverity::Warning;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FString Message;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FSoftObjectPath Actor;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FVerticalCellId Cell;
};
/** An error is a finding that blocks (or partially blocks) the build. Same shape. */
USTRUCT(BlueprintType)
struct FVerticalPartitionError
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
EVerticalWarningType Type = EVerticalWarningType::None;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FString Message;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FSoftObjectPath Actor;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FVerticalCellId Cell;
};
/** Result of Analyze / Build, surfaced in the editor UI and dumpable to a report. */
USTRUCT(BlueprintType)
struct FVerticalPartitionBuildReport
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 TotalActorsScanned = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 ActorsInsideVolume = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 ActorsExcluded = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 GeneratedCells = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 EmptyCells = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 WarningCount = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 ErrorCount = 0;
/** The Z cell (and XY sub-cell) with the most actors, for the summary panel. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 LargestCellActorCount = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
FVerticalCellId LargestCell;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
TArray<FVerticalPartitionWarning> Warnings;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
TArray<FVerticalPartitionError> Errors;
void Reset() { *this = FVerticalPartitionBuildReport(); }
};
/** Live runtime counters surfaced by the debug overlay / vp.DumpState. */
USTRUCT(BlueprintType)
struct FVerticalRuntimeStats
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 FullCellsLoaded = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 HLODCellsVisible = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 CellsUnloaded = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 CellsLoading = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 ActiveAsyncLoads = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
float EstimatedMemoryMB = 0.f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
float LastUpdateTimeMS = 0.f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
float LastStreamingTimeMS = 0.f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 HitchesOverThreshold = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
int32 PlayerCellZ = 0;
};

View File

@ -0,0 +1,89 @@
// Copyright IHY. Vertical Partition Streaming plugin.
//
// The authoring volume. Drop one around the whole playable spiral, tune the
// settings, and press Build. Bounds come from a UBoxComponent so they are trivial
// to read at both editor and runtime (no BSP brush).
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "VerticalPartitionTypes.h"
#include "VerticalPartitionVolume.generated.h"
class UBoxComponent;
class UVerticalPartitionDescriptor;
class AVerticalPartitionVolume;
/**
* Decouples the runtime volume from the editor-only builder. The editor module
* installs Handler in StartupModule; the volume's CallInEditor buttons and the
* vp.* build console commands route through it. Lives in the runtime module so the
* volume can reference it, but is only ever populated in editor builds.
*/
struct VERTICALPARTITION_API FVerticalPartitionEditorBridge
{
#if WITH_EDITOR
static TFunction<void(AVerticalPartitionVolume*, EVerticalPartitionAction)> Handler;
static void Execute(AVerticalPartitionVolume* Volume, EVerticalPartitionAction Action)
{
if (Handler) { Handler(Volume, Action); }
}
#endif
};
UCLASS(HideCategories=(Collision, Physics, Navigation, HLOD, Input, Replication))
class VERTICALPARTITION_API AVerticalPartitionVolume : public AActor
{
GENERATED_BODY()
public:
AVerticalPartitionVolume();
/** Box that defines the bounded playable region to partition. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
TObjectPtr<UBoxComponent> Box;
/** Per-volume settings (seeded from project defaults on placement). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition", meta=(ShowOnlyInnerProperties))
FVerticalPartitionSettings Settings;
/** Descriptor produced by Build; consumed by the runtime manager. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
TSoftObjectPtr<UVerticalPartitionDescriptor> Descriptor;
/** World-space bounds of the volume (box transform * extent). */
UFUNCTION(BlueprintPure, Category="VerticalPartition")
FBox GetVolumeBounds() const;
virtual void OnConstruction(const FTransform& Transform) override;
#if WITH_EDITOR
// ---- Detail-panel buttons (mirror the editor tool window) ----
UFUNCTION(CallInEditor, Category="Vertical Partition|1 - Analyze")
void AnalyzeCurrentLevel();
UFUNCTION(CallInEditor, Category="Vertical Partition|2 - Build")
void BuildVerticalPartition();
UFUNCTION(CallInEditor, Category="Vertical Partition|2 - Build")
void RebuildChangedCells();
UFUNCTION(CallInEditor, Category="Vertical Partition|2 - Build")
void RebuildHLOD();
UFUNCTION(CallInEditor, Category="Vertical Partition|3 - Validate")
void ValidateCells();
UFUNCTION(CallInEditor, Category="Vertical Partition|3 - Validate")
void PreviewChunks();
UFUNCTION(CallInEditor, Category="Vertical Partition|3 - Validate")
void SpawnHLODPreview();
UFUNCTION(CallInEditor, Category="Vertical Partition|3 - Validate")
void ClearPreview();
UFUNCTION(CallInEditor, Category="Vertical Partition|4 - Commit")
void CommitConversion();
UFUNCTION(CallInEditor, Category="Vertical Partition|4 - Commit")
void RestoreFromBackup();
UFUNCTION(CallInEditor, Category="Vertical Partition|5 - Maintenance")
void ClearGeneratedData();
UFUNCTION(CallInEditor, Category="Vertical Partition|5 - Maintenance")
void DumpReport();
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
};