Skip to main content

Game Model

client.gameModel is the SDK surface for Game Models — server-authoritative containers, properties, and functions. It targets the Game API, so every call needs an app-scoped token for the target app — drive it from a per-game client (see Portals & app-scoped tokens). Studio authoring (seed, upsertFunction, …) additionally requires manage_apps; a studio admin can mintAppToken for their own app even without player entitlement. The examples below assume client is a per-game client holding that app's app-scoped token.

JSON values cross the wire as JSON-encoded strings (the *Json fields), so you JSON.stringify inputs and JSON.parse outputs.

Authoring a model (studio)

Requires manage_apps on the app. Seed types, property schemas, and functions in one call:

await client.gameModel.seed({
appId: "1",
containerTypes: [
{ typeName: "Character", displayName: "Character", instantiableBy: "member" },
],
propertyDefinitions: [
{ containerTypeName: "Character", key: "hp", valueType: "int", defaultValueJson: "100", visibility: "public" },
{ containerTypeName: "Character", key: "str", valueType: "int", defaultValueJson: "10", visibility: "public" },
],
functions: [
{
name: "attack",
containerTypeName: "Character",
returnType: "int",
parameters: [{ name: "target_id", valueType: "container_ref", required: true }],
mutations: [
{ target: "ref($target_id)", property: "hp",
expression: "max(0, ref($target_id).hp - self.str)" },
],
returnExpression: "ref($target_id).hp",
// Only the unit's owner, only on their turn.
invokePolicyJson: JSON.stringify({
type: "and",
rules: [{ type: "owner_of_self" }, { type: "is_current_turn" }],
}),
},
],
});

Individual helpers exist too: upsertContainerType, upsertPropertyDef, deletePropertyDef, deleteContainerType, upsertFunction, deleteFunction, setPolicy, typeSchema, plus the tier gating defineFeature and grantTierFeature. Runtime helpers include createContainer, deleteContainer, addEdge, and deleteEdge.

Running a session

// Start a battle with two participants.
const { sessionId } = await client.gameModel.createSession({
appId: "1",
name: "Skirmish",
participantUserIds: ["90001", "90002"],
});

// Omit ownerUserId for member/owner-instantiable types — server defaults to the caller.
const hero = await client.gameModel.createContainer({
appId: "1", sessionId, typeName: "Character", displayName: "Hero",
properties: [{ key: "hp", valueType: "int", valueJson: "100" }],
});

// Whose turn it is (drives the is_current_turn authority requirement).
await client.gameModel.setSessionTurn({ appId: "1", sessionId, userId: "90001" });

Invoking a function

const result = await client.gameModel.invoke({
appId: "1",
functionName: "attack",
selfContainerId: hero.containerId,
sessionId,
paramsJson: JSON.stringify({ target_id: enemyId }),
});

if (result.success) {
console.log("enemy hp:", JSON.parse(result.returnValueJson!));
} else {
console.warn("attack failed:", result.errorMessage);
}

If the caller is not authorized the call throws; if the logic errors the call returns success: false and rolls back.

Reading state

const state = await client.gameModel.containerState({ appId: "1", containerId: enemyId });
const props = JSON.parse(state.propertiesJson); // only properties you may see

containers, container, and traverse round out the read surface.

Reacting to changes

Clients pull model state — there is no server-push subscription. Poll the event log with events (filter by session, container, function, or success), or re-read containerState after a change:

const recent = await client.gameModel.events({ appId: "1", sessionId });
for (const e of recent) {
console.log(e.functionName, e.success, JSON.parse(e.returnValueJson ?? "null"));
}

To avoid blind polling, have the acting client send a lightweight "model changed" ping over the realtime path; peers then re-pull. Both carriers are already wrapped by the SDK:

  • Recommended — channels. Publish to a per-session channel with client.udp.sendChannelMessage; members receive a channelMessage notification and re-read client.gameModel.containerState(...).
  • Alternative — spatial. For location-bound changes, client.udp.sendClientEvent pings nearby players, who re-pull.

See Game Models › Reacting to changes for the full pattern.

See Game Models for the full concept guide, the expression language, the authority model, and property visibility. For ready-made mappings of common concepts (inventory, lockable objects, NPCs) onto this API, see the Game Kit.