Gameplay

Control contexts

State-machine wrappers that group ECS entities under a named state and activate or deactivate them as one unit when the state is entered or left.

A ControlContext groups the entities that belong to one logical mode — on-foot, in-vehicle, in-menu, cutscene — and activates or deactivates those entities atomically when the game transitions between modes. A StatefulController ties one or more contexts to a SimpleStateMachine so that entering a state starts its context and leaving it shuts it down.

The problem

Input bindings, cameras, UI overlays, and other mode-specific entities are easy to build but awkward to manage. Without a coordination layer you end up with ad-hoc flags, mixed teardown code, and entities that forget to unlink when the player climbs into a vehicle. ControlContext formalizes the lifecycle so activation and deactivation happen as one step via the state machine.

ControlContext

ControlContext is a base class. Subclass it and set up your entities in initialize:

import { ControlContext } from "@woosh/meep-engine/src/engine/control/ControlContext.js";
import InputController from "@woosh/meep-engine/src/engine/input/ecs/components/InputController.js";

class OnFootContext extends ControlContext {
    initialize(ecd) {
        super.initialize(ecd);

        const ic = new InputController();
        // ... bind keys ...

        // makeEntity() registers the entity with the context.
        // It will be built on startup() and destroyed on shutdown().
        this.makeEntity()
            .add(ic)
            .add(SerializationMetadata.Transient);
    }
}

The lifecycle is:

MethodTriggerWhat it does
initialize(ecd)Called once before first useCreates entity builders via makeEntity(); transitions state to Ready
startup()Called when its state is enteredCalls .build(ecd) on every registered entity; transitions to Running
shutdown()Called when its state is leftCalls .destroy() on every built entity; transitions back to Ready
dispose()Called when the context is no longer neededCleans up; transitions to Disposed

States are Initial → Ready → Running → Ready → Disposed. Calling startup() or shutdown() twice is safe (idempotent). The transientEntities flag (default true) marks every managed entity as SerializationMetadata.Transient automatically.

initialize(ecd)startup()shutdown()dispose()InitialReadyRunningDisposed

ControlContextState

The four states are integers from the ControlContextState enum:

import { ControlContextState } from "@woosh/meep-engine/src/engine/control/ControlContextState.js";

// ControlContextState.Initial  = 0
// ControlContextState.Ready    = 1
// ControlContextState.Running  = 2
// ControlContextState.Disposed = 3

Read the current state with context.getState().

StatefulController

StatefulController binds contexts to a SimpleStateMachine so that state transitions wire context activation automatically.

import { StatefulController } from "@woosh/meep-engine/src/engine/control/StatefulController.js";
import { SimpleStateMachine } from "@woosh/meep-engine/src/core/fsm/simple/SimpleStateMachine.js";
import { SimpleStateMachineDescription } from "@woosh/meep-engine/src/core/fsm/simple/SimpleStateMachineDescription.js";

const desc = new SimpleStateMachineDescription();
const STATE_ON_FOOT  = desc.createState();
const STATE_VEHICLE  = desc.createState();
desc.createEdge(STATE_ON_FOOT, STATE_VEHICLE);
desc.createEdge(STATE_VEHICLE, STATE_ON_FOOT);

const sm = new SimpleStateMachine(desc);
const controller = new StatefulController(sm);

const onFootCtx  = new OnFootContext();
const vehicleCtx = new VehicleContext();

// Initialize before binding so makeEntity() has ecd access.
onFootCtx.initialize(ecd);
vehicleCtx.initialize(ecd);

// Register: entering STATE_ON_FOOT calls onFootCtx.startup(),
// leaving it calls onFootCtx.shutdown().
controller.addContext(STATE_ON_FOOT, onFootCtx);
controller.addContext(STATE_VEHICLE, vehicleCtx);

// Drive the machine.
sm.setState(STATE_ON_FOOT);   // starts on-foot context
sm.setState(STATE_VEHICLE);   // shuts down on-foot, starts vehicle context

removeContext reverses the binding if a context needs to be detached at runtime.

SimpleStateMachine and SimpleStateMachineDescription

The underlying state machine is generic and not specific to control. Its description defines the graph; the machine traverses it.

const desc = new SimpleStateMachineDescription();

// States are plain non-negative integers allocated by the description.
const stateA = desc.createState();   // returns e.g. 0
const stateB = desc.createState();   // returns e.g. 1

// Edges define legal transitions.
desc.createEdge(stateA, stateB);
desc.createEdge(stateB, stateA);

// Optionally assign a selector function that picks the next state given input.
desc.setAction(stateA, (input) => input.wantsVehicle ? stateB : stateA);

const sm = new SimpleStateMachine(desc);

// Explicit transition.
sm.setState(stateB);

// Advance — calls the selector for the current state and transitions.
sm.advance(myInput);

// Shortest-path navigation (BFS over the edge graph).
sm.navigateTo(stateB);

Entry and exit handlers can be registered directly on the machine without going through StatefulController:

sm.addEventHandlerStateEntry(stateA, () => console.log("entered A"));
sm.addEventHandlerStateExit(stateA,  () => console.log("left A"));

Relationship to InputController

A control context is one clean way to manage the lifetime of an InputController entity. Instead of building and destroying the entity by hand, create it inside initialize with makeEntity(). The context takes care of building it on startup() and destroying it on shutdown(), so input bindings activate and deactivate exactly when the state changes.

See Input devices for the binding-path API the InputController component uses.