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, orStructureBodyhas no HP and cannot take damage.getBodyModule()returns null andattemptDamageis 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.
ThingTemplateisOverridable. 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.