Entity / Actor Model
The runtime representation of “a thing in the world” — Claw, an enemy, a coin, a moving platform — as a thin Actor identified by a stable id and assembled from data-driven components.
Why it matters
Captain Claw has dozens of object types (officers, rats, powder kegs, ammo, elevators) that share behaviour in overlapping subsets. A class hierarchy (Enemy : Character : Actor) collapses under that combinatorics — a “destructible moving platform that drops loot” has no natural parent. OpenClaw instead makes Actor an aggregate: an id plus a bag of components built from XML, so new object types are authored as data, not C++ subclasses. This is the entity half of the entity-component split.
How it works
OpenClaw’s model (following the “Game Coding Complete” actor design):
- Actor = id + component map. An
Actorholds anActorId(auint32_t) and amap<ComponentId, StrongActorComponentPtr>. It has almost no logic of its own — it is a handle that owns components. See smart-pointers-unique-ptr-shared-ptr-weak-ptr. - Ownership vs reference. The
ActorFactory/level ownsshared_ptr<Actor>(strong); systems that refer to an actor store the bareActorIdand resolve it on demand, never a rawActor*. A stale id resolves to null instead of dangling. - Id-keyed registry. Live actors sit in an
unordered_map<ActorId, StrongActorPtr>inGameLogic. Lookup is by id (hash-tables); component lookup inside an actor is by a compile-timeComponentIdhash of the component name string. - Data-driven creation.
ActorFactory::CreateActor(xml)reads a<Actor>node, instantiates each<...Component>child via a name→creator registry, callsVInit(xml)per component, thenVPostInit()once all siblings exist. Levels come from the wwd-level-format. - Destruction is deferred. Killing an actor queues an
ActorDestroyedEvent(event-system-input-handling); the registry erases it between frames so you never delete an actor mid-iteration.
Example
A pickup authored entirely as data — no new C++ type:
<Actor type="TreasurePowerup">
<PositionComponent x="640" y="480"/>
<ActorRenderComponent><Image>GAME/IMAGES/COINS</Image></ActorRenderComponent>
<TriggerComponent type="aabb"/> <!-- fires overlap events -->
<PickupComponent scoreValue="100"/> <!-- grants score on touch -->
</Actor>CreateActor builds four components, wires them under one ActorId, and the world now contains a working coin. Adding a gem is a new XML file with scoreValue="500" — zero recompilation.
Pitfalls
- Storing raw
Actor*across frames. The actor can be destroyed; cache theActorIdand re-resolve, or the pointer dangles. - God-Actor. Pushing logic into
Actoritself rebuilds the inheritance mess you escaped; keepActordumb, put behaviour in components. - Id reuse. Recycling a freed
ActorIdtoo soon makes a stale reference silently point at a new actor; use a monotonic counter or a generation tag. mapvsunordered_map. A per-actorstd::map<ComponentId,...>is fine (few entries), but the world registry should beunordered_map— id lookups happen thousands of times per frame.