Outcome focus: Promoted Abuela from a note to an essay by defining a loop architecture that keeps Unity runtime state, web companion flows, reward rules, and content iteration aligned.
unitynext.jsgame loopsystems architectureintegration
The rejected design was two games pretending to be one.
One version of Abuela lived in Unity: moment-to-moment play, state changes, rewards, and unlocks. Another version lived on the web: companion content, progression explanations, seasonal prompts, and lightweight interactions around the game.
Keeping those surfaces separate looked clean at first. Unity could own gameplay. The web app could own content. Each stack could move at its own pace.
Then the first state question appeared: which surface owns an unlock?
If Unity unlocks an item and the web surface does not know, the companion layer lies. If the web surface grants a reward and Unity treats it as decorative, the loop breaks. If both surfaces encode the same reward rule, the first balance change creates two sources of truth.
Abuela's loop has to be one system even when the surfaces are different.
Core loop#
The loop I want to protect is:
The web layer is not a marketing page around the game. It is part of the loop when it changes what the player understands, expects, or can do next.
The tradeoff#
The tradeoff is consistency over local optimization.
Letting Unity and the web app each implement their own state rules would be faster for the first few experiments. A designer could change a web prompt without waiting on a runtime package. A gameplay prototype could add a reward without touching the web app.
That speed is tempting and dangerous. Once the same concept exists in two places, every iteration creates a reconciliation task.
The architecture I prefer is slower at the boundary and faster everywhere else:
- Unity emits progression events.
- A shared contract defines event shape and reward identifiers.
- The web surface reads derived state and content eligibility.
- Reward rules live in one place.
- Experiments can vary content without redefining progression.
Event contract#
The operational artifact is the event contract. It is small enough to reason about and strict enough to prevent drift.
type ProgressionEvent =
| {
type: "encounter_completed";
playerId: string;
encounterId: string;
choiceId: string;
outcomeId: string;
occurredAt: string;
}
| {
type: "reward_granted";
playerId: string;
rewardId: string;
source: "unity" | "web_companion" | "event";
occurredAt: string;
}
| {
type: "unlock_changed";
playerId: string;
unlockId: string;
status: "available" | "claimed";
occurredAt: string;
};The exact schema will change. The point is that the contract names the domain. It does not send vague blobs like { event: "update", data: ... } and ask both surfaces to guess.
Boundary map#
| Concern | Unity owns | Web owns | Shared contract |
|---|---|---|---|
| Moment-to-moment input | Yes | No | Event ids |
| Progression state updates | Yes | Limited | State transition names |
| Companion explanation | No | Yes | Content eligibility |
| Reward definitions | No | No | Reward id and metadata |
| Seasonal prompt copy | No | Yes | Prompt id and state requirements |
| Telemetry events | Emits | Emits | Event schema |
The mistake I would avoid is letting "web owns content" become "web owns hidden progression." Content eligibility can live on the web. Progression should not fragment.
Rejected loop design#
The rejected loop was web-first.
In that version, the web companion selected prompts and told Unity what the player should see next. That made iteration easy for narrative content. It also made runtime behavior depend on web availability and content state in a way that felt fragile.
The better split is for Unity to remain authoritative over runtime progression while the web surface responds to state. The web can influence the next prompt through configured eligibility, but it should not secretly mutate core progression without an event contract.
Iteration hook#
The iteration hook is a content eligibility table.
| Content id | Requires | Excludes | Purpose |
|---|---|---|---|
| reflection-first-choice | encounter:intro.completed | none | Explain the first meaningful choice |
| reward-garden-unlocked | unlock:garden.available | unlock:garden.claimed | Prompt player to inspect new space |
| return-after-break | days_since_session >= 3 | active event | Re-entry context |
| festival-preview | chapter >= 2 | event:festival.completed | Seasonal anticipation |
This gives the web surface room to change content without changing the underlying progression logic. Designers can add reflections, explanations, and companion prompts by attaching them to state, not by inventing state.
Telemetry#
I would track the loop with a few events:
encounter_started
choice_selected
encounter_completed
reward_granted
unlock_available
unlock_claimed
companion_content_viewed
next_prompt_selectedThe important metric is not only completion. It is whether companion content changes the next session.
I would look at:
- return rate after companion reflection,
- reward claim rate after web prompt,
- abandoned unlocks,
- time between unlock available and unlock claimed,
- prompt variants that lead to repeated choices.
If the web layer does not change understanding, return intent, or reward uptake, it may be decorative. Decorative can be fine. But it should not be architected like core loop infrastructure unless it earns that role.
A test case for the boundary#
The boundary is easiest to test with one unlock.
Suppose Unity completes encounter:intro, grants reward:garden_key, and marks unlock:garden.available. The web companion should be able to explain the garden, show optional context, and invite the player back. It should not independently decide that the garden is claimed.
The test case is:
| Step | Expected behavior |
|---|---|
| Unity completes encounter | Emits encounter_completed |
| Reward rule fires | Emits reward_granted with garden_key |
| Unlock becomes available | Emits unlock_changed with available |
| Web companion loads | Shows garden reflection because eligibility is true |
| Player claims unlock in Unity | Emits unlock_changed with claimed |
| Web companion reloads | Removes claim prompt and can show follow-up content |
This is small enough to automate and important enough to protect. If this path drifts, the player sees contradictory state across surfaces.
The practical rule: every cross-surface feature needs one test that proves the same state transition means the same thing everywhere.
What to do differently#
Treat cross-surface game loops as state-contract problems early.
It is easy to start with screens: Unity screen here, web page there, companion feature later. The system gets cleaner when the first question is not "where does this UI live?" but "which state changed, who owns it, and which surfaces are allowed to react?"
Abuela's loop can span Unity and web surfaces, but only if the boundary is explicit. Shared state contracts, reward ids, content eligibility, and telemetry are the difference between one coherent loop and two parallel products slowly disagreeing.