Skip to content

Actors

An actor is a thing in the world. It carries a transform, holds components, and takes part in every frame in spawn order. A game writes its own actors by subclassing Actor, attaching components in the constructor, and overriding the lifecycle methods it needs.

type ActorClass<A extends Actor = Actor> = new () => A;

An actor class takes no constructor arguments. The world constructs it and then applies the spec it was spawned from, so a spec’s configure callback supplies whatever the instance needs before it begins play.

interface Transform {
position: Vec3;
rotation: Quat;
scale: Vec3;
}
FieldDefaultMeaning
position{ x: 0, y: 0, z: 0 }Position in world units.
rotation{ x: 0, y: 0, z: 0, w: 1 }Orientation, as a unit quaternion.
scale{ x: 1, y: 1, z: 1 }Scale along each local axis.

The defaults together are the identity. The world is right-handed with +Y up, and an actor at the identity rotation faces along -Z, so FORWARD rotated by rotation is the direction the actor faces. A Partial<Transform> on an ActorSpec or a SpawnSpec fills its absent fields from the identity, and a field that is given is given whole.

A transform is a plain mutable record, so movement is an assignment to position, rotation, or scale. The math page specifies the Vec3 and Quat types and the pure helpers that build quaternions from Euler angles or an axis and angle, rotate vectors, and compose transforms. A component composes its offset with its actor’s transform through worldTransform() and worldMatrix(), in the order scale, then rotation, then translation, as three composes a matrix.

class Actor {
readonly world: World;
readonly id: number;
readonly transform: Transform;
readonly components: readonly Component[];
readonly tags: ReadonlySet<string>;
readonly alive: boolean;
tickEnabled: boolean;
tickWhenPaused: boolean;
beginPlay(): void;
tick(dt: number): void;
endPlay(reason: EndPlayReason): void;
attach<C extends Component>(component: C): C;
detach(component: Component): void;
component<C extends Component>(type: ComponentClass<C>): C | null;
componentsOf<C extends Component>(type: ComponentClass<C>): readonly C[];
addTag(tag: string): void;
removeTag(tag: string): void;
hasTag(tag: string): boolean;
destroy(): void;
}
MemberSemantics
worldThe world the actor belongs to. Assigned after construction and before beginPlay.
idUnique within the world, assigned in spawn order from 1.
transformThe actor’s own position, rotation, and scale, in world units. Mutable in place.
componentsThe attached components, in attachment order.
tagsThe tags the actor carries.
alivefalse from the moment destroy is called.
tickEnabledDefaults to true. A false actor and its components skip their tick.
tickWhenPausedDefaults to false. A true actor ticks with its components while the world is paused.
beginPlayRuns once, after the actor has a world.
tickRuns once per frame, with the frame’s delta in seconds.
endPlayRuns once, when the actor is destroyed or when its world closes.
attachAttaches the component and returns it. Attaching after beginPlay runs the component’s beginPlay before returning.
detachRuns the component’s endPlay("destroyed") and removes it.
componentThe first attached component that is an instance of type, or null.
componentsOfEvery attached component that is an instance of type, in attachment order.
addTagAdds a tag to the actor.
removeTagRemoves a tag from the actor.
hasTagWhether the actor carries the tag.
destroyMarks the actor. alive becomes false at once, and the actor leaves the world at the end of the frame.

The base class’s beginPlay, tick, and endPlay do nothing, so a subclass overrides only what it needs.

type EndPlayReason = "destroyed" | "level-closed";

endPlay receives "destroyed" when the actor was destroyed and "level-closed" when the world it belongs to closed.

A constructor runs before the actor has a world. It attaches components and sets defaults; anything that reads the world belongs in beginPlay. world is assigned once the constructor has returned and before beginPlay runs, so it is present for every line of code that runs after construction.

Every actor a level declares exists before any of their beginPlay runs, and those beginPlay calls run in spawn order. An actor finds its peers there, through world.byTag, world.ofType, or world.find, regardless of the order the level declared them in. The game mode’s beginPlay runs after every declared actor has begun play.

world.spawn constructs the actor, applies its spec, attaches it to the world, and runs its beginPlay and each component’s beginPlay before returning. The actor is live and reachable from world.actors() the moment spawn returns, and its first tick is the next frame. Spawning emits actor:spawned.

Each live actor ticks in spawn order, after every controller has ticked and before the collision pass runs. Immediately after each actor, that actor’s enabled components tick in attachment order. dt is seconds, and every quantity an actor writes down is per second.

A paused world runs no actor tick. An actor whose tickWhenPaused is true ticks anyway, together with its components, which is how a pause menu drives itself.

destroy marks the actor and defers its removal to the end of the frame, so a tick never observes a half-removed world. alive becomes false at once: a destroyed actor stops ticking immediately, stops rendering immediately, and takes no part in the frame’s collision pass.

At the end of the frame each component’s endPlay("destroyed") runs and then the actor’s, in reverse spawn order across the actors destroyed that frame. The actor then leaves the world and actor:destroyed is emitted. A destroyed pawn is unpossessed first, and the game mode’s pawnDied runs after the pawn’s endPlay.

Closing a world ends play for every actor it holds: each actor’s components’ and then each actor’s endPlay("level-closed") runs, in reverse spawn order.

class Pawn extends Actor {
readonly controller: Controller | null;
possessedBy(controller: Controller): void;
unpossessed(): void;
}
MemberSemantics
controllerThe controller holding this pawn, or null.
possessedByNotification that controller has taken the pawn.
unpossessedNotification that the pawn has been released.

possessedBy and unpossessed are notifications; possession itself is the controller’s call. The base implementations do nothing.

A pawn is an actor in every other respect, so it carries a transform, holds components, and ticks in spawn order alongside everything else in the world.

ConditionResult
A beginPlay throws while the start level is builtengine.initialize rejects with the cause
A tick throws under runThe error propagates to the host, and the loop schedules the next frame
A tick throws under advanceadvance rejects with the cause, and the remaining frames do not run

A throw under run leaves the loop alive, so one bad frame does not freeze the game permanently. A throw under advance stops immediately, because a caller stepping an exact number of frames needs the failure rather than the frames after it.

Actor and Pawn are exported as classes from @clockwyrks/structured-3d. ActorClass, Transform, EndPlayReason, Vec3, and Quat are exported as types from the same entry point.