Loose tags that reach clients
The server adds State.Downed. Server-side checks work. Clients never see it, so their UI, animation and ability checks disagree with the server. Loose tags are local by design. Here is what to use instead.
Why they vanish
AddLooseGameplayTag changes the tag count on the machine that calls it, and nothing else. That is intentional. It lets each machine add tags it can compute itself, such as a client-only "aiming" state, without network traffic.
Call it on the server and expect clients to see it, and they will not. Every check on a client that reads the tag (an ability's blocked tags, an animation blueprint, a widget) now disagrees with the server.
Option 1: a gameplay effect
For gameplay state, the cleanest answer is an infinite-duration gameplay effect that grants the tag:
- It replicates through the normal effect path.
- It can be predicted by the owning client.
- It has a handle, so you remove exactly what you added.
- It shows up in
showdebug abilitysystemwith a name.
Create GE_State_Downed with Duration Policy: Infinite and add State.Downed to its granted tags. Apply it on the server, keep the handle, and remove it with RemoveActiveGameplayEffect when the player is revived.
Use this when the tag is game state: downed, stunned, carrying the flag.
Option 2: replicated loose tags
For a tag that is not tied to an effect, the ASC has a replicated loose tag container. The free GAS Helpers wrap it:
bool URSAbilitySystemLibrary::AddReplicatedLooseTag(AActor* Actor, FGameplayTag Tag)
{
UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor);
if (!ASC || !Tag.IsValid() || !ASC->IsOwnerActorAuthoritative())
{
return false;
}
#if UE_VERSION_NEWER_THAN(5, 6, 99)
ASC->AddLooseGameplayTag(Tag, 1, EGameplayTagReplicationState::TagOnly);
#else
ASC->AddLooseGameplayTag(Tag);
ASC->AddReplicatedLooseGameplayTag(Tag);
#endif
return true;
}It does nothing without authority, which catches the most common misuse: calling it from client code and expecting the server to see it.
Up to 5.6, two calls are needed: the plain loose tag updates the server's own tag count at once, and the replicated container carries the tag to clients, where the ASC applies it when the container replicates. RemoveReplicatedLooseTag does the reverse; always pair them.
UE 5.7 changed this
5.7 removed AddReplicatedLooseGameplayTag and RemoveReplicatedLooseGameplayTag. The loose tag functions now take an EGameplayTagReplicationState: TagOnly replicates the tag to everyone, CountToOwner adds the count for the owner, TagAndCountToAll sends both to everyone, and the default None stays local. Code that calls the old functions stops compiling on 5.7, which is why the helper checks the engine version.
Two catches in 5.7 and 5.8
Clients see presence, not count. With TagOnly, a client's count for the tag is capped at 1, however many times the server added it. Use TagAndCountToAll if clients need the number.
Single-tag removal can skip count events. RemoveLooseGameplayTag(Tag) only runs the tag's change callbacks when the count reaches 0. Going from 2 to 1 fires no AnyCountChange, so a stack counter in the UI stays at 2. The container overload, RemoveLooseGameplayTags(FGameplayTagContainer(Tag)), fires them every time. The helper uses it, and an automation test in the helper's test suite checks the engine behaviour on each version.
Use this for flags the server sets from outside GAS: match phase, team role, "has the objective".
Option 3: add it everywhere
When every machine can work the tag out on its own, from replicated data it already has, add it locally on each machine with plain AddLooseGameplayTag in that data's OnRep. No extra replication.
Example: a replicated bIsCarryingFlag bool already exists. In OnRep_IsCarryingFlag, add or remove State.CarryingFlag locally, and do the same on the server where you set the bool.
Which one to pick
| Situation | Use |
|---|---|
| Gameplay state with a start and an end, may need prediction | Gameplay effect that grants the tag |
| Server-decided flag from outside GAS | Replicated loose tag |
| Derived from data that already replicates | Local loose tag in that data's OnRep |
| Client-only state (aim, UI focus) | Plain local loose tag, on purpose |
Check both sides
Add the tag, then call DumpAbilitySystemState from the helpers on the server and on a client. The Tags line should match. Remove it and check again: a count left at 1 on one side means an add and a remove did not pair up.
Get the code
AddReplicatedLooseTag, RemoveReplicatedLooseTag and DumpAbilitySystemState are in GAS Helpers, free under the MIT license. Version 0.2.0 adds Wait Gameplay Tag Count, an async node that fires on every count change, for stack and status UI.
Tags that disagree between server and client are one of the most common causes of "works for the host only". If you have more than a few, the Diagnostic maps every tag source in your project.