SetAnimation
Play-scoped version:
native void SetAnimation(Name animName, double framerate = -1, int startFrame = -1, int loopFrame= -1, int endFrame = -1, int interpolateTics = -1, int flags = 0)
Play-scoped version compatible with weapons:
action void A_SetAnimation(Name animName, double framerate = -1, int startFrame = -1, int loopFrame= -1, int endFrame = -1, int interpolateTics = -1, int flags = 0)
UI-scoped version:
native ui void SetAnimationUI(Name animName, double framerate = -1, int startFrame = -1, int loopFrame= -1, int endFrame = -1, int interpolateTics = -1, int flags = 0)
Usage
If the calling actor has a 3D model attached, and said model has named animation sequences in it, this function will play the specified animation sequence on the model. Note, currently IQM is the only model format that supports named animation sequences.
Note: When setting animations on weapons (i.e. from states handled by the PSprite class), use A_SetAnimation rather than SetAnimation. Alternatively, using invoker.SetAnimation
will work the same way.
This allows for a simple way to play a specific model animation without relying on the tedious process of binding separate key frames to sprite names in MODELDEF. For this to work, the model must include multiple named animation sequences. For example, in Blender they can be created through the use of Nonlinear Animation.
Note: There are several prerequisites in order to be able to use this feature: 1. The actor must have the DECOUPLEDANIMATIONS flag. 2. The model must include multiple named animation sequences. 3. The MODELDEF definition for the model must have the BaseFrame keyword. |
Scopes
SetAnimation can be called on an actor pointer from play scope, while SetAnimationUI can be called from UI scope. This means that the latter cannot be called from an actor state (since those are play-scoped). The other difference is that SetAnimationUI allows for truly instant animation change, rather than on the next tic.
Parameters
- Name animName
- The name of the animation sequence to play.
- Note: in Blender this is the name of the action (can be seen in the Dope Sheet), not the name of the Nonlinear Animation strip.
- double framerate
- The framerate at which the animation sequence should play. If not specified, the sequence's own framerate will be used.
- int startFrame
- The frame of the animation sequence to start at. By default starts at the beginning.
- int loopFrame
- If specified and the SAF_LOOP flag is used, after the animation has played in full once, it'll keep looping from this frame instead of startFrame.
- int endFrame
- If specified, the model's animation will only play until this frame. Otherwise, will play until the end of the animation.
- Note, this argument wasn't initially present when this function was originally added to the engine, but it is present in GZDoom 4.12.
- int interpolateTics
- The number of tics to interpolate between the model's current animation sequence and the new sequence set with animName. A negative value is interpeted as 1 tic.
- int flags
- Multiple flags can be combined with
|
. The following flags are available:- SAF_INSTANT - By default, when setting a new animation, the model will interpolate from its current animation to the new one for the duration set by interpolateTics. This flag disables it.
- SAF_LOOP - If set, the animation will be looped until a new animation is set. If not set, the sequence will be played from start to end and stop at the final frame.
- SAF_NOOVERRIDE - If set, the animation will not start if the same animation is already playing. Mostly intended to be used alongside SAF_LOOP when the function is being repeatedly called with the same animName, so it doesn't get continuously restarted.
Examples
Examples below reference a sprite called M000A
. Note, this is NOT something included with GZDoom; instead this is assumed to be a dummy sprite made via TEXTURES, such as Sprite M000A0, 1, 1 {}
(empty 1x1 image, no patch).
An actor with a single looped animation
This actor will start the model's 'idlebreathing' animation action as soon as it spawns. Note, since this actor is meant to be a decoration and it uses no state logic, its states don't need any duration.
Model PulsingThing
{
Path "models/props"
Model 0 "pulsingthing.iqm"
Skin 0 "pulsingthing.png"
BaseFrame // Decoupled animations REQUIRE this
}
class PulsingThing : Actor
{
Default
{
+DECOUPLEDANIMATIONS
}
States
{
Spawn:
M000 A -1 NoDelay SetAnimation('idlebreathing', flags:SAF_LOOP);
stop;
}
}
A monster
However, for any regular actor that needs state logic (such as a monster), normal state logic rules will still apply. For example, if a Zombieman was created using decoupled animations, its state code could look like this:
class Zombieman3D : Zombieman
{
Default
{
+DECOUPLEDANIMATIONS
}
States
{
// This will call A_Look as usual, once per 10 tics,
// and the idle animation is looped. SAF_NOOVERRIDE
// makes sure the idle animation isn't constantly
// restarted:
Spawn:
M000 A 0 A_Look();
M000 A 10 SetAnimation('idle', flags: SAF_LOOP|SAF_NOOVERRIDE);
loop;
// Same logic with walking animation:
See:
M000 A 0 A_Chase();
M000 A 4 SetAnimation('walk', flags: SAF_LOOP|SAF_NOOVERRIDE);
loop;
// As usual, zombieman takes 10 tics to aim,
// then fires and returns to See 16 tics later.
// It is implied that you've crafted your animations
// in a way that they will match these durations.
// Splitting aiming and firing makes it easier:
Missile:
M000 A 10
{
SetAnimation('aim');
A_FaceTarget();
}
M000 A 16
{
SetAnimation('fire');
A_PosAttack();
}
goto See;
// The pain animation is started right away,
// but the sound is delayed by 3 tics, since
// this is how the default Zombieman does it:
Pain:
M000 A 3 SetAnimation('pain');
M000 A 3 A_Pain;
goto See;
Death:
M000 A 5 SetAnimation('death');
M000 A 5 A_Scream;
// The remaining states can be combined
// into one, since nothing else needs
// to happen here logic-wise:
M000 A -1 A_NoBlocking;
stop;
XDeath:
M000 A 5 SetAnimation('gibbed');
M000 A 5 A_XScream;
M000 A -1 A_NoBlocking;
stop;
Raise:
M000 A 20 SetAnimation('resurrect');
Goto See;
}
}
Important note: All functions that change states (such as A_Look and A_Chase in the examples above) must be placed before a SetAnimation call, specifically, on a different state. The reason is, state changes can be performed instantly, and when a monster wakes up, it can easily call A_Look, then A_Chase, and then enter its Missile/Melee state on the same tic. If the SetAnimation call also happens on the same tic, the actor will try to start several animations on the same tic, which can cause issues such as the next animation not playing and the actor being stuck in its previous animation or not moving.
A weapon
This is a basic example of how a weapon could be animated with decoupled animations and A_SetAnimation:
class CustomPistol : Weapon
{
Default
{
+DECOUPLEDANIMATIONS
Weapon.AmmoType1 'PistolMagazine';
Weapon.AmmoUse1 1;
Weapon.AmmoType2 'Clip';
Weapon.AmmoGive2 15;
Weapon.SlotNumber 2;
}
// Returns true if magazine is not full and player
// has some reserve ammo:
bool IsReloadable()
{
return invoker.ammo1.amount < invoker.ammo1.maxamount && invoker.ammo2.amount > 0;
}
// Wrapper for A_WeaponReady that allows using the
// Reload button by default if the weapon is currently
// reloadable:
action void A_MagWeaponReady(int flags = WRF_ALLOWRELOAD)
{
if (!invoker.IsReloadable())
{
flags &= ~WRF_ALLOWRELOAD;
}
A_WeaponReady(flags);
}
action void A_ReloadMagazine()
{
// This assumes a common system where ammotype2 is the reserve ammo,
// while ammotype1 is the magazine ammo. This instantly moves reserve
// ammo into magazine as long as it's possible:
while (invoker.ammo1.amount < invoker.ammo1.maxamount && invoker.ammo2.amount > 0)
{
invoker.ammo2.amount--;
invoker.ammo1.amount++;
}
A_StartSound("pistol/reload/insertmag");
}
// If the magazine is empty but the weapon can be
// reloaded, make attack functions go to the
// Reload state sequence:
override State GetAtkState (bool hold)
{
if (invoker.IsReloadable() && !CheckAmmo(PrimaryFire, false, true))
{
if (owner && owner.player)
{
owner.player.refire = 0;
}
return FindState('Reload');
}
// Otherwise returns the usual attack states:
return Super.GetAtkState(hold);
}
States {
Select:
M000 A 8
{
// This assumes that the raising motion is baked into
// the animation, so the PSprite itself should be
// instantly moved up instead of slowing raising
// from below the screen:
A_WeaponOffset(0, WEAPONTOP);
A_SetAnimation("pistol_select", flags:SAF_INSTANT);
// Allow switching during the animation:
A_WeaponReady(WRF_NOFIRE|WRF_NOBOB);
}
goto Ready;
Deselect:
M000 A 6 A_SetAnimation("pistol_deselect", flags:SAF_INSTANT);
M000 A 0 A_Lower; //loop this to deselect intantly once the animation has played out
wait;
Ready:
M000 A 1
{
A_MagWeaponReady();
// Assumes two different idle animations for empty and non-empty gun:
if (invoker.ammo1.amount > 0)
{
A_SetAnimation("pistol_idle", flags:SAF_NOOVERRIDE);
}
else
{
A_SetAnimation("pistol_idle_empty", flags:SAF_NOOVERRIDE);
}
}
wait;
Fire:
M000 A 7
{
if (invoker.ammo1.amount <= 1)
{
A_SetAnimation("pistol_fire_lastshot");
}
else
{
A_SetAnimation("pistol_fire");
}
A_FireBullets(3, 2, 1, 15);
A_StartSound("pistol/fire", CHAN_WEAPON, CHANF_OVERLAP);
}
M000 A 5 A_ReFire;
goto Ready;
Reload:
M000 A 20
{
if (invoker.ammo1.amount <= 0)
{
A_SetAnimation("pistol_reload_empty");
A_StartSound("pistol/reload_empty");
}
else
{
A_SetAnimation("pistol_reload");
A_StartSound("pistol/reload");
}
}
M000 A 10 A_ReloadMagazine();
goto Ready;
}
}