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:
| Method | Trigger | What it does |
|---|---|---|
initialize(ecd) | Called once before first use | Creates entity builders via makeEntity(); transitions state to Ready |
startup() | Called when its state is entered | Calls .build(ecd) on every registered entity; transitions to Running |
shutdown() | Called when its state is left | Calls .destroy() on every built entity; transitions back to Ready |
dispose() | Called when the context is no longer needed | Cleans 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.
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.