Define a custom actor
Give a Vishnu cartridge its own actor, appearance, and movement.
The starter library supplies a tree and a wandering cat. A cartridge can define
another actor with actor(): attach the components it needs, give it a visual
id, and add it to the cartridge’s actors list. Place it in scene, or let a
rule spawn it later.
Body and Traversal make an actor eligible for native movement; they do not
move it by themselves. A deterministic behavior chooses a destination and
submits move(). The native runtime checks the route and applies the movement.
Add this small snail to the Acorn starter’s cartridge.ts. The unique
acorn.snail visual id below uses the library cat figure as temporary artwork;
replace that binding with your cartridge’s art when it is ready.
import { actor, action, behavior, Body, component, Destination, MEADOW_VERTICAL_METRES, move, Position, predicate, query, Traversal, Visual, type VisualBinding,} from "vishnu";import { cat, libraryVisuals, tree } from "vishnu/library";
const SnailRace = component<{ finishX: number; nextAt: number }>( "acorn.snail-race", { version: 1, fields: { finishX: "number", nextAt: "number" } },);
const snailIsReady = predicate("acorn.snail-ready", { reads: [SnailRace, Destination], test: (subject, context) => context.clock.now >= subject.get(SnailRace).nextAt && !context.query(query(Destination)).some((row) => row.id === subject.id),});
const crawl = action("acorn.snail-crawl", { reads: [SnailRace, Position], writes: [SnailRace], facts: ["terrainSurfaces"], exclusive: "movement", run(subject, context) { const race = subject.get(SnailRace); const at = subject.get(Position); if (at.x >= race.finishX) return; const x = Math.min(race.finishX, Math.floor(at.x) + 1); const z = Math.round(at.z); const surface = context.terrainSurfaces([[x, z]])[0]; if (!surface) return; context.action( move(subject.id, { x, y: (surface.cell[1] + 0.5) * MEADOW_VERTICAL_METRES, z, frame: null, }), ); context.write(SnailRace, subject.id, { ...race, nextAt: context.clock.now + 1, }); },});
const snailMovement = behavior( "acorn.snail-movement", (scene) => scene .find(SnailRace, Position, Body, Traversal) .where(snailIsReady) .do(crawl), { every: 1 },);
export const snail = actor("acorn.snail") .with(Position) .with(Body, { speed: 0.5 }) .with(Traversal, { clearanceCells: 1, maxStepCells: 1 }) .with(Visual, { sprite: "acorn.snail", label: "Snail" }) .with(SnailRace, { finishX: 0, nextAt: 0 }) .behaves(snailMovement);
// Add `snail` to the existing actors list, SnailRace to components,// and place(snail, { at: [-3, 3] }) to the existing scene.// Merge this binding into page.visuals beside ...libraryVisuals.const snailVisuals = { ...libraryVisuals, "acorn.snail": { kind: "figure", key: "cat", worldRole: "actor", motion: { kind: "foot", stride: 0.25 }, } satisfies VisualBinding,};In the existing defineCartridge call, add the actor and its component, place
it on the meadow, and use the merged visual bindings:
actors: [tree, cat, snail],components: [TreeOrder, SnailRace],scene: [place(tree, { at: [0, 0] }), place(snail, { at: [-3, 3] })],page: { // Keep the Acorn subtitle and its other page settings. subtitle: "Plant acorns on a meadow; every third tree lures a cat.", visuals: snailVisuals,},Visual stores the sprite id and readable label on the actor. page.visuals
maps that id to presentation art; the example reuses the library’s cat figure so
the snippet runs without an art pack. The simulation identity stays
acorn.snail, independent of that temporary picture.
Add this test to cartridge.test.ts. It checks the snail’s own movement rule,
not just that its definition typechecks:
test("the snail crawls to the finish", () => { const world = testWorld(acorn); try { world.until(() => world.find(snail).some(({ x }) => x >= 0), { within: 10, what: "the snail crossing the finish", }); } finally { world.dispose(); }});React to projectile hits
Section titled “React to projectile hits”A behavior can read the projectile impacts of each step: a round striking a
body, a cannonball hitting a hull. Pass consumesImpacts: true, and its
predicates and actions see context.impacts. Each impact reaches the behavior
once. Behaviors that don’t ask see no impacts, and their compiled system is
unchanged.
const takeHits = action("acorn.take-hits", { reads: [Hull], writes: [Hull], run(ship, context) { const hits = context.impacts.filter((hit) => hit.targetId === ship.id); if (hits.length) context.write(Hull, ship.id, { hull: ship.get(Hull).hull - hits.length }); },});
const struck = behavior( "acorn.struck", (scene) => scene.find(Hull).do(takeHits), { consumesImpacts: true },);If the actor is a dressed, human-like character, the cartridge can also own a
Cast and a paper-doll pack. That art pipeline is separate from Visual and
Body; use it when a biped look needs authored heads, clothes, or poses.
vishnu check && vishnu test && vishnu build