LOG IN
Docs
By Opus

The Object Model

Object, Drawable, and ThingTemplate — how one unit or structure is represented in memory, on screen, and in INI.

Every unit, building, rock, and tree the engine knows about goes through the same three-layer pipeline. INI parses into a ThingTemplate — the blueprint, one per unit type. The ThingFactory stamps out Object instances from that blueprint — the game-logic entities, many per match. Each Object may or may not have a Drawable bound to it — the view-side entity. The decoupling is deliberate: logic runs without ever touching geometry, and a replay viewer could in principle render without spinning up the logic state.

ThingTemplate: the blueprint

Parsed once at load, stored in the ThingFactory keyed by name. Inherits from Overridable, so mods patch fields instead of replacing the template wholesale (ThingTemplate.h:346). A template carries the kind-of flags, geometry, armor template name, body-module choice, weapon-set table, module lists for behaviors/updates/damage/die, selection sound, art references, and a pile of editor-only metadata. isKindOf(KindOfType t) is the canonical type test and lives at ThingTemplate.h:401.

An Object block in INI looks roughly like this:

Object AmericaTankCrusader
  SelectPortrait = SUCrusader_L
  ButtonImage    = SUCrusader

  ; *** ART Parameters ***
  Draw = W3DTankDraw ModuleTag_01
    OkToChangeModelColor = Yes
    DefaultModelConditionState
      Model = AVCrusader
    End
    ; ... turret state, tread animation, etc.
  End

  ; *** ENGINEERING Parameters ***
  KindOf = PRELOAD SELECTABLE CAN_ATTACK VEHICLE SCORE
  Body   = ActiveBody ModuleTag_04
    MaxHealth = 420.0
  End
  Behavior = AIUpdateInterface ModuleTag_05
    Turret
      TurretTurnRate = 180
    End
  End
  ArmorSet
    Armor  = TankArmor
    Weapon = PRIMARY CrusaderTankGun
  End
  Geometry       = BOX
  GeometryMajorRadius = 14.0
End

The Draw = and Body = and Behavior = lines are module declarations. Each one selects a class that the factory will allocate and attach. For how the field names map onto C++ setters, see INI Field Parse System.

Object: the runtime instance

Objects are constructed by TheThingFactory->newObject(tmplate, team, statusMask) (ThingFactory.h:87), which then calls Object::initObject() to wire modules. The constructor is:

// Object.h:167
Object(const ThingTemplate *thing,
       const ObjectStatusMaskType &objectStatusMask,
       Team *team);

An Object inherits from Thing (position, orientation, geometry) and Snapshot (save/replay serialization). It holds a Team* — always, even in single-player, because civilians and scenery belong to a neutral team (Object.h:244). It holds next/prev pointers for the global GameLogic object list (Object.h:173). It owns a BodyModuleInterface* — that's where HP lives, not on the Object directly (Object.h:292). It owns an AIUpdateInterface*, a PhysicsBehavior*, a ContainModuleInterface*, an array of behavior modules, status bit masks, upgrade masks, a veterancy tracker, and its bound Drawable pointer.

Post-construction, initObject() runs. Modules get their onObjectCreated() calls; the object's setTeam logic fires; the object is ready for the first sim tick.

Drawable: the view side

Drawables are the render-side twin. They hold DrawModule instances (the things that put polygons on screen — W3DModelDraw, W3DTankDraw, W3DTruckDraw, and friends) and ClientUpdateModule instances (view-only tickers — smoke emitters, model-condition animation drivers, squash/stretch). For the details of how draw modules drive skinned meshes and model-condition state, see W3D Model Draw Modules.

Binding is one-way from logic. GameLogic calls obj->friend_bindToDrawable(draw) when the Object is created; the Drawable also gets a back-reference. See Object.cpp:3012 for the implementation and GameLogic.cpp:4358 for the call site. When a Drawable is detached (usually on destroy), GameLogic calls friend_bindToDrawable(nullptr) (GameClient.cpp:910, GameLogic.cpp:4380).

ObjectID and lookup

Every Object has an ObjectID — a small typed integer handle returned by getID() (Object.h:183). Game-logic code stores IDs, not pointers. The reason is simple: an Object can be destroyed between the frame you recorded the reference and the frame you use it. Saves and replays need stable handles across sessions. Pointers don't serialize; IDs do.

Resolution goes through TheGameLogic->findObjectByID(id) (GameLogic.h:155, inlined at GameLogic.h:428). Returns nullptr if the Object is gone. Any system that holds a long-lived reference — AIGuard, target tracking, producer/builder lineage on Object::m_producerID — uses this pattern.

Quirks

  • Body is a module, not a baked-in field. An Object built without ActiveBody, ImmortalBody, UndeadBody, or StructureBody has no HP and cannot take damage. getBodyModule() returns null and attemptDamage is a no-op on the damage side.
  • A Drawable can outlive its Object by a few frames during death sequences. That's how you get a rubble Drawable that persists while the logical Object is already gone from the GameLogic list.
  • ThingTemplate is Overridable. Mods chain a patched copy onto the base template; the base ID and name stay stable so netplay and replays match across machines (ThingTemplate.h:389).
  • Every Object carries a Team*. Neutral civilians and map scenery are owned by a neutral team — team is never null.
  • A handful of template fields are still parsed for backward compatibility but no longer consulted at runtime. The field-parse tables tolerate extras; the logic doesn't read them. Don't trust an INI field to be live just because it parses.