Corecom.zoa.gameplay · v0.1.0

ZOA Gameplay Composition

Seeds, levels, modes, and the spawn and session services that turn an empty scene into a playable session.

Every other core package owns a domain: weapons, inventory, AI, interaction. Gameplay owns the composition of those domains into a session. It is the package that answers what level is loaded, what mode is being played, where the player appears, what rig gets instantiated there, and what happens when that player dies. None of that logic belongs to any one system, so it lives in its own package instead of accreting inside the player package.

The organising idea is that a scene on disk contains almost nothing. It carries anchors, a spawn request, and a scene installer. The player rig does not exist until scene load, at which point `IPlayerSpawnService` instantiates it from a prefab, `ISceneSeedApplier` stamps AI zones and pickups onto the anchors a generator left behind, and `IPlayerSessionService` sequences the whole start into a state machine you can observe. The indirection makes a scene reusable across modes, and it lets a procedurally generated scene play without a hand-authored player sitting in it.

The package is aggressively string-keyed and interface-first. Levels, modes, seeds, spawn points, and vitals are all addressed by stable string ids rather than typed references or enums, so a project adds a mode or a vital without a Foundry migration. Every service is resolved through `FoundryServiceRegistry` and published by a scene installer, which means a test can construct the plain C# implementation directly and never touch play mode.

How it works

Concepts

A seed populates what a generator stamped

`SceneSeedDefinition` is the layer above scene generation. A generator produces terrain and drops `SceneSeedAnchor` components that declare where things may go: AI zones, hero spawns, loot, objectives, extraction. The seed is a separate asset describing what to put there: an AI roster, a hero list, a pickup roster, a genre bundle id, a level id, a mode id, and an optional environment profile.

The separation buys the combinatorics. One seed produces N scene flavours by varying its rosters, and one generator consumes any seed that fits its anchor contract. Each roster slot carries a `TargetCount` and an optional `AnchorTagFilter`, so a slot saying three zones tagged perimeter fans out across the matching anchors and reports a shortfall rather than failing when the scene has only two.

`SceneSeedApplier` is plain C# with no MonoBehaviour base, so tests instantiate it directly. It reads the scene's anchors once at Apply time, stamps `AiSpawnZone` components for AI slots and Armory pickups for pickup slots, converts matched Hero anchors into `SpawnPointMarker` components, and returns a `SceneSeedApplyResult` with per-category counts plus a human-readable `UnplacedSlots` list the wizard's issue tray can render verbatim.

Hero spawning is indirect. The applier records hero placements in the result instead of spawning, because the player factory lives in Nucleon and taking that dependency would invert the composition. The host bootstrap reads the placements back and feeds them to whatever factory the project wired in. The environment profile is applied by reflection for the same reason.

Levels and modes are string-keyed registries

`LevelDefinitionAsset` and `GameModeDefinition` are ScriptableObjects registered against `ILevelService` and `IGameModeService` by the gameplay scene installer. Each exposes an ordered `All` list for a New Game picker, a `GetById` lookup, and a `Changed` event. Registering an id that already exists replaces the prior registration rather than throwing, so a content pack can override a shipped mode.

Compatibility is expressed on both sides and both sides treat empty as permissive. `LevelDefinitionAsset.SupportsMode` returns true when the level lists no modes; `GameModeDefinition.SupportsLevel` returns true when the mode lists no levels. Cross-filtering a picker is therefore an intersection of two permissive predicates, with no compatibility matrix to maintain.

`ILevelService.LoadLevel` is a chokepoint. Callers could resolve a scene id themselves and hand it to the scene flow service, but routing through the level service centralises level-id to scene-name resolution and gives a loading screen one event, `LevelLoadRequested`, carrying the whole `LevelDefinitionAsset` so it can show a display name and preview art instead of a raw scene id.

`GameModeKind` flattens seven variants into one discriminator: SurviveExtract, WaveDefense, Objective, MissionDriven, MobaMatch, FreeRoam, and Tutorial. Only the first three have shipped SO assets, emitted on first editor load by `BundledGameModeInstaller`. The flat shape keeps the inspector legible and avoids SerializeReference until the polymorphic win-condition design that needs it actually lands.

The rig does not exist until scene load

A gameplay scene carries a `PlayerSpawnRequest`: the local prefab, an optional remote prefab, an optional weapon pack id, an optional starting weapon definition id, and a `SpawnMode` topology hint. At scene load `ZOAPlayerSpawnSceneInstaller` finds the request, resolves a position (preferring a sibling spawn anchor over the request's own transform), and calls `SpawnLocal`.

`SpawnMode` is a hint. A scene authored as Networked with no real backend installed degrades to single-player loopback instead of failing, so one scene serves both a solo playtest and a session with peers. In Networked mode with a live backend, the installer subscribes to peer join and leave and calls `SpawnRemote` per peer with the stripped remote prefab: no camera, no audio listener, no `PlayerInput`, no interactor, just the visual, animator, IK, and a sync bridge consuming the replication stream.

Local rigs are keyed by slot. `LocalRigs` maps slot id to rig and is the single source of truth for which local players are present, with `LocalRig` a convenience for the primary slot. `SpawnLocal` is the N equals one special case of `SpawnLocalSlot`, so split-screen is the same code path rather than a parallel one. Respawning an occupied slot despawns that slot's prior rig first and leaves the others alone.

The idempotency contract is worth internalising. The service spawns the local rig at most once per scene load, and a second `PlayerSpawnRequest` in the same loaded scene is logged as a warning and ignored. Multiple spawn anchors are legitimate because the resolver picks one; multiple authoring payloads is always a mistake.

Adorners are how a scene customises a rig it does not contain

Because the rig is instantiated at runtime, a saved scene cannot add components to it. `IPlayerRuntimeAdorner` is the answer: drop a MonoBehaviour implementing it anywhere in the scene, and the spawn service discovers it at scene load and calls `Adorn(rig)` immediately after the rig instantiates, before any general subscriber sees `LocalRigSpawned`.

Multiple adorners coexist and ordering is controlled by `[DefaultExecutionOrder]` on each adorner MonoBehaviour, with the convention that foundational work sits around -1000, composition around 0, and anything needing a fully composed rig around +1000. Because they are MonoBehaviours they clean up on scene unload automatically. Implementations should treat a repeat invocation as a rebuild: a despawn followed by a respawn calls `Adorn` again.

A session is a state machine you can observe

`IPlayerSessionService` wraps spawning in a lifecycle. `PlayerSessionState` runs None, Starting, LoadingScene, SpawningPlayer, RestoringState, Ready, Ending, Failed, and every transition is published through `StateChanged` alongside narrower events for starting, local player ready, ending, and failure. HUD binders and camera handoff subscribe to those rather than polling for a player to appear.

Failure is enumerated rather than thrown. `PlayerSessionStartStatus` distinguishes Started, AlreadyReady, MissingSpawnService, MissingSpawnRequest, MissingLocalPrefab, SpawnFailed, and MissingSpawnPoint, and `PlayerSessionResult.Succeeded` treats AlreadyReady as success. The precision lets an editor validator name the specific authoring fault instead of reporting a generic failure.

A session is described by `PlayerSessionDescriptor`, which pairs a session id with a level id, mode id, profile id, scene id, spawn point id, weapon pack id, and an optional snapshot to restore from. A start request layers per-call overrides on top: a prefab, a position, a rotation, a weapon pack. When a mode wants to own the spawn moment itself it sets `AutoSpawnFromSceneRequest` to false on the spawn installer before the scene-load hook runs, which is public API precisely so hosts never reach for reflection.

Vitals abstract whatever health model the project actually has

`IPlayerVitalsService` is a facade over whatever health model the project runs. Implementations may be backed by Nucleon's survival attributes, per-limb health, or a network authority stream, and callers are explicitly told not to depend on which. `NucleonSurvivalPlayerVitalsService` is the shipped binding to Nucleon's survival attributes.

Vitals are addressed by `PlayerVitalId`, a string-backed struct with case-insensitive equality and static conveniences for Health, Shields, Stamina, Armor, and Oxygen. A project adds Hunger or Radiation by constructing an id, with no enum migration. Each value carries Current, Max, and a clamped `Normalized`, which is the number a bar widget binds to.

Mutations are explicit about intent and outcome. `ApplyDelta` and `SetVital` take a `PlayerVitalsMutationReason` (Damage, Healing, Respawn, SaveRestore, NetworkReconcile, Scripted) and return a result carrying the before and after values plus a status: Applied, ServiceUnavailable, VitalNotFound, or Rejected. Because the reason travels with the change, a listener can render a damage flash for Damage and stay silent for SaveRestore without inferring intent from the delta's sign.

Death, respawn, and terminal outcomes are separate mechanisms

`IPlayerTeleportService` is the low-level move: a request carrying a position, rotation, a `PlayerTeleportReason`, and flags for clearing velocity, syncing the camera, and preserving input focus. `IPlayerRespawnService` sits above it and adds spawn-point resolution, health restoration, and optional snapshot restore. `KillFloorVolume` is the simplest consumer, resolving the local player through the session service and calling respawn with reason KillFloor, and it refuses to mutate the player directly when no respawn service is registered.

`GameOutcomeHandler` is the terminal case, and it carries no content of its own. It owns the mechanism: pause the world, dim the screen, release the cursor, and show a UI Toolkit panel with a headline and one or two buttons. The copy and the accent colours come from `GameOutcomePresentation`, and the button actions come from the caller. `Show` is idempotent so a double-fired kill event cannot stack overlays, and a built-in `RestartActiveScene` covers the common reload.

`IPlayerStateSnapshotService` handles the persistence side, capturing and restoring a `PlayerStateSnapshot` with a reason and a `PlayerSnapshotRestorePolicy` that toggles transform, vitals, inventory, equipment, and contributor sections independently, and decides whether a newer schema or a missing required section is fatal. Other packages extend the snapshot by registering an `IPlayerSnapshotContributor` with its own section id and schema version.

In the editor

Screens

Screenshot pending

/screenshots/gameplay-scene-seed-wizard.png

The wizard window with the five-step rail, the AI Roster step showing two spawner rows with target counts and tag filters, and the Review button visible but not yet reached.

The Scene Seed Wizard on the AI Roster step.

Screenshot pending

/screenshots/gameplay-scene-anchors.png

A generated scene viewed from above with anchor gizmos in their category colours: blue AI zones around a perimeter, a green hero anchor, yellow loot anchors, and a red objective marker, with one anchor selected so its category and tag are visible in the inspector.

Scene seed anchors in the scene view.

Screenshot pending

/screenshots/gameplay-spawn-request-inspector.png

Inspector showing a PlayerSpawnRequest with both local and remote prefabs assigned, a weapon pack id filled in, and Mode set to Networked, alongside a ZOAPlayerSpawnSceneInstaller in the same hierarchy.

A PlayerSpawnRequest configured for a networked scene.

Screenshot pending

/screenshots/gameplay-outcome-overlay.png

Game view dimmed by the backdrop with a large coloured headline, a subtitle line, a primary and a secondary button, and a greyed hint line beneath them.

The GameOutcomeHandler overlay in play mode.

Setup

Workflow

  1. 01

    Install the composition services

    Add ZOAGameplaySceneInstaller to your runtime services hierarchy and list the GameModeDefinition and LevelDefinitionAsset assets you want registered. Add ZOAPlayerSpawnSceneInstaller beside it with DontDestroyOnLoad on, and PlayerSessionSceneInstaller if you want the full session lifecycle on top of raw spawning. Execution orders are staged (-9200, -9050, -9040) so the dependency chain resolves without ordering configuration.

  2. 02

    Author levels and modes

    Run the bundled installers once to emit the three shipped modes and the three default seeds, then author your own through the create-asset menus. Set SupportedModeIds on a level or SupportedLevelIds on a mode only where an actual restriction exists: empty means permissive on both sides.

  3. 03

    Author a scene seed

    Open the Scene Seed Wizard and walk Identity, Bundle and Mode, AI Roster, Heroes, and Review. The mode dropdown is fixed to the seven canonical ids. Give exactly one hero slot IsLocalPlayer. Slots with an empty AnchorTagFilter place anywhere in their category, which is the right default until a scene needs perimeter and interior AI to differ.

  4. 04

    Prepare the scene

    A generator stamps SceneSeedAnchor components by category, or you place them by hand. Add a PlayerSpawnRequest with the local prefab, and the remote prefab if the scene will run networked. Place SpawnPointMarker components for team and role lookups, and PlayerSpawnPoint components where the session needs an id-addressed spawn.

  5. 05

    Build the player prefab

    Use the Standalone Player Prefab builder to produce a self-contained rig under Assets/ZOA/Generated/Players. It composes through Nucleon's PlayerProfileFactory and the authoring services, builds in a temporary scene, saves the prefab, and tears the temp scene down, so nothing scene-side leaks into the asset. Point the spawn request's local prefab at the result.

  6. 06

    Start a session

    Either let the spawn installer auto-spawn from the scene request, or set AutoSpawnFromSceneRequest to false and drive IPlayerSessionService.StartSession yourself when a mode owns the spawn moment. Subscribe to LocalPlayerReady for anything that needs the rig, and to SessionFailed for anything that needs to explain why it did not appear.

  7. 07

    Validate before you ship the scene

    Run the scene-seed completeness validator over the project's seeds and the player-session scene validator over the loaded scenes. Both emit project-neutral issue codes, so the Workbench, tests, and a CI gate all report the same problems.

Surface

Key types

ISceneSeedApplier

service

Applies a seed's rosters to the active scene. Resolve it through FoundryServiceRegistry after the runtime services are installed, and read the result for anchor shortfalls.

  • SceneSeedApplyResult Apply(SceneSeedDefinition seed)

SceneSeedApplyResult

class

Per-category placement counts plus a human-readable list of under-placed slots. A non-empty UnplacedSlots means the scene lacked anchors, which is recoverable but worth surfacing.

  • int AiZonesPlaced, HeroesPlaced, PickupsPlaced, SpawnPointsAuthored
  • bool EnvironmentApplied
  • List<string> UnplacedSlots

ILevelService

service

Registry of authored levels and the single chokepoint for level-id to scene-name resolution. LoadLevel fires LevelLoadRequested with the full asset so a loading screen can show the level's display name and preview art.

  • IReadOnlyList<LevelDefinitionAsset> All { get; }
  • LevelDefinitionAsset GetById(string levelId)
  • void Register(LevelDefinitionAsset level)
  • bool LoadLevel(string levelId)
  • event Action<LevelDefinitionAsset> LevelLoadRequested
  • event Action<string> Changed

IGameModeService

service

Registry of authored modes, ordered by GameModeDefinition.Order. Re-registering an id replaces the prior entry; re-registering the same instance is a no-op.

  • IReadOnlyList<GameModeDefinition> All { get; }
  • GameModeDefinition GetById(string modeId)
  • void Register(GameModeDefinition mode)
  • void Unregister(string modeId)
  • event Action<string> Changed

IPlayerSpawnService

service

Instantiates and tears down player rigs. Owns scene-local presentation rigs for both the local slots and the per-peer remote puppets, and raises events either side of each lifecycle edge.

  • GameObject LocalRig { get; }
  • IReadOnlyDictionary<string, GameObject> LocalRigs { get; }
  • IReadOnlyDictionary<string, GameObject> RemoteRigs { get; }
  • GameObject SpawnLocal(GameObject prefab, Vector3 position, Quaternion rotation, string weaponPackId, string initialWeaponDefinitionId = null)
  • GameObject SpawnLocalSlot(string slotId, int slotIndex, GameObject prefab, Vector3 position, Quaternion rotation, string weaponPackId, string initialWeaponDefinitionId = null)
  • GameObject SpawnRemote(GameObject prefab, string peerId, Vector3 position, Quaternion rotation)
  • void DespawnLocal(), DespawnLocalSlot(string slotId), DespawnRemote(string peerId), DespawnAllRemotes()
  • event Action<GameObject> LocalRigSpawned, LocalRigDespawned
  • event Action<string, GameObject> RemoteRigSpawned, RemoteRigDespawned

IPlayerSessionService

service

Sequences a full session start: scene, spawn, state restore, ready. Subscribe to LocalPlayerReady to learn when the rig is live.

  • PlayerSessionState State { get; }
  • PlayerSessionDescriptor CurrentDescriptor { get; }
  • GameObject LocalRig { get; }
  • PlayerSessionResult StartSession(PlayerSessionStartRequest request)
  • void EndSession(bool despawnLocal = true, bool despawnRemotes = true)
  • bool TryGetLocalPlayer(out GameObject player)
  • event Action<PlayerSessionChangedEvent> StateChanged
  • event Action<GameObject, PlayerSessionDescriptor> LocalPlayerReady
  • event Action<PlayerSessionDescriptor, string> SessionFailed

PlayerSessionDescriptor

struct

What a session is: an id plus the level, mode, profile, scene, spawn point, and weapon pack it was started with, and whether to restore from a snapshot. A null session id is replaced with a fresh GUID.

  • string SessionId, LevelId, ModeId, ProfileId, SceneId, SpawnPointId, WeaponPackId
  • bool RestoreFromSnapshot; string SnapshotId
  • static PlayerSessionDescriptor CreateDefault()

PlayerSessionStartStatus

enum

Started, AlreadyReady, MissingSpawnService, MissingSpawnRequest, MissingLocalPrefab, SpawnFailed, MissingSpawnPoint. Enumerated failure lets a validator name the specific authoring fault.

ISpawnPointResolver

service

Registry of scene spawn markers with a team-and-role lookup. Empty team or role matches any, higher priority wins, ties break by registration order, and used one-shots are skipped.

  • IReadOnlyList<SpawnPointMarker> All { get; }
  • void Register(SpawnPointMarker marker)
  • SpawnPointMarker PickSpawn(string team, string role)
  • SpawnPointMarker PickPlayerSpawn()

IPlayerRuntimeAdorner

interface

Per-scene runtime composition on a rig the scene does not contain. Discovered at scene load and invoked before LocalRigSpawned reaches general subscribers. Order with DefaultExecutionOrder; treat a repeat call as a rebuild.

  • void Adorn(GameObject rig)

IPlayerVitalsService

service

The player-facing facade over whatever health model the project uses. Read vitals by id, mutate with an explicit reason, and capture or restore a versioned snapshot.

  • bool IsAlive { get; }
  • PlayerVitalsState State { get; }
  • bool TryGetVital(PlayerVitalId id, out PlayerVitalValue value)
  • IReadOnlyList<PlayerVitalValue> GetVitals()
  • PlayerVitalsMutationResult ApplyDelta(PlayerVitalId id, float delta, PlayerVitalsMutationReason reason = PlayerVitalsMutationReason.Scripted)
  • PlayerVitalsMutationResult SetVital(PlayerVitalId id, float value, PlayerVitalsMutationReason reason = PlayerVitalsMutationReason.Scripted)
  • PlayerVitalsSnapshot CaptureSnapshot()
  • event Action<PlayerVitalsChangedEvent> VitalsChanged; event Action<PlayerDeathEvent> Died; event Action<PlayerReviveEvent> Revived

PlayerVitalId

struct

String-backed vital identifier with case-insensitive equality and an implicit conversion from string. Health, Shields, Stamina, Armor, and Oxygen are provided; anything else is one constructor call away.

  • static PlayerVitalId Health, Shields, Stamina, Armor, Oxygen
  • string Value { get; }
  • implicit operator PlayerVitalId(string value)

IPlayerRespawnService

service

Resolves a spawn point, moves the player, and optionally restores health or a snapshot. Returns false with a reason string rather than throwing, so a kill volume can log a specific failure.

  • bool TryRespawn(PlayerRespawnRequest request, out string reason)
  • event Action<PlayerRespawnRequest> Respawning
  • event Action<PlayerRespawnContext> Respawned

IPlayerTeleportService

service

The primitive move, separate from respawn. The request carries flags for clearing velocity, syncing the camera, and preserving input focus, so a debug warp and a scene handoff differ only in the data they pass.

  • bool TryTeleport(PlayerTeleportRequest request, out string reason)
  • event Action<PlayerTeleportRequest> Teleporting, Teleported

IPlayerStateSnapshotService

service

Captures and restores the player's persisted state under an explicit policy. Other packages extend the payload by registering an IPlayerSnapshotContributor with its own section id and schema version.

  • PlayerStateSnapshot Capture(PlayerSnapshotReason reason)
  • bool TryRestore(PlayerStateSnapshot snapshot, PlayerSnapshotRestorePolicy policy, out string reason)
  • bool TryGetLastSnapshot(out PlayerStateSnapshot snapshot)
  • event Action<PlayerStateSnapshot, string> RestoreFailed

IPlayerSnapshotContributor

interface

One package's slice of the player snapshot. A required section that fails to restore can fail the whole restore, depending on the policy the caller passed.

  • string SectionId { get; }
  • int SchemaVersion { get; }
  • bool IsRequired { get; }
  • string CaptureSection(GameObject playerRoot)
  • bool RestoreSection(GameObject playerRoot, string payloadJson, int schemaVersion, out string reason)

ICharacterRagdollController

interface

Enables and disables a ragdoll with context attached. Activation carries the damage and impulse that caused it, and recovery decides whether to re-enable the animator and snap to the pelvis.

  • bool IsRagdollActive { get; }
  • void Enable(RagdollActivationContext context)
  • void Disable(RagdollRecoveryContext context)
  • void ApplyImpulse(DamageImpulse impulse)

WeaponHoldTuningStore

class

JSON persistence and live cache for per-weapon hold tuning, keyed by weapon definition id. Files live under StreamingAssets so they are writable in the editor and readable in a build, and the cache is shared so a live edit reaches both rigs immediately.

  • static WeaponHoldTuning GetOrLoad(string weaponId)
  • static bool HasCommittedOverride(string weaponId)
  • static string FilePathFor(string weaponId)

Surface

Authoring assets

SceneSeedDefinition

asset

The aggregate authoring asset for a populated scene: a genre bundle id, a level id, a mode id, AI and hero and pickup rosters, and an optional environment profile. IsApplicable is the authoring-time probe for whether the seed has enough to do visible work.

  • Tools/ZOA/Gameplay/Scene Seed
  • string SeedId, GenreBundleId, LevelId, GameModeId
  • IReadOnlyList<AiRosterSlot> AiRoster; IReadOnlyList<HeroSlot> Heroes; IReadOnlyList<PickupRosterSlot> PickupRoster
  • bool IsApplicable()

SceneSeedDefinition.AiRosterSlot

struct

One spawner materialised as TargetCount zones. An empty AnchorTagFilter matches every AI anchor; a filter such as perimeter narrows placement to anchors carrying that tag.

  • AiSpawnerDefinition Spawner
  • int TargetCount
  • string AnchorTagFilter

SceneSeedDefinition.PickupRosterSlot

struct

Placed pickups stamped onto Loot anchors. Kind selects Weapon, Ammo, or Attachment, and only the fields relevant to that kind are read, so an unused definition slot on a slot is harmless.

  • PlacedPickupKind Kind (Weapon, Ammo, Attachment)
  • WeaponDefinition Weapon; WeaponOperationProfileDefinition OperationProfile; WeaponPresetDefinition Preset; WeaponPresentationDefinition Presentation
  • WeaponCaliberDefinition Caliber; string DisplayLabel
  • AttachmentDefinition Attachment
  • int TargetCount; string AnchorTagFilter

SceneSeedDefinition.HeroSlot

struct

One hero archetype. Order matters: the first applicable slot drives the player and the rest become allies, and exactly one slot should set IsLocalPlayer.

  • ScriptableObject Archetype
  • string DisplayName, AnchorTagFilter
  • bool IsLocalPlayer

LevelDefinitionAsset

asset

One playable level: a stable id, a scene name, presentation copy and preview art, an estimated playtime, the modes it supports, and whether saving is permitted while it is loaded. The scene is a string rather than a SceneAsset because the asset lives in a runtime assembly.

  • ZOA/Gameplay/Level Definition
  • string LevelId, SceneId, DisplayName, Description
  • IReadOnlyList<string> SupportedModeIds; bool SupportsMode(string modeId)
  • Sprite PreviewArt; int EstimatedMinutes; bool SupportsSave

GameModeDefinition

asset

One selectable mode: an id, a GameModeKind discriminator, presentation copy, the levels it supports, and flat per-kind rule fields. Only the fields matching the kind are read, keeping the inspector simple.

  • ZOA/Gameplay/Game Mode Definition
  • string ModeId; GameModeKind Kind; string DisplayName, Description
  • int WaveCount; float TimeLimitSeconds; string ObjectiveIdsCsv; bool AllowRespawn
  • IReadOnlyList<string> SupportedLevelIds; bool SupportsLevel(string levelId)

SceneSeedAnchor

component

A featherweight placement declaration stamped by a generator: a category and an optional free-form tag, with a gizmo so authors can see it. No behaviour and no Update; the applier reads it once and never again.

  • ZOA/Gameplay/Scene Seed Anchor
  • AnchorCategory: AiZone, Hero, Loot, Objective, Extraction, Custom
  • bool MatchesTag(string filter)
  • void Configure(AnchorCategory category, string tag = null)

PlayerSpawnRequest

component

The per-scene spawn payload: local prefab, optional remote prefab, weapon pack id, an optional starting weapon override, and the topology hint. Runs at execution order -9000 so it exists before anything looks for it.

  • ZOA/Gameplay/Player Spawn Request
  • GameObject LocalPlayerPrefab, RemotePlayerPrefab
  • string WeaponPackId, InitialWeaponDefinitionId
  • SpawnMode Mode; bool IsValid
  • void EditorSetPrefabs(GameObject local, GameObject remote)

SpawnPointMarker

component

A scene spawn candidate tagged by team and role as strings rather than enums, so a project invents its own factions and classes. Auto-registers with the resolver on enable and unregisters on disable. OneShot consumes the marker after a successful pick.

  • ZOA/Gameplay/Spawn Point Marker
  • string Team, Role; int Priority; bool OneShot, Used
  • void MarkUsed()
  • void Configure(string team, string role, int priority = 0, bool oneShot = false)

PlayerSpawnPoint

component

The session-side spawn point, addressed by id and optionally scoped to a level. Carries tags, a priority, a safe radius, and a fallback flag for when the requested point is unusable.

  • ZOA/Gameplay/Player Spawn Point
  • string SpawnPointId, LevelId; IReadOnlyList<string> Tags
  • int Priority; float SafeRadius; bool AllowFallback
  • bool MatchesLevel(string requestedLevelId); bool HasTag(string tag)

ZOAGameplaySceneInstaller

component

Scene-root bootstrap at execution order -9200. Publishes ISceneSeedApplier, IGameModeService, ILevelService, and ISpawnPointResolver, registers the mode and level assets listed on it, and optionally applies a seed on Awake. An already-registered service is adopted rather than replaced.

  • ZOA/Gameplay/Gameplay Scene Installer
  • void EnsureRegistered()
  • ISceneSeedApplier Applier; IGameModeService Modes; ILevelService Levels; ISpawnPointResolver Spawns

ZOAPlayerSpawnSceneInstaller

component

Publishes IPlayerSpawnService at execution order -9050 and hooks sceneLoaded to auto-spawn from a scene's PlayerSpawnRequest. Recommended DontDestroyOnLoad so a cross-scene transition despawns the prior rig and spawns a fresh one at the new anchor.

  • ZOA/Gameplay/Player Spawn Scene Installer
  • bool AutoSpawnFromSceneRequest { get; set; }
  • excludedSceneNames, defaultLocalPrefab, allowLegacyPatternFallbacks

PlayerSessionSceneInstaller

component

Publishes IPlayerSessionService at execution order -9040, locating the spawn installer on itself, a parent, or a child. Unregisters on destroy only when its own instance is still the registered one, so a longer-lived service is never torn down by a scene unload.

  • ZOA/Gameplay/Player Session Scene Installer
  • IPlayerSessionService EnsureRegistered()

KillFloorVolume

component

A trigger that respawns the local player on entry with reason KillFloor. Forces its collider to isTrigger on Reset and OnValidate, resolves the player through the session service before falling back to the Player tag, and warns rather than mutating the player when no respawn service is registered.

  • ZOA/Gameplay/Kill Floor Volume
  • respawnPointId, restoreFullHealth, preferRespawnService

GameOutcomeHandler

component

The reusable terminal-outcome overlay: pauses the world, dims the screen, releases the cursor, and shows a UI Toolkit panel with one or two buttons. Show is idempotent so a double-fired kill event cannot stack overlays.

  • ZOA/Gameplay/Game Outcome Handler
  • void Show(GameOutcomePresentation presentation, Action primaryAction = null, Action secondaryAction = null)
  • void InvokePrimary(), InvokeSecondary(), Dismiss(bool resumeWorld = true)
  • bool IsShowing; event Action Shown, Dismissed

GameOutcomePresentation

class

The copy and colour for one outcome, authored inline or in the inspector. Title, subtitle, button labels, hint text, a title colour, and a backdrop colour whose alpha dims the world without hiding it.

  • string Title, Subtitle, PrimaryButtonText, SecondaryButtonText, HintText
  • Color TitleColor, BackdropColor
  • static GameOutcomePresentation Default

PlayerCameraHandoff

component

Bridges session readiness to camera state without assuming a camera stack. The spawned rig stays authoritative for its own Camera and Cinemachine hierarchy; this only enables it, optionally disables auto-discovered scene fallback cameras, and restores them when the session ends.

  • ZOA/Gameplay/Player Camera Handoff

NucleonSurvivalPlayerVitalsService

component

The shipped IPlayerVitalsService binding onto Nucleon's survival attributes. Swap it for your own implementation when the project's health model is not survival-attribute-backed.

  • ZOA/Player/Nucleon Survival Player Vitals Service

Usage

Examples

Applying a seed at runtimecsharp
using UnityEngine;
using ZOA.Gameplay.Unity.Seed;
using ZOA.Messaging;

public sealed class LevelBootstrap : MonoBehaviour
{
    [SerializeField] private SceneSeedDefinition seed;

    private void Start()
    {
        if (!FoundryServiceRegistry.TryResolve<ISceneSeedApplier>(out var applier))
            return;

        var result = applier.Apply(seed);
        Debug.Log(result);

        // Anchor shortfall is recoverable, but the author should hear about it.
        foreach (var unplaced in result.UnplacedSlots)
            Debug.LogWarning($"[SceneSeed] {unplaced}", this);
    }
}
SceneSeedApplyResult.ToString already formats the per-category counts, so a single log line covers the happy path.
Starting a session and waiting for the rigcsharp
using UnityEngine;
using ZOA.Gameplay.Unity.Session;
using ZOA.Messaging;

public sealed class SessionStarter : MonoBehaviour
{
    private IPlayerSessionService _session;

    private void Awake()
    {
        _session = FoundryServiceRegistry.Get<IPlayerSessionService>();
        _session.LocalPlayerReady += OnLocalPlayerReady;
        _session.SessionFailed += OnSessionFailed;
    }

    private void Start()
    {
        var descriptor = new PlayerSessionDescriptor(
            sessionId: null,
            levelId: "level.plateau",
            modeId: "mode.wave_defense",
            weaponPackId: "pack.starter");

        var result = _session.StartSession(new PlayerSessionStartRequest(descriptor));
        if (!result.Succeeded)
            Debug.LogError($"Session refused: {result.Status} {result.Message}", this);
    }

    private void OnLocalPlayerReady(GameObject rig, PlayerSessionDescriptor descriptor) =>
        Debug.Log($"Player ready in {descriptor.LevelId}", rig);

    private void OnSessionFailed(PlayerSessionDescriptor descriptor, string reason) =>
        Debug.LogError($"Session {descriptor.SessionId} failed: {reason}", this);
}
A null session id is replaced with a fresh GUID, so a descriptor is cheap to construct for a one-off session.
Adorning the rig from the scenecsharp
using UnityEngine;
using ZOA.Gameplay.Unity.Spawn;

// Composition tier. Foundational work uses -1000; anything that needs
// a fully composed rig uses +1000.
[DefaultExecutionOrder(0)]
public sealed class ArenaHudAdorner : MonoBehaviour, IPlayerRuntimeAdorner
{
    [SerializeField] private GameObject hudPrefab;

    public void Adorn(GameObject rig)
    {
        // Adorn may run again after a despawn/respawn: rebuild, don't append.
        var existing = rig.GetComponentInChildren<ArenaHud>(includeInactive: true);
        if (existing != null)
            Destroy(existing.gameObject);

        Instantiate(hudPrefab, rig.transform);
    }
}
Reading and mutating vitalscsharp
using ZOA.Gameplay.Unity.Vitals;
using ZOA.Messaging;

var vitals = FoundryServiceRegistry.Get<IPlayerVitalsService>();

if (vitals.TryGetVital(PlayerVitalId.Health, out var health))
    healthBar.fillAmount = health.Normalized;

// The reason travels with the change, so a listener can flash red for
// Damage and stay silent for SaveRestore.
var result = vitals.ApplyDelta(PlayerVitalId.Health, -25f, PlayerVitalsMutationReason.Damage);
if (!result.Succeeded)
    Debug.LogWarning(result.Message);

vitals.Died += evt => outcomeHandler.Show(defeatPresentation, GameOutcomeHandler.RestartActiveScene);
Picking a spawn by team and rolecsharp
using ZOA.Gameplay.Unity.Spawn;
using ZOA.Messaging;

var resolver = FoundryServiceRegistry.Get<ISpawnPointResolver>();

// Empty team or role matches any. Higher priority wins; ties break by
// registration order; used one-shot markers are skipped.
var marker = resolver.PickSpawn(team: "enemy", role: "boss")
             ?? resolver.PickSpawn(team: "enemy", role: string.Empty);

if (marker != null)
{
    Instantiate(bossPrefab, marker.Position, marker.Rotation);
    marker.MarkUsed();
}
Contributing a section to the player snapshotcsharp
using UnityEngine;
using ZOA.Gameplay.Unity.PlayerState;

public sealed class JournalSnapshotContributor : IPlayerSnapshotContributor
{
    public string SectionId => "com.example.journal";
    public int SchemaVersion => 2;
    public bool IsRequired => false;

    public string CaptureSection(GameObject playerRoot) =>
        JsonUtility.ToJson(playerRoot.GetComponent<Journal>().ToPayload());

    public bool RestoreSection(
        GameObject playerRoot, string payloadJson, int schemaVersion, out string reason)
    {
        if (schemaVersion > SchemaVersion)
        {
            reason = "Journal payload was written by a newer build.";
            return false;
        }

        playerRoot.GetComponent<Journal>().Load(JsonUtility.FromJson<JournalPayload>(payloadJson));
        reason = string.Empty;
        return true;
    }
}
IsRequired false means a failed restore of this section does not have to fail the whole snapshot; the caller's policy decides.

Tooling

Editor tools

Scene Seed Wizard

Tools > ZOA > Advanced > Define > World > Scene Seed Wizard

A five-step wizard authoring a SceneSeedDefinition: Identity, Bundle and Mode, AI Roster, Heroes, and Review. The mode dropdown is fixed to the seven canonical ids, and the first hero row added is marked as the local player automatically, with the flag enforced as exclusive across rows.

Standalone Player Prefab builder

Tools > ZOA > Advanced > Build > Characters > Player > Create Standalone Player Prefab

Produces a self-contained player prefab under Assets/ZOA/Generated/Players. It builds the rig in a temporary scene through Nucleon's PlayerProfileFactory plus the authoring services, saves the prefab asset, then tears the temp scene down, so nothing scene-side leaks into the output.

Bundled player prefabs

Tools > ZOA > Advanced > Generate > Characters > Player > Bundled Player Prefabs

Ensure Assets emits the bundled local-player prefab variants on demand; two Regenerate entries force a rebuild, one of them dropping the survival HUD. The bundled variants cover the western FPS, MOBA and RTS, top-down RPG, and sci-fi tabletop configurations.

Bundled game modes and scene seeds

Tools > ZOA > Advanced > Generate > Gameplay

Emits the three shipped modes (Survive and Extract, Wave Defense, Objective) and three default seeds (Mountain Encounter, Forest Skirmish, Desert Patrol) as on-disk assets on first editor load. Both installers are idempotent: a re-run detects existing assets at the canonical path and skips them, so local edits survive.

Scene seed completeness validator

A dashboard check across every authored SceneSeedDefinition. A seed must be applicable and its pickup slots must carry the definitions their kind requires, mirroring the applier's own check so an authoring gap surfaces before an apply silently under-places.

Player session scene validator

Editor validation of player-session scene readiness, operating on loaded scenes and emitting project-neutral issue codes so the Workbench, the test suite, and a CI gate all report identically.

Weapon hold tuning widget and gizmos

F8 in play mode; Tools > ZOA for the JSON import and export

A runtime IMGUI panel for live-tuning how a weapon sits in third person: hip and ADS mount positions, scale, rotation, both hand grips, and head-look pitch. Scene-view move handles edit the same WeaponHoldTuning as the sliders. Commit writes JSON under StreamingAssets; a separate bake folds the values back onto the authoritative WeaponPresentationDefinition through SerializedObject so undo and dirty marking behave.

Read this

Notes and caveats

See also