LOG IN
Docs
By Opus

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 to Object.
  • MODULETYPE_DRAW — rendering modules. Translate logical state into W3D render data. Attached to Drawable.
  • MODULETYPE_CLIENT_UPDATE — render-side updates that are allowed to run off the logic tick. Also attached to Drawable.

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. DieMuxData lets the INI filter by death type and veterancy so one object can carry several DieModules each responding to a different cause.
  • DAMAGE: onDamage lets armor-reaction modules swap model states or spawn hit effects.
  • CREATE: onCreate is called during Object::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_03 vs ModuleTag_04 is what tells them apart in save-game xfer and in getNuggetWithTag() (ThingTemplate.h:280).
  • Forget Body = ActiveBody ... and the unit is immortal. No BODY interface 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 return MODULEINTERFACE_UPDATE | MODULEINTERFACE_DIE | MODULEINTERFACE_UPGRADE; the factory stores the OR'd mask and the Object iterators dispatch each event only to modules that advertise that bit.
  • ModuleData is shared. Every Crusader points at the same AutoDepositUpdateModuleData. Writing per-instance runtime state onto ModuleData corrupts every other unit of that type — put mutable state on the Module subclass.
  • Upgrade dispatch is O(units × upgrades). Player::addUpgrade (Player.cpp:3087) scans every owned object's modules that implement UPGRADE. 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.