Root motion source tasks are, in my opinion, one of the most underused parts of the Gameplay Ability System ( GAS ). In my Quod Combat Framework there is not a single call to LaunchCharacter, every dash and every knockback is one of these tasks, and in this post I want to explain why.
So what problem do they solve? A dash needs the capsule to travel a certain distance in a certain time. If that motion is root motion in the animation, the distance is fixed by the clip and retuning it means going back to the animator. A knockback is worse, as the direction and the strength are not known until the hit lands, so there is nothing to bake in a clip. The UAbilityTask_ApplyRootMotion family injects the motion into the Character Movement Component ( CMC ) directly from code, with numbers that you compute when the ability activates.
The five ApplyRootMotion tasks
There are five of them, all in the Abilities/Tasks folder of the GameplayAbilities plugin:
| Task | Motion it produces |
|---|---|
UAbilityTask_ApplyRootMotionConstantForce | a straight push along a direction for a duration. The dash and knockback workhorse |
UAbilityTask_ApplyRootMotionJumpForce | a parameterized arc with a landing callback, for leaps and jump attacks |
UAbilityTask_ApplyRootMotionMoveToForce | move to a fixed world location over a fixed duration |
UAbilityTask_ApplyRootMotionMoveToActorForce | like move to, but it keeps re-aiming at a moving target, for grabs and lock-on lunges |
UAbilityTask_ApplyRootMotionRadialForce | push away from ( or pull toward ) a point, with optional falloff |
They are useful for more than dashes. Ex: a ladder climb with MoveToForce, flying mode and one move-to per rung chained with the finish delegate, no physics and no root motion clips.
What the task actually does
Even though the name says “root motion”, these tasks do not read any animation. The Activate of the task builds an FRootMotionSource struct, declared in GameFramework/RootMotionSource.h, gives it to the CMC through ApplyRootMotionSource and stores the ID that the CMC returns. While the source is in the CMC, the CMC adds it to its own velocity every move, and the task only tracks the duration and fires its finish delegate at the end.
To put things simple, the task leaves a note in the CMC saying “move this capsule at this velocity for this long”, and the CMC does the moving.
The bIsAdditive parameter decides how the source mixes with normal movement. With it false the source overrides movement for its duration, input, friction and braking stop mattering, which is what you want for a dash. With it true the force is added on top of normal movement, which fits a shove while you keep running.
A minimal dash ability
void UGA_Dash::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;
}
const FVector DashDirection =
GetAvatarActorFromActorInfo()->GetActorForwardVector();
UAbilityTask_ApplyRootMotionConstantForce* DashTask =
UAbilityTask_ApplyRootMotionConstantForce::ApplyRootMotionConstantForce(
this, TEXT("Dash"),
DashDirection,
DashDistance / DashDuration, // Strength is a speed in cm/s
DashDuration,
/*bIsAdditive=*/ false,
/*StrengthOverTime=*/ nullptr,
ERootMotionFinishVelocityMode::ClampVelocity,
/*SetVelocityOnFinish=*/ FVector::ZeroVector,
/*ClampVelocityOnFinish=*/600.f,
/*bEnableGravity=*/ false);
DashTask->OnFinish.AddDynamic(this, &UGA_Dash::OnDashFinished);
DashTask->ReadyForActivation();
}
void UGA_Dash::OnDashFinished()
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo,
true, false);
}
Strength is a speed in cm per second, so the distance is simply strength multiplied by duration. If you pass DashDistance / DashDuration you get a dash that travels the distance you asked for, and both values are plain properties on the ability that a designer can tune without touching any animation.
On top of this you play a cosmetic dash montage with PlayMontageAndWait, a clip with no root motion at all. The source moves the capsule and the animation only has to look right, so the same numbers work with any character.
What you should not do is combine an animation with real root motion and an active root motion source on the same character. That is two systems trying to move one capsule, and which one wins is arbitrary. Keep one movement driver per ability. If what you want is an attack that adjusts to the target, that is a different tool, motion warping.
Knockbacks
The knockbacks in my framework use this same constant force task, with one difference that I recommend to anybody doing hit reactions: designers do not author a force, they author a distance in meters.
The task receives that distance plus a normalized strength-over-time curve that shapes how the push slows down. When it activates it integrates the curve once to get its average value and solves the strength so the push covers the requested distance, Strength = ( Distance / Duration ) / AverageCurveValue. This way “this attack pushes 2.5 meters” is true whatever the shape of the curve, and combat designers tune knockback in the same units they use to think about spacing.
The hurt reaction montage has its root motion disabled while the push is active, the montage gives the pose and the force gives the translation. We also delay the start of the force a little bit ( around 0.09 seconds ) so the impact frames of the reaction are visible before the body starts moving. Small thing, but the hits feel much better with it.
The finish velocity
What happens when the duration runs out? If you are not careful the character covers the right distance and then keeps sliding for a few more meters while braking friction stops it. The cause is ERootMotionFinishVelocityMode. With MaintainLastRootMotionVelocity the final velocity of the source is handed to normal movement, so a 1500 cm/s dash turns into a 1500 cm/s skid.
SetVelocity lets you choose the exit velocity and ClampVelocity caps it at the value you pass. My recommendation is SetVelocity with a zero vector for knockbacks and ClampVelocity at your run speed for dashes, so the dash blends back into locomotion.
Cancelling the ability
Remember that the source lives in the CMC, and the only thing that removes it is the OnDestroy of the task calling RemoveRootMotionSourceByID. The task is destroyed when it finishes or when the owning ability ends, so every cancel path of the ability has to reach EndAbility or CancelAbility.
If one path does not, the character gets stunned mid dash and keeps travelling anyway, with no animation explaining why. The stun cancelled the montage, but the source is still inside the CMC pushing the capsule for its full duration. And if you apply an FRootMotionSource to the CMC by hand, without a task owning it, removing it is on you.
Multiplayer
Root motion sources do not use GAS prediction at all. They are a character movement feature, so they replicate and predict the same way CMC movement does. The owning client applies the source locally and starts moving immediately, the source is recorded in its saved moves, the server runs the same ability and applies the same source, and if the two diverge the correction comes back through the normal CMC adjustment. Simulated proxies get the motion too, as the tasks replicate their parameters through the gameplay task machinery, the shared code is in AbilityTask_ApplyRootMotion_Base.h.
Also take into account that if the server rejects a locally predicted activation, the prediction key rollback undoes the tags and effects that GAS predicted, but it does not un-move the capsule. What snaps the character back is the CMC correction, and on screen that looks like a rubber band. So for movement abilities keep CanActivateAbility cheap and checking things that client and server agree on, and do not try to gate the source on prediction key callbacks, the task and the CMC already handle both sides.
Movement mode, ledges and walls
The CMC keeps running its movement mode while a source is active, and the mode has a say in the result:
- In walking mode the floor logic stays active, so a dash that leaves a ledge transitions to falling mid dash, and ground snapping fights any upward component of your force.
- bEnableGravity decides if gravity accumulates during the push. With it disabled a flat dash holds its line over a small drop.
- The move-to tasks expose bSetNewMovementMode, usually flying, so walking physics leaves the move alone, and they restore the previous mode when the task ends.
- None of these tasks navigate or resolve collision. A constant force pushes into a wall for its full duration and the capsule slides along it, so validate destinations and accept that a blocked dash ends where the wall says.
Finally, a warning. The correction code lives in FRootMotionSourceGroup in GameFramework/RootMotionSource.h and it has changed between engine versions before, so if you subclass FRootMotionSource or depend on precise correction timing, check the headers of your engine version.
Recap
- The source lives in the CMC and only the OnDestroy of the task removes it. Every cancel path of the ability has to end the ability, if not the character keeps moving with nothing on screen explaining why.
- MaintainLastRootMotionVelocity turns the dash speed into a skid. Use SetVelocity with zero for knockbacks and ClampVelocity at run speed for dashes.
- The movement predicts through CMC saved moves, not GAS prediction keys, so a denied activation rubber-bands instead of rolling back. Keep activation checks for movement abilities cheap and the same on client and server.


