Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

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.

A node is an AiNode:

  • id: the entity’s key in dump(), the same one its devtools row carries, so the tree, the dump and describe() agree.
  • name: the entity’s row name in the devtools tree, such as Rock, only where it differs from is. Where nothing else describes the entity, is is 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.

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().

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.

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 you or below you rather 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 rules every description follows:

  • Relative to a subject. A fact such as in your path is 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 now
    under 1 right in front of you
    under 2 just ahead
    under 4 further ahead
    4 or more far ahead
    passed behind you
  • Width, not centre. A thing is in your path while the subject’s line crosses its extent across. Otherwise it is to your left or to your right, and close within a metre of its edge or well clear beyond.

  • 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, never 30.

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:

Terminal window
spawnite simulate --scene run --seconds 1 --ai

Over 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.

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.