Files
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

17 KiB

Vertical Partition Streaming

This is not an infinite open-world partition system. This is a bounded vertical route partition system for a hand-crafted spiral climbing map (Only Up / Only Climb style).

It takes an already finished level, scans the actors inside a volume, segments them into vertical Z cells (optionally XY sub-cells), bakes a descriptor, generates offline HLOD proxies, and streams the route at runtime with a vertical-aware policy (full / preload / HLOD / unloaded bands, hysteresis, velocity prediction, fall-risk-below), plus World-Partition-style debug visualization.

It is not World Partition, it is not Level Instances, and it does no runtime mesh merging. Full cells use classic persistent-level + streaming-sublevel loading (or, in the default safe mode, visibility toggling of the existing actors).


1. How it understands the task

Requirement How it's met
Bounded vertical route, spiral, handcrafted AVerticalPartitionVolume (a box) defines the playable region; cells are slices of Z.
Convert an existing map UVerticalPartitionBuilder scans the level in place — nothing is authored from scratch.
World-Partition by meaning, not infinite XY Z is the primary axis; XY sub-cells are optional refinement.
Full / HLOD / preload / unload ranges FVerticalPartitionSettings + UVerticalPartitionStatics::GetDesiredStateForCell.
Offline HLOD, no runtime merge MeshMergeUtilities at build time → per-cell proxy UStaticMesh.
Two conversion modes NonDestructive (ships first, safe) and DestructiveCommit (V2).
WP-style debug UVerticalPartitionDebugComponent (3D boxes + overlay) + vp.* console.

2. Architecture

EDITOR (VerticalPartitionEditor module)            RUNTIME (VerticalPartition module)
┌───────────────────────────┐                      ┌────────────────────────────────┐
│ AVerticalPartitionVolume   │  CallInEditor btns   │ AVerticalPartitionManager       │
│  • box bounds + settings   │──┐  via bridge        │  • finds tracked pawn           │
│  • Descriptor ref          │  │                    │  • builds streaming context     │
└───────────────────────────┘  │                    │  • drives subsystem @ interval  │
            ▲                   ▼                    │  • hosts DebugComponent         │
            │        FVerticalPartitionEditorBridge  └───────────────┬────────────────┘
            │                   │                                     │ UpdateStreaming()
┌───────────┴────────────┐     ▼                    ┌────────────────▼────────────────┐
│ UVerticalPartitionBuilder│  Analyze/Build/...      │ UVerticalPartitionRuntimeSubsystem│
│  • GatherActors          │────────────────────────▶│  • per-cell state machine        │
│  • BuildCells            │  writes                  │  • streaming-level OR visibility  │
│  • GenerateHLODProxy     │     ┌──────────────────┐ │  • proxy actors, async budget     │
│  • CreateBackup/Commit   │────▶│ UVerticalPartition │◀│  • FVerticalRuntimeStats          │
└──────────────────────────┘     │ Descriptor (asset) │ └───────────────────────────────────┘
                                  │  • cells + bounds  │
UVerticalPartitionCommandlet ────▶│  • settings snapshot│   UVerticalPartitionStatics (pure math,
 (offline -run=VerticalPartition) │  • last report     │    shared by editor + runtime)
                                  └──────────────────┘

Module split keeps the heavy editor APIs (mesh merge, level surgery) out of packaged builds. The volume lives in the runtime module so it can be streamed/placed at runtime, and its editor buttons reach the builder through FVerticalPartitionEditorBridge (a TFunction the editor module installs on startup).


3. Editor conversion pipeline

  1. Open the finished map.
  2. Drop an AVerticalPartitionVolume around the whole playable spiral; scale its box.
  3. Tune Settings on the volume (segmentation, ranges, HLOD, large-actor policy, exclude classes, mode).
  4. Press the detail-panel buttons (or vp.*):
    • Analyze → report only (no writes): counts, cells, largest cell, warnings.
    • Build → backup → scan → classify → VPD_<Map> descriptor → HLOD proxies → save → spawn a manager.
    • Validate / Preview Chunks / Rebuild HLOD / Rebuild Changed / Clear Generated / Commit / Restore Backup / Dump Report.
  5. Press Play — the manager registers the descriptor with the subsystem and streaming starts.
  6. Toggle debug (vp.Debug 1).

Generated content lives under Settings.GeneratedPackageRoot (default /Game/VPGenerated/<Map>/): VPD_<Map> (descriptor) and Proxies/HLOD_Cell_Zxxxx (proxy meshes). Backups go to Saved/VerticalPartition/Backups/.

Conversion modes

  • NonDestructive (default, ships first): the persistent level is untouched. The descriptor records each cell's source actors by soft path; at runtime far cells are hidden (SetActorHiddenInGame + collision/tick off) and their HLOD proxy shown. Zero risk, fully reversible, validates the whole pipeline. No memory savings (actors stay resident).
  • DestructiveCommit (V2): actors are baked into per-cell streaming sub-level .umap packages and removed from the persistent level → true memory streaming. A backup is taken first. The exact baking steps are documented in CommitConversion() and below; the runtime already supports this backend (a cell with a valid FullCellPackage switches to streaming).

Recommended MVP: NonDestructive. It's safe, gives draw-call savings + correct streaming behavior, and lets you tune ranges before committing to destructive level surgery.


4. Runtime streaming pipeline

AVerticalPartitionManager::Tick (throttled to UpdateInterval) → builds FVerticalStreamingContext (player Z cell, velocity Z, camera pitch, teleport flag) → UVerticalPartitionRuntimeSubsystem::UpdateStreaming:

  1. PollStreaming each cell (refresh state from in-flight async ops).
  2. Desired state per cell = GetDesiredStateForCell (or forced / show-only).
  3. Budget pass — nearest MaxFullCells stay Full, nearest MaxVisibleHLODCells stay HLOD, the rest demote.
  4. Apply, nearest-first, within MaxAsyncLoadsPerFrame / MaxAsyncUnloadsPerFrame:
    • LoadCellFullAsync(visible) — streaming backend: SetShouldBeLoaded/Visible.
    • ShowCellHLOD — spawn/show the proxy AStaticMeshActor.
    • UnloadCell — hide actors / unload streaming level / hide proxy.
  5. Rebuild stats, record update time, flag hitches.

bSnap (teleport / checkpoint / first frame) bypasses the per-frame budget for instant convergence.


5. Streaming ranges (all editable per-volume)

Bands are measured in cells from the player cell. Example with Full=±1, Preload above=3, HLOD=±... and player in Z10:

Z15  ── Unloaded ───────────────
Z14  ─┐
Z13  ─┤ HLOD (proxy visible)
Z12  ─┘ (+ Preload: full data resident but hidden)
Z11  ─┐
Z10  ─┤ FULL  ← player
Z9   ─┘
Z8   ─┐ HLOD
Z7   ─┘
Z6   ── Unloaded ───────────────

Key knobs (see FVerticalPartitionSettings): FullLoadCells{Below,Above}, PreloadCells{Below,Above}, HLODLoadCells{Below,Above}, UnloadCells{Below,Above}, HysteresisDistanceZ, HysteresisCells, UpdateInterval, MaxAsyncLoads/UnloadsPerFrame, MaxVisibleHLODCells, MaxFullCells, bPrioritizeAbovePlayer, bKeepBelowPlayerLoadedOnFallRisk, bUseCameraDirectionPriority, bUsePlayerVelocityPrediction, VelocityPredictionSeconds.

GetDesiredStateForCell accounts for: bands, sticky hysteresis (a loaded cell at the edge stays loaded for HysteresisCells extra), velocity prediction (shifts the effective player cell), camera direction, fall-risk-below (keeps below-player cells loaded to the unload edge), forced cells and show-only mode. Teleport is handled by the manager raising bSnap.


6. Data structures (VerticalPartitionTypes.h)

FVerticalCellId{X,Y,Z} · EVerticalCellState{Unloaded,LoadingHLOD,HLOD,PreloadingFull,LoadingFull,Full,Unloading,Error} · EVerticalCellDesiredState{Unloaded,HLOD,Full} · EVerticalConversionMode{NonDestructive,DestructiveCommit} · ELargeActorPolicy{KeepInPersistentLevel,AssignToDominantCell,DuplicateAsHLODOnly,SplitNotSupportedWarning,ExcludeFromPartition} · FVerticalPartitionSettings (segmentation + streaming + conversion + HLOD + debug) · FVerticalCellDescriptor · FVerticalPartitionWarning / FVerticalPartitionError · FVerticalPartitionBuildReport · FVerticalRuntimeStats.


7. Classes

Class Module Role
AVerticalPartitionVolume Runtime Bounds + settings + descriptor ref + editor buttons.
UVerticalPartitionDeveloperSettings Runtime Project-default settings + global debug (Project Settings → Plugins).
UVerticalPartitionStatics Runtime Pure cell math + the desired-state policy.
UVerticalPartitionDescriptor Runtime The build artifact (cells, bounds, settings snapshot, report).
UVerticalPartitionRuntimeSubsystem Runtime The streaming state machine + stats.
AVerticalPartitionManager Runtime Per-level coordinator; drives the subsystem; hosts debug.
UVerticalPartitionDebugComponent Runtime 3D cell boxes + on-screen overlay.
UVerticalPartitionBuilder Editor Analyze / Build / RebuildHLOD / Validate / Commit / Backup.
UVerticalPartitionCommandlet Editor -run=VerticalPartition -Map=... offline build.

8. HLOD / proxy strategy

  • Offline only. Built during Build / Rebuild HLOD via IMeshMergeUtilities::MergeComponentsToStaticMesh over each cell's static mesh components → one proxy UStaticMesh per cell, saved under Proxies/.
  • The runtime spawns one hidden AStaticMeshActor per cell from that mesh and toggles its visibility for the HLOD band; Full load hides the proxy, so there's no double-draw.
  • Exclusions (never proxied): dynamic / movable components, physics actors, gameplay actors, moving platforms, lights, audio. Proxy collision is off.
  • Graceful degrade: if a cell has no static geometry or merge fails, the cell still streams — the far representation just hides instead of showing a proxy, and a warning is recorded.

Method (Settings.HLODMethod):

  • MergedMesh (default) — MergeComponentsToStaticMesh, one merged mesh with original materials; ISM/HISM instances are auto-flattened into the single object.
  • SimplifiedProxyCreateProxyMesh (proxy LOD: simplified geometry + one baked atlas material, the cheapest "true HLOD", same tech as Epic's HLOD Simplified Mesh). Requires the ProxyLODPlugin enabled; auto-falls-back to MergedMesh with a warning if absent.

Level Instances & Packed Level Actors (universal)

  • Packed Level Actor — its baked ISM/HISM live as owned components, caught by GetComponents<UStaticMeshComponent> and merged (instances flattened to one object).
  • Level InstanceCollectStreamLeafActors recursively flattens the LI (runtime API ULevelInstanceSubsystem::ForEachActorInLevelInstance, gated on IsLoaded) into its static child actors; nested LIs recurse; nested PLAs stay whole. Its static geometry is baked per cell; its static children are hidden/shown at runtime; lights / gameplay inside the LI stay persistent. Scan skips LI content (GetOwningLevelInstance) so nothing is double-counted. An unloaded LI is skipped with a warning — load it in the editor and rebuild. Toggle with Settings.bStreamLevelInstances (default on).

9. Debug / validation

Console: vp.Debug, vp.DebugDraw, vp.DebugOverlay (CVars); commands vp.Analyze, vp.BuildFromSelectedVolume, vp.Validate, vp.Rebuild, vp.RebuildHLOD, vp.ForceCellFull/HLOD <Z>, vp.UnforceCell <Z>, vp.UnforceAll, vp.ShowOnlyCell <Z>, vp.TeleportToCell <Z>, vp.SetFullRange <b> <a>, vp.SetHLODRange <b> <a>, vp.SetCellHeight <cm>, vp.DumpState, vp.DumpCells, vp.DumpWarnings, vp.ProfileStreaming.

3D draw colors: Green=Full · Blue=HLOD · Yellow=Loading/Transition · Red=Error · Gray=Unloaded · Purple=Forced/locked. Overlay: player Z cell, full/HLOD/unloaded counts, loading queue, async ops, est. memory, update ms, hitches. Editor preview: Preview Chunks draws the Z slices in the editor viewport without entering PIE.

Validation warnings (analyze/build): crosses-multiple-cells, too-large, physics, blueprint-tick, dynamic-mobility, audio/light/nav, replicated/not-stream-safe, cell-too-many-actors, HLOD-generation-failed, package-save-failed, descriptor-out-of-date.


10. Edge cases & how they're handled

Case Handling
Actor crosses Z cells Assigned to dominant (centre) cell + ActorCrossesMultipleCells warning.
Origin in one cell, bounds in another bUseActorBoundsInsteadOfOrigin picks the rule; assignment uses bounds centre.
Attached actors split across cells Grouped per cell in the descriptor; cross-cell attaches are a documented warning to review.
Blueprint tick / physics / interactable / movable Flagged; LargeActorPolicy/exclude classes keep them in the persistent level.
SaveGame / replicated actors ActorNotStreamSafe warning — keep in persistent level (NonDestructive leaves them resident).
Hard ref into another cell Dependencies array on the descriptor (dependency graph is a V2 expansion).
Decor visible far / silhouette That's the HLOD proxy's job.
Light in one cell lighting another Lights flagged; recommended kept in persistent level or Full cell, not proxied.
Level Instance / Packed Level Actor Universal: PLA's HISM and LI's flattened static children baked per-cell HLOD + streamed; LI lights/gameplay stay persistent. bStreamLevelInstances.
HISM / ISM (incl. inside PLA) Merge auto-expands instances into one merged proxy object.
Unloaded Level Instance at build Skipped with a warning — load it in the editor and rebuild.
Navmesh on a vertical map Out of scope of streaming; keep navmesh in the persistent level.
Player falls 10 cells / flies up fast Velocity prediction + bKeepBelowPlayerLoadedOnFallRisk + bSnap on big jumps.
Teleport / checkpoint Manager detects the jump (>3 cells) → bSnap immediate convergence.
Multiplayer Streaming is per-world around the local tracked pawn (client-side); replicated actors flagged. Server-authoritative union streaming is V2.
PIE vs packaged Editor module is Type:Editor (excluded from packaged); runtime is self-contained.
Package load failure Cell → Error state (red), logged.
HLOD gen failure Warning; cell still streams (hides far).
Source map changed after build Validate flags DescriptorOutOfDate via content hash.
Rebuild only changed cells RebuildChangedCells (currently full rebuild; incremental diff is V2).

11. MVP roadmap

MVP (implemented): one volume · Z-only segmentation · CellHeight · Analyze · Build (NonDestructive) · descriptor asset · runtime manager + subsystem · full load/unload via visibility · offline HLOD proxy for static-mesh actors · Z-slice debug draw · stats overlay · warnings for large/dynamic/blueprint/physics actors · full vp.* console.

V2 (scaffolded / documented): XY sub-cells (settings + math in place) · DestructiveCommit baking (CommitConversion stub + documented steps) · incremental rebuild (dependency graph) · full HLOD LOD chains · commandlet hardening · multiplayer union streaming · velocity/camera preload tuning · memory profiler integration (Insights) · editor tool window (today: detail buttons + console).


12. Build & enable

New C++ modules were added, so a Live-Coding pass is not enough — regenerate project files and do a full rebuild with the editor closed, then enable the plugin (it's EnabledByDefault), or add to IHY.uproject:

{ "Name": "VerticalPartition", "Enabled": true }

Files live under Plugins/VerticalPartition/Source/{VerticalPartition,VerticalPartitionEditor}/.