---
title: "Pitfalls: gameplay logic & readiness gates"
slug: /workflows/pitfalls-gameplay/
markdown: /workflows/pitfalls-gameplay.md
section: pitfalls
tier: 2
summary: "Silent readiness signals, retry flags, stable keys and shared placement paths."
sources: ['pitfalls/gameplay.md']
canonical: https://unityworkflowlibrary.dev/workflows/pitfalls-gameplay/
read_first: /read-first.md
---

> Read first, on every prompt: [non-destructive rules](/read-first.md).

# Pitfalls: gameplay logic & readiness gates

> Silent readiness signals, retry flags, stable keys and shared placement paths.

Generalized pitfalls harvested from shipped and active projects. Each entry: rule, what broke, why, check. Record your own project's traps the same way: [Keeping a project pitfall file](/workflows/pitfall-file/).
All rules here are LOCKED evidence (the customizable rules page LOCKED-VS-CUSTOMIZABLE). Status: locked (empirical, seen in shipped/active projects).

- **SILENT-SIGNAL-COUNTS-AS-SATISFIED — a readiness input that is never produced in this build must count as satisfied, not pending.** Why: "not reported" was treated as "not ready". Check: for every gate input, name which builds/worlds write it; inputs with no writer in a configuration are excluded or pre-satisfied there. Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (SILENT-SIGNAL-COUNTS-AS-SATISFIED): A loading gate waited on a streamer signal that only existed in the Editor, so every player build sat at the gate until its timeout.
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

- **FIRST-TIME-FLAG-IS-NOT-A-RETRY-FLAG — never key a "first time" test off a flag that also means "retry next tick".** Why: two states shared one bit. Check: one-shot init and retry have separate fields; a failure path cannot reset the "initialised" field. Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (FIRST-TIME-FLAG-IS-NOT-A-RETRY-FLAG): A streamer re-ran its one-time initialisation every frame after a transient failure because the same bool marked both "never ran" and "try again".
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

- **CATALOG-ORDER-IS-NOT-PRIORITY-ORDER — fixed-order iteration over a category-sorted list streams the world in category order, not distance order.** Why: catalog order encodes authoring categories, not relevance. Check: streaming/LOD work queues sort by distance or importance each pass, never by source-list order. Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (CATALOG-ORDER-IS-NOT-PRIORITY-ORDER): Content streamed in "backwards" (far props first, near ground last) because the loader walked the visual catalog in its authored sort order.
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

- **BUDGETING-SPREADS-COST-DOES-NOT-REMOVE-IT — a per-frame cap on expensive boot work only moves the total.** Why: frame budgeting changes distribution, not work. Check: measure total cost before budgeting; remove or cache the work first, then budget what remains. See also LOADING-WORK-IS-FRAME-BUDGETED (the scene flow page). Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (BUDGETING-SPREADS-COST-DOES-NOT-REMOVE-IT): Capping convex collider builds per scan turned a one-frame hitch into seconds of sustained low FPS with the same total cost.
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

- **BASELINE-IS-CAPTURED-ONCE — a scale/offset applier captures the authored baseline once per target, never from the current (already modified) value.** Why: the captured value already contained the last application. Check: baselines are keyed by target and written only when absent; re-apply from the stored baseline. Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (BASELINE-IS-CAPTURED-ONCE): A bone-scale applier re-captured its "baseline" on every apply, so scales compounded each time the effect was re-triggered.
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

- **STABLE-KEYS-OUTLIVE-PLACEMENT-ALGORITHMS — ids derived from a procedural placement sequence break every saved reference when the sequence changes.** Why: identity was the sequence index, not a stable cell+salt key. Check: persisted world-object keys are derived from a stable spatial cell and a per-cell slot, and placement changes ship with a key migration or a version stamp. Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (STABLE-KEYS-OUTLIVE-PLACEMENT-ALGORITHMS): A planned switch of the scatter sequence (e.g. to a low-discrepancy sequence) would have renumbered every placed node, orphaning harvested/drained ledgers and persisted keys.
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

- **SERVER-AND-CLIENT-SHARE-ONE-PLACEMENT-PATH — when server and client both generate the same procedural content, they must run the same code path.** Why: two implementations of "the same" math diverge. Check: one shared deterministic function (same compilation mode) or the server replicates the result; a parity test compares both outputs. Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (SERVER-AND-CLIENT-SHARE-ONE-PLACEMENT-PATH): The server placed vegetation in managed code while clients placed it in Burst jobs; float differences put trees (and their colliders) in different spots near tile edges.
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

- **WORLD-EVENTS-ARE-SEEN-BY-EVERYONE — a state change triggered by one player (felling, destroying, opening) must replicate to all observers, not play only on the actor's client.** Why: the effect was client-local and the authoritative update rode a long cadence. Check: every world mutation has a replicated event or state change; test with a second client watching. Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (WORLD-EVENTS-ARE-SEEN-BY-EVERYONE): Only the acting client played a tree fall; other players kept drawing the tree and colliding with its trunk until a slow periodic push.
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

- **TIMEOUT-ADVANCE-HIDES-THE-SCREEN — an auto-advancing timeout makes the screen it leaves impossible to inspect or test.** Why: the timeout doubled as navigation. Check: timeouts surface a visible state (error/retry) rather than silently navigating; tests can disable the timer. Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (TIMEOUT-ADVANCE-HIDES-THE-SCREEN): A matchmaking screen timed out to the next state, so neither QA nor tests could ever observe its failure branch.
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

- **PICKER-CARRIES-THE-ACTION-AND-CLOSES-PER-CHOICE — a target/choice picker result must carry which action it answers, and must close after each choice.** Why: the result event relied on ambient "current action" state. Check: picker result payloads include the action id; each open has exactly one close. Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (PICKER-CARRIES-THE-ACTION-AND-CLOSES-PER-CHOICE): A game froze at an opening action because the chosen target arrived without its action and the picker stayed open for a second prompt.
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

- **IDLE-PRESENTATION-KEEPS-THE-TURN-ACTOR — an idle/attract presentation must not drop the current-turn player.** Why: presentation code wrote gameplay state. Check: presentation layers read turn state, never write it. Status: locked (empirical, seen in shipped/active projects).

> **Why this rule exists** (IDLE-PRESENTATION-KEEPS-THE-TURN-ACTOR): After an idle animation, the next seat lit up early because the idle path cleared the turn actor.
> *Source: Pitfall log of a shipped or active Unity project, generalized.*

