arcade2d
Class

World

world/world.ts:280

The root container for an arcade2d simulation. A World owns the set of live WorldObjects, drives the per-frame update loop that animates them, and itself hosts world-scoped Components — i.e. components that conceptually belong to the simulation as a whole rather than to any single object (a Scene graphics root, a physics broadphase, an input sampler, an audio mixer, and so on). It also optionally resolves named prefab lookups against a PrefabRegistry passed at construction.

The class is designed so a typical game's "main loop" is one line: construct the world, then call World.update once per animation frame. Everything else — when components run, what order they run in, how spawns/destroys interact with the running tick — is the responsibility of this class and is described below.

The update tick

Each call to World.update executes the following schedule, in this exact order. Understanding the schedule is the single most important thing about working with arcade2d, because everything else (spawn semantics, destroy semantics, cross-component wiring, ordering pitfalls) is downstream of it.

┌──────────────────────────────────────────────────────────────────┐
│ Phase 1 — Pre-update                                             │
│   1a. World components, in insertion order, call `onPreUpdate`   │
│   1b. Each live WorldObject, in spawn order, has every component │
│       (in component insertion order) call `onPreUpdate`          │
├──────────────────────────────────────────────────────────────────┤
│ Phase 2 — Update                                                 │
│   2a. World components, in insertion order, call `onUpdate`      │
│   2b. Each live WorldObject, in spawn order, has every component │
│       (in component insertion order) call `onUpdate`             │
├──────────────────────────────────────────────────────────────────┤
│ Phase 3 — Post-update                                            │
│   3a. World components, in insertion order, call `onPostUpdate`  │
│   3b. Each live WorldObject, in spawn order, has every component │
│       (in component insertion order) call `onPostUpdate`         │
├──────────────────────────────────────────────────────────────────┤
│ Phase 4 — Sweep + flush                                          │
│   4a. Run `onDestroy` on every object marked destroyed this tick │
│       (or earlier) and remove them from the live set + id map    │
│   4b. Promote objects spawned mid-tick into the live set so they │
│       participate in the *next* tick                             │
└──────────────────────────────────────────────────────────────────┘

Two ordering rules that follow from the schedule

  1. Phases are strict. Every component on every host finishes its onPreUpdate before any component anywhere starts onUpdate. Every onUpdate finishes before any onPostUpdate starts. This is the contract you rely on to make camera-follows-player and similar "I need everyone else's results" patterns work without race conditions.

  2. Within a phase, world components run before object components. A world-scoped input sampler in Phase 1a will have finished polling by the time a controller component in Phase 1b reads it. A world-scoped physics step in Phase 2a will have resolved collisions by the time a per-object damage handler in Phase 2b decides what to do about them.

Within each of those two sub-orderings (world components among themselves; object components on a single host among themselves), the order is insertion order — i.e. the order they were registered via addComponents (or the order they appear in a Prefab's component map). There is no priority or weight system; if A must run before B, register A first.

Choosing the right phase

The three update hooks aren't "early, middle, late" so much as three specific roles. Picking the right one is mostly about asking what does this code need to be true when it runs?

onPreUpdate — sample and prepare

Use when you produce state that other components will consume during onUpdate. The hook is optional: components that don't have prep work to do should omit it. Canonical uses:

  • Input polling. A world-scoped InputSystem reads keyboard/mouse state once per tick in onPreUpdate and exposes it via getters. Every controller component then queries it in onUpdate and sees a consistent snapshot.
  • Per-frame buffer clears. A world-scoped collision broadphase clears the previous tick's overlap set in onPreUpdate so per-object colliders can populate it during their own onUpdate without stepping on the previous frame's results.
  • Interpolation snapshots. A graphics component caches its pre-update transform so any other system that wants to interpolate between "before" and "after" positions during the post phase has a clean snapshot to work from.

onUpdate — do the work

The main per-frame body of behaviour. Required on every component — even if it's empty. Use when the work doesn't depend on having seen the results of everyone else's update this frame. The vast majority of components only need this hook. Canonical uses:

  • Movement and behaviour. Controllers reading input and translating it into changes to the host's position, AI components evaluating their decision logic, projectile components advancing themselves.
  • World simulation steps. A world-scoped physics system stepping its solver, a particle emitter advancing emission timers, a wave manager spawning enemies on a cadence.
  • Lifetime accounting. A bullet decrementing its remaining lifetime and self-destructing when it hits zero.

onPostUpdate — react to everyone else's updates

Use when your work requires that every other component has finished its onUpdate first. Optional; skip when not needed. Canonical uses:

  • Camera follow. A world- or object-scoped camera component reads the player's already-moved position in onPostUpdate and centres the viewport on it. If you did this in onUpdate and the player's controller ran later in the iteration order, you'd lag by a frame.
  • Transform sync to a renderer. A graphics component copying host.position into a PIXI display object in onPostUpdate guarantees the final visual reflects every behaviour change that happened this tick, including ones from late-running components.
  • Late audits and assertions. A debug overlay that needs to inspect the world's settled state, count active enemies, etc.

Rule of thumb

If the code reads state set by other components: pre-update produces, update consumes, post-update reacts. If you only set state, you almost certainly want onUpdate. If you're not sure, start with onUpdate and move to a different phase only if you see a one-frame-lag bug.

Spawn and destroy semantics

Both spawns (createFromPrefab, createFromPrefabName, createEmpty) and destroys (WorldObject.destroy) are deferred and re-entrancy-safe so that running components can't observe inconsistent state mid-tick.

Spawning

  • Outside a tick (e.g. during world setup before the first update() call), spawned objects join the live set immediately and participate in the very next tick from Phase 1 onward.
  • During a tick, spawned objects are queued and only join the live set during Phase 4b. They first participate in the next tick. This keeps Phase 2/3 iteration order stable even if a component spawns ten new objects mid-update.
  • Either way, the new object is findable via World.findById immediately — the id map is updated synchronously on spawn — so cross-references in the same tick are valid even if the spawned object hasn't yet been driven by an update. Bulk tag queries are deliberately the other way: World.findByTag and World.findOneByTag exclude pending objects by default, because iterating over a mix of "fully ticked" and "exists but has never ticked" objects silently produces wrong answers (an AOE damaging enemies that have not initialised, a counter reporting non-yet-rendered units). Opt back into the same-tick view with { includePending: true } when you genuinely want it.

Destroying

WorldObject.destroy only marks the object. The real removal happens in Phase 4a. Consequences:

  • An object destroyed mid-tick is skipped for all remaining phases. If a controller destroys itself in Phase 1, neither Phase 2 nor Phase 3 will touch it. This is the "no final tick" rule and exists so destroyed objects can't observe state from after their own death.
  • onDestroy is idempotent. Calling destroy() twice, or destroying an already-cleaned object during teardown, is a no-op. Components on a destroyed object also have their onDestroy run exactly once during sweep.
  • Spawned-and-destroyed-in-the-same-tick objects never enter the live set but do receive onDestroy during Phase 4b, so component cleanup is honoured.

Enable/disable: per-component and per-host

Two gates control whether update hooks run, layered from most specific to most general:

  1. Per-component — each Component carries an optional enabled flag. When explicitly false, the engine skips that component's onPreUpdate, onUpdate, and onPostUpdate.
  2. Per-host — every host (the World itself, and every WorldObject) carries an AbstractComponentHost.enabled field. When false, every component on that host has all three update phases skipped at a single early-return — the cheap way to freeze an entire object during a cutscene or pause a UI widget while a menu is up. The world-level toggle gates the world's own components only; object iteration is controlled by whether update() is called.

Neither gate touches onAdded or onDestroy. Both fire regardless so a component is never left half-attached and a destroyed host is always cleanly torn down.

Error isolation

Every component hook (onPreUpdate, onUpdate, onPostUpdate, onDestroy) is wrapped in a try/catch by the engine. A thrown error in one component does not abort the tick — the rest of the host's components, and every other host, continue to run. Errors are routed through World.reportError, which forwards to the optional WorldOptions.onError handler (defaulting to console.error). If you want fail-fast, throw from inside onError and the engine's try/finally will let the exception propagate out of update().

Cross-references

Example

// The blessed path: bootstrap a Game, then ask it for a World.
const game = await Game.bootstrap({ canvas: { fill: 'window' } });

const world = game.createWorld({
  components: (world) => ({
    // Phase 1a (pre-update) — registered first so it runs first.
    input: () => new InputSystem(world),
    // Phase 2a (update) — runs second, sees fresh input.
    physics: () => new PhysicsSystem(world),
    // Phase 3a (post-update) — runs third, sees settled positions.
    camera: () => new CameraSystem(world),
  }),
  prefabs: prefabRegistry,
});

Constructors

#
constructor(game: Game, options: WorldOptions): World

Parameters

Properties

protected readonly #
components: Map<string, Component<World, 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().

Accessors

readonly #
camera: Camera

The world's auto-attached Camera. Always present — the engine registers a camera during construction before any user components run, so this getter never returns null and game code can treat the camera's existence as an invariant.

readonly #
game: Game

The Game this world belongs to. Always present — passed positionally at construction and held as a non-null invariant. Test/headless callers that don't want to bootstrap a real renderer mint one via Game.createHeadless.

readonly #
prefabs: null | PrefabRegistry

The PrefabRegistry this world resolves prefabs against by name, or null if no registry was attached at construction. Exposed so callers can introspect or share the registry with other systems (e.g. editor tools).

Methods

protected #
_createDependencyResolver(component: Component<World>, key: string): WorldComponentDependencyResolver

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

WorldComponentDependencyResolver
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 #
add(object: WorldObject): WorldObject

Parameters

Returns

WorldObject
#
addComponent(key: string, component: Component<World>, options: AddComponentOptions): Component<World>

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

Returns

Component<World>
#
addComponentFromFactory(key: string, factory: ComponentFactory<World>, options: AddComponentOptions): Component<World>

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

Returns

Component<World>
#
addComponents(components: ComponentMap<World>, options: AddComponentOptions): ComponentMap<World>

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

Returns

ComponentMap<World>
#
addComponentsFromFactories(map: ComponentFactoryMap<World>, options?: AddComponentOptions): ComponentMap<World>

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

Returns

ComponentMap<World>
#
createEmpty(position: PointPrimitive, tags?: readonly string[]): WorldObject

Creates a new empty world object. Useful for creating one-off objects that don't necessarily need to be based on a prefab definition.

Parameters

Returns

WorldObject
#
createFromPrefab(prefab: Prefab, position: PointPrimitive): WorldObject

Creates a new world object from a target prefab. The object is added to the world's live set immediately if called outside a tick, or queued into the pending set if called from within an onUpdate handler — see the World class docs for the full spawn-timing contract.

Parameters

Returns

WorldObject
#
createFromPrefabName(name: string, position: PointPrimitive): WorldObject

Creates a new world object from a prefab looked up by name in the world's attached PrefabRegistry. Throws if no registry was passed at construction, or if the registry has no prefab under the given name.

This is the entry point intended for deserialised world state — saved data refers to prefabs by name, and this method is how the engine rehydrates them.

Parameters

Returns

WorldObject
#
destroy(): void

Immediately destroys the world, all of its objects (including any spawned during the current tick that have not yet been flushed into the live set), and any included components.

Safe to call against objects that have already been marked destroyed but not yet swept — WorldObject.onDestroy is idempotent, so cleanup runs exactly once regardless of prior state.

Returns

void
#
findById(id: string): null | WorldObject

Finds an object in the world using its ID. Newly spawned objects are findable from the moment they are created, even before they are promoted into the live iteration set at the end of the current tick — this is the "spawn and immediately wire references by id" pattern. Bulk tag queries are deliberately not symmetric with this behaviour; see World.findByTag for the reasoning.

Parameters

  • id string

Returns

null | WorldObject

The object with the given ID, or null if it does not exist.

#
findByTag(tag: string, options: FindByTagOptions): readonly WorldObject[]

Finds all live objects in the world with the given tag. By default excludes objects that were spawned during the current tick and are still awaiting promotion into the live set — bulk iteration that mixes fully-ticked objects with ones whose components have never run is the canonical source of "I damaged the enemy before it existed" bugs, so the engine defaults to live-only. Use includePending to opt back into the same-tick view (debug overlays, editor tooling, the rare gameplay system that genuinely wants both).

Parameters

Returns

readonly WorldObject[]

An array of objects with the given tag.

#
findOneByTag(tag: string, options: FindByTagOptions): null | WorldObject

Finds a single live object in the world with the given tag. Same default exclusion rule as World.findByTag — pending objects are skipped unless FindByTagOptions.includePending is set. Returns the first live match in spawn order, or the first pending match (when opted in) if no live object matches.

Parameters

Returns

null | WorldObject

The first object with the given tag, or null if no object is found.

#
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(): World

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

World
#
getKeyboardState(): KeyboardState

Returns the current keyboard state — the set of physical keys held at tick-snapshot time, plus an isDown(code) predicate for membership tests. Sourced from the parent Game's Keyboard component; this method is a thin pass-through that exists for symmetry with World.getMouseState (keyboards have no camera-projected variant of their own — the same held-key set is meaningful in screen space and world space alike).

See KeyboardState for the KeyboardEvent.code convention used to identify physical keys.

Returns

KeyboardState

Example

onUpdate() {
  const keys = this.host.world.getKeyboardState();

  if (keys.isDown('KeyW')) this.host.position.y -= 5;
  if (keys.isDown('KeyS')) this.host.position.y += 5;
}
#
getMouseState(): MouseState

Returns the current mouse state — cursor position (both in world space, with the camera transform inverted, and in raw canvas-local pixels) and the held/released state of the three standard buttons.

The returned MouseState is a fresh snapshot per call; the engine deliberately never hands back a live reference, so a caller stashing the value for a frame won't see it change mid-tick.

The screen-space snapshot is sourced from the parent Game's Mouse component; this method then projects the screen coordinate through the world's Scene (which knows the camera transform) to populate the world-space position. Worlds constructed via Game.createWorld have both components attached automatically.

Returns

MouseState

Throws

EngineError with code ErrorCode.COMPONENT_NOT_FOUND when the world has no Scene component registered (so screen→world projection is not possible).

Example

onUpdate() {
  const { position, buttons } = this.host.world.getMouseState();

  if (buttons.left) {
    this.host.position.moveTowards(position, 5);
  }
}
#
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
#
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
#
removeComponent(key: string): void

Removes a component from the host object. Care should be taken when manually removing components, as methods like getComponent() will throw if components do not exist. Removal is idempotent and will do nothing if the component does not exist i.e. was already removed, or never existed.

Parameters

  • key string

Returns

void
#
reportError(context: WorldErrorContext): void

Routes a runtime component error through the configured handler, or to console.error when no handler is supplied. Called by the engine whenever a component's onUpdate or onDestroy throws. Safe to call from user code too if you want to surface your own errors through the same channel.

Parameters

Returns

void
#
update(): WorldUpdate

Returns

WorldUpdate
ESC