arcade2d
Class

WorldObject

world/world-object.ts:129

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.scale and 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.rotation to face a target, dynamic physics writing back simulated results in onPostUpdate.

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 / scale fields 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

Properties

protected readonly #
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().

readonly #
metadata: WorldObjectMetadata

Metadata about the object and its relationship with the world it is part of.

readonly #
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 if that simplifies their logic.

readonly #
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.

readonly #
world: World

The world that the object exists within.

Accessors

readonly #
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.

readonly #
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.

readonly #
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.

readonly #
parent: null | WorldObject

This object's WorldObject.parent in the transform hierarchy, or null when it is a root. Reassign with WorldObject.setParent.

readonly #
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.

readonly #
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.

readonly #
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.

readonly #
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

protected #
_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

Returns

WorldObjectComponentDependencyResolver
protected #
_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

  • error unknown
  • key string

Returns

void
protected #
_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

  • error unknown
  • key string
  • errorPhase 'component-pre-update' | 'component-update' | 'component-post-update'

Returns

void
protected #
_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'
  • errorPhase ComponentUpdatePhase
  • update WorldUpdate

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

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

#
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

#
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

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

#
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

  • key string

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

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

Returns

readonly T[]
protected #
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

  • key string

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

Returns

null | T
#
hasComponent(key: string): boolean

Checks if the host object has a component with the specified key.

Parameters

  • key string

Returns

boolean
#
hasComponentByType(type: ComponentHostConstructor<T>): boolean

Checks if the host object has a component with the specified type.

Parameters

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

Returns

Point

A new Point expressing the same location in world space.

#
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

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

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

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

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

  • key string

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

  • keys readonly 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

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

Returns

Point

A new Point expressing the same location in the host's local space.

ESC