World Grid
Shared spatial cell grid: per-zone chunked opaque cells (terrain edits, tiles, build pieces), streamed to each player's chunk window and delta-batched on change. Seed + persistence SPIs; server-set only.
Seams you implement 1
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayWorldGrid(); and it wins.
IWorldGridSeed game SPI
The procedural-generation seam: fills a chunk's cells the FIRST time it is touched (no stored copy existed). Terrain noise, dungeon stamps, default tile fills — whatever the game's world is made of; the framework never interprets the values. The default leaves chunks zeroed.
public void Seed(string zone, int chunkX, int chunkZ, Span<byte> cells)
{
for (int z = 0; z < ChunkCells; z++)
for (int x = 0; x < ChunkCells; x++)
WorldGridMath.WriteCell(cells,
WorldGridMath.LocalIndex(x, z, layer: 0, ChunkCells), cellBytes: 2,
Noise(chunkX * ChunkCells + x, chunkZ * ChunkCells + z));
} -
void Seed(string zone, int chunkX, int chunkZ, Span<byte> cells)Fills a fresh chunk's zeroed cell buffer with the game's initial content.
Providers you can swap 1
Infrastructure seams. Crossplay ships a working implementation of each — replacing one changes where data lives or how it moves, never a game rule.
IWorldGridStore provider
Persistence seam for chunk payloads — one record per (zone, chunkX, chunkZ). The default is in-memory (single node, lost on restart); wiring AddCrossplayPersistentStores after a persistence provider swaps in the document-backed store so edits survive restarts. Chunk saves are the classic write-behind profile (rebuildable, high churn, last-write-wins per chunk) — the write-behind decorator composes well in front of the document store.
-
void Save(string zone, int chunkX, int chunkZ, byte[] cells)Persists a chunk's full cell payload (called on the dirty-save cadence; the buffer must be copied or serialized before this returns — the engine keeps mutating it).
-
bool TryLoad(string zone, int chunkX, int chunkZ, out byte[] cells)Loads a chunk's cell payload, or false when no stored copy exists (seed it).
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
IWorldGridService
The shared spatial cell grid — per zone, chunked, streamed to each player's chunk window and kept live with coalesced delta batches. A cell is an OPAQUE fixed-width game number (a block id, a terrain type, a build piece, farm soil state); the framework never interprets it, and there is no client mutation path — game verbs (Interactions, abilities, admin tools) validate an edit and then call TrySet. Presentation is 100% the game's: the same cells render as a voxel mesh, a tilemap, or ASCII.
-
event GridCellChangedHandler CellChangedRaised after a cell's value actually changed (server-side game hook; the wire delta to subscribers batches separately per tick).
-
ValueTuple<int, int> CellOf(float worldX, float worldZ)World position → global cell coordinate, using the configured origin/cell size.
-
uint GetCell(string zone, int cellX, int cellZ, int layer)Reads one cell (materializes the chunk — loads or seeds it — when cold). Returns 0 for out-of-range layers.
-
void ReleaseZone(string zone)Saves the zone's dirty chunks and drops all of its chunks from memory — call when an instanced zone is released (the Dungeons RELEASED hook) so abandoned instances don't pin their grids forever.
-
bool TryFillRect(string zone, int cellX, int cellZ, int layer, int width, int height, uint value)Writes every cell in a rectangle (inclusive corner, exclusive extent) to one value — the bulk stamp for prefabs/clears. Same validation as TrySet; the whole rect is refused when the arguments are invalid.
-
bool TrySet(string zone, int cellX, int cellZ, int layer, uint value)Writes one cell. False when the zone name is empty, the layer is out of range, or the value does not fit the configured CellBytes width. A write with the cell's current value succeeds silently — nothing is broadcast or dirtied.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
WorldGridOptions
Configurable grid parameters, passed to AddCrossplayWorldGrid. Defaults here, never inline (the no-hardcoded-values rule). The chunk geometry is validated at composition time: ChunkCells² × Layers × CellBytes must stay under MaxChunkBytes so a full snapshot always fits comfortably inside one wire message.
-
int CellBytes { get; set; }Bytes per cell value: 1, 2, or 4 (fixed-width; a cell is an opaque game number — rich per-cell payloads belong to entities + EntityState, not cells).
-
float CellSize { get; set; }World units per cell edge (the game's spatial resolution — 1 m blocks, 0.5 m terrain quads, 2 m build tiles…).
-
int ChunkCells { get; set; }Chunk edge length in cells (chunks are square).
-
int ChunkViewRadius { get; set; }Chunk view radius, in chunks, around each player (window edge = 2r+1).
-
float DirtySaveSeconds { get; set; }Seconds between persistence sweeps of dirty chunks (0 disables persistence writes; chunks then live only in memory and the store is never touched after load).
-
int Layers { get; set; }Layer count — 1 for a flat grid (heightmap, tilemap); more for height slices / build levels. Full 3D voxel volumes are out of scope (see the design review).
-
int MaxChunkBytes { get; set; }Composition-time ceiling for one chunk's cell payload — keeps every snapshot far under the transport's inbound frame cap and out of pathological fragmentation territory. AddCrossplayWorldGrid throws when the configured geometry exceeds it.
-
int MaxChunkSnapshotsPerTickPerPlayer { get; set; }Maximum chunk snapshots sent to ONE player per subscribe tick — the anti-burst budget: a teleport streams its window over a few ticks instead of flooding megabytes.
-
float OriginX { get; set; }Grid origin (world X) — cell 0 starts here; negatives extend left of it.
-
float OriginZ { get; set; }Grid origin (world Z).
-
float SubscribeCheckSeconds { get; set; }Seconds between chunk-window re-evaluations per player (the subscribe cadence).
Wire messages 3
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
GridChunkSnapshot Server → one subscriber: a full chunk of the zone's cell grid, streamed as the player's chunk window moves (budgeted — a teleport streams over a few ticks instead of bursting). The snapshot is SELF-DESCRIBING (ChunkCells/Layers/CellBytes ride along), so the client needs no geometry configuration and can never disagree with the server. A cell value is an opaque game number — a block id, a terrain height, a build piece — rendered however the game likes (voxel mesh, tilemap tint, ASCII), which is the presentation-agnostic litmus holding.
GridCellsChanged Server → the chunk's subscribers: a coalesced batch of ABSOLUTE cell writes (all edits that landed in the chunk since the last flush). Absolute values make the batch idempotent — on the reliable-ordered channel every subscriber holds exactly the server's grid after applying it, and a snapshot + later batches always compose (versions only detect resubscribe staleness).
GridChunkForget Server → one subscriber: this chunk left your view window — drop it from the cache (a later return re-streams a fresh snapshot). Zone travel needs no forgets: the client drops a zone's chunks wholesale when its player changes zones.
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
IWorldGridClient
The WorldGrid piece, client side: a cache of the chunks the server streams for your view window, kept live by coalesced cell-delta batches. A cell value is an OPAQUE game number — this client never interprets it; the game binds values to its own voxel meshes, tilemaps, or text. Geometry (chunk size, layers, cell width) is self-describing: it rides on every snapshot, so there is nothing to configure here.
-
event Action<GridChunkSnapshot> ChunkLoadedA chunk entered your view window (or re-streamed after a return) — its full cells are now cached. Rebuild that chunk's visuals from TryGetCell.
-
event Action<GridCellsChanged> CellsChangedCells changed in a cached chunk (one coalesced batch per server flush). The cache is already updated when this fires.
-
event Action<string, int, int> ChunkForgottenA chunk left your view window — it was dropped from the cache; tear down its visuals. Arguments: zone, chunkX, chunkZ.
-
event Action<string> ZoneDroppedThe whole cache was dropped because your player changed zones (the new zone's chunks stream next). Argument: the OLD zone.
-
string ZoneThe zone the cached chunks belong to (empty before the first snapshot).
-
int ChunkCellsChunk edge length in cells (0 before the first snapshot).
-
int LayersLayer count (0 before the first snapshot).
-
int CellBytesBytes per cell value (0 before the first snapshot).
-
int LoadedChunkCountHow many chunks are currently cached.
-
bool TryGetCell(int cellX, int cellZ, int layer, out uint value)Reads a cell from the cache. False when the cell's chunk is not cached (outside your window / not yet streamed) or the layer is out of range.
UI views you can replace 1
Each ships a working uGUI panel you can drag into a scene. Substitute your own view to keep the logic and change the look — the presenter never knows.
ICrossplayWorldGridView
Narrow contract the presenter drives. UI-only, read-only: the WorldGrid wire is server-set (no client mutation path), so there are no user intents to raise.