Spatial Streaming
For worlds too large to keep resident at once — procedural terrain, voxel landscapes, endless grids — you want to stream mesh cells in and out around the player. The RealtimeMeshSpatial module provides exactly that: a tickable world subsystem tracks streaming sources, and a per-actor manager runs a quadtree/LOD state machine that fires four lifecycle delegates as cells load, become visible, hide, and unload.
You provide the geometry; the module decides which cells should exist right now and when. It pairs naturally with custom providers — a single shared generator produces the geometry for every cell, with per-cell variation carried in the descriptor.
Add the module to your Build.cs:
PrivateDependencyModuleNames.AddRange(new string[] { "RealtimeMeshComponent", "RealtimeMeshExt", // generator + URealtimeMeshConstructed "RealtimeMeshSpatial", // streaming manager + subsystem});The shipped worked example is ARealtimeMeshSpatialStreamingActor in Source/RealtimeMeshExamples/Public/RealtimeMeshSpatialStreamingActor.h (implementation in Source/RealtimeMeshExamples/Private/RealtimeMeshSpatialStreamingActor.cpp). It streams a plane field around the player using the plane generator. We’ll follow it throughout.
The moving parts
Section titled “The moving parts”-
URealtimeMeshSpatialStreamingSubsystem— aUTickableWorldSubsystem, one per world. It holds the registered streaming sources and managers and ticks every manager each frame. You don’t create it; the engine does. It ticks in editor and when paused, so streaming keeps working in PIE and the editor viewport. -
URealtimeMeshSpatialStreamingManager— aUObjectyou create and own (typically on your streaming actor). It runs the quadtree/LOD traversal and calls your lifecycle delegates. You initialize it once and it registers itself with the subsystem. -
Streaming sources — points the streaming radiates from (usually the player). The subsystem auto-adds a source for the player Character in PIE; for a custom pawn you add a
URealtimeMeshSpatialStreamingSourceComponent. -
A structure provider — your implementation of
IRealtimeMeshSpatialStreamingStructureProvider, which defines the grid: its transform, the maximum LOD, whether streaming is 2D or 3D, and which cells are valid.
Cell identity: FRealtimeMeshSpatialComponentLocation
Section titled “Cell identity: FRealtimeMeshSpatialComponentLocation”Every delegate is keyed by a cell location — the unit of streaming:
struct FRealtimeMeshSpatialComponentLocation{ FInt64Vector Location; // integer grid coordinates int32 LOD; // quadtree level (0 = finest)};It hashes and compares by (Location, LOD), so it works directly as a TMap key — which is how the example tracks the component it created for each live cell.
The lifecycle delegates
Section titled “The lifecycle delegates”You drive everything through four delegates on FRealtimeMeshSpatialStreamingStateInitParams:
struct FRealtimeMeshSpatialStreamingStateInitParams{ TSharedPtr<RealtimeMesh::IRealtimeMeshSpatialStreamingStructureProvider> ChunkProvider;
TDelegate<void(const FRealtimeMeshSpatialComponentLocation&)> OnActivateCell; TDelegate<void(const FRealtimeMeshSpatialComponentLocation&)> OnDeactivateCell; TDelegate<TFuture<bool>(const FRealtimeMeshSpatialComponentLocation&, RealtimeMesh::FRealtimeMeshCancellationToken)> OnLoadCell; TDelegate<void(const FRealtimeMeshSpatialComponentLocation&)> OnUnloadCell;};The lifecycle for a cell is:
OnLoadCell— the cell entered streaming range. This is the only async delegate: it returnsTFuture<bool>so you can build the geometry off the game thread and report success/failure when done. It also receives aFRealtimeMeshCancellationToken— if the cell leaves range before the load finishes, honor the token and abandon the work. Create the mesh here, but keep it hidden.OnActivateCell— the loaded cell is now close enough to be visible. Show it.OnDeactivateCell— the cell moved out of the visible band but is still loaded. Hide it (don’t destroy — it may re-activate cheaply).OnUnloadCell— the cell left streaming range entirely. Destroy its resources.
This load-then-activate split lets the manager pre-load geometry slightly outside the visible radius so cells pop into view already built.
Defining the grid: the structure provider
Section titled “Defining the grid: the structure provider”The manager needs to know the shape of your world. Implement IRealtimeMeshSpatialStreamingStructureProvider:
class IRealtimeMeshSpatialStreamingStructureProvider{protected: int32 MaxLOD; // coarsest LOD the grid streams to uint32 bShouldUse3dStreaming : 1; // false = 2D quadtree, true = 3D octreepublic: int32 GetMaxLOD() const; bool ShouldUse3dStreaming() const;
virtual bool IsCellValid(const FRealtimeMeshSpatialComponentLocation& CellLocation) const = 0; virtual FTransform GetGridTransform() const = 0;};MaxLOD and bShouldUse3dStreaming are set in your constructor; IsCellValid lets you carve holes or bound the world (return false for cells that shouldn’t exist); GetGridTransform anchors and orients the grid in world space. The example’s minimal provider is an unbounded 2D grid anchored at the actor:
class FExampleChunkProvider : public IRealtimeMeshSpatialStreamingStructureProvider{public: FExampleChunkProvider(int32 InMaxLOD, const FTransform& InGridTransform) : GridTransform(InGridTransform) { MaxLOD = InMaxLOD; bShouldUse3dStreaming = false; } virtual bool IsCellValid(const FRealtimeMeshSpatialComponentLocation& Cell) const override { return true; } virtual FTransform GetGridTransform() const override { return GridTransform; }private: FTransform GridTransform;};Grid vs. cell sizing. The manager’s base chunk size (
64units) and your generator’s per-cell size must agree so a generated patch exactly fills its streaming cell. The example pins both to a sharedGBaseChunkSize = 64constant; a cell at LOD n coversBaseChunkSize << nunits. Getting these out of sync leaves visible seams or overlaps.
Wiring it up: Initialize / Start / Stop / Reset
Section titled “Wiring it up: Initialize / Start / Stop / Reset”The manager’s control surface:
// URealtimeMeshSpatialStreamingManagervoid Initialize(const FRealtimeMeshSpatialStreamingStateInitParams& InitParams, bool bAutoStart = true);void Start();void Stop();void Reset();bool IsStreamingStable() const; // no pending loads/unloads and nothing changed last frameInitialize takes your init params and, when bAutoStart is true, registers with the subsystem so it begins ticking immediately. Start/Stop toggle that registration; Reset tears everything down. IsStreamingStable() returns true once every pending load/unload has settled — useful to gate teleports, screenshots, or “world ready” logic.
From the example’s BeginPlay:
void ARealtimeMeshSpatialStreamingActor::BeginPlay(){ Super::BeginPlay();
// Resolve the shared generator from the (editor-picked) provider config. Generator = FRealtimeMeshProviderRegistry::Get().CreateGenerator(ProviderConfig); if (!Generator.IsValid()) { return; }
ChunkProvider = MakeShared<FExampleChunkProvider>(MaxLOD, GetActorTransform()); Manager = NewObject<URealtimeMeshSpatialStreamingManager>(this);
FRealtimeMeshSpatialStreamingStateInitParams Init; Init.ChunkProvider = ChunkProvider; Init.OnLoadCell.BindUObject(this, &ARealtimeMeshSpatialStreamingActor::OnLoadCell); Init.OnActivateCell.BindUObject(this, &ARealtimeMeshSpatialStreamingActor::OnActivateCell); Init.OnDeactivateCell.BindUObject(this, &ARealtimeMeshSpatialStreamingActor::OnDeactivateCell); Init.OnUnloadCell.BindUObject(this, &ARealtimeMeshSpatialStreamingActor::OnUnloadCell);
// bAutoStart registers with the streaming subsystem, which then ticks and drives us. Manager->Initialize(Init, /*bAutoStart*/ true);}And clean teardown in EndPlay — Reset() the manager, then destroy any components still alive:
void ARealtimeMeshSpatialStreamingActor::EndPlay(const EEndPlayReason::Type EndPlayReason){ if (Manager) { Manager->Reset(); Manager = nullptr; } for (auto& Pair : CellComponents) { if (Pair.Value) { Pair.Value->DestroyComponent(); } } CellComponents.Empty(); Super::EndPlay(EndPlayReason);}Handling the delegates
Section titled “Handling the delegates”Load — build the mesh, return a future
Section titled “Load — build the mesh, return a future”OnLoadCell creates a URealtimeMeshComponent, places it at the cell corner, initializes a URealtimeMeshConstructed, binds the shared generator with a per-cell descriptor, and kicks off Regenerate. It returns the regenerate future mapped to bool. The component starts hidden — activation makes it visible.
TFuture<bool> ARealtimeMeshSpatialStreamingActor::OnLoadCell( const FRealtimeMeshSpatialComponentLocation& Cell, FRealtimeMeshCancellationToken CancellationToken){ const int64 ChunkSize = GBaseChunkSize << Cell.LOD; const FVector Corner(double(Cell.Location.X) * ChunkSize, double(Cell.Location.Y) * ChunkSize, 0.0);
URealtimeMeshComponent* Component = NewObject<URealtimeMeshComponent>(this); Component->RegisterComponent(); Component->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); Component->SetRelativeLocation(Corner); Component->SetVisibility(false); // shown later, in OnActivateCell CellComponents.Add(Cell, Component);
URealtimeMeshConstructed* Mesh = Component->InitializeRealtimeMesh<URealtimeMeshConstructed>(); if (!Mesh) { return MakeFulfilledPromise<bool>(false).GetFuture(); }
// Per-cell descriptor: same generator, different cell. FRealtimeMeshPlaneCellDescriptor Descriptor; Descriptor.CellLocation = FIntPoint(int32(Cell.Location.X), int32(Cell.Location.Y)); Descriptor.LOD = Cell.LOD; Descriptor.CellSize = FVector2f(float(GBaseChunkSize), float(GBaseChunkSize)); Descriptor.CellHeight = CellHeight;
Mesh->SetGenerator(Generator.ToSharedRef(), FInstancedStruct::Make(Descriptor));
return Mesh->Regenerate(FRealtimeMeshDirtyFlags::AllDirty()) .Next([](ERealtimeMeshProxyUpdateStatus) { return true; });}Note the shape: one Generator, many descriptors. Every cell shares the generator resolved in BeginPlay; only the descriptor differs. That is what makes streaming hundreds of cells concurrently cheap.
Activate / deactivate — toggle visibility
Section titled “Activate / deactivate — toggle visibility”void ARealtimeMeshSpatialStreamingActor::OnActivateCell(const FRealtimeMeshSpatialComponentLocation& Cell){ if (URealtimeMeshComponent* Component = CellComponents.FindRef(Cell)) { Component->SetVisibility(true, true); }}
void ARealtimeMeshSpatialStreamingActor::OnDeactivateCell(const FRealtimeMeshSpatialComponentLocation& Cell){ if (URealtimeMeshComponent* Component = CellComponents.FindRef(Cell)) { Component->SetVisibility(false, true); }}Unload — destroy
Section titled “Unload — destroy”void ARealtimeMeshSpatialStreamingActor::OnUnloadCell(const FRealtimeMeshSpatialComponentLocation& Cell){ if (URealtimeMeshComponent* Component = CellComponents.FindRef(Cell)) { Component->DestroyComponent(); CellComponents.Remove(Cell); }}Streaming sources
Section titled “Streaming sources”Streaming radiates from sources. In PIE the subsystem auto-adds a source for the player Character, so the plane field simply follows the player with no extra setup. If your pawn is not a Character, attach a URealtimeMeshSpatialStreamingSourceComponent to it:
UCLASS(Meta = (BlueprintSpawnableComponent))class URealtimeMeshSpatialStreamingSourceComponent : public UActorComponent, public IRealtimeMeshSpatialStreamingSourceProvider{public: UFUNCTION(BlueprintCallable, Category = "Streaming") void EnableStreamingSource(); UFUNCTION(BlueprintCallable, Category = "Streaming") void DisableStreamingSource(); UFUNCTION(BlueprintPure, Category = "Streaming") bool IsStreamingSourceEnabled() const;
// True once all managers in this world have settled their loads/unloads. UFUNCTION(BlueprintCallable, Category = "Streaming") bool IsStreamingStable() const;};The component registers itself with the subsystem on OnRegister and reports the pawn’s position as a streaming source each frame. It exposes enable/disable and an IsStreamingStable() you can call from Blueprint to gate gameplay until the streamed world is stable.
Under the hood a source carries tuning knobs (FRealtimeMeshSpatialStreamingSource): MaxVisibilityDistance, LODFalloff, LODVisibilityThreshold, and StreamingDistanceMultiplier control how far cells stream and how aggressively LOD falls off with distance.