>and a little prediction #### Task Brief I have the shooting ability of a water pistol. It uses water (obviously). But you can’t shoot water indefinitely. That’s why you have some amount of water in a tank. And after the water is out, you want to replenish it. So, we need a reload ability for our weapon. 💪 #### Tech Brief I use the Gameplay Ability System in Unreal Engine for the game. It’s pretty complicated, but the framework Attributes-Effects-Abilities fits my game well. But beware — GAS is not a silver bullet! To store water amount and tank capacity, I use the attributes Ammo and MaxAmmo. Using such generic names is fine at this point *of* development. If more weapons are added in the future, this solution won’t fit, but I’d like not to complicate it prematurely. > [!NOTE] > If you develop a *true* shooter game, I urge you to **_not use attributes_** for the Ammo since each weapon might have a different current Ammo in the magazine. One more note before we jump into the details: my game supports PvP mode, so I need to make sure the following solution works in a client-server environment. --- ## Development I come up with the following steps I need to implement the reload ability: 1. Ability Trigger: via input and auto-reload 2. Other systems notification. i.e., HUD 3. Reloading process itself Let’s go step by step. ### Ability Trigger via Input Unreal Engine has a nice action-based input system called EnhancedInput. However, GAS doesn’t support it natively. When we give abilities, we can only provide an integer InputID with which to bind. And it’s the responsibility of the game code to bind the InputID to the actual input action. The abilities themselves support input pressed and released. They are used to trigger abilities and for AbilityTasks like WaitInputPressed/Released. This is needed to achieve classical behaviors such as hold-to-aim and release-to-shoot. > [!warning] > **But be cautious: it doesn’t mean the actual buttons are pressed or released!** In the game code, you can map the input action trigger state to the inputID pressed state in GAS. However, this means the GAS “pressed” state can be _released_ state of the button. I extended the Ability System Component to give an ability alongside input action so that InputAction is bound to InputID under the hood. The ability system listens to the enhanced input component of the owning player and triggers corresponding pressed/released methods of the GAS. ![[input_ability.png|ability setup]] ```cpp for (int InputID = 0; InputID < AbilityInputs->AbilityInputActions.Num(); ++InputID) { if (!AbilityInputs->AbilityInputActions[InputID]) { UE_LOG(LogBaby, Warning, TEXT("Ability Input Data Asset: Input Action %d is Invalid!"), InputID); continue; } InputComponent->BindAction(AbilityInputs->AbilityInputActions[InputID], ETriggerEvent::Triggered, this, &ThisClass::PressInputID, InputID); InputComponent->BindAction(AbilityInputs->AbilityInputActions[InputID], ETriggerEvent::Completed, this, &ThisClass::ReleaseInputID, InputID); } ``` ### Ability Initial Setup GAS is very powerful due to its support for client-server architecture. The main network settings for the abilities can be found in the *Advanced* section of ability properties. ![[advanced.png]] I use the default local predicted option of Net Execution Policy. It means that when I activate the ability locally, its instance is created on the local client, and the request is sent to the server. The server validates whether the ability can be started and confirms or denies this fact to the owning client. In case of confirmation, the server instance of the ability is created and activated. This flow satisfies our requirements because we want to start reloading immediately (a.k.a predicted) on the local client for the best game feeling. Even though the prediction of the ability is achieved easily, you need to be careful since nothing is easy with client-server cooperation. ### Notifying other systems There are a few ways to notify other systems: - Direct Function Call We bind hard our ability to other systems, and if the system interface is changed, we doom ourselves to change all the calls among many abilities. Also, you need to maintain the dependencies. You can’t depend on a higher level, but you can depend on lower/same-level systems. i.e., for HUD, it’s okay to be dependent on the actual gameplay events/messages, but the ability to be dependent on the HUD is wrong. It’s weird if you remove a widget, and it breaks the gameplay. - Gameplay Messages For this solution, we need to add the GameplayMessageSubsystem plugin beforehand. This system provides the most decoupled relations between different parts of gameplay. On the other hand, everyone can listen to messages. It’s especially tricky for the multiplayer game due to many instances of player controllers, player states, characters, etc. So, to determine which object needs to handle the message, you must add an identifier. - Gameplay events This is the feature of GAS. It allows the sending (and waiting) of Game Events for specific AbilitySystemComponent holders like player state or controller. Any actor can wait for the events sent to the specific owner. You are more restricted on the payload than messages, but usually, there is no need to send tons of data anyway. I need now only notify widgets about reload starting. That’s why I prefer to use a gameplay event for this case. ![[send_event.png]] ### Reloading process itself The reload itself consists of two stages: 1. Wait for reload time 2. Update Ammo value #### Wait for reload time This part is straightforward. Just use a task wait delay. Beware of using a delay node instead of a task wait delay. In most cases, you won’t find any differences, but using the task allows you to cancel the waiting process and auto-destroy it when the ability ends. Everything changes during the game development, and in some scenarios, the ability can be finished before the delay node fires, which can lead to various *pleasureful* bugs. #### Update Ammo value The easiest way to update the Ammo attribute is to apply a gameplay effect after the waiting task finishes. The tricky part is setting up the effect to respect the MaxAmmo value. Of course, we can add some big numbers to the Ammo and handle overflow in C++, but a more precise way is to override the Ammo attribute with the MaxAmmo attribute value. ![[effect.png]] ### Test Okay, everything is set, let’s launch the game in a client mode. ![[shoot_broken.gif]] During the play, reloading doesn’t affect the ammo number. Let’s launch the game in a standalone mode. ![[shoot_broken_standalone.gif]] It works. That means the problem is with client-server interaction. ### Bug Fixing Look at the ability blueprint more carefully. ![[bp_reload.png]] We wait for the reload time, apply effect, and end ability. Everything is robust and straightforward. I wrote before that locally predicted abilities are created locally first, and the server instance is created when the request is received there. It results in the server instance always activating with a delay. This means that the wait task finishes faster on the client, which results in a faster call to the EndAbility node. Let’s take a look at the EndAbility node implementation: ```cpp void UGameplayAbility::K2_EndAbility() { check(CurrentActorInfo); bool bReplicateEndAbility = true; //<--- look at me bool bWasCancelled = false; EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled); } ``` There is a very suspicious variable for replicating the ability end. And since we end ability on the client faster, the client replicates (sends RPC, to be precise) it to the server to its instance BEFORE it updates the attributes. No update of attribute on the server side, no ammo for the client. 🤷‍♂️ There are three possible fixes for this problem: - Turn off the Server Respects Remote Ability Cancellation checkbox. This way, when the client ends the ability, it won’t interfere with the server instance ability and will allow it to end gracefully. - Change Net Security Policy to Server Only Termination. In practice, it works almost the same way. The only difference is that the client ability instance ends **after** the server one. - Disable the End Ability node on the client. A straightforward solution is to add a HasAuthority node. Every solution has pros and cons, but I prefer to check the Server Respects Remote Ability Cancellation checkbox. We don’t have any complicated ability state anyway, so it doesn’t complicate the solution. ![[ability_respect_authority.png]] ### Auto-reload Now everything works as expected. Let’s improve our ability to be triggered when we are out of Ammo. The easiest way is to send a game event when the attribute is set to zero. It can be done in the attribute set in the *PostAttributeChange* method. > [!warning] > If you decide to trigger the event in the PostAttribute**Base**Change method, it fires only on the server. In this case, the ability doesn’t activate on the client. ```cpp void UAmmoAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) { Super::PostAttributeChange(Attribute, OldValue, NewValue); if (Attribute == GetAmmoAttribute()) { if (GetAmmo() <= 0.f) { UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(GetOwningActor(), TAG_GAMEEVENT_OUTOFAMMO, FGameplayEventData()); // <-- event send } } } ``` The only thing left is to add the event tag to the ability triggers in its properties. ![[game_event.png]] ## Final Result ![[final_reload.gif]] You can find the blueprint [here](https://blueprintue.com/blueprint/p5-9l-hy/). Even though the ability is simple and doesn’t require anything complicated, client-server communication can make even the simplest code tricky. Good luck! #GAS #unrealEngine #cpp #gameplayabilitysystem #ability