Skip to content
Edit on GitHub

Game architecture

Voxel Sandbox is arranged around ownership, not around Unity folders or a giant game manager. Core creates the composition root. Chemistry is an independent, Unity-free simulation root. Data turns authored Unity assets into validated immutable values. World owns generated and edited voxel state, streaming, jobs, meshes, colliders, and slot-level persistence. Inventory owns discrete stacks and compositional bulk matter. Industry hosts world-embedded process control volumes without making World know machine types. Player translates input into authoritative commands; UI reads and presents state; Editor-only assemblies aggregate the runtime graph to author and validate it. Dependencies point from the caller to the owner it needs, never back from a lower-level owner to a consumer.

This diagram is deliberately an ownership map rather than the complete .asmdef graph. Solid arrows are existing dependencies or control/data hand-offs. Dashed arrows are boundaries that must remain absent.

flowchart TD
  CORE["Core\ncomposition root"]
  CHEM["Chemistry\nheadless simulation"]
  DATA["Data\nauthoring + validation"]
  WORLD["World\nvoxels + stream + save"]
  INVENTORY["Inventory\nstacks + bulk matter"]
  INDUSTRY["Industry\nworld apparatus"]
  PLAYER["Player\ninput to commands"]
  UI["UI\nread-only presentation"]
  EDITOR["Editor tools\nauthoring + proof"]

  DATA --> CORE
  DATA --> CHEM
  WORLD --> DATA
  INVENTORY --> DATA
  INDUSTRY --> WORLD
  INDUSTRY --> INVENTORY
  PLAYER --> WORLD
  PLAYER --> INDUSTRY
  PLAYER --> INVENTORY
  UI --> PLAYER
  UI --> INDUSTRY
  EDITOR --> UI
  CORE -. no gameplay dependency .-> UI
  CHEM -. no Unity dependency .-> WORLD
  WORLD -. no machine-type dependency .-> INDUSTRY

The exact compile-time edges live in the .asmdef files. Assemblies & boundaries is the exhaustive reference; this chapter explains why the graph has this direction and how an action moves through it.

Owner Owns May know about Must not become
Core Startup composition and a small app-service registry Unity lifecycle and app-wide startup services A gameplay service locator
Chemistry Deterministic thermodynamics, reactions, charge, and causal events Plain C# values only A Unity or ScriptableObject consumer
Data Catalogues, definitions, and their bake into solver-ready values Unity authoring and Chemistry values Mutable world state or generation execution
World Chunk data, coordinate conversion, generation, edit overlay, streaming, mesh/collider presentation, local saves Data, Chemistry, Unity Jobs/Burst/HDRP Player input, UI policy, or Industry types
Inventory Item stacks, transfers, and bulk matter with composition/provenance Data and Chemistry values A loaded-world requirement
Industry Physical apparatus and fixed-cadence process domains World position/context, Inventory matter, Chemistry runs A World-owned type or a second solver
Player Input, targeting, movement, command coordination, and session-facing glue Lower runtime owners The source of terrain, inventory, or chemistry truth
UI Rendering state legible to a player Broad read access to runtime owners An owner of domain state or simulation policy
Workshop / Gates Editor authoring, diagnostics, and acceptance proof Every runtime owner A runtime dependency

The two roots are intentional. GameBootstrap uses Unity to establish a persistent app root before the first scene. VoxelSandbox.Chemistry.asmdef instead declares noEngineReferences: true. The first owns application composition; the second protects a deterministic core that can run identically in EditMode or a future headless process.

The central conversion happens once, at the Data boundary:

Unity asset -> Data validation/bake -> immutable runtime value -> owner-specific execution

For chemistry, authored SubstanceDefinition and ReactionDefinition records become immutable Chemistry entries. For blocks and items, catalogues validate stable identities and rules; World snapshots generation settings into Burst-compatible data before jobs run. This keeps serialized Unity objects out of the pure solver and worker jobs, while still giving designers an Inspector-facing authoring layer.

World then owns the mutable consequence. ChunkStreamingRuntime.Update turns focus movement into a desired plan, lets the scheduler budget unload/generation/meshing work, and applies completed job output on the main thread. Its TrySetBlock changes loaded chunk data, records only an edit overlay against deterministic terrain, and refreshes the affected meshes. No caller reaches into a chunk dictionary or reimplements negative-coordinate math.

Chunk generation and meshing are asynchronous, but their authority is not. ChunkStreamingRuntime owns the complete lifecycle of every NativeArray, NativeList, job handle, version token, and pooled view it creates:

flowchart LR
  PLAN["ChunkStreamingPlanner\ndesired coordinates"]
  SCHEDULE["ChunkScheduler\nversioned lifecycle"]
  START["ChunkStreamingRuntime\nallocate + schedule"]
  WORKER["Burst jobs\nimmutable snapshots"]
  ACCEPT["Runtime\ncomplete + validate version"]
  APPLY["Main thread\nchunk + mesh + collider"]
  DISPOSE["Request owner\ndispose buffers"]

  PLAN --> SCHEDULE --> START --> WORKER --> ACCEPT --> APPLY
  ACCEPT --> DISPOSE
  START -. stale or disabled .-> DISPOSE

ProcessGeneration allocates its column table, discovered-root list, and output buffer before scheduling their dependency chain. CompleteFinishedGenerationJobs reports completion to the scheduler, applies a result only when its token remains current, and disposes every owned native collection in finally. Meshing uses the same discipline: a mesh request carries both a monotonically increasing mesh version and its source-chunk reference; IsCurrentMeshResult rejects an obsolete result before a view is changed, while its request is still disposed.

This is the architecture rule behind a seemingly mundane detail: a worker result is provisional until its owner accepts it. Do not let a job mutate scene objects, keep a native buffer in a static cache, or let a consumer dispose a request it did not schedule. Disable/unload paths must also complete or dispose outstanding work and release derived views; otherwise a harmless focus move becomes a native leak or an old mesh overwriting an edited chunk.

Canonical state, derived state, and save boundaries

Section titled “Canonical state, derived state, and save boundaries”

Architecture becomes clearer when every value is placed in one of three categories.

Category Examples Rule
Canonical authored data catalogue assets, definitions, generator settings Validate and bake once; do not mutate it as gameplay state.
Canonical runtime state chunk voxels/overlay, inventory payload, vessel checkpoint, player snapshot The owning domain validates mutation and persists the minimal truth needed to recover it.
Derived state meshes, colliders, material property blocks, HUD text, Workshop views Rebuild or refresh from canonical state; never make it the sole record of a game decision.

The World save layer uses generic DTO methods for an important dependency reason. SaveGameService.TrySaveVessels<TVesselData> writes a caller-owned vessel record but does not reference an Industry type; the Industry caller owns format version and content validation. The same approach applies whenever an upper assembly needs slot storage owned by World: World provides an atomic persistence mechanism, while the higher-level feature owns the meaning and validity of its DTO.

That split prevents two subtle failures. World cannot silently reinterpret a machine checkpoint it does not own, and Industry does not grow its own ad-hoc slot writer. Save canonical values, validate before restore, then rebuild meshes, view objects, and UI from the accepted state.

From a player action to a durable consequence

Section titled “From a player action to a durable consequence”

Player code is intentionally a translator, not a domain owner:

  1. BlockInteractionController converts configured input and an aim ray into either a voxel hit or a nearer apparatus target.
  2. For a voxel command, it delegates to InventoryBlockInteractionService, which validates item/tool conditions and asks World to perform the edit.
  3. On a successful break, an event lets DroppedItemSpawnerRuntime create a physical pickup. The break service does not place an item directly into an inventory.
  4. World persists the edit overlay; Inventory pickup later performs its own transfer transaction.
  5. UI observes the target, inventory, and result and formats them. It does not decide whether an edit was valid.

BlockInteractionController.Update shows the command boundary: it resolves targeting, routes connected-client input to the server, and otherwise invokes the authoritative local interaction service. This is the right place to add a new verb; it is the wrong place to add ore grade, chunk mutation internals, or an inventory rule.

Industry follows the same shape. A vessel MonoBehaviour adapts scene assets and elapsed Unity time into a plain-C# domain; that domain owns the fixed chemistry cadence, state transitions, event record, and checkpoint. Player requests a charge or heat change; World supplies location and an atmospheric context; UI reads the bounded result. See In-world vessels for that complete bridge.

Presentation is downstream, but not disposable

Section titled “Presentation is downstream, but not disposable”

Presentation is allowed to know broadly because it must make the result visible. That includes chunk meshes/colliders in World, appearance helpers in Industry, player targeting feedback, and HUD/UI formatting. Its authority is narrow:

  • Meshes are derived from owned chunk data and can be rebuilt.
  • A glow reads simulated temperature; it does not supply heat.
  • A UI label reads a model or runtime value; it does not modify it every frame as a substitute for a command.
  • A dropped-item visual is not its payload; the item identity and inventory transaction remain authoritative.

This distinction lets a renderer, HUD, or Editor view change without becoming a second source of truth. It also keeps headless and EditMode tests meaningful: the owner’s values can be tested without a loaded scene.

Editor tools are an intentional one-way aggregate

Section titled “Editor tools are an intentional one-way aggregate”

The Voxel Workshop and Editor Gates may reference the full runtime graph because they are explicitly Editor-only. VoxelSandbox.VoxelWorkshop.Editor.asmdef and VoxelSandbox.Editor.Gates.asmdef restrict themselves to the Editor platform. They can compose assets, inspect a catalogue, drive a diagnostic, or prove a generated result using public runtime APIs. A runtime assembly must never reverse that dependency to import UnityEditor or a workshop type.

  1. Put state beside the system that can enforce its invariants. World owns voxel edits; Chemistry owns reaction state; Inventory owns transfer and conservation rules.
  2. Pass values and narrow interfaces downward. Do not solve a dependency problem by making a lower owner reference Player, UI, or Editor.
  3. Keep Unity adaptation at the edge. ScriptableObjects bake in Data; MonoBehaviour wrappers forward lifecycle/input; pure domains decide state.
  4. Treat async work as owned resources. The runtime that schedules a native job also version-checks, completes, rejects stale results, and disposes its native collections.
  5. Persist canonical state, not derived presentation. Save world edits, snapshots, and payloads; rebuild meshes, materials, and UI from them.
  6. Use events for consequences, not invisible authority. A break event can spawn a drop, but the World edit stays the authoritative act.
  7. Make tests mirror the owner boundary. A test assembly references the smallest runtime area it exercises, and pure logic gets focused EditMode coverage.

When adding a feature, ask these in order:

  1. What state changes, and which existing owner can validate that change?
  2. Is this authored data, a pure calculation, canonical runtime state, a Unity adapter, or presentation?
  3. Does the caller need a concrete type, or can it receive a value, command, event, or narrow interface?
  4. Which direction does the dependency point today, and would the proposed reference create a return path?
  5. What exact persisted payload remains after the scene and all derived visuals are gone?
  6. What is the smallest owner-level test that can falsify the new rule?

Answer those before choosing a folder. The folder follows the boundary; it does not create one.

User-contributed notes

Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works: **bold**, *italic*, `code`, and links.

Notes policy

Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked nofollow.

  1. Loading notes…