Modules And Behaviors
How object behavior is composed from pluggable modules parsed from INI — Update, Die, Damage, Upgrade, Create, and friends.
A Crusader tank needs to update pathfinding, take damage, die dramatically, accept veterancy upgrades, crush infantry on collision, and wreckage-smoke for ten seconds after death. None of that lives in a Tank class — there is no Tank class. The engine leans on composition: behavior is a bag of modules glued to each Object, and INI picks which modules go in the bag. Add a Behavior = AutoHealBehavior ModuleTag_07 ... End block and the unit regenerates. Delete the line and it doesn't. No recompile.
The modules are instantiated during Object::initObject() (Object.cpp:520) from data that was parsed once into the unit's ThingTemplate at load time.
Module types
Three top-level categories, set in Module.h:55:
MODULETYPE_BEHAVIOR— game-logic modules. Run on the authoritative simulation. Attached toObject.MODULETYPE_DRAW— rendering modules. Translate logical state into W3D render data. Attached toDrawable.MODULETYPE_CLIENT_UPDATE— render-side updates that are allowed to run off the logic tick. Also attached toDrawable.
The split is load-bearing: logic is lockstep-deterministic across peers, drawing is not. A logic-safe module cannot read camera angles; a draw-safe module cannot advance RNG.
Within MODULETYPE_BEHAVIOR, modules declare which interfaces they implement by OR-ing flags from ModuleInterfaceType (Module.h:80):
| Interface | Callback | Common uses |
|---|---|---|
| UPDATE | per-frame update() |
AI, locomotion, auto-repair |
| DIE | onDie(DamageInfo*) |
explosions, wreckage spawn, score credit |
| DAMAGE | onDamage() |
armor reactions, damage-state swaps |
| CREATE | onCreate() |
one-shot init when object spawns |
| COLLIDE | onCollide() |
crushing, boarding, trigger zones |
| BODY | health storage | the damage model itself |
| CONTAIN | passengers | transports, buildings with garrisons |
| UPGRADE | upgradeImplementation() |
tech-tree effects |
| SPECIAL_POWER | activation | superweapons, general powers |
One class can implement several. AIUpdateInterface typically advertises UPDATE | COLLIDE; many siege weapons advertise UPDATE | UPGRADE | SPECIAL_POWER at once.
From INI to instance
A module declaration in INI is three things: the module's class name, a tag, and a parameter block.
Behavior = AutoDepositUpdate ModuleTag_03
DepositTiming = 5000
DepositAmount = 15
InitialCaptureBonus = 0
End
At INI-parse time the line hits ModuleFactory::newModuleDataFromINI (ModuleFactory.h:80). The factory looks up AutoDepositUpdate in its ModuleTemplateMap, calls that template's friend_newModuleData(INI*) (synthesized by the MAKE_STANDARD_MODULE_DATA_MACRO_ABC macro, Module.h:156), and hands back a ModuleData subclass holding the parsed fields. That ModuleData is stored in the ThingTemplate's ModuleInfo along with the tag and interface mask (ThingTemplate.h:248):
const ModuleInfo& getBehaviorModuleInfo() const { return m_behaviorModuleInfo; } const ModuleInfo& getDrawModuleInfo() const { return m_drawModuleInfo; } const ModuleInfo& getClientUpdateModuleInfo() const { return m_clientUpdateModuleInfo; }
When ThingFactory stamps out an Object, the factory walks getBehaviorModuleInfo() and calls each entry's friend_newModuleInstance(thing, data) to build the per-instance module. The ModuleData* is shared; the Module* is not.
Common behavior interfaces
- UPDATE (
UpdateModule.h:130):virtual UpdateSleepTime update() = 0;runs under the sleepy-update scheduler. Return a sleep hint so idle units don't burn ticks. - DIE (
DieModule.h:83):virtual void onDie(const DamageInfo *damageInfo) = 0;fires exactly once.DieMuxDatalets the INI filter by death type and veterancy so one object can carry severalDieModules each responding to a different cause. - DAMAGE:
onDamagelets armor-reaction modules swap model states or spawn hit effects. - CREATE:
onCreateis called duringObject::initObject()— use it for one-shot setup (weapon-slot seeding, initial kind-of flags) that must happen before the first update. - UPGRADE:
upgradeImplementation()runs when the owning player acquires a matching upgrade.
A minimal Die module:
class ExplodeOnDeath : public DieModule { public: virtual void onDie(const DamageInfo *damageInfo) override { if (!isDieApplicable(damageInfo)) return; TheWeaponStore->createAndFireTempWeapon(m_explosion, getObject(), getObject()->getPosition()); } };
Draw modules live in a sibling world
Draw modules are stored in a separate ModuleInfo on the same ThingTemplate, instantiated against the Drawable rather than the Object. They see logical state through the Object pointer but their output is pure render data. W3DModelDraw is the default and does most of the heavy lifting — model condition states, tread animation, recoil, bone-attached particles. See W3D Model Draw Modules.
Quirks
- Module tags matter. Two
Behavior = AutoDepositUpdate ...entries on the same template are legal;ModuleTag_03vsModuleTag_04is what tells them apart in save-game xfer and ingetNuggetWithTag()(ThingTemplate.h:280). - Forget
Body = ActiveBody ...and the unit is immortal. NoBODYinterface means damage calls short-circuit and queries for current health return null — it ships as a stealth bug where the unit just won't die. - One class, many interfaces.
getInterfaceMask()can returnMODULEINTERFACE_UPDATE | MODULEINTERFACE_DIE | MODULEINTERFACE_UPGRADE; the factory stores the OR'd mask and theObjectiterators dispatch each event only to modules that advertise that bit. ModuleDatais shared. Every Crusader points at the sameAutoDepositUpdateModuleData. Writing per-instance runtime state ontoModuleDatacorrupts every other unit of that type — put mutable state on theModulesubclass.- Upgrade dispatch is O(units × upgrades).
Player::addUpgrade(Player.cpp:3087) scans every owned object's modules that implementUPGRADE. With a thousand units and a full tech tree unlocked, this shows up in profiles — it's fine but it's not free. - Module order in INI matters. Die modules that poke a Body module assume the Body has already been parsed. Reorder the blocks and the dependency fails silently at parse time; the crash lands later.