A montage can look fine while you test alone, then in coop your teammate sees an enemy hitting you without moving. The animation played on your machine, but it did not reach theirs.
For abilities in the Gameplay Ability System ( GAS ), I use PlayMontageAndWait. Here we will look at what it does, what the Ability System Component ( ASC ) replicates, and why calling Montage_Play directly from an ability misses that replication.
How GAS Ability Tasks Wait for a Montage
ActivateAbility is a normal function call, it runs and returns in the same call stack, so it can not stop in the middle to wait for an animation to finish. Every time an ability “waits” for something, what happens is that it spawns an ability task: a small object that outlives the function call and calls back into the ability through delegates when the thing it was waiting for happens.
I went over the most useful ones in From Wait Delays to Play Montage: 10 useful GAS Ability Tasks. Here we will use UAbilityTask_PlayMontageAndWait, the usual task for melee, cast and channel abilities.
CreatePlayMontageAndWaitProxy and ReadyForActivation
A typical melee ability starts like this:
void UGA_MeleeAttack::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
UAbilityTask_PlayMontageAndWait* MontageTask =
UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
this, TEXT("Attack"), AttackMontage);
MontageTask->OnCompleted.AddDynamic(this, &UGA_MeleeAttack::OnMontageFinished);
MontageTask->OnInterrupted.AddDynamic(this, &UGA_MeleeAttack::OnMontageInterrupted);
MontageTask->OnCancelled.AddDynamic(this, &UGA_MeleeAttack::OnMontageInterrupted);
MontageTask->ReadyForActivation();
}
If you work in Blueprint, the Play Montage and Wait node is this same task with the delegates as output pins. In C++ you have to call ReadyForActivation yourself, and if you forget it the task simply does not start.
The factory also accepts these parameters:
- Rate scales the playback speed.
- StartSection starts the montage at a named section, and StartTimeSeconds starts it at an offset into the timeline. The offset one is quite useful to skip the wind up when a buffered combo continues into the next attack, for instance.
- AnimRootMotionTranslationScale scales the root motion baked in the clip, so one ability can shorten a dash without touching the animation.
- bStopWhenAbilityEnds decides if a normal EndAbility stops the montage. This flag only covers the normal end, an explicit ability cancel stops the montage anyway.
You can see all of them in the engine source, in Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/Abilities/Tasks/AbilityTask_PlayMontageAndWait.h.
OnBlendOut, OnCompleted and OnInterrupted
Seems that OnBlendOut and OnCompleted are two versions of “the montage is over”, but they are bound to different montage notifications and fire at different moments.
OnBlendOut fires the instant the montage starts blending out. OnCompleted fires once the montage has fully finished, blend included, and only if nothing interrupted it. A montage that plays out normally fires both, one after the other. If another montage plays over this one before it finishes, OnInterrupted fires and OnCompleted does not.
I use OnCompleted for ending the ability and cleanup, and OnBlendOut for anything that should overlap the tail of the montage. Remember to handle OnInterrupted as well, or an interrupted swing will skip the cleanup.
Calling EndAbility while the montage is playing can stop it too, if bStopWhenAbilityEnds is true. The task sees this as an interruption even though no other montage replaced it. The cleanup needs to handle this exit as well.
How the ASC Replicates Montages
PlayMontageAndWait calls PlayMontage on the owning ASC. The ASC stores which montage is playing for this actor’s abilities and replicates that state. The anim instance plays what it is told, but does not manage this replication itself.
That state is a small replicated struct, FGameplayAbilityRepAnimMontage, you can read it in GameplayAbilityRepAnimMontage.h. It has the montage asset, the play rate, the blend time, the next section and some flags. When a montage starts on the authority the struct is replicated down, and on each client OnRep_ReplicatedAnimMontage reads it and starts the same montage locally. That is how a simulated proxy, the enemy or the remote player you are just watching, plays the same attack the server is playing.
The position is not replicated every frame, as clients can play the montage on their own once they are told to start it. It only travels when a correction is needed, and the client nudges its local playback toward the corrected value instead of snapping to it.
At the same time, there is a field whose only job is replaying the same montage. If you play the same montage twice in a row, ex: a rapid double tap of the same attack, the second play would write the same values, the struct would be identical and the replication callback would not fire, so simulated proxies would miss the second swing. To avoid this the struct has a small play counter that gets bumped on every start. PlayMontage on the ASC handles this for you.
Calling Montage_Play directly on the anim instance skips writing this struct. The montage plays locally, but simulated proxies do not see it. You need two players to catch this while testing.
Replicated Montage Not Playing on a Client
One bug you can get in coop, even with the montage replicating correctly, is a client that does not see an attack that everybody else saw.
The struct arrives at the client with the right data, but before that client has an anim instance for that pawn. Ex: the pawn just became network relevant for that player and its mesh is not fully set up yet.
OnRep_ReplicatedAnimMontage checks that the avatar is ready before playing. You can override the virtual IsReadyForReplicatedMontage with your own conditions. If it is not ready, playback is deferred until the anim instance exists.
If the avatar takes half a second to be ready, the attack plays half a second late. For a fast enemy attack, this can look like the animation did not play at all.
So when one client misses an animation that everybody else saw, the ability is most probably not the problem. Check when that client creates the skeletal mesh and the anim instance for that character. And if you have overridden the readiness check yourself, confirm it eventually returns true, if not every montage for that actor gets deferred forever.
Change Montage Sections Through the ASC
Mid montage control follows the same rule as playback. If your combo jumps to a named montage section when the player buffers the next attack, do not call Montage_JumpToSection on the anim instance. The ASC has its own entry points, CurrentMontageJumpToSection and CurrentMontageSetNextSectionName, and they mirror the section change to the other machines through a server RPC, so every simulated proxy shows the same section at the same time.
The buffered inputs from the input buffering post advance the attack montage section by section, and if those jumps went through the anim instance, the owning client and everybody watching would drift apart after the first combo.
Recap
Use the ASC for montage playback and section changes: PlayMontage, CurrentMontageJumpToSection and CurrentMontageSetNextSectionName. Calling Montage_Play on the anim instance only plays it locally.
OnBlendOut marks the start of the blend; OnCompleted marks a full uninterrupted finish. Handle interruptions and early ability endings when doing cleanup. If only one client misses an animation, check when it creates the mesh and anim instance, as the ASC may be deferring playback until they exist.


