Quick answer: Apply changes via UGameplayEffect + ModifierMagnitude. Override PostGameplayEffectExecute for clamping. Don’t call SetAttribute directly.
You call AbilitySystem->SetNumericAttributeBase(HealthAttribute, NewHp). Server logs change. Clients don’t see it. Direct setters skip the GE pipeline that replicates.
The Symptom
Server-side attribute changed but the OnAttributeChanged delegate doesn’t fire on clients. UI stays stale.
The Fix
// Damage GE
UCLASS()
class UGE_Damage : public UGameplayEffect {
GENERATED_BODY()
public:
UGE_Damage() {
DurationPolicy = EGameplayEffectDurationType::Instant;
FGameplayModifierInfo Mod;
Mod.Attribute = UMyAttributeSet::GetHealthAttribute();
Mod.ModifierOp = EGameplayModOp::Additive;
Mod.ModifierMagnitude = FScalableFloat(-10.f);
Modifiers.Add(Mod);
}
};
// Apply
auto Spec = ASC->MakeOutgoingSpec(UGE_Damage::StaticClass(), 1.f, ASC->MakeEffectContext());
ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data);
// In UMyAttributeSet
void UMyAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) {
Super::PostGameplayEffectExecute(Data);
Health.SetCurrentValue(FMath::Clamp(Health.GetCurrentValue(), 0.f, GetMaxHealth()));
}
Spec-based application replicates through the GAS network layer. PostGameplayEffectExecute clamps after every change.
Verifying
Server damages. Clients receive replicated attribute, OnAttributeChange fires, UI updates. Without the GE: silence on clients.
“GE for changes. PostExecute for clamp. Replication just works.”
Related Issues
For replication condition, see replication scope. For Niagara user param, see user param.
GE applies. Replication free. Clamp on execute.