AI metadata
Every entity in a game can carry AI metadata: what it is, its state, and what can be done with it, in words. A behaviour writes its share from its own props, an ai prop on Entity adds to it or overrides it, and one util gathers every entity into the AI tree for one subject, the player or an NPC. The tree is a game’s counterpart to a browser’s accessibility tree: each node has a role, a name, a state and the actions it supports, and an agent picks one node and one action. An agent reads the tree as a game’s state and its actions as what can be done.
Declaring metadata is optional everywhere. An entity that declares nothing still gets a node from its row name, its behaviours and its place in the world.
What a node holds
Section titled “What a node holds”A node is an AiNode:
id: the entity’s key indump(), the same one its devtools row carries, so the tree, the dump anddescribe()agree.name: the entity’s row name in the devtools tree, such asRock, only where it differs fromis. Where nothing else describes the entity,isis its row name, so the node never says one word twice.is: what it is, in a noun phrase:a coin,a slab across the lane.facts: its state now, each a short clause:in the air,badly hurt, and first, where it stands against the subject:just ahead,in your path.actions: what the subject can do with it, each a verb phrase:collect it,jump it.
The subject’s own node comes first, then the rest nearest first.
Describing a behaviour
Section titled “Describing a behaviour”A behaviour declares an ai beside its description: a function of its props that returns an AiMetadata, the same shape the Entity prop takes. A flag is one expression. A kind is a table.
export const HealthBehaviour = defineBehaviour({ name: "health", trait: HealthTrait, description: "Tracks damage, reaps an entity at zero, and downs a player's character or an entity with a respawn until it comes back.", ai: ({ current, maximum }) => ({ facts: [current >= maximum / 2 ? "hurt" : "badly hurt"], }),});
const courseWords: Record<CourseKind, AiMetadata> = { [CourseKind.Slab]: { is: "a slab across the lane", actions: ["jump it", "go round it"], }, [CourseKind.Coin]: { is: "a coin", actions: ["collect it"] },};
export const CourseBehaviour = defineBehaviour({ name: "course", trait: CourseTrait, ai: ({ kind }) => courseWords[kind],});The function also receives an AiDescribeContext: the entity, for a fact a sibling trait holds, and the subject the tree is built for, so a behaviour can write a fact relative to it.
The tree reads every declared behaviour. The engine declares its own; a game’s own is made with defineBehaviour, which declares it and also names its trait in dump().
Describing an Entity
Section titled “Describing an Entity”The ai prop on Entity takes the same shape, as words rather than a function. Its is overrides what the behaviours say; its facts and actions add to theirs.
import { Entity, Interact } from "@spawnite/engine";
export function Chest({ onOpen }: { onOpen: () => void }) { return ( <Entity model="chest" position={[4, 0, -6]} ai={{ is: "a treasure chest", actions: ["open it"] }} > <Interact prompt="Open the chest" radius={2} onInteract={onOpen} /> </Entity> );}The words live on the entity’s AiTrait, so a headless world and a room read them as they read any trait.
What the tree infers
Section titled “What the tree infers”The tree fills in what nobody declared:
- The name and the id come from the devtools row and the dump key.
- The subject is the player: her character, else the track mover under the client’s authority, else the first track mover.
- Where a thing stands against the subject is measured, never declared. On a track, the subject’s mover is measured against each track trigger on the same track, height included, so a trigger over or under the mover is
above youorbelow yourather than in its path. Anywhere else, the subject’s transform and facing are measured against each thing’s transform, with its extent from its collider, turned as its body is, or from the radius its behaviour reaches. The speed is the subject’s speed toward the thing, or its walking speed while it stands or moves away, so the band says how soon it could reach the thing. - Which entities are in the tree: any with a row name or any metadata. A camera rig or a ground has neither and stays out.
The words
Section titled “The words”The rules every description follows:
-
Relative to a subject. A fact such as
in your pathis written for one entity, which the util takes as an argument. -
Distance as time. Nearness is a band of the seconds to reach a thing at the subject’s speed, measured to its near edge:
Seconds Words under 0.5 reaching you nowunder 1 right in front of youunder 2 just aheadunder 4 further ahead4 or more far aheadpassed behind you -
Width, not centre. A thing is
in your pathwhile the subject’s line crosses its extent across. Otherwise it isto your leftorto your right, andclosewithin a metre of its edge orwell clearbeyond. -
Counting stays in code. A count or a threshold becomes words before an agent sees it: a health of 30 in 100 reads
badly hurt, never30.
Reading the tree
Section titled “Reading the tree”Every reader gets the same words from the same call:
// Headless, with no React: a test, a room, an ask.import { buildAiTree } from "@spawnite/engine/core";const tree = buildAiTree({ world });const guardsView = buildAiTree({ world, subject: guard });
// The devtools store, in the page, in simulate and in a test that mounts a scene.useDevtools.getState().readAiTree();useDevtools.getState().describe("7").ai;In the browser, window.__GAME_DEVTOOLS__.getState().readAiTree() prints it from the console while the overlay is mounted. The tree is built when read, since its words change every step.
From the command line, spawnite simulate --ai prints the tree after the steps and spawnite play ai prints the running playtest’s; --subject <id|name> sees it from another entity. Both print it as JSON, one node a line:
spawnite simulate --scene run --seconds 1 --aiOver the MCP, simulate takes ai and subject for the same tree, and read_ai_tree reads the running playtest’s with an optional subject, taking no shot.
How other engines do it
Section titled “How other engines do it”Godot’s set_meta and Roblox’s attributes and tags attach free-form data to a node or an instance. The web’s ARIA roles and its accessibility tree describe a thing for a model and list what a user can do with it, and they are what browser agents read, so this follows the web.