---
title: "Tutorial: Create a Playable Game"
description: "A learning-oriented walkthrough that extends a world into a playable game (possession, input, a win/score loop, and a HUD) ending on the v1 playability proof obligation."
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/tutorials/create-a-playable-game/
---

# Tutorial: Create a Playable Game

{% version engineVersion="v1.0.232" coordinate="docs-public.v0" /%}

This tutorial extends the world from [Create a World](create-a-world.md) into a **playable game**: a player can enter it, possess and control an entity, score points, win, and read a HUD. It is the natural sequel, the previous tutorial proved the world *renders*; this one is about making it *playable*, which is a different and stronger claim.

"Playable" is not "renders". A scene that draws on screen is not a game until the runtime can demonstrate that a player spawned, took control, and changed state. This page describes the **canonical flow** to author that; the closing section states exactly what proof is required before anyone may claim the result is playable.

{% proof class="docs.playable_tutorial" registry="docs/cycles/v1-engine-readiness-loop/PROOF_REGISTRY.json" status="pending" %}
This tutorial is verified against the current generated action, component, semantic-patch, and world-build references. The proof obligation below explains which runtime receipts must exist before a playable claim is accepted.
{% /proof %}

> Media placeholder: add a capture showing the authored player, input binding, HUD state update, and final Play diagnostics/proof receipt.

## Prerequisites

Start from a created world with a camera, a light, and a floor, see [Create a World](create-a-world.md). As before, every edit goes through a canonical [action](../reference/action-catalog.md) or a semantic operation; the workbench shows a projection of authoritative state.

## What "playable" requires in v1

The v1 acceptance model distinguishes a graph that *contains* the right pieces from a runtime that *proves* control. The minimum runtime evidence is:

- **Camera**: a viewpoint the runtime drives for the player.
- **Spawn**: the player (or controlled entity) is placed into the running world.
- **Possession**: input is bound to a controlled entity so the player can actually move and act.
- **State change**: the runtime shows authoritative state advancing (a score increments, a phase transitions), not just a static frame.

Until those hold under a live session, the world is not playable. See [Explanation: Playability Proofs](../explanation/playability-proofs.md) and [Reference: Runtime Playability](../reference/runtime-playability.md).

## Step 1: Add a player and a spawn point

Create a **player** entity with {% action name="project.entity.create" /%} `project.entity.create` (MCP: {% mcp-tool name="project_create_entity" /%} `project_create_entity`). Give it `component.transform` for placement and `component.collider` so it interacts with the floor and props. Tag it (for example `player`) so scripts and the runtime can find it.

There is no `PlayerControllerComponent` and no `SpawnPoolComponent` in v1, control and spawning are behavior, authored as scripts, not as component fields. A "spawn point" is simply the transform you place the player at on start; you can hold it as a named entity or as a typed-state field.

**Expected result:** a player entity stands in the world. It is inert, nothing controls it yet.

## Step 2: Bind input and possess the player

Input is declared and bound through the three canonical input components: {% component name="InputActionDeclarationComponent" /%} `InputActionDeclarationComponent`, {% component name="InputBindingSetComponent" /%} `InputBindingSetComponent`, and {% component name="InputContextStackComponent" /%} `InputContextStackComponent`, which the workbench surfaces as a single composite **Input** palette entry. These are consumed by the runtime input, movement, and action systems (`runtime.movementSystem`, `runtime.actionSystem`). Read their field contracts in the [ECS component reference](../../../spec/generated/component-types.md) before setting them.

With input declared, author movement and possession as a script on the player. Create the script shell with {% action name="project.script.create" /%} `project.script.create`, then apply behavior with {% action name="project.script.patch.apply" /%} `project.script.patch.apply` (MCP: {% mcp-tool name="project_apply_script_semantic_patch" /%} `project_apply_script_semantic_patch`). Use the `mcp_safe` operations:

- `addActionHandler`: run logic when a declared input action resolves (move, jump, interact).
- `addTickHandler`: advance per-frame movement.

The compiler lowers these to canonical Script IR; you never write node ids on this path. See [How-To: Write GessaScript](../how-to/write-gessascript.md) and the [Script Semantic Patch reference](../../../spec/generated/script-semantic-patch.md).

{% generated-reference file="docs/spec/generated/script-semantic-patch.md" label="Script Semantic Patch operations" /%}

**Expected result:** input is bound and the player has a control script attached. Whether the player actually moves under control is a runtime property to be *proven*, not assumed (see the closing section).

## Step 3: Make the runtime camera follow the player

Point the camera at the player so the viewpoint tracks control. `CameraComponent` carries `followEntityId`, `followOffset`, and `followSmoothing` fields; set `followEntityId` to the player and tune the offset. Update the camera through {% action name="project.entity.component.set" /%} `project.entity.component.set`, reading the exact fields from the [generated reference](../../../spec/generated/component-types.md).

**Expected result:** the camera is rigged to follow the controlled entity, the "runtime-driven camera" half of the playability requirement.

## Step 4: Author a score and win loop with typed state

A game needs authoritative, durable state (a score and a win phase) not a number that lives only on the client. Author it with the typed-state semantic operations, which read and write **warehouse** state through validated paths:

- `writeTypedStateValue` / `setBooleanState`: initialize the score and a `won` flag.
- `incrementNumericState`: add to the score deterministically when the player collects a prop or completes an objective (drive this from the prop's `addTriggerHandler` from the previous tutorial).
- `compareRemainingCount` and `declareWinCondition`: transition the match phase when the remaining-collectible count reaches the declared threshold.

These operations carry `durable`, `writeState`, and `requiresAuthority` effect tags, the win condition is decided by the server, not the client. See the operation list and effect tags in the [semantic-patch reference](../../../spec/generated/script-semantic-patch.md).

**The contract behind this step:** typed state is a server-authoritative warehouse contract. A score that only updates in the browser is not state, it is a projection. The win is real because the authority wrote it.

## Step 5: Show a HUD bound to typed state

Create a HUD panel with {% action name="project.ui_panel.create" /%} `project.ui_panel.create` (MCP: {% mcp-tool name="project_create_ui_panel" /%} `project_create_ui_panel`), then bind a widget to the score with the `wireHudStateBinding` semantic operation, which reads a state field and pushes it through `UI.setWidgetValue` for projection. The HUD reflects authoritative state; it does not own it.

**Expected result:** a score readout that updates from server state, and a win message when the win condition fires.

## Step 6: Play

Open **Play** in the workbench to run the game in an ephemeral preview session against the live Project Graph. Play is a workbench surface, not an MCP tool. Move the player, collect a prop, watch the score increment, and trigger the win. To publish a sharable build instead of an in-editor preview, use the publish action {% action name="publish.dev" /%} `publish.dev` and then play the deployment, see [How-To: Publish and Play](../how-to/publish-and-play.md).

## The proof obligation

You have now authored everything a playable game needs. Authoring it is not the same as proving it. Here is the obligation that stands between "I built it" and "it is playable".

{% contract id="playable.game.runtime" engineVersion="v1" status="unaccepted" %}
A spatial world is **playable** only when a live runtime session proves player spawn, possession, control-driven movement under a runtime-driven camera, and authoritative state change, not when a graph merely contains a camera, a player entity, and a script. The runtime playability and admission row is currently `unaccepted`.
{% /contract %}

The [World Build Contract](../../../spec/generated/world-build-contract.md) makes the acceptance policy explicit for a `playable_game_factory` build. Playable spatial-world acceptance requires, at minimum:

- a **collision proxy**,
- a **nav/query proxy**,
- a **semantic anchor**, and
- `playability.accepted` receipts.

Its policy states the rule directly: *renderability alone is not playable proof.* Accepted build proof additionally requires semantic-contract proof, Project Graph validation, visible unsupported requirements, passing QA receipts, and a timeline receipt. The `playable_game_factory` claim names runtime proof lanes including `runtime_play` and `factory_acceptance`; missing proof is recorded as `not_run`, never silently as pass.

{% generated-reference file="docs/spec/generated/world-build-contract.md" label="World Build Contract acceptance policy" /%}

{% proof class="playability" registry="docs/cycles/v1-engine-readiness-loop/PROOF_REGISTRY.json" status="pending" %}
To claim this game is playable you need runtime evidence (collision proxy, nav/query proxy, semantic anchor, and a `playability.accepted` receipt) captured under a live session and recorded against the readiness registries. In the proof model, a markdown description of the behavior is explicitly insufficient evidence; a proof references exact source files and a focused runtime check and must be re-run after relevant changes.
{% /proof %}

So: build the flow above, open Play, and observe it, but state the result honestly. "It renders and I authored control and a win loop" is a true claim. "It is proven to be playable" is a claim you may only make once the proofs above exist and the corresponding ledger row is accepted. See [Explanation: Playability Proofs](../explanation/playability-proofs.md) for why the gate is shaped this way.

## Where to go next

- Ship a build: [How-To: Publish and Play](../how-to/publish-and-play.md).
- What "playable" formally means: [Reference: Runtime Playability](../reference/runtime-playability.md).
- The evidence model in depth: [Explanation: Playability Proofs](../explanation/playability-proofs.md).

## Status

This tutorial describes the canonical flow for authoring a playable game in v1. Every action, component, semantic operation, and tool named here is verified against the generated Tier-2 reference and the World Build Contract. The final playable claim tracks the runtime proof obligations above, not this prose.
