Type: epic / interface and implementation plan. Replaces the original framing with the per-engine MapProvider interface decided in the 2026-05-03 architecture sessions.
Quick Summary
- π‘ Goal: game developers never insert terrain geometry into a cluster's physics world by hand. The cluster runtime owns chunk-loading; the game implements a per-engine
MapProvider that the runtime calls.
- Game owns the storage decision (object storage / SpacetimeDB / on-disk / procedural / hybrid) and the authoring tool (UE editor / Unity scene / Godot scene / Blender / voxel editor / generator).
- Arcane owns the interface (
MapProvider per engine), the chunks-needed computation (driven by entity positions), and the integration with the cluster's physics engine (handing collision data to Rapier / Chaos / PhysX / Bullet / Jolt).
- The same interface supports static mesh terrain, voxel terrain, and procedural terrain β implementation differs per game.
- Per-engine API discipline applies (per
entity-model.md Β§8): each engine plugin defines its own MapProvider in its native language. No shared MapProvider type across Rust/UE/Unity/Godot.
Why this matters
Terrain is the substrate that physics needs to do anything useful. A cluster owning 3 players in the eastern desert must have eastern-desert terrain colliders in its physics world for raycasts and entity-vs-floor collision to work. It must NOT have polar-icecap terrain loaded β irrelevant cost. As entities migrate (PGP affinity may relocate a player westward), the terrain set must update.
Having game developers manage this is a non-starter:
- They'd hardcode chunk loads in
on_tick, defeating dynamic clustering.
- Every game would reinvent the chunk-load logic, badly.
- Terrain ownership conflicts with cluster ownership unless centrally coordinated.
- Cross-cluster terrain coherence (raycasts near chunk boundaries) needs a single source of truth.
Arcane should compute the right terrain set automatically and orchestrate loading via a game-implemented provider.
What "terrain" is in Arcane
Terrain is the only persistent world layer that's not an entity. Three terrain shapes need to work in Arcane, all through the same interface:
| Terrain shape |
Storage (game's choice) |
Mutability |
Example |
| Static / mesh |
Object storage (S3, CDN, on-disk asset bundle) |
Read-only at runtime |
UE level export, Unity scene, glTF import |
| Voxel |
SpacetimeDB (voxel grid as durable state β every block edit persists) |
Mutable per-block, durable across sessions |
Minecraft, Valheim, Astroneer |
| Procedural / hybrid |
Seed in SpacetimeDB; geometry generated on demand; modifications stored as durable diffs |
Effectively mutable via overrides |
No Man's Sky, procedural sandboxes |
If a game has destructible structures that are entity-flavored (a wall placed by a player that takes damage), those are entities, not terrain edits. Voxel chunks are terrain, just with mutable durable state at chunk granularity rather than per-block entity_ids.
The interface β MapProvider
Per-engine plugin defines its own MapProvider. The Rapier-Rust shape:
pub trait RapierMapProvider: Send + Sync {
/// Compute which chunks need to be loaded given the cluster's currently
/// owned entity positions. Pure function; called per cluster tick or
/// on entity arrival/departure events.
fn chunks_in_range(&self, entity_positions: &[Vec3]) -> Vec<ChunkId>;
/// Fetch the collision geometry for a chunk. Implementation reads from
/// wherever the game stores it β game's choice.
fn load_chunk(&self, chunk_id: ChunkId) -> Result<ChunkCollision, MapError>;
/// Optional: called when the runtime is notified (via Redis pub/sub on
/// the existing replication channel) that a chunk's state changed in
/// another cluster. Default impl is no-op; mutable-map providers
/// (voxel, destructible) override to invalidate caches and trigger
/// reload from the local chunk source. Static-map providers ignore.
fn on_chunk_changed(&self, _chunk_id: ChunkId) {}
/// Optional: clean up when a chunk is no longer needed by the cluster.
fn unload_chunk(&self, _chunk_id: ChunkId) {}
}
pub enum ChunkCollision {
TriMesh { vertices: Vec<Vec3>, indices: Vec<[u32; 3]> },
HeightField { width: usize, height: usize, samples: Vec<f32> },
// Voxel games typically convert to TriMesh via greedy meshing
// before returning, or expose per-block boxes if blocks are sparse.
}
pub struct ChunkId(pub u64); // game-defined opaque id; 64 bits is fine for any practical world
The UE plugin defines IArcaneUnrealMapProvider returning UE-native collision (UStaticMesh* / ULandscapeComponent* / equivalent). Unity defines its own with Mesh / MeshCollider. Godot defines its own returning ConcavePolygonShape3D. Same conceptual contract, different language and types β per the per-engine API discipline.
Per-cluster runtime contract
- Cluster startup: instantiate the game's
MapProvider. Optionally read a manifest from SpacetimeDB to know which chunks exist.
- On entity arrival (via
add_entity / first-sight): call provider.chunks_in_range(...) with the new owned-entity-position set, diff against currently-loaded chunks, call provider.load_chunk(...) for newly-needed ones, insert resulting collision geometry into the cluster's physics world as fixed bodies.
- On entity departure (
pending_removals / migration out): same diff; remove chunks no longer needed by any owned entity; cleanup their physics bodies.
- Per tick (low-frequency): optionally recompute the chunks-needed set as entities move. Chunks shift slowly relative to tick rate, so this can be coarse (e.g., every 10 ticks).
- Chunks loaded as
RigidBodyType::Fixed bodies (per-engine equivalent). Solver-skipped; only AABB tracking. Per-tick cost essentially zero.
Where things live (typical layout)
| Data |
Storage |
| Static map content (mesh files, prebaked geometry) |
Object storage / asset bundle / on-disk β game's choice |
| Voxel terrain content |
SpacetimeDB (durable per-chunk row) |
| Map manifest (chunk catalog, version pointers) |
SpacetimeDB row(s) β small, always available, read at cluster startup |
| Mutable per-chunk state (destruction events on a mesh chunk, voxel diffs, runtime modifications) |
SpacetimeDB for durability + Redis pub/sub for real-time cross-cluster sync |
| Per-chunk entities (placed structures, drops) |
SpacetimeDB β already entities, already durable |
Cross-cluster coordination for mutable terrain
For mutable terrain (voxel chunks, destructible mesh chunks, runtime-edited geometry), cross-cluster sync uses the same Redis pub/sub channel pattern Arcane already uses for entity replication. SpacetimeDB is the durable source of truth; Redis is the real-time replication layer.
When Cluster X modifies a chunk:
- Cluster X writes the change to SpacetimeDB (durable transactional row).
- Cluster X publishes the modification on Redis (real-time, on the existing replication channel).
- Cluster Y, with the same chunk loaded for its own owned entities, receives the Redis notification.
- Cluster Y's
MapProvider::on_chunk_changed(chunk_id) is called by the runtime.
- Cluster Y's provider invalidates its local cache and reloads the chunk's collision geometry (from SpacetimeDB if needed, or from a local cache that's just been refreshed).
- On any cluster restart: rehydrate from SpacetimeDB; Redis events from before the restart are already reflected in durable state.
This is the same write-to-SpacetimeDB-and-publish-on-Redis pattern Arcane uses for EntityStateDelta. Voxel chunks and destructible-terrain modifications are conceptually just another kind of cross-cluster game state that's both immediate and durable. Don't propose "SpacetimeDB pub/sub" or "Redis as durable storage" β both are wrong by Arcane's design (memory: project_redis_vs_spacetimedb_split.md).
Engine-native streaming bridging
The MapProvider's role differs by engine. For engines with sophisticated native streaming systems (UE / Unity / Godot), the plugin's MapProvider wraps the native system rather than re-implementing chunk loading. For engines without native streaming (Rapier-Rust pure, voxel games regardless of engine), the plugin's MapProvider is fully implemented by the game.
| Engine |
Native streaming system |
Plugin's MapProvider role |
| Unreal |
World Partition (UE5) β automatic spatial cell load/unload, async asset loading, distance streaming |
Plugin's default impl wraps World Partition: ChunksInRange β WP cell ids β WorldPartitionSubsystem::LoadCells(...). Plugin reads collision from whatever WP loaded. Game devs author in UE Editor; for typical mesh-terrain games they don't override anything. |
| Unity |
SceneManager (additive scene loading) + Addressables |
Plugin's default impl wraps SceneManager.LoadSceneAsync(..., LoadSceneMode.Additive). Provider returns scene names / addressable keys; Unity handles asset acquisition. |
| Godot |
Scene loading (.tscn) + ResourceLoader |
Plugin's default impl wraps ResourceLoader.load_threaded_request(...) and adds resulting nodes to the scene tree. |
| Rapier-Rust pure / voxel / custom |
None native |
Game implements full MapProvider: reads from SpacetimeDB chunks / generator / asset bundles and returns collision data directly. |
So for traditional mesh-terrain games on UE/Unity/Godot, the dev workflow is "author levels in the engine editor; plugin handles everything." For voxel/procedural/custom games, the dev implements a custom MapProvider that reads from wherever they store map data.
Map types β capability checklist
Each engine plugin needs these capabilities. Start from the basics; layer up as games need them. The first row is V1 baseline for any engine plugin; subsequent rows are added when the first game using that pattern is built.
The plugin doesn't need to implement all rows on day 1. Plugin needs:
- The interface definition (so games CAN implement any pattern).
- Default implementation for the basic case (row 1).
- Documentation + example game patterns for the others, lit up as concrete games adopt them.
What Arcane does NOT provide
- A map asset format. Game decides.
- An authoring tool. Game uses its engine's editor or generates procedurally.
- A default storage backend. Game picks SpacetimeDB / object storage / on-disk / procedural / hybrid.
- Voxel-specific or mesh-specific implementations. The interface is uniform; game's
MapProvider impl is type-aware.
- Terrain replication. Map content is the same on every server (every node has the manifest + can read storage); not on the wire.
Open design questions
- Default
MapProvider impls? Arcane could ship a few common cases (e.g., "TriMesh from glTF in S3"), reducing boilerplate for typical mesh-terrain games. Voxel and procedural impls stay game-specific. Probably worth a small companion library, deferred until first user.
- Chunk-id schema. Game-defined opaque
u64 is the proposed default. Alternatives: structured (x, y, z) tuple, hierarchical / quadtree id, hash-based. Game's choice; runtime treats it as opaque.
- Cross-cluster boundaries. When an entity is near a chunk boundary, the neighbor cluster's chunks must be visible for raycasts. Same kinematic-proxy mechanism as cross-cluster entities; or a shared read-only chunk cache; or replicate chunk-id sets across clusters. Deferred to the cross-cluster physics work.
- Hot reload / live edits to static mesh terrain. Out of scope. Map is read-only at runtime in this epic. Live editing for voxel games is straightforward (it's just mutable durable state); for mesh games, deferred.
- Coordination with PGP clustering. Does the clustering model need to know about chunks, or is chunk loading purely a downstream consequence of entity positions? Likely the latter: clustering stays affinity-driven; terrain loading is a runtime side-effect. Note: spatial-bound entities (Fixed body kind, structures, walls) DO depend on chunk ownership for migration semantics β that's the related clustering-binding work, separate epic.
Out of scope
- Map authoring tooling (separate concern; uses the game engine's own editor).
- Destructible terrain (modeled as entities, not terrain edits β see
entity-model.md).
- Cross-cluster terrain coherence specifics (separate epic once cross-cluster physics work begins).
- Per-pool terrain optimization (e.g., a logic-only ambient pool may not need terrain at all if its members never raycast).
Acceptance criteria
Reference
- Architecture doc:
docs/architecture/entity-model.md Β§7 (terrain) and Β§8 (per-engine API discipline).
- Builds on:
#117 (Rapier minimum integration), #118 (per-entity colliders + contact events), the Rapier physics work in flight via PR #123.
- Related: PGP / Affinity Clustering β entities migrate by affinity; chunk-load set follows.
- Related:
#33 (heterogeneous node tiers β terrain loading is per-cluster regardless of node kind).
- Related:
#8 (cluster physics backends β terrain colliders live in whichever physics engine the cluster uses).
- Parallel work:
#124 (Unreal Cluster Node) β UE-native MapProvider design lives in that epic; same conceptual contract.
Memory anchor for future sessions
- Per-engine API pattern (
project_per_engine_api_pattern.md in memory): each engine plugin defines its own MapProvider in its native language. Arcane does not try to share MapProvider types across Rust/UE/Unity/Godot.
- Unified entity model (
project_unified_entity_model.md in memory): terrain is the one thing that's not an entity. Every persistent in-world thing (player / NPC / projectile / structure / dropped item) is an entity; every persistent in-world substrate is terrain.
- Wire format stays engine-neutral; terrain doesn't cross clusters via wire format because every cluster reads the same map content (manifest + storage).
Type: epic / interface and implementation plan. Replaces the original framing with the per-engine
MapProviderinterface decided in the 2026-05-03 architecture sessions.Quick Summary
MapProviderthat the runtime calls.MapProviderper engine), the chunks-needed computation (driven by entity positions), and the integration with the cluster's physics engine (handing collision data to Rapier / Chaos / PhysX / Bullet / Jolt).entity-model.mdΒ§8): each engine plugin defines its ownMapProviderin its native language. No sharedMapProvidertype across Rust/UE/Unity/Godot.Why this matters
Terrain is the substrate that physics needs to do anything useful. A cluster owning 3 players in the eastern desert must have eastern-desert terrain colliders in its physics world for raycasts and entity-vs-floor collision to work. It must NOT have polar-icecap terrain loaded β irrelevant cost. As entities migrate (PGP affinity may relocate a player westward), the terrain set must update.
Having game developers manage this is a non-starter:
on_tick, defeating dynamic clustering.Arcane should compute the right terrain set automatically and orchestrate loading via a game-implemented provider.
What "terrain" is in Arcane
Terrain is the only persistent world layer that's not an entity. Three terrain shapes need to work in Arcane, all through the same interface:
If a game has destructible structures that are entity-flavored (a wall placed by a player that takes damage), those are entities, not terrain edits. Voxel chunks are terrain, just with mutable durable state at chunk granularity rather than per-block entity_ids.
The interface β
MapProviderPer-engine plugin defines its own
MapProvider. The Rapier-Rust shape:The UE plugin defines
IArcaneUnrealMapProviderreturning UE-native collision (UStaticMesh*/ULandscapeComponent*/ equivalent). Unity defines its own withMesh/MeshCollider. Godot defines its own returningConcavePolygonShape3D. Same conceptual contract, different language and types β per the per-engine API discipline.Per-cluster runtime contract
MapProvider. Optionally read a manifest from SpacetimeDB to know which chunks exist.add_entity/ first-sight): callprovider.chunks_in_range(...)with the new owned-entity-position set, diff against currently-loaded chunks, callprovider.load_chunk(...)for newly-needed ones, insert resulting collision geometry into the cluster's physics world as fixed bodies.pending_removals/ migration out): same diff; remove chunks no longer needed by any owned entity; cleanup their physics bodies.RigidBodyType::Fixedbodies (per-engine equivalent). Solver-skipped; only AABB tracking. Per-tick cost essentially zero.Where things live (typical layout)
Cross-cluster coordination for mutable terrain
For mutable terrain (voxel chunks, destructible mesh chunks, runtime-edited geometry), cross-cluster sync uses the same Redis pub/sub channel pattern Arcane already uses for entity replication. SpacetimeDB is the durable source of truth; Redis is the real-time replication layer.
When Cluster X modifies a chunk:
MapProvider::on_chunk_changed(chunk_id)is called by the runtime.This is the same write-to-SpacetimeDB-and-publish-on-Redis pattern Arcane uses for
EntityStateDelta. Voxel chunks and destructible-terrain modifications are conceptually just another kind of cross-cluster game state that's both immediate and durable. Don't propose "SpacetimeDB pub/sub" or "Redis as durable storage" β both are wrong by Arcane's design (memory:project_redis_vs_spacetimedb_split.md).Engine-native streaming bridging
The MapProvider's role differs by engine. For engines with sophisticated native streaming systems (UE / Unity / Godot), the plugin's MapProvider wraps the native system rather than re-implementing chunk loading. For engines without native streaming (Rapier-Rust pure, voxel games regardless of engine), the plugin's MapProvider is fully implemented by the game.
ChunksInRangeβ WP cell ids βWorldPartitionSubsystem::LoadCells(...). Plugin reads collision from whatever WP loaded. Game devs author in UE Editor; for typical mesh-terrain games they don't override anything.SceneManager.LoadSceneAsync(..., LoadSceneMode.Additive). Provider returns scene names / addressable keys; Unity handles asset acquisition..tscn) + ResourceLoaderResourceLoader.load_threaded_request(...)and adds resulting nodes to the scene tree.So for traditional mesh-terrain games on UE/Unity/Godot, the dev workflow is "author levels in the engine editor; plugin handles everything." For voxel/procedural/custom games, the dev implements a custom MapProvider that reads from wherever they store map data.
Map types β capability checklist
Each engine plugin needs these capabilities. Start from the basics; layer up as games need them. The first row is V1 baseline for any engine plugin; subsequent rows are added when the first game using that pattern is built.
MapProviderimpl wraps the engine's native streaming.on_chunk_changed(chunk_id)called when neighbor cluster modifies a chunk this cluster has loaded.instance_idin its config / SpacetimeDB row.The plugin doesn't need to implement all rows on day 1. Plugin needs:
What Arcane does NOT provide
MapProviderimpl is type-aware.Open design questions
MapProviderimpls? Arcane could ship a few common cases (e.g., "TriMesh from glTF in S3"), reducing boilerplate for typical mesh-terrain games. Voxel and procedural impls stay game-specific. Probably worth a small companion library, deferred until first user.u64is the proposed default. Alternatives: structured(x, y, z)tuple, hierarchical / quadtree id, hash-based. Game's choice; runtime treats it as opaque.Out of scope
entity-model.md).Acceptance criteria
MapProvidertrait shape designed and documented for the Rapier-Rust plugin.ChunkCollisionenum (or equivalent) defined with TriMesh + HeightField variants.arcane-infra::rapier_clusterβ agnostic of where chunks come from (game'sMapProviderdecides).IArcaneUnrealMapProviderinterface designed in parallel; tracked in the Unreal Cluster Node epic (#124).arcane-demos/) where entities walk on terrain, fall into pits, and shoot bullets that hit walls β all without the demo author writing chunk-loading code. The author writes aMapProviderimpl that reads a glTF; everything else is plumbing.Reference
docs/architecture/entity-model.mdΒ§7 (terrain) and Β§8 (per-engine API discipline).#117(Rapier minimum integration),#118(per-entity colliders + contact events), the Rapier physics work in flight via PR#123.#33(heterogeneous node tiers β terrain loading is per-cluster regardless of node kind).#8(cluster physics backends β terrain colliders live in whichever physics engine the cluster uses).#124(Unreal Cluster Node) β UE-nativeMapProviderdesign lives in that epic; same conceptual contract.Memory anchor for future sessions
project_per_engine_api_pattern.mdin memory): each engine plugin defines its ownMapProviderin its native language. Arcane does not try to shareMapProvidertypes across Rust/UE/Unity/Godot.project_unified_entity_model.mdin memory): terrain is the one thing that's not an entity. Every persistent in-world thing (player / NPC / projectile / structure / dropped item) is an entity; every persistent in-world substrate is terrain.