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

TrackTrigger

TrackTrigger marks a region of a track in track coordinates and reports a TrackMover that rides through it. A rock’s hit box, a coin, and a finish line across the whole width are each one. Unreal calls this a trigger volume, Godot an Area3D, and Roblox fires Touched. Those three test a physics body, and a mover has none, so the step tests the mover’s own coordinates instead.

Its props are in TrackTriggerProps.

The region is a box in track coordinates:

  • start and end: metres along the centerline.
  • left and right: metres across, positive to the right of travel.
  • bottom and top: metres above the surface, against the mover’s height. A mover that jumps higher than top clears the region.

A bound left out is open, so a finish line names only start and end. The test reads the mover’s point and not its size: to hit a rider a metre wide, pad the region by half a metre each side. Only a mover on the trigger’s own track counts.

The step writes the report for the region as a whole on the entity:

  • inside on TrackTriggerTrait: a mover stands in the region.
  • TriggerEnteredEvent: an event the step adds when a mover comes in while none stood in the region, with the mover as other and its key as otherKey.
  • TriggerExitedEvent: an event the step adds when the last mover leaves, with that mover as other.

Both come from the authoritative step alone, the room’s or a game played alone, so a track trigger is heard once. They are the events a physics trigger raises, with a track trigger’s own rule: first in and last out, swept over the mover’s path along the track, where a physics trigger samples what overlaps it at each step, as the entity page says.

An event lasts until the start of the next step. A fast mover can pass a thin region inside one step. The test covers the ground the mover’s speed carried it over during the step, so that pass adds both TriggerEnteredEvent and TriggerExitedEvent on one step, with inside false. Setting a mover’s distance moves it without passing the ground between, so a reset to the start enters nothing on the way back. Across and up, the test reads where the step ended.

A trigger marked lap counts a lap of the mounted Round on each entry.

A game’s system reads TriggerEnteredEvent with readEvents to act once per pass, and a view hears it with useEvent. A rider sits inside a rock’s box for several steps, and TriggerEnteredEvent is emitted on the first of them only. The report is data, so a headless test steps a course and reads it without a screen.

The step also names each mover. On a trigger’s first step it adds TrackTriggerMoversTrait beside TrackTriggerTrait, with two lists of mover entities:

  • inside: each mover that stands in the region.
  • entered: each mover that came in this step, including a fast one that passed through.

A second rider that rides into a rock while the first still stands in it is in entered, though the trigger gets no TriggerEnteredEvent because the region never emptied. No list names a mover that left; TriggerExitedEvent says when the last one did. The state dump, from dumpState or spawnite play dump, shows both lists under the trigger’s trackTriggerMovers, each mover named by the key its own entry in the dump carries. In a room, the stream carries both lists to every page, so a page reads the movers of the room’s last send. A mover destroyed since the step is already out of each list: a destroy takes an entity out of every list that names it.

Godot’s Area3D emits body_entered for each body, Unreal’s OnComponentBeginOverlap fires for each actor, and Roblox’s Touched fires for each part. Here the step writes a list rather than calling back, so a game’s system in its plugin’s rules reads it after the move, as it reads the rest of the report:

packages/engine/test/outside/payOnEntry.ts
import type { World } from "koota";
import {
addCoins,
createQuery,
definePlugin,
readEach,
TrackTriggerMoversTrait,
type AuthoritativeStep,
} from "@spawnite/engine/core";
const regions = createQuery(TrackTriggerMoversTrait);
/** Pays a coin to each mover that rides into a region. */
function payOnEntry(world: World, step: AuthoritativeStep) {
readEach(world, regions, ([movers]) => {
for (const mover of movers.entered) addCoins(step, mover, 1);
});
}
export const tolls = definePlugin({
name: "tolls",
systems: { rules: { payOnEntry } },
});

Client. The test runs in the step’s move block, straight after the move, on the coordinates it wrote. What an entry does, such as a stun, a coin or a finish, is the game’s own system.

import { Entity, Track, TrackTrigger } from "@spawnite/engine";
const run = new Track([
{ x: 0, y: 0, z: 0, width: 6 },
{ x: 0, y: -10, z: -80, width: 6 },
]);
export function Course() {
return (
<>
{/* A rock 40 m down, a metre wide, cleared by a jump over 0.4 m. */}
<Entity name="Rock">
<TrackTrigger
track={run}
start={39.5}
end={40.5}
left={-1}
right={1}
top={0.4}
/>
</Entity>
{/* The finish: the last 10 m, across the whole width. */}
<Entity name="Finish">
<TrackTrigger track={run} start={70} end={80} />
</Entity>
</>
);
}