Gameplay Representation and Spawning¶
Quick Start covers the initial lobby workflow. This guide explains gameplay context, object ownership, input and session lifecycle for 1–8 players. Joining/selection do not prescribe how your game represents a player; spawn points and PlayerInput are optional.
| Representation mode | Choose it when |
|---|---|
| Spawn Selected Character | The confirmed Character Definition supplies a Gameplay Prefab that Couch should instantiate and clean up. |
| Bind Existing Objects | Characters, vehicles or other representations already exist in the scene; bind each authored object by Player Index. |
| Custom Provider | Your pool, team/grid or creation system chooses and owns a live object for each context. |
| Handle It Myself / API Only | Your architecture consumes the public contexts and manages its own representations. No object setup is required. |
Resolution chooses the object; input configuration separately chooses how it consumes owned controls. Your game supplies movement, physics, scoring and match rules. Shared Camera and Split Screen is an optional module for assignment/viewports; your game supplies camera follow/framing. No Couch base class or prefab inheritance is required.
Scene setup¶
Add Couch Co-op Builder > Gameplay Player Setup to a scene object. Set Mode in the Inspector. Bootstrap may be explicitly assigned; otherwise the component uses CouchCoopBootstrap.Active. Initialize the bootstrap before enabling the setup (automatic execution order accommodates a normally initialized persistent bootstrap).
For Handle It Myself, the component creates and binds no gameplay objects and requires no object array/provider. The component itself is optional when your game only uses the public context API.
For Bind Existing Objects, assign an object at each required zero-based array index. Index 0 belongs to P1, index 7 to P8, independent of object names or hierarchy. Objects must belong to the setup's scene. The framework adds/reuses GameplayPlayerBinding and assigns its Context; it preserves transforms, parents and active state. Unused authored objects remain unassigned with their authored active state, including objects authored inactive. It does not activate/deactivate, instantiate or destroy them. Your game controls visibility and gameplay lifetime.
For Custom Provider, assign a MonoBehaviour implementing IGameplayPlayerProvider. Runtime code can alternatively call Configure(mode, authoredObjects, provider, bootstrap) with a plain interface implementation. This is a small example; replace the indexed decision with your own pool, vehicle or team logic:
using LevelArgento.CouchCoopBuilder;
using UnityEngine;
public sealed class MyGameplayProvider : MonoBehaviour, IGameplayPlayerProvider
{
[SerializeField] private GameObject[] authoredVehicles;
public bool TryResolve(GameplayPlayerContext context,
out GameObject playerObject, out string failureMessage)
{
playerObject = context.PlayerIndex < authoredVehicles.Length
? authoredVehicles[context.PlayerIndex] : null;
failureMessage = playerObject != null ? null : "No vehicle assigned to this player index.";
return playerObject != null;
}
}
The provider receives the persistent context including character metadata and input ownership. It returns one live scene GameObject (never a prefab asset), or returns false with an explanation. Resolution attempts occur when sessions enter the enabled setup; repeated Initialize() does not retry failures or repeat successful calls. Rejoin after reversible Drop Out requests a fresh association for the same session. Disable/re-enable or call Configure to begin a new resolution cycle. Explicit lifecycle owners can call Release() and then Initialize(). Never assume the framework will retry or instantiate a fallback. Two active contexts cannot bind the same object. The provider owns creation and reclamation; subscribe to PlayerReleased if it needs to return objects to a pool.
Read GameplayPlayerBinding.Context on a resolved object, or use setup.TryGetPlayerObject(playerIndex, out object) from a scene system. Consumers may subscribe to ContextChanged, where null means released. Composition is optional for API-only integrations and requires no base-class inheritance or PlayerInput. Character changes update the live context and do not automatically replace the resolved gameplay object.
Notifications and lifetime¶
GameplayPlayers.PlayerAdded/PlayerRemoved: joined/left context. Subscribe, then replayPlayersfor existing sessions.GameplayPlayers.SelectionChanged: selection/catalog notification. Built-in spawning uses the first selection of a waiting live join; existing representations are not replaced.GameplayPlayerSetup.PlayerResolved/PlayerReleased: scene object association/release. Subscribe beforeInitializeif you need initial replay notifications.GameplayPlayerSetup.ResolutionFailed: failure for that context; also logged with player index and session ID.
Unsubscribe scene-owned consumers in OnDisable. The setup unsubscribes before teardown and clears its scene references on disable/destroy; bindings clear their Context without reconstructing UI. Destroying a bound object releases the association immediately and reports a useful error on the next surviving setup frame. Deferring that diagnostic avoids false errors during ordinary scene unloading. The bootstrap stores no resolved objects, so scene reload gets fresh bindings and one provider call per joined session per visit.
Existing live Join creates a context and resolves it if a scene setup is enabled. Leave removes its context and releases its binding. Built-in owned instances are destroyed; customer-owned Existing/Custom objects are preserved. An explicit rejoin creates a new session and resolves the available index. Physical input loss keeps existing reservation semantics and does not rerun a provider. These are association semantics only: gameplay scoring, match participation, respawn and pool reclamation remain customer responsibilities. The optional reversible Drop Out policy preserves the session while releasing scene bindings. See Gameplay Input Integration for optional input adapters.
Missing indexed bindings, missing/destroyed providers, false/null provider results, provider exceptions and conflicting object ownership report isolated failures. Other players continue resolving; there is no fallback strategy. Use character-resolution status/message for optional metadata failures. No scene-wide searches or per-frame hierarchy scans are used. Destruction detection checks at most eight cached object/binding references per setup frame, covering never-active objects whose Unity OnDestroy callback may be skipped; diagnostics process a bounded pending queue.
Spawn Selected Character¶
Spawn Points and PlayerInput are optional. This gameplay representation mode requires GameplayPrefab; Existing Objects, Custom Provider and API Only do not. The separate built-in lobby selection rule above still requires valid prefabs.
Choose Spawn Selected Character on GameplayPlayerSetup, then one placement strategy. Runtime owners can call ConfigureSpawning(placement, points, origin, offset, placementProvider, bootstrap).
- Explicit Spawn Points: assign zero-based indexed Transforms. PlayerIndex 5 uses element 5 even when other sessions have left. World position/rotation come from that Transform; its scale does not affect the prefab. Missing/destroyed required points fail only that index; there is no reuse or world-zero fallback.
- Origin + Offset (Single Origin And Offset): assign one Origin Transform and Per Player Offset. Position is
origin.position + origin.rotation * (offset * PlayerIndex); rotation isorigin.rotation. Offset is expressed along the origin's local axes in world units; origin scale is ignored. With an unrotated origin at (10,1,5) and offset (2,0,0), P1 is (10,1,5), P2 is (12,1,5), P3 is (14,1,5). Rotating the origin rotates the formation. XY offsets support 2D, XZ offsets support 3D. An origin is required; there is no implicit world-zero placement. - Custom Placement Provider: assign a MonoBehaviour implementing
IGameplaySpawnPlacementProvider. Runtime configuration also accepts a plain interface implementation. The provider receives the real context and returns a world Pose; it never needs to create the character or implement IGameplayPlayerProvider. Each attempted spawn calls it once. A false result/exception/invalid pose affects only that player.
using LevelArgento.CouchCoopBuilder;
using UnityEngine;
public sealed class MyPlacement : MonoBehaviour, IGameplaySpawnPlacementProvider
{
[SerializeField] private Transform gridOrigin;
public bool TryGetSpawnPose(GameplayPlayerContext context,
out Pose pose, out string failureMessage)
{
pose = default;
failureMessage = null;
if (gridOrigin == null)
{
failureMessage = "Assign the grid origin.";
return false;
}
Vector3 offset = new Vector3(context.PlayerIndex * 3, 0, 0);
pose = new Pose(gridOrigin.position + gridOrigin.rotation * offset,
gridOrigin.rotation);
return true;
}
}
The selected ID resolves through GameplayPlayerContext against the current catalog. Only the definition's GameplayPrefab is instantiated. Missing/ambiguous character IDs or a missing prefab fail with the existing ResolutionFailed event and diagnostic. SelectionPreviewPrefab and portrait are never gameplay fallbacks. The source prefab is never changed. Root scale and active state remain prefab-authored; placement supplies world position/rotation. A compatible GameplayPlayerBinding is reused, otherwise added to the runtime clone only. Clones are moved into the setup's scene even when another additive scene is active.
Ownership and live sessions¶
Built-in clones are framework-owned. Release, component deactivation, Leave and scene teardown unbind and destroy only those clones. PlayerReleased fires after unbinding and before destruction is scheduled; normal Play Mode Destroy completes at frame end. A disabled setup removes its built-in representations; re-enabling starts a fresh cycle. Existing and Custom Provider objects remain customer-owned and are never destroyed by this cleanup.
No additional InputUser is created, no pairing is performed and no action maps are switched. Physical Device Lost keeps the reserved session and its existing object; the live context reports input unavailability. Reconnection does not duplicate or respawn it. An intentional middle-player Leave destroys only that player's built-in instance and leaves survivors' indexes and transforms unchanged.
A live Join normally has no character selection: the built-in mode waits quietly for its first selection notification. That selection attempts one spawn. Existing joined sessions replayed at scene entry require a selection and report missing metadata as configuration failure. Failed attempts are not automatically retried; selection/catalog notifications do not repeat placement calls for an attempted context. To retry after correcting configuration, disable/re-enable, call ConfigureSpawning, or Release then Initialize. A character change after successful spawning updates the context but does not replace its representation; your game must implement respawn or mid-match character switching explicitly. Unexpected runtime destruction is released and diagnosed, with no automatic respawn.
The persistent bootstrap never stores clones. Resolution is driven by scene initialization, Join and the first selection of a waiting Join. Update checks only up to eight cached associations for destruction; it performs no catalog resolution, instantiation or global scene search.
Customer initialization timing¶
Unity Instantiate can invoke prefab Awake and OnEnable before Context assignment. Do not assume context is available in either callback. Use the existing GameplayPlayerBinding.ContextChanged notification on a prefab that already contains that component, or GameplayPlayerSetup.PlayerResolved. ContextChanged guarantees the binding's Context; PlayerResolved additionally means the setup lookup has been registered. Subscribe before enabling/initializing setup if you need all initial resolutions. A late consumer can replay contexts and TryGetPlayerObject instead.
For a prefab with an existing binding, Awake may subscribe to ContextChanged; unsubscribe in OnDestroy using Unity's binding != null check. A prefab without a binding should use a scene-level PlayerResolved consumer because the binding is added after Instantiate. Use these notifications rather than relying on prefab initialization order. Use the optional gameplay input strategies below for action ownership; movement remains customer-owned.