Playing AI Animations

From The DarkMod Wiki
Jump to navigationJump to search

Animation Channels

Each Actor has a set of animation channels (all AIs are deriving from Actors, by the way):

  • ANIMCHANNEL_TORSO
  • ANIMCHANNEL_LEGS
  • ANIMCHANNEL_HEAD
  • ANIMCHANNEL_EYELIDS
  • (ANIMCHANNEL_ALL)

Each channel can play an animation independently from the other channels, hence it's possible to play the walk animation on the LEGS channel and the cough animation on the TORSO.

Where to define the Animations

Assuming the animation is ready (produced in Maya/Motion Builder/whatever) and available as md5anim, the animation must be defined in the AI's model definition. Take a look at the citywatch model def:

model tdm_ai_citywatch {
  mesh                  models/md5/chars/guards/citywatch/tdm_ai_citywatch.md5mesh
  
  anim run              models/md5/chars/guards/citywatch/run.md5anim { ... }
  anim draw             models/md5/chars/guards/citywatch/drawsword.md5anim { ... }
  ...
}

So the model def is the place to look for the various animation names.

Btw: an easy way to preview these animations without code changes is to call this in the Doom 3 console:

testModel tdm_ai_citywatch
testAnim draw

This will place a citywatch torso in front of you and loops the draw animation. You can enter noclip to conveniently inspect the animation.

Animation Groups

For a given animation name (let's say "melee_attack") the game code automatically tries to lookup one of the animations "melee_attack1", "melee_attack2", etc. These animations, sharing the same prefix with a different integer number, form an animation group.

The number is chosen randomly by the SDK code, so you can randomly choose between N different animations by specifying a single anim name.

Note that you can still pick a specific animation by requesting a numbered anim name in the first place (e.g. you request "melee_attack2").

Playing Animations

The most basic way to play an animation is to call the script event playAnim (via script) or Event_PlayAnim (SDK). Both take the desired animation channel and the animation's name as argument:

scriptEvent float playAnim(float channel, string animName);
void idActor::Event_PlayAnim(int channel, const char *animname);

However, for most applications this is not enough. When a channel is done playing the anim, it will stop playing anything, which might cause the AI's hands to hang around uselessly (in the case of the TORSO channel). Because of this, Animation States were introduced:

Animation States

The AI is always in a certain Animation State, which is usually the Idle state (this is set in the AI's init() script). Each Animation State has an accompanying script function (normally defined in ai_darkmod_base.script), whose task is to monitor the animation status and take the according actions. The Animation State of an AI can be changed by calling the scriptEvent animState or idAI::SetAnimState():

scriptEvent void animState(float channel, string stateFunction, float blendFrame);
void idActor::SetAnimState(int channel, const char *statename, int blendFrames);

When an animation state is entered, the according script function carrying the same name as the AnimState is looked up on the actor's scriptobject and called. The script is responsible for controlling the animation.

Let's have a look at this example:

void ai_darkmod_base::Torso_QuickMelee() {
   // play the attack animation
   playAnim(ANIMCHANNEL_TORSO, "melee_attack");
   
   while (!animDone(ANIMCHANNEL_TORSO, 4)) {
       waitFrame();
   }
   
   // finish up and start idling again
   finishAction("melee_attack");
   animState(ANIMCHANNEL_TORSO, "Torso_Idle", 4);
}

As you can see, this scriptEvent plays the animation, waits until its done playing and switches back to the Idle anim state after resetting a certain flag.

The important part is the while (!animDone) loop which basically waits until the animation is done playing. As soon as the anim is done, the script interpreter exits the loop and the actor is set back to the Idle state (to prevent the TORSO channel from becoming inactive).

If the programmer's intention is just to let the "melee_attack" animation play on the torso channel and to fall back into the Idle animation state afterwards, it's enough to call:

actor->SetAnimState(ANIMCHANNEL_TORSO, "Torso_QuickMelee", 4);

However, if the programmer intends to do something after the animation is done, things get more involved, that's what WaitStates are for:

Animation WaitStates

In the previous example, you might wonder what the finishAction is supposed to do. The main reason for this call is to allow the SDK code to check whether the animation is done playing. As there is no equivalent to eachFrame in the SDK code, we must rely on flags being set. The flags are not set automatically (don't ask me why), this is done by the calling code, so let's look at how it's done (this is adapted from MeleeCombatTask.cpp and gets called nearly each frame):

 // Can we damage the enemy already?
 if (owner->GetMemory().canHitEnemy)
 {
     // Read the Anim WaitState from the AI (= "owner")
     idStr waitState(owner->WaitState());
     
     if (waitState != "melee_attack")
     {
         // Waitstate is not matching, this means that the animation can be started.
         owner->SetAnimState(ANIMCHANNEL_TORSO, "Torso_QuickMelee", 5);
 
         // greebo: Set the waitstate, this gets cleared by 
         // the script function when the animation is done.
         owner->SetWaitState("melee_attack");
     }
 }

Basically this is checking each frame whether the enemy can be hit and if we're not playing the animation already, we're starting it. Note that after calling owner->SetAnimState(), this code also sets the actor's waitstate by invoking owner->SetWaitState("melee_attack") manually.

This code relies on the waitstate being cleared by the Animation State's script method Torso_QuickMelee, which is exactly what the previously mentioned finishAction() script call does. So, after the script interpreter in Torso_QuickMelee is exiting the loop, the flag (set by the SDK) is cleared again and this allows the above code to restart the animation.

Things to remember:

  • Animations are ideally played by setting the Animation State (this triggers the scriptEvent with the same name).
  • If you intend to do things after the animation is done playing, set the WaitState.
  • Be sure to let the WaitState be cleared by the script after the animation is done.