World partition
Split a large world into cell scenes, stream cells around players and workers, and shift the Unity origin.
Use Nebula's optional world partition when a level is too large for one Unity scene or stable 32-bit coordinates. The system divides the world into additive cell scenes and shifts the Unity origin as an anchor moves.
You can use the world-partition package without Nebula networking. You can also use Nebula networking with a single scene.
Use a procedural world
Assign a WorldDefinition to NebulaConfig.RuntimeWorld to enable origin shifts without loading authored cell scenes. Nebula loads the definition before game scripts initialize. Your game registers containers through ContainerRegistry.RegisterRuntime and chooses when to call NebulaWorld.Streamer.ShiftOrigin. NebulaWorld.Definition returns the runtime definition. IsActive remains false because it indicates authored scene streaming.
Leave NebulaConfig.WorldManifest empty for a runtime world. When both fields are assigned, the baked manifest takes precedence and Nebula > Validate Project reports the conflict. Code that uses world partition without Nebula networking can call NebulaWorld.LoadRuntime(definition) directly instead.
For a complete runtime-grid example with on-demand chunk leases, role-specific content loading, a predicted player, floating-origin shifts, and persistent placement, follow Build an infinite runtime world.
For distant regions, set ContainerRegistry.RuntimeBoundsInFrame to calculate each public runtime container's bounds from its stable ID and the current origin. Subtract precise coordinates before converting the result to floats. The callback runs during registration and origin shifts; shifts use its center and preserve the container's size. Clear the callback when unloading your world.
Override NebulaGameMode.PrepareSpatialFrame if workers need to shift the origin before checking an entity's container membership. This hook runs for each authoritative entity, so avoid expensive work in it. Game code remains responsible for procedural content and the origin-shift policy.
The model
Create a WorldDefinition asset to define a three-dimensional grid of cells. Each cell uses the same CellSize and its own additive scene. The default cell size is 256 meters on each axis.
Cell (x, y, z) is centered at (x, y, z) * CellSize. With the default size, cell (0, 0, 0) covers 128 meters in each direction from the origin. Keep a flat world on one Y layer, or add Y layers when the level needs vertical cells.
Cells are authored cell-local: the content of a cell scene sits around that scene's own origin, and at runtime the WorldStreamer translates the whole scene to where its cell belongs. Nothing in the pipeline ever holds an absolute world coordinate, which is what makes worlds beyond float range possible.
The floating origin (WorldOrigin) is the cell that currently sits at Unity's (0,0,0). When it moves, the streamer translates every loaded cell and raises WorldOrigin.Shifted(delta) so anything else caching a position can follow.
Setting up in one step
Open the level and run Nebula > Set Up World Partition. It:
- runs Nebula > Set Up Active Scene, so the level is the game scene with a configured
NebulaBootstrap; - creates a
WorldDefinitionnext to the scene (World/<Scene>.asset, 256 m cells) and itsWorldContainerManifest, or reuses the ones the project already has; - moves the level's root objects into cell scenes by position. Cameras, directional lights, post-processing volumes, UI, the game mode, the Nebula objects and any roots you selected beforehand stay in the hub;
- clears Batching Static on the moved content, saves the hub and the cell scenes, bakes the manifest and sets
NebulaConfig.WorldManifest.
Running it again on a partitioned hub only rebakes. The rest of this page describes what it sets up and how to author from there. To partition the scene from the tutorial step by step, see Partitioning the tutorial arena. Nebula > Validate Project reports cell scenes saved after the last bake, cells missing from the build settings and Batching Static objects in open cells.
Authoring cells
Open Nebula > World > World Window. Pick or create a WorldDefinition, then use the map:
- Click an empty cell to create its scene. The scene gets a
WorldCellroot at its origin and is added to the build settings. - Click a defined cell to open it additively, or close it. Open cells are placed next to each other in the scene view, exactly as they will be in the game, and the scene view draws every defined cell's bounds.
- Right-click for Open alone, Set as editor origin, Delete.
- The Layer (Y) field switches which vertical layer the map shows.
When you open a cell, the editor moves its scene into the correct visible position. When you save it, the editor removes that translation so the scene stays cell-local.
Do not move the WorldCell root. Its position records the editor translation. To work on a region far from cell (0, 0, 0), set the editor origin cell. The editor then places open cells around that origin.
Partitioning an existing level
Use Partition an existing scene to move root objects from one scene into cells based on their positions. Select the roots that must remain in the hub scene, such as lights, cameras, NebulaBootstrap, and your game mode. Then select Everything except selection. You can also select only the roots you want to move.
Keep the hub scene as the game scene that every process loads. Nebula streams the cell scenes under it. You can begin with one cell and add more cells as the world grows.
Streaming at runtime
Put a WorldStreamer in the hub scene (Nebula does this for you when the world is enabled in NebulaConfig, see below) and give it anchors:
- A
WorldAnchorcomponent keeps the cube of cells withinRadiusCellsof its transform loaded. Put one on the player or the camera and mark itPrimary; the floating origin follows the primary anchor once it strays more thanShiftThresholdCellsfrom the origin cell. streamer.SetRequired(cells)pins an explicit set of cells, for code that knows what it needs.
Cells that nothing wants any more linger LingerSeconds before unloading. CellLoaded, CellUnloading, CellUnloaded and OriginShifted are raised for game code.
Two Unity caveats apply to anything that moves geometry at runtime. Static batching bakes positions into the combined mesh, so leave it off for cell content (the SRP batcher and GPU instancing are unaffected). Baked light probes are positioned in world space and do not follow a shift; lightmaps and reflection probes do.
With Nebula
A partitioned world plugs into the container model: every cell is a container, and the Container volumes you author inside cell scenes (a building, a room) become nested containers of their cell, exactly as they nest inside an outdoor container today.
- In the World window, select Create next to Container manifest, then select Bake. Nebula writes a
WorldContainerManifestwith one container per cell and every nestedContainerfound in the cell scenes. - Press Use in NebulaConfig.
NebulaConfig.WorldManifestis what switches the partition on; unset, everything behaves as a single scene. - Rebake after adding, moving or resizing containers or cells. The bake also adds cell scenes to the build settings.
At startup, every process creates the full container hierarchy as empty transforms. This keeps container indices consistent even when processes load different cells. As a cell scene loads, Nebula places its content under the matching transforms and removes the authoring-only Container components.
Each role then streams what it needs:
| Role | Cells loaded | Origin |
|---|---|---|
| Worker | The cells of every container it leases plus WorkerLoadRingCells rings around them, so seam physics and ghosts have the neighbors' geometry. | Follows the centroid of the leased cells. |
| Client and bots | NearCells around the client's content anchor (the origin cell before a pawn exists). | Follows the content anchor. |
| Gateway, orchestrator | Nothing. The manifest gives them the container maths. | Never moves. |
Interest management computes a client's load radius as NearCells = max(ClientLoadRadiusCells, ceil((InterestRadius + InterestExitMargin + overshoot) / cell size)), where overshoot = InterestMaxFocusSpeed * (InterestLingerSeconds + 1 / InterestEvalHz). Raising InterestRadius therefore widens client streaming, and ClientLoadRadiusCells sets the minimum radius. The content anchor is the local pawn by default; NebulaClient.SetContentAnchor moves it to another transform, such as a strategy camera, and both cell streaming and the runtime chunk origin follow it. See the strategy camera recipe.
The origin shifts only when the player or worker centroid is more than OriginShiftThresholdCells from the origin cell. When it shifts, Nebula moves the containers and refreshes each entity's interpolation, pose, and prediction history.
If your component caches a position in the current Unity coordinate frame, override NetworkBehaviour.OnOriginShifted(delta) and add delta to the cached value.
The orchestrator deals containers in manifest order, so each worker's share is a contiguous run of spatially neighboring cells: a compact region it loads as a block. The dashboard shows each container's cell.
Interest management subdivides each baked cell into interest regions of InterestCellSize (default 64 m), snapped so region edges land on cell edges. A 256 m cell is subdivided into a 4x4 grid of regions; a gateway subscribes only the regions its clients need, not the whole cell, so a client near one edge of a large cell does not cause a worker to publish everything inside it.
Partitioning the tutorial arena
This section converts the Arena scene from the tutorial: the four quadrant containers, the PlayerController prefab and TutorialGameMode. The arena stays small, so everything lands in one cell. Adding more cells works the same way afterwards.
Partition the scene
Open Arena and run Nebula > Set Up World Partition. Confirm Partition and save.
Nebula,Game, the camera and the directional light stay in the hub scene.- The floor, the walls and the
Containersparent move into cell(0, 0, 0). With the default 256 m cells, that cell covers 128 m in each direction from the origin, which contains the entire 40 × 40 m arena. - The four quadrant
Containercomponents are now inside a cell scene. The bake records them as nested containers of the cell containercell_0_0_0.
If the report warns that containers were left in the hub scene, you selected their objects before running the command, so they stayed in the hub. Move them into the cell scene and run the command again to rebake.
Keep the player prefab as it is
The player prefab needs no changes:
- The client streams cells around its local player (
NebulaClient.LocalPlayer) automatically. Do not add aWorldAnchorto the prefab.WorldAnchoris for using world partition without Nebula networking. - When the origin shifts, Nebula moves entities with their containers and updates remote interpolation and the prediction history.
PlayerControllerstores no position values between frames. It keeps only_yaw, which does not depend on the origin.- The prefab stays in
NebulaConfig.NetworkPrefabswithSceneIdat0. Prefab assets do not belong to any cell.
If you later add a field that stores a Unity-frame position, such as a last hit point or a patrol target, correct it after an origin shift. On a PredictedBehaviour, call the base method:
private Vector3 _lastHitPoint;
public override void OnOriginShifted(Vector3 delta)
{
base.OnOriginShifted(delta);
_lastHitPoint += delta;
}Update the game mode
The gateway can choose any container with an active lease, and each cell container is now one of them. NebulaContainerHelpers.RandomPointIn(container) spreads points over the container's whole volume. For cell_0_0_0, a point can be up to 126 m from the center, well outside the 40 m floor. When the gateway selects a cell container, spawn inside one of the containers authored in that cell instead:
using System.Linq;
using Nebula;
using UnityEngine;
public sealed class TutorialGameMode : NebulaGameMode
{
public GameObject PlayerPrefab;
public override NetworkIdentity OnSpawnPlayer(NebulaWorker worker, ulong clientId, string playerName, Container container)
{
var area = SpawnArea(container);
var position = NebulaContainerHelpers.RandomPointIn(area, margin: 2f, y: 0f);
var identity = NetworkPrefabs.Instantiate(NetworkPrefabs.IdOf(PlayerPrefab), position, Quaternion.identity, area.transform);
identity.name = $"Player {playerName} ({clientId})";
worker.Spawn(identity, area, clientId);
return identity;
}
// A cell container spans its whole cell. Spawn inside one of the containers authored in that cell instead.
private static Container SpawnArea(Container container)
{
if (!container.IsCell) return container;
var nested = ContainerRegistry.InCell(container.Cell).Where(c => !c.IsCell).ToList();
return nested.Count > 0 ? nested[Random.Range(0, nested.Count)] : container;
}
}ContainerRegistry.InCell returns an empty list when the world is not partitioned, so this game mode also works in a single-scene project. The chosen quadrant can belong to a different worker. In that case the entity is handed over to the owning worker, just as when a player walks across a boundary.
y: 0f is a height in Unity coordinates. That is correct while the world has a single Y layer, because the origin cell's Y coordinate is always 0. In a world with vertical layers, compute the height in the container's local space with area.ToWorld instead.
Build and run
nebula build
nebula start --open-uiRun Nebula > Validate Project first. It reports cells missing from the build settings and cell scenes saved after the last bake.
Each worker log now includes containers: 5 (partitioned world) and cell (0,0,0) loaded. The orchestrator distributes five containers across four workers in manifest order, with the cell container first, so w1 controls cell_0_0_0 and one quadrant. Walking between quadrants behaves as it did in the tutorial. The dashboard also shows each container's cell.
Grow the world
In the World Window, click cell (1, 0, 0) to create its scene. The cell is centered 256 m along X. Build its content around the cell scene's origin, add Container objects for its rooms or regions, then select Bake or run Nebula > Set Up World Partition again. Connect the new area with floor that crosses from the arena, because the arena's edge is at x = 20 m and the new cell begins at x = 128 m.
Players in cell (0, 0, 0) load cell (1, 0, 0) when NearCells reaches it. Workers load it when it is within WorkerLoadRingCells of a cell they control.
To make cells smaller than 256 m, consider lowering InterestRadius and InterestExitMargin if the resulting content load is too large. A cell smaller than InterestRadius + InterestExitMargin still works — NearCells widens client streaming to cover it — but a cell several times larger than that sum keeps a client's loaded area close to one cell instead of several.
Balancing hints per cell
The World window edits a balancing hint per cell, so the map itself says which cells are special before the mesh ever runs.
- Bake the container manifest first: hints live on its entries.
- Ctrl-click cells on the map to pick them. Picked cells lighten; a cell that already carries a hint gets an orange corner tab.
- Set the values under Balancing hints and press Apply to N picked cell(s). Clear hint on picked resets them, and Copy from first picked loads an existing hint back into the fields.
A typical use is marking the city hub cell Dedicated, or giving the cells of one landmark a shared affinity group. Hints are carried across a rebake by container id, so re-baking after moving scenery does not lose them. The runtime value set through the dashboard or game code still wins over the baked one.
Settings
NebulaConfig field | Default | Meaning |
|---|---|---|
WorldManifest | none | The baked manifest. Unset = single scene. |
ClientLoadRadiusCells | 1 | Floor on the cells around the local pawn a client keeps loaded (1 = the 3x3x3 block). The effective radius is NearCells, which also covers InterestRadius + InterestExitMargin. |
WorkerLoadRingCells | 1 | Rings of cells around every leased cell a worker keeps loaded. |
OriginShiftThresholdCells | 4 | Distance in cells from the origin cell before the origin moves. |
WorldDefinition.CellSize sets the cell size per world. Pick it against the ghost band and interest management: a cell several times larger than InterestRadius + InterestExitMargin keeps a client's loaded cells close to a 3x3x3 block; a smaller cell still works correctly, because NearCells (not a fixed cell count) is what actually bounds client streaming, regions, and the runtime allocator ring together.