Nebula

Physics frames

Give a ship, station, or planet a physics scene of its own in which it stands still, so what is inside simulates in the container's own coordinates and can be simulated by a different worker than the hull.

Use a physics frame when entities move around inside something that moves: the crew and cargo of a ship, the people in a station, the bases on a rotating planet. A container with a physics frame of its own gets a separate physics scene in which the container stands still. Everything inside simulates in the container's local coordinates, with the container's own down as gravity, whatever the container does in the world.

A frame also makes it safe for another worker to simulate part of the inside. The worker that simulates an engine room works in the ship's still coordinates, so it never needs the ship's pose, which another worker owns.

Choose when to use a frame

Give a container a frame when:

  • entities walk, fall, or collide inside it while it moves, pitches, or rolls;
  • rigidbodies inside it must stay at rest while it accelerates or turns;
  • part of the inside should be leased to another worker than the one that moves the container; or
  • it is a planet-sized area whose contents need regions of their own.

Leave it out for small vehicles such as a car or a lift. Each frame is a Unity scene with its own physics on every process that holds the container, so a dynamic container without a frame is cheaper.

Give a container its own frame

On a carrier prefab, select the Container and turn on Own Physics Frame. Nebula creates the frame when the carrier spawns on a process and releases it when the carrier despawns there. Released frame scenes are kept for reuse (PhysicsFrames.PoolSize, 8 by default).

For a runtime container, ask for one in its placement:

var placement = ContainerPlacement.Root(stationCenter, new Vector3(400, 200, 400))
    .WithPhysicsFrame();
worker.RequestRuntimeContainer(stationId, placement);

stationCenter is a Double3 in absolute coordinates, and stationId is the 64-bit id your game chose for the container.

Interior geometry

A frame needs the container's interior colliders inside its own scene. By default Nebula copies the carrier's colliders into the frame: every non-trigger collider under the carrier, except those of entities riding in it. Parts of the carrier's own prefab count as the carrier's, including doors and seats whose network behaviors added a NetworkIdentity of their own. Nebula keeps each copy where its original is relative to the carrier, and enabled when the original is, on workers every tick and on clients every rendered frame, so a ramp that lowers or a door that opens blocks and can be aimed at inside the frame too. The originals stay in the world scene for everything outside.

On a worker, a copy whose original moves becomes a kinematic Rigidbody the first time it moves, and Nebula moves it with MovePosition from then on. A crate on a lowering ramp or a rising lift rides it instead of being pushed out of it. A part that never moves stays a static collider. Clients do not simulate frames, so they keep placing their copies directly.

To use your own geometry instead, set Frame Content on the Container to a prefab of colliders (and visuals, if you want them). The prefab must not contain a NetworkIdentity.

Understand what your code sees

A frame is simulated at the identity pose and rendered at its world pose:

Where the code runsWhat an entity inside the frame reports
NetworkTick on a workerContainer-local coordinates. Vector3.up is the container's up.
NetworkTick for the local player's predicted pawn on a clientContainer-local coordinates, exactly as on the worker.
Update, LateUpdate, cameras, and rendering on a clientWorld coordinates. The frame is posed at the carrier's interpolated pose.

Movement code that uses Vector3.up, transform.position, and physics queries therefore works inside a frame without changes. Query through Identity.PhysicsScene, which is the frame's physics scene while the entity is inside it.

When code relates something inside a frame to something outside it, convert the position first:

CallResult
NetworkIdentity.SpaceThe framed container whose coordinates the entity is in, or null outside frames.
identity.FromScope(worldPosition)A world position, such as a door on the hull, in the entity's coordinates.
identity.ToScope(position)A position in the entity's coordinates as a world position.
identity.PlaceInScope(position, rotation)Moves the entity to a world pose. It leaves its current frame first, and enters the frame at the target when the target is inside one, such as another ship's interior. Use it for respawns and teleports.
identity.PlaceInFrame(frame, localPosition, localRotation)Moves the entity to a pose in a frame's own coordinates, into the deepest container of that frame that holds the point. Use it when the target belongs to a frame, such as a landing pad on a planet or a seat in a ship.
PhysicsFrames.Convert(position, fromSpace, toSpace)Converts between any two spaces. null is the world.
PhysicsFrames.ConvertLocal(localPosition, fromSpace, toSpace)Converts a Double3 between two frames' own coordinates in double precision. null is the world.
PhysicsFrames.SourceOf(collider)The carrier's original collider for a copy inside a frame.

For example, a reach check on the worker compares the player's eyes with a button on the hull:

var button = Identity.FromScope(doorButton.transform.position);
if ((EyePosition - button).sqrMagnitude > reach * reach) return;

A raycast inside the frame hits the frame's copies of the hull colliders. To find a component on what you hit, look at the original:

if (Identity.PhysicsScene.Raycast(EyePosition, AimDirection, out var hit, range))
{
    var target = PhysicsFrames.SourceOf(hit.collider).GetComponentInParent<DoorButton>();
}

DoorButton stands for any component your game puts on the carrier.

Do not write a world position straight onto the transform of an entity inside a frame. The frame reads it as container-local coordinates. Call PlaceInScope instead.

Place an entity far from the world's origin

A world position is a float. Far from the world's origin, a float can't hold small distances: 300,000 km out, neighboring values are 32 m apart. If a framed container's carrier stands that far out, such as a planet in a star system, PlaceInScope rounds the target to that spacing and a player placed on a landing pad can end up meters under it.

To place an entity exactly, give the pose in the frame's own coordinates with PlaceInFrame. The frame's own coordinates are what frame.Frame.SimulationToLocal returns on the worker. They don't change when the frame's floating origin moves. Nebula carries the pose into the container the entity lands in using double-precision arithmetic, and never through a world position:

// On the worker that has authority over the pawn. planet is the framed Container the pad belongs to.
var pad = new Double3(204_801.25, 1.5, -1_234.75);
pawn.PlaceInFrame(planet, pad, Quaternion.Euler(0f, heading, 0f));

heading stands for the direction your game wants the pawn to face. The frame must be in the entity's own scope. To move an entity into a frame in another scope, commit a transfer at a frame-local pose instead. See Move an entity into another scope.

Clients receive the same pose. Nebula sends an entity's pose relative to its container, so the exact pose on the worker is the pose every client gets.

Positions computed while an entity simulates are in the frame's coordinates, too. Convert them before you draw something with them. A predicted shot's tracer, for example, goes through identity.ToScope before the client draws it. A position that a worker sends to other clients, such as where a shot hit, is best sent in the frame's coordinates, with each client converting it at the carrier's pose it shows:

// Worker: the frame's own coordinates, independent of where this worker has the carrier right now.
var frame = Identity.Space != null ? Identity.Space.Frame : null;
Vector3 local = frame != null ? frame.SimulationToLocal(hitPoint) : hitPoint;

// Client: where the carrier is drawn.
Vector3 drawn = Identity.Space != null ? PhysicsFrames.Convert(local, Identity.Space, null) : local;

hitPoint is where the worker's raycast hit, in the shooter's simulation space. Send local in your RPC and convert it in the client's handler.

Cross the frame's boundary

An entity enters a frame when the worker resolves it into the framed container's box, and leaves when it is farther than HandoverHysteresis outside the inside of that box. Nebula converts its position, rotation, and velocity at the frame's pose for that tick. A leaving entity gains the frame's velocity, including the part that comes from the frame turning, so a crate that falls out of a moving ship keeps moving with the ship. A Rigidbody's angular velocity gains the frame's spin the same way, and an entering one loses it, so a crate turning with a turning ship stands still once inside it.

Nebula converts Identity.Motion.Velocity and a Rigidbody's velocity. A controller that keeps a velocity of its own, such as a vertical speed for jumping, reads it back from Identity.Motion.Velocity in OnContainerChanged on the worker, so a fall or a jump continues along the new space's up:

public override void OnContainerChanged(Container previous, Container current)
{
    bool newSpace = (previous != null ? previous.InnerSpace : null) != (current != null ? current.InnerSpace : null);
    if (HasAuthority && newSpace) _verticalSpeed = Identity.Motion.Velocity.y;
}

_verticalSpeed stands for your controller's own field. Write it into Identity.Motion.Velocity each tick so Nebula has the value to convert.

Crossings work the same way for every entity a worker simulates, whether a player owns it or not. A character spawned with SpawnServerDriven walks onto a moving ship and off again as a player's pawn does: write its movement in its own space, turn targets from the scope's space into that space with Identity.FromScope, and write what it did into Identity.Motion.Velocity each tick. Clients need no code for it, because they interpolate it across the crossing like any other entity. The Server-owned boarding sample in the package (Samples~/ServerOwnedBoarding) has a walker and a moving platform that show this.

Only the worker that simulates the carrier knows its pose exactly. That worker, the pose owner, performs every crossing. Any other worker that would cross an entity hands it to the pose owner first, unconverted. The pose owner crosses it, then hands it on if the destination belongs to another worker. A crossing costs at most one extra handover and never a position error.

To limit where entities may cross, install a crossing policy. This one lets entities leave only through an airlock at the stern of a ship:

public sealed class AirlockPolicy : IFrameCrossingPolicy
{
    public FrameCrossing Decide(NetworkIdentity entity, PhysicsFrame frame, bool leaving, Container from, Container to)
    {
        if (!leaving) return FrameCrossing.Allow;
        var local = frame.SimulationToLocal(entity.transform.position);
        return local.z < -9f && Mathf.Abs(local.x) < 1.5f ? FrameCrossing.Allow : FrameCrossing.Veto;
    }
}

PhysicsFrames.CrossingPolicy = new AirlockPolicy();

Veto keeps the entity where it is. Keep it inside with your own collision, such as the hull's walls. Defer asks again on the next tick, for example while an airlock cycles. The policy runs on the pose owner.

An entity belongs to one frame at a time. A ship half inside a hangar, or a crane that reaches out of a hull, is on one side or the other and switches at the hysteresis threshold.

Size the container's box with that in mind. If the box of a ship's hold ends on its ramp rather than at its door, a crate pushed down the ramp leaves the frame while it still rests on the ramp's copy, and lands on the ground outside, where the ramp's original is.

Read the frame's motion

container.Frame.State is a PhysicsFrameState. It holds the frame's position, rotation, velocity, angular velocity, and acceleration in the space around it, updated once per tick on every process that holds the frame. On the worker that simulates the carrier it is exact. Elsewhere it comes from the replicated carrier pose, one replication delay late.

The space around a frame can itself be a frame. When a ship with a frame lands inside a carrier that has one, the ship's State is its motion relative to the carrier from the next tick on, so a ship resting on a cruising carrier's deck reads zero.

Nebula applies no fictitious forces unless you ask for them. To make a body feel the frame's motion, add FrameInertia to it. To apply your own, read State.LocalAcceleration and State.LocalAngularVelocity, which are in the frame's own axes.

Carry loose cargo

A crate inside a frame needs no Nebula component beyond those of any physics prop: a NetworkIdentity, a NetworkTransform, a Rigidbody, a NetworkRigidbody, and its colliders. It simulates in the frame's scene, where the ship stands still, so a stack of crates stays put through takeoff, turns, cruise at any speed, and landing. It crosses out of the frame with the ship's velocity like any other entity, and a handover of the ship to another worker keeps each crate's pose, velocity, and sleep.

These Rigidbody settings keep stacks stable:

SettingValue
solverIterations8 to 10, for stacks of three or more
collisionDetectionModeContinuousSpeculative
Sleep thresholdThe default
PhysicsMaterial frictionAbout 0.6

Inside a frame, Identity.Motion.Velocity and the Rigidbody's velocity are relative to the ship: a crate at rest in a ship flying at 1 km/s reads zero.

Let cargo feel the ship move

Add FrameInertia next to a crate's NetworkRigidbody to make it slide when the ship accelerates, brakes, or turns. On the authoritative worker, each tick, it applies the frame's fictitious acceleration to the body: the ship's acceleration and the centrifugal, Coriolis, and angular-acceleration terms of its rotation, all reversed.

FieldDefaultMeaning
Scale0How much of the ship's motion the crate feels, from 0 (none) to 1 (all of it). Set it per ship, like inertial dampers.
MaxAcceleration20 m/s²The most it applies in one tick, after Scale. A teleport or a hard landing nudges the crate rather than launching it.
SmoothingTicks6Ticks it smooths the acceleration over.
MinAcceleration0.05 m/s²Below this it applies nothing, so a crate in a cruising ship can sleep.

FrameInertia does nothing outside a frame, on a ghost, on a client, or while the body is kinematic. It reads the frame's motion one tick late, and a frame leased to another worker than the ship's reads it one replication delay late. It does not rotate the frame's gravity: down stays the ship's floor.

Stow a ship with its cargo

worker.Despawn(ship) sets down whatever rides in the ship where the ship stood. To put a ship away, in a hangar list or an inventory, and keep its cargo aboard, despawn it with CargoPolicy.Stow:

worker.Despawn(ship, keepPersisted: true, CargoPolicy.Stow);

Nebula saves and despawns every rider this worker simulates that has a PersistentEntity and no owning player, riders of riders first, so each record names the ship. When the ship's record is restored, persistence brings the cargo back aboard it. A player's pawn and any entity without a PersistentEntity are set down, as with a plain despawn. The ship itself must have a PersistentEntity, and keepPersisted must be true. Otherwise the cargo could never come back, so Nebula sets it down and logs a warning.

Attach an entity to a container

Add FrameAttachment to an entity that should stay fixed in a container: a crate strapped to a cargo grid, a turret on a deck, a crate on a shelf in a building. It works in a ship's frame, in a fixed container such as a building or a chunk, and in a carrier without a frame. The entity can have a NetworkRigidbody or no body at all.

On the worker that has authority over the entity:

var attachment = crate.GetComponent<FrameAttachment>();

// At a pose in the container's space: here, a slot on a cargo grid in the ship's hold.
attachment.Attach(ship.Carried, slotPosition, slotRotation);

// Or in the container it is in, where it is now.
attachment.Attach();

// Let it go.
attachment.Detach();

slotPosition and slotRotation stand for a pose your game computes in the container's local space. From another worker that holds a copy of the entity, call RequestAttach or RequestDetach. They reach the authority as an authority RPC, and they name the container by its ID, so the authority must know the container too.

While the entity is attached:

  • It sits in the container it is attached to. Nebula does not move it into a neighboring container, out of a frame, or through a portal into another scope, whatever its position. It is still handed to the worker that owns the container, so attaching it to a container another worker owns moves it there on the next tick.
  • Its body is kinematic on the authority, and Nebula puts it back at the attached pose every tick. Loose crates can rest on it.
  • A handover, a restore from persistence, and a client that joins later all see it attached. With a PersistentEntity it comes back attached, even with PersistPose off, and attached cargo stowed with CargoPolicy.Stow comes back attached aboard its ship.

Attached, AttachedTo, AttachedLocalPosition, and AttachedLocalRotation describe the attachment. AttachedChanged is raised on every process that holds the entity.

Detach leaves the entity where it is, in its container. The body turns dynamic with the velocity the attached pose had over the last tick: zero in a frame or a fixed container, the carrier's velocity in a carrier without a frame. For DetachSettleTicks (10) ticks, its depenetration speed is capped at DetachDepenetrationVelocity (1 m/s), so a crate attached slightly inside another is eased apart.

If the entity's container changes for any other reason, it detaches and raises AttachedChanged(false). For example, a ship despawned with a plain Despawn sets its attached cargo down, detached. To move an attached entity, call Attach again with the new pose.

Lease part of the inside to another worker

A leased container sits under a moving parent only when that parent has a physics frame. To let a second worker simulate a ship's engine room, register the room as a runtime container inside the ship and lease it:

var engineRoom = ContainerPlacement.Child(
    ship.Carried.ContainerId,          // the ship's container, e.g. "interior#281474976710657"
    new Vector3(0f, 3f, -6f),          // centre in the ship's local space
    new Vector3(10f, 6f, 8f),
    ContainerAuthority.Leased);
worker.RequestRuntimeContainer(engineRoomId, engineRoom);

The orchestrator deals the room like any other leased container. The ship's worker keeps simulating the hull and the rest of the inside. Crew walking between the two parts are handed over like between two containers on the ground, in the ship's still coordinates, with no error from the ship's speed.

Without a frame on the ship, Nebula logs a warning, and the room is simulated by the ship's worker until it is under a frame (Container.AuthorityDemoted). An owner of a leased room under a moving parent would otherwise simulate against a pose that arrives one replication delay late.

A change in one part that affects the other, such as an engineer cutting power to the engines, is an ordinary cross-worker call. Use [AuthorityRpc] on a behavior of the ship.

Land a ship inside a moving carrier

A carrier with a frame can hold other carriers with frames: a ship that lands on a cruising capital ship, a shuttle in a station's hangar, or a ship on a planet. Nothing extra is needed beyond a frame on each. While the outer carrier moves and turns:

  • The ship enters the carrier's frame at the carrier's box face, on the carrier's pose owner, and leaves it the same way with the carrier's velocity added, including the part from the carrier turning.
  • The ship's crew stay in the ship's frame, in its coordinates, through both crossings and every handover.
  • A crew member can walk out of the ship into the carrier's frame and back. Leaving the ship needs the ship's pose owner, and leaving the carrier needs the carrier's.
  • Positions are composed in double through both frames, on workers with PhysicsFrames.ConvertLocal and on clients with PhysicsFrame.LocalToRenderPrecise.
  • A pilot whose client predicts the ship is not corrected at either crossing, at any speed of the carrier. The ship's drawn position eases over about 100 ms at each crossing, because the carrier is drawn a little in the past while the ship is predicted a little ahead.

Set the boxes up like this:

  • Make the carrier's box cover everything a ship can land on, plus the room above it that the ship flies through to land: for example its deck and 12 m over it. A ship is inside the carrier once its origin is more than HandoverHysteresis inside that box.
  • Keep the ship's box smaller than the carrier's. Of two framed carriers whose boxes hold each other's origins, the smaller always goes inside the larger.
  • Extend the ship's floor colliders past its box by more than HandoverHysteresis where the crew step out, and give the carrier a floor where they land, so a crew member leaves the ship's box while still standing on it.
  • To let the crew out only through a ramp or an airlock, give the ship a crossing policy. The carrier needs none for the ship.

To have another worker simulate the ships that land, lease part of the carrier's inside, such as a hangar bay, to it. A ship that lands in the bay is handed to the bay's owner with its crew, and is handed back to the carrier's pose owner when it takes off.

Make a planet a frame

A planet is a carrier with a physics frame of its own. It can rotate, orbit, or stay still. Give it:

  • Own Physics Frame on its Container;
  • Frame Interest set to OwnRegions;
  • optionally, Frame Approach Margin, in metres, so that a pilot above the planet sees what stands on it (see below); and
  • leased runtime children for the areas that different workers simulate, such as eight octants, with bases inside them leased or inherited as your game needs.

With OwnRegions, interest management buckets what is on the planet by its position in the planet's coordinates. The planet's rotation therefore never moves anything between regions. A client standing on the planet looks at the planet's regions around it and at the space around the planet from the same point, so ships overhead stay in view. A client outside the planet's box sees the planet itself, but not what is on its surface, unless the planet sets Frame Approach Margin (Container.FrameApproachMargin, 0 and so off by default). A client in the space around the planet whose pawn is within that many meters of the planet's box then also looks into the planet's regions, from the point of the box nearest the pawn, so a pilot 3 km above the ground sees the outposts and players standing on it. Set it to the distance from which your game needs the ground in view, and no more: every client that comes that near looks into the planet, and a margin on a container that is crowded costs interest work for each of them. The same setting serves a station seen from a ship.

If you replace the interest policy, give a focus at the player the space the player is in: InterestFocus.Point(client.PawnX, client.PawnY, client.PawnZ, 1f, client.PawnNetId, client.PawnSpace).

A frame's coordinates can be large. Each worker keeps a floating origin per frame near the entities it simulates in it: when their average position is farther than PhysicsFrames.OriginShiftThreshold (2,048 m) from the frame's origin, the worker moves the origin to them, in steps of PhysicsFrames.OriginShiftStep (1,024 m). Container-local coordinates, and so everything on the wire and every region key, do not change. Subscribe to PhysicsFrame.Shifted to move positions your game caches in simulation space.

Draw a turning planet without shaking

A client draws everything in a frame through the frame's pose. When a planet turns, ground 200 km from its centre would be rounded differently every frame, and it would shake by centimeters against the camera. To prevent this, Nebula draws a frame about a point near the camera, called the render origin, once the camera is more than PhysicsFrames.RenderOriginThreshold (256 m) from the frame's own origin. The ground near the camera then holds still to within a fraction of a millimeter. A ship's frame, whose contents stay near its origin, is drawn as before.

The render origin follows the client's content anchor: the local player's pawn, or the transform you passed to NebulaClient.SetContentAnchor. If your camera can be far from that anchor, set PhysicsFrames.RenderAnchor to the camera's transform.

Under a render origin, the frame root's position is no longer exact. To place something at a frame-local point for drawing, use the frame's render pose instead:

var frame = planet.Carried.Frame;
Vector3 drawn = frame.LocalToRender(localPoint);        // a frame-local point in the drawn scene
Vector3 local = frame.RenderToLocal(drawnPoint);        // and back
Vector3 center = frame.RenderPosition;                  // where the planet's origin is drawn
Quaternion tilt = frame.RenderRotation;                 // and how it is turned

localPoint and drawnPoint stand for positions in your game. LocalToRenderPrecise and RenderToLocalPrecise take and return Double3 values.

While cameras draw, Nebula also moves the render origin from the frame's root onto the root's direct children, so a ship or vehicle moving inside a chunk far from the planet's center holds still too. It puts every object back exactly when drawing ends, so your scripts never see the change. If your game draws outside Unity's camera callbacks, set PhysicsFrames.ShiftChildrenWhileDrawing = false.

Unity stores every object's position in scene coordinates, and a float is about 2 mm apart 20 km from the scene's origin, where a scope with large origin cells can leave the camera. So while cameras draw, Nebula also moves the whole scene near the camera (SceneRenderOrigin): once the camera is more than 1,024 m from the scene's origin, it moves every root object of the loaded scenes, each frame and the cameras by the camera's position rounded to 64 m, and puts them back when drawing ends.

  • Inside rendering callbacks, positions are shifted, Camera.main.transform.position included. SceneRenderOrigin.Offset is the amount.

  • If your game places its camera from a position it computes in double, place it again in SceneRenderOrigin.Shifted, at that position minus the offset:

    SceneRenderOrigin.Shifted += offset => cameraTransform.position = (eyePosition - offset).ToVector3();

    cameraTransform and eyePosition (a Double3) stand for your camera and its position. Nebula puts the camera back after drawing.

  • Nebula can't move what a transform doesn't carry: world-space particles, trails and lines, statically batched meshes, light probes, and meshes you draw with your own matrices. Offset them in Shifted and Restoring, or turn the scene shift off with SceneRenderOrigin.Enabled = false.

  • Particles in a custom simulation space don't follow the shift either: Unity places them from that space's pose when it simulates them, not when it draws them. If a particle system should follow, simulate it in local space under the transform instead.

  • To queue draws you issue with your own world matrices (instanced meshes), use SceneRenderOrigin.Drawing. Nebula raises it every time cameras start drawing, after it has moved both the scene and the frames' children, so every transform reads where it's drawn. The offset is zero when the scene isn't moved:

    SceneRenderOrigin.Drawing += offset => QueueInstancedDraws(offset);

    QueueInstancedDraws stands for your own code: a scene position p is drawn at p - offset.

  • To move objects in a scene Nebula doesn't enumerate, call SceneRenderOrigin.AddRoot.

To turn render origins off for one frame, set frame.UseRenderOrigin = false. To turn them off everywhere, set PhysicsFrames.RenderOriginThreshold = float.PositiveInfinity. Prediction is not affected either way: a predicted pawn always simulates at the frame's exact simulation pose.

Stream a planet's ground around players

Instead of leasing a planet's areas yourself, you can host a chunk grid in the planet: its chunks are leased runtime children of the planet's container, laid out in the planet's own coordinates, and each worker leases them around the players it simulates on or near the planet.

To host a grid, call NebulaChunkedWorld.ActivateHostedGrid once the planet's container exists, from any process with a control plane:

NebulaChunkedWorld.ActivateHostedGrid(worker.ControlPlane, planet.Carried, "planet/terra", new ChunkGridDefinition
{
    CellSize    = new Vector3(256f, 4096f, 256f), // a column 4 km tall, centred on the planet's y = 0
    Planar      = true,
    Ring        = 2,     // chunks leased around each player's chunk
    LeadSeconds = 2f,    // and along their velocity, two seconds ahead
    Reach       = 3000f, // for players up to 3 km outside the planet's box and above their chunk
});
  • The grid has its own key, here "planet/terra". Use it with NebulaChunks.GridFor, NebulaChunks.EnsureAt and NebulaChunks.AllocatorFor, exactly as you would use a scope key. The key must not be the key of any scope.
  • Its chunks belong to the scope the planet is in. Entities in them are in that scope, and the scope's lifecycle checkpoints and retires them with the rest of the scope.
  • A chunk's container ID depends only on the grid key and the chunk coordinate, so persisted entities come back in the same chunk after a restart. Activate the grid again with the planet's new container after the planet is restored; a chunk can only exist once the planet's container does.
  • LeadSeconds leases a band of chunks along each player's path, so a ship at 300 m/s finds its next chunks leased before it reaches them without a much wider ring all round.
  • A planar grid keeps the planet frame's floating origin level: workers shift it across, never up or down.
  • grid.Frame.OriginOffset converts between the planet's coordinates and simulation space. A client draws the planet where it is, so convert a planet point for rendering with planet.Carried.Frame.ToParent.

For a grid whose cells are not boxes in a row, such as the six faces of a cube-sphere, implement IChunkGridGeometry and return it from NebulaChunkedWorld.HostedGeometry on every process. Nebula then asks it which cell a point is in, each cell's box, and each cell's neighbours. A player leases chunks only within Reach of the box of the cell below them, so a ship flying high inside a sphere's bounding cube leases no ground until it comes down.

Put a portal in a framed container

A portal can stand on a planet that is a carrier, such as a door into a cave or a building built as an instance. The planet's side of the portal simulates in the planet's frame, so everything Nebula builds for that side stands in the frame's physics scene, in the frame's coordinates, wherever the planet is in its scope and however it turns: the thin collider over the opening, the copy of the other side's static geometry in the doorway, and the kinematic proxies of the bodies that straddle the face. A crate resting across such a door sleeps, and a pawn on the planet predicts against the same pieces on a client.

Only one level of frame is supported for each side of a portal. A body in a frame nested inside the planet's, such as a ship parked at the door, is mirrored, but the ship's interior is not part of the doorway's geometry. See Collide across a portal for what a game has to do.

Limits

  • Every process that holds a framed container also holds its frame scene.
  • A carrier goes inside the smallest carried box that holds its origin. Two carriers with frames are the exception: the larger never goes inside the smaller. For carriers without frames, keep a carrier's origin out of a smaller carrier's box, such as a truck parked over a cart.
  • An entity leaves one frame per tick. A crew member who steps out of a ship where the ship overhangs its carrier's box is in the carrier's frame for one tick before it leaves that frame too.
  • Raycasts do not pass from one frame into another. A shot fired out of a ship's open ramp hits nothing outside the ship's frame unless your game casts again in the space around it.
  • A client predicts in the frame of its own player only. Other entities in other frames are shown at their replicated poses.
  • Leased containers inside a frame are found by a linear scan. Keep their number in the hundreds per frame, not the thousands.
  • A hosted grid lives in a scope. The public world cannot host one.
  • A client places its copies of moving hull parts directly, so a pawn it predicts on a moving ramp is predicted against a copy that jumps from pose to pose.
  • CargoPolicy.Stow stows only the riders the despawning worker simulates. A rider that another worker simulates, in an interior leased to that worker, is set down.
  • An entity attached in a carrier without a frame moves with the carrier through its transform, so bodies resting on it are moved the same way as bodies resting on the carrier's own colliders.

On this page