This post is written for users of the General Movement Component (https://www.unrealengine.com/marketplace/en-US/product/general-movement-component) that are interested in how the Unreal Gameplay Ability System can be used to affect movement abilities.
GMC is an amazing client-prediction, replication, and server reconciliation plugin that is highly customizable and infinitely more extendable than what comes bundled with Unreal (lookin’ at you CMC…).
GAS is a very powerful framework for building abilities and comes with extremely useful functionality like effects (buffs/debuffs) and an intricate gameplay tag system. It was built with Multiplayer in mind, and allow for client-side predicting of ability usage.
These two very powerful tools sounds like they should work great together. They’re both highly focused on multiplayer, and both provide client-side prediction with server reconciliation. However, this isn’t necessarily a good thing. They both have their own prediction and reconciliation system, completely independent of each other.
If you’re familiar with client-side prediction concepts, then you’ll know that a major component of making it all work is having full control over stateful data. (If you’re NOT familiar, go read up on it, others can explain it all way better than me). Velocity, Position, Acceleration, etc., all need to be able to be corrected by the reconciliation system. Your client should be able to replay its moves on server-validated states and end up at the same result that the server did (the prediction part of client-side prediction!). If things get de-synced, issues pop up. Rapidly. How can these de-syncs happen? Well one common way is that a state managed by your movement system is altered by something that ISN’T your movement system. This is exactly why the GMC Docs mention that all movement code should be contained inside the GMC its self: so that it can properly manage predicting and reconciling the data.
Before diving into GAS and its pitfalls, let’s first look at a “healthy” example of GMC usage. All code here: https://github.com/reznok/ARLO/blob/a8b35d60cde88a3b27142c1993b1fbdb6fadd85a/Source/Arlo/ActorComponents/ALMovementComponent.cpp
First let’s look at the main movement logic code: https://github.com/reznok/ARLO/blob/a8b35d60cde88a3b27142c1993b1fbdb6fadd85a/Source/Arlo/ActorComponents/ALMovementComponent.cpp#L92C3-L92C60
“GenPredictionTick” is the main Update function called by GMC to handle all movement. Anything that is client-predicted and operates on predicted data, the most common thing being movement, MUST be contained in this function. If you read through the code (there isn’t much), you’ll see the function is handling client input for movement, applying gravity, and handling jump actions.
Client inputs are being handled by EnhancedInput in “ALHero.cpp”, and are being passed directly into the MovementComponent to let it actually process the inputs. Here’s a clip of this in action, simulated at ~110ms lag:
It could use some polish, but it’s doing what it needs to be doing network wise. So time to get GAS involved! I’m going to assume that whoever is reading this has some experience with GAS. It has quite a learning curve and getting it all configured and set up is way outside the scope of this. As long as you’re able to get GAS to a point where you can start making abilities, you should know everything you need to keep going here.
For my GAS setup, I used GasCompanion (https://www.unrealengine.com/marketplace/en-US/product/gas-companion), a $25 asset that makes GAS take about 5 minutes to get going instead of a few hours. It comes with a bunch of helper stuff, and I cannot recommend it enough if you’re using GAS, however everything I do here will work without it.
So hand waving all the GAS setup, here’s what we’ve got:
- An attribute set with Health, Stamina, and Energy
- An AbilitySystemComponent on our Hero
- Inputs configured to execute GAS abilities
So let’s make our first GAS Ability to move our pawn! Keeping it simple, let’s focus on reimplementing our Jump functionality as a GAS Ability.
First, the most basic example, just reimplementing the jump logic from the MovementComponent as a GAS ability:
Let’s see how that looks when we run it at 110ms latency…
Oh… that’s not good. That’s not good at all. It’s a desync’d stuttery mess! And that make sense, because we’re modifying the actor from outside the movement component’s update, which is an absolute requirement for the GMC. So now we know we can never actually have movement from the GAS Ability side, let’s try another approach.
This time we’ll get a reference to our MovementComponent and set the bWantsToJump flag directly. This is essentially how our EnhancedInput version works above. The MovementComponent then uses this bound bool to handle the jumping logic from inside the MovementComponent’s update.
*** A CanJump bool was added to the MovementComponent to do the ground checks / extra jump checks. It’s nothing special, and the code can be found here:
https://github.com/reznok/ARLO/blob/13ee97d8337a75b3c3c0cf1decf9ad413e3e10b8/Source/Arlo/ActorComponents/ALMovementComponent.cpp#L4
Now let’s see how that looks…
Hey that looks pretty good, problem solved! Well… sort of. This DOES work, but let’s go one step deeper into the GAS rabbit hole and try adding a cost for Jumping. Stamina is already setup as an attribute, so let’s add a cost GameplayEffect.
Then the only thing that needs to be added to the Ability is a “CommitCost” node. Here’s what the new Ability looks like:
And let’s see it in action:
[ Video Missing 🙁 ]
Everything looks to be working correctly! Except this article is how NOT to use GAS with GMC… so what’s the issue? Well yes, this does work, however it’s now susceptible to cheaters. If the ability is used as intended, stamina is used up, and we can’t jump after running out of stamina if we use the ability. However cheaters don’t care about doing things correctly. If someone can find a way to set the “bWantsToJump” boolean on the MovementComponent directly, WITHOUT using the GameplayAbility, then the character will still jump, completely bypassing the Stamina cost. This is because “bWantsToJump” is still an input with full client authority. As someone who has made cheats for Unreal games, believe me when I say that this is a very trivial thing to do.
As a quick example, let’s look at when the old EnhancedInput method used in the first example is used at the same time as the GAS ability. I’ve made a new InputAction and bound it to a different key, but it’s functionally the same:
A solution here is to check and apply the cost in the MovementComponent its self. Which can be done, but then that begs the question, what is the GAS Ability actually doing at that point? And this issue isn’t just limited to this jump problem, the MovementComponent has to actually be moving the character, but it now needs to be intertwined with all sorts of GAS stuff such as checking what effects are applied, what tags are present, cooldowns, etc. All of this needs to be double checked inside the MovementComponent at some point, meaning the benefits from GAS are basically… very little. You end up essentially having to rewrite chunks of GAS inside the GMC.
So what’s the solution? I’m genuinely not sure, but at this point I’m going to say that GAS and GMC are just not compatible when trying to move a character. It’s possible you could Frankenstein them together, but that would not be an easy task. Even this most basic example has issues, and it only gets more complicated from there. It’s two great systems that are fighting each other for control, and unfortunately there’s no happy middle ground.