WorldObject
world/world-object.ts:129 Extends AbstractComponentHost<WorldObject>
A single addressable thing inside a World. A WorldObject is a
spatial node that hosts Components — controllers, visuals,
colliders, audio sources, anything else — and provides them with a
canonical, shared transform (position, rotation, scale) that they all
read from or write into.
The behaviour and appearance of an object is defined by its components. The transform fields, by contrast, are owned by the host: there is one position, one rotation, and one scale per object, regardless of how many components reference them. This is the engine's answer to the "ten components agreeing about where the thing is" coordination problem — authoritative state lives on the host, and components are either:
- Projections of the host transform — a PolygonGraphics
reading
host.position/host.rotation/host.scaleand pushing them to its underlying PIXI display object once per frame. A collider reading the same fields to transform its local shape into world space. - Writers of the host transform — controllers and AI setting
host.rotationto face a target, dynamic physics writing back simulated results inonPostUpdate.
Pick one role per component. Mixing both — having two components fight
over host.rotation in the same phase, for instance — is exactly the
coordination bug the host-owned transform exists to prevent. If two
systems both need to author rotation, decide who owns it and have the
other read.
The transform hierarchy
Objects form a tree. Every object may have one WorldObject.parent and any number of WorldObject.children, established with WorldObject.setParent (or the WorldObject.addChild / WorldObject.removeChild conveniences). The tree exists so that moving, rotating, or scaling a parent carries its descendants with it — a turret rotating with its tank, a health bar tracking the unit beneath it, a whole UI panel sliding on one tween.
This splits the transform into two readings:
- Local — the
position/rotation/scalefields below are measured relative to the parent. They are what you set. For a root object (no parent) local space is world space, so these fields read as absolute — which is exactly how every object behaved before hierarchy existed, and why a flat game needs to know nothing about any of this. - World — the absolute transform after composing every ancestor's transform down the chain, exposed read-only via WorldObject.worldPosition, WorldObject.worldRotation, WorldObject.worldScale, and the full WorldObject.worldMatrix. This is what renderers, colliders, and "where actually is it" queries read. It is derived on demand by walking to the root, so it always reflects the latest local edits with no resolve step to remember.
A parent rotating its children around the parent's origin, nested to any depth, falls straight out of that composition — there is no special case for depth, and WorldObject.localToWorld / WorldObject.worldToLocal thread the same ancestry so a parented object's collision shape transforms correctly too.
Destroying a parent destroys its whole subtree (see Lifecycle below): children never outlive the object they are pinned to.
Lifecycle
An object has three internal states: live (in the world, ticking),
marked (WorldObject.destroy has been called, awaiting the
world's sweep at the end of the current/next tick), and cleaned
(onDestroy has fired, the object is inert). Transitions are one-way and
the API is idempotent — calling destroy() repeatedly or on an
already-cleaned object is safe.
Enabling and disabling
Setting AbstractComponentHost.enabled to false on an object
gates all three of its per-frame component phases (onPreUpdate,
onUpdate, onPostUpdate) at a single early-return: a paused enemy, a
frozen UI widget, a temporarily-disabled debug overlay. The object keeps
its components and their state; flip enabled back to true and it
resumes ticking from where it was. onAdded and onDestroy are not
gated — a half-attached or half-destroyed object would be worse than a
paused one.
Example
// A controller sets the host's rotation; the graphics component reads
// it back out in the same tick (no coupling between the two).
class ChaseAI implements WorldObjectComponent {
constructor(public readonly host: WorldObject) {}
onAdded() {}
onUpdate() {
const target = this.host.world.findOneByTag('player');
if (target) {
this.host.rotation = this.host.position.angleTo(target.position);
}
}
onDestroy() {}
}Constructors
constructor(world: World, position: PointPrimitive, metadata: WorldObjectMetadata, rotation: number, scale: Point): WorldObject Parameters
-
worldWorld -
positionPointPrimitive -
metadataWorldObjectMetadata -
rotationnumber -
scalePoint
Properties
components: Map<string, Component<WorldObject, unknown>> enabled: boolean Master gate on every component update phase this host runs. When
false, the host's onPreUpdate, onUpdate, and onPostUpdate
iterations short-circuit at a single check — useful for freezing a
single object during a cutscene, pausing a UI widget while a menu is
up, or temporarily disabling a debug overlay without tearing the
components down.
The flag is not propagated to onAdded or onDestroy. Those
always fire so a host can never end up with half-attached components,
and a disabled object is still cleanly torn down when destroyed.
Defaults to true (active). Flip back to true and the host resumes
ticking from its preserved state on the next world update().
metadata: WorldObjectMetadata Metadata about the object and its relationship with the world it is part of.
position: Point The object's position relative to its WorldObject.parent, in
pixels — or in world space when the object has no parent. Constructed
fresh from the value passed to the constructor so external mutations of
that input cannot leak in; the Point exposed here is mutable and
intended to be written by controllers / physics / movement code
(host.position.x += dx). For the absolute, ancestry-composed position,
read WorldObject.worldPosition.
rotation: number The object's rotation relative to its WorldObject.parent, in
radians, measured clockwise from the positive x-axis (i.e. 0 faces
right, matching the convention used by Point.angular and
Point.angleTo). Mutable — controllers, AI and physics write into
this directly; visual components read it back to orient themselves. With
no parent this is the object's world rotation; otherwise read
WorldObject.worldRotation for the composed value.
Defaults to 0 (facing right) for newly-constructed objects. The
engine does not normalise the value, so callers may freely accumulate
angles past 2π if that simplifies their logic.
scale: Point The object's scale relative to its WorldObject.parent,
expressed as a 2D Point so x and y can be scaled independently.
Defaults to 1,1 (no scaling). The exposed Point is mutable in place —
host.scale.x = 2 works — and components projecting from the host
transform are expected to honour both axes. For the ancestry-composed
scale, read WorldObject.worldScale.
Like WorldObject.position, scale is cloned from the value passed to the constructor so the inbound point can be safely reused or mutated by the caller without affecting this object.
Accessors
children: readonly WorldObject[] This object's direct children in the transform hierarchy, in the order they were parented. The returned array is a live, read-only view of the engine's internal list — do not mutate it; add and remove children through WorldObject.setParent / WorldObject.addChild / WorldObject.removeChild, which keep the parent/child links consistent on both sides.
destroyed: boolean Whether this object is no longer alive — either marked for destruction
and awaiting the next sweep, or already cleaned up. Live objects return
false; everything else returns true.
localMatrix: Matrix This object's local transform as a single Matrix — the composition of its WorldObject.position, WorldObject.rotation, and WorldObject.scale, measured relative to its parent. Allocated fresh per call.
Most game code wants WorldObject.worldMatrix (the absolute transform); this is exposed for the rarer cases that need the parent-relative matrix on its own.
parent: null | WorldObject This object's WorldObject.parent in the transform hierarchy, or
null when it is a root. Reassign with WorldObject.setParent.
worldMatrix: Matrix This object's fully-resolved world transform as a Matrix — every ancestor's transform composed down to this node, top of the tree first. For a root object this equals WorldObject.localMatrix.
Computed on demand by walking to the root, so it always reflects the latest edits to any ancestor's local transform with no separate resolve pass to keep in sync. This is the canonical thing a renderer or collider reads to place the object in the world, and the only representation that survives a rotation composed with a non-uniform parent scale (which produces shear that WorldObject.worldScale cannot express). Allocated fresh per call.
worldPosition: Point This object's absolute position in world space — the origin of its local space mapped all the way up through its ancestry. For a root object this equals WorldObject.position. Allocated fresh per call.
Read this (not position) whenever you need to know where an object
actually is on screen — e.g. a camera following a parented unit, or
measuring the distance between two objects in different subtrees.
worldRotation: number This object's absolute rotation in world space, in radians — its own rotation plus every ancestor's. For a root object this equals WorldObject.rotation.
Recovered from WorldObject.worldMatrix, so under a parent that combines rotation with non-uniform scale (a sheared transform) it is the best-effort decomposed angle rather than an exact one — see Matrix.decompose.
worldScale: Point This object's absolute scale in world space — its own scale multiplied through every ancestor's. For a root object this equals WorldObject.scale. Always non-negative on both axes; see Matrix.decompose for how mirrored or sheared ancestries are approximated. Allocated fresh per call.
Methods
_createDependencyResolver(component: Component<WorldObject>, key: string): WorldObjectComponentDependencyResolver Subclass hook that produces the concrete dependency resolver the host
hands to a component's resolveDependencies. The World hosts a
resolver scoped to siblings only; a WorldObject hosts one that
also exposes cross-tier lookups against the parent world.
Engine-internal — never called by user code.
Parameters
-
componentComponent<WorldObject> -
keystring
Returns
WorldObjectComponentDependencyResolver _handleComponentDestroyError(error: unknown, key: string): void Hook for subclasses to intercept errors thrown by a component's
onDestroy during AbstractComponentHost.removeAllComponents.
Default behaviour is to log and swallow — a single bad component must
not prevent the rest of the host's components from being torn down.
Subclasses may override to route errors through their own reporting
channel.
Parameters
-
errorunknown -
keystring
Returns
void _reportPhaseError(error: unknown, key: string, errorPhase: 'component-pre-update' | 'component-update' | 'component-post-update'): void Routes an update-phase throw from one of this object's components to the
parent World's error channel — a WorldObject has no
reporter of its own, so it delegates. The shared per-component dispatch
loop in AbstractComponentHost calls this; the symmetric
WorldObject._handleComponentDestroyError handles onDestroy.
Parameters
-
errorunknown -
keystring -
errorPhase'component-pre-update' | 'component-update' | 'component-post-update'
Returns
void _runComponentPhase(method: 'onPreUpdate' | 'onUpdate' | 'onPostUpdate', errorPhase: ComponentUpdatePhase, update: WorldUpdate): void Drives every enabled component through one update phase, isolating each
invocation so a single throwing component can't abort the rest of the
host's components (or the wider tick). The host-level
AbstractComponentHost.enabled gate short-circuits the whole phase
before any component is touched; a per-component enabled === false
skips that one; a component that doesn't implement the optional hook is
skipped at a single property read. The cached dependencies are threaded
in as the trailing argument.
Failures are routed to AbstractComponentHost._reportPhaseError, which each host kind implements to reach its error-reporting channel.
Parameters
-
method'onPreUpdate' | 'onUpdate' | 'onPostUpdate' -
errorPhaseComponentUpdatePhase -
updateWorldUpdate
Returns
void addChild(child: WorldObject, options?: SetParentOptions): void Attaches child beneath this object — the mirror of
WorldObject.setParent, reading naturally when the parent is what
you have in hand. Equivalent to child.setParent(this, options), including
the default of preserving the child's world transform.
Parameters
-
childWorldObject -
options?SetParentOptions
Returns
void Throws
EngineError with the same codes as WorldObject.setParent.
addComponent(key: string, component: Component<WorldObject>, options: AddComponentOptions): Component<WorldObject> Adds a new component to the host object. Throws if a component with the
specified key already exists. Calls onAdded() on the component once
registered with its host.
Parameters
-
keystring -
componentComponent<WorldObject> -
optionsAddComponentOptions
Returns
Component<WorldObject> addComponentFromFactory(key: string, factory: ComponentFactory<WorldObject>, options: AddComponentOptions): Component<WorldObject> Adds a new component to the host object using a factory function.
Internally produces the new component using the factory function, then
calls addComponent() with the result.
The advantage of using this method over addComponent() is that the
factory function is provided with the host.
Parameters
-
keystring -
factoryComponentFactory<WorldObject> -
optionsAddComponentOptions
Returns
Component<WorldObject> addComponents(components: ComponentMap<WorldObject>, options: AddComponentOptions): ComponentMap<WorldObject> Adds a new set of components to the host object. Throws if a component with
the specified key already exists. Calls onAdded() on each component
after they are all registered with the host, rather than one by one.
This is important for components that may want to reference each other
during the addition phase via host.getComponent() or similar methods.
It is recommended to use this method rather than addComponent() in
situations like initialization of a new host object.
Parameters
-
componentsComponentMap<WorldObject> -
optionsAddComponentOptions
Returns
ComponentMap<WorldObject> addComponentsFromFactories(map: ComponentFactoryMap<WorldObject>, options?: AddComponentOptions): ComponentMap<WorldObject> Adds a new set of components to the host object based on an input map of
component keys to factory functions. Behavious is equivalent to
addComponents() using the key and output of each factory function.
Parameters
-
mapComponentFactoryMap<WorldObject> -
options?AddComponentOptions
Returns
ComponentMap<WorldObject> destroy(): void Marks the object as destroyed. This does not immediately remove it from the world or destroy its components — the world must tick at least once for that to happen.
If called during a World.update tick, the object is removed at
the end of that tick. If the object has not yet had its onUpdate called
during the same tick (e.g. it was destroyed by a component or by an
earlier object in the iteration), its onUpdate is skipped — destroyed
objects do not get one final tick.
Calling destroy on an already-marked or already-cleaned object is a
no-op.
Destruction cascades to the subtree: every * descendant is marked too, so a child can never outlive the parent it is
pinned to. Each marked object is swept (and has its onDestroy run)
independently by the world, honouring the same deferred timing.
Returns
void getComponent(key: string): T Gets a component from the host object using the key it was registered with.
Throws if the component does not exist. Performs an efficient lookup on a
local Map instance.
Parameters
-
keystring
Returns
T getComponentByType(type: ComponentHostConstructor<T>): T Gets a component from the host object using its type. Throws if no
component of the type exists, or if more than one component of the type
exists — in the multi-match case, getComponentByType deliberately
does not pick one for you. Use ComponentHost.getComponentsByType
when you genuinely expect multiple matches, or look the component up by
its string key.
Performs an O(n) lookup once per type initially, then caches the resolved key for O(1) lookups on subsequent calls. The cache is invalidated whenever a component is removed.
Parameters
-
typeComponentHostConstructor<T>
Returns
T getComponentsByType(type: ComponentHostConstructor<T>): readonly T[] Gets every component on the host of the given type, in the order they were originally registered. Returns an empty array if no components match.
Unlike ComponentHost.getComponentByType, this method never throws on multiplicity — it is the explicit "I expect more than one" accessor.
Parameters
-
typeComponentHostConstructor<T>
Returns
readonly T[] getHostReference(): WorldObject Gets a reference to the host object that this component is attached to. Required for the compiler to be able to resolve the type of the host object correctly in some internal function calls (e.g. when resolving the type of the host object from a factory function).
Returns
WorldObject getNullableComponent(key: string): null | T Gets a component from the host object using the key it was registered with.
Returns null if the component does not exist, rather than throwing an
error. Useful for referencing transient or optional components without
manually handling errors.
Parameters
-
keystring
Returns
null | T getNullableComponentByType(type: ComponentHostConstructor<T>): null | T Gets a component from the host object using its type. Returns null if
the component does not exist or if more than one component of the type
is registered (i.e. the lookup is ambiguous) — the nullable variant
collapses both "not found" and "ambiguous" into a single null. Use
ComponentHost.getComponentsByType when you need to distinguish
them.
Parameters
-
typeComponentHostConstructor<T>
Returns
null | T hasComponent(key: string): boolean Checks if the host object has a component with the specified key.
Parameters
-
keystring
Returns
boolean hasComponentByType(type: ComponentHostConstructor<T>): boolean Checks if the host object has a component with the specified type.
Parameters
-
typeComponentHostConstructor<T>
Returns
boolean localToWorld(point: PointPrimitive): Point Maps a point expressed in this object's local space into world
space, threading the full ancestry: the point is taken through this
object's own scale → rotate → translate (relative to its parent) and
then up through every ancestor. (0, 0) always maps to the host's
WorldObject.worldPosition, and a local point "10 units along +x"
lands wherever the host is facing in the world, scaled by the host's
and every ancestor's scale. For a root object this is just the host's
own scale → rotate → translate.
Pairs with WorldObject.worldToLocal — round-tripping a point through both methods is the identity (modulo floating-point error).
Allocates a fresh Point per call so callers can mutate the result without affecting host state.
Parameters
-
pointPointPrimitive
onDestroy(): void Lifecycle hook called when this object is actually removed from the world. Idempotent — repeat invocations are no-ops, so callers can fire it defensively without worrying about double-cleanup of components.
Detaches the object from its parent so the parent's
WorldObject.children list never holds a cleaned object. Children
of this object are not touched here: WorldObject.destroy
already marked the whole subtree, so each child receives its own
onDestroy (and detaches itself) during the same world sweep.
Returns
void onPostUpdate(update: WorldUpdate): void Drives the onPostUpdate phase across this object's components.
Called by the World during the post-update pass of each tick.
Skips components whose enabled is explicitly false, and components
that do not implement the optional hook.
Parameters
-
updateWorldUpdate
Returns
void onPreUpdate(update: WorldUpdate): void Drives the onPreUpdate phase across this object's components. Called
by the World during the pre-update pass of each tick. Skips
components whose enabled is explicitly false, and components that
do not implement the optional hook.
Parameters
-
updateWorldUpdate
Returns
void onUpdate(update: WorldUpdate): void Drives the onUpdate phase across this object's components. Called by
the World during the main update pass of each tick. Skips
components whose enabled is explicitly false.
Parameters
-
updateWorldUpdate
Returns
void removeAllComponents(): void Removes all components from the host object. Typically called internally
when the lifecycle of the host object is terminated. Differs from
individually removing components in that it first calls onDestroy() on
each component, then removes references from the host object in a separate
step. This allows cleaner teardown of components that may reference each
other.
Returns
void removeChild(child: WorldObject, options?: SetParentOptions): void Detaches child from this object, returning it to the world root.
Equivalent to child.setParent(null, options) — but a no-op (rather than
a re-rooting) if child is not actually a child of this object, so it is
safe to call defensively.
Parameters
-
childWorldObject -
options?SetParentOptions
Returns
void removeComponent(key: string): null | Component<WorldObject, unknown> Removes a component from the host object and returns it, so callers can
keep inspecting the detached component (or move it to another host).
Care should be taken when manually removing components, as methods like
getComponent() will throw if components do not exist. Removal is
idempotent: removing a key that does not exist (already removed, or never
present) is a no-op that returns null.
The component's onDestroy runs while it is still registered (so it can
reach its siblings during teardown); a throw from onDestroy is routed
to the host's error channel rather than propagating, and the component is
removed from the host either way.
Parameters
-
keystring
Returns
null | Component<WorldObject, unknown> The removed component, or null if no component was registered
under key.
removeComponents(keys: readonly string[]): void Removes several components in one batch, mirroring the four
addComponents* variants on the add side. Unlike calling
ComponentHost.removeComponent in a loop, this runs every targeted
component's onDestroy first and only then deletes them — so two
interdependent components can still reach each other during teardown,
the same guarantee ComponentHost.removeAllComponents provides,
scoped to the named subset. Unknown keys are skipped silently.
Parameters
-
keysreadonly string[]
Returns
void setParent(parent: null | WorldObject, options: SetParentOptions): void Re-homes this object in the transform hierarchy, making parent its new
parent (or detaching it to become a root when parent is null). The
parent's WorldObject.children list and this object's
WorldObject.parent are updated together so the two never
disagree.
By default the object's world transform is preserved — its local
position/rotation/scale are recomputed so it does not visibly move (see
SetParentOptions.keepWorldTransform to opt out and keep the local
transform instead).
Parameters
-
parentnull | WorldObject -
optionsSetParentOptions
Returns
void Throws
EngineError with code
ErrorCode.WORLD_OBJECT_PARENT_FOREIGN when parent belongs to
a different World.
EngineError with code
ErrorCode.WORLD_OBJECT_HIERARCHY_CYCLE when parent is this
object itself or one of its descendants (which would form a cycle).
Example
// Pin a health bar above a unit; it now moves and rotates with it.
healthBar.setParent(unit);
// Detach a power-up from the crate it was riding, leaving it exactly
// where it currently appears on screen.
powerUp.setParent(null);worldToLocal(point: PointPrimitive): Point Maps a point expressed in world space into this object's local coordinate system — the inverse of WorldObject.localToWorld, threading the full ancestry in reverse (root-ward ancestors first, then this object). This is the primitive that hit-tests and other shape-vs-world queries are built on: convert the world point to local, then ask the local-space shape (a Polygon, a Circle, ...) whether it contains it — and because it accounts for ancestry, the query is correct even for a parented object whose shape is defined in local space.
Axes with zero WorldObject.scale are left untouched on that axis (rather than dividing by zero); a fully zero-scaled object collapses to a point and containment against it is undefined either way, so this is just the cheaper of two equally-degenerate behaviours.
Allocates a fresh Point per call.
Parameters
-
pointPointPrimitive