1

In my Unreal Engine project, which uses GameplayAbilitySystem and the default server->client architecture, the client gets notified of an attribute value changes that happened on the server.

Additionally, I'm trying to get not only the new value, but also the amount the value changed (delta = new value - old value). This should be possible using the attribute value change delegate, since it contains FOnAttributeChangeData with its members NewValue and OldValue.

On the server, both values are correct. However, on the client, FOnAttributeChangeData::NewValue == FOnAttributeChangeData::OldValue and both have the value being identical to NewValue on the server.

This is because the delegate is called after the replication happened ...

UPROPERTY(ReplicatedUsing=OnRep_MyAttribute)
FGameplayAttributeData MyAttribute;

void UAttributeSetBase::OnRep_MyAttribute()
{
    GAMEPLAYATTRIBUTE_REPNOTIFY(UAttributeSetBase, MyAttribute);
}

(this is default GAS setup of ActionRPG)

... so the client has no knowledge about the value it had before replication.

  1. How do I get the value of the attribute, which it had before it got updated by the server?
  2. How do I forward this value to the delegate?
Roi Danton
  • 7,933
  • 6
  • 68
  • 80

1 Answers1

2

Getting the old value (question 1)

UnrealEngine OnRep functions provide the previous state of an replicated variable as first parameter in the OnRep function. So add the parameter

void UAttributeSetBase::OnRep_MyAttribute(const FGameplayAttributeData& Previous)
{
    const auto PreviousValue = Previous.GetCurrentValue(); // See below for possible usage.
    GAMEPLAYATTRIBUTE_REPNOTIFY(UAttributeSetBase, MyAttribute);
}

Thanks @Dan from Unreal GAS discord channel.

Forward the value to the delegate (question 2)

Idea

When your goal is to not modify the UE4 source code, one possibility is to cache the previous value within the attribute set, so you are able to access it from outside.

  1. Cache that value for each attribute in the attribute set OnRep function.
  2. Use the cached value in the delegate, but only if it is valid. Since the value is assigned within the OnRep function, it won't exist on the server. This is perfectly fine since we want to retain the behaviour on the server, which uses FOnAttributeChangeData::OldValue (which has the correct value only on the server).

Example implementation

Caching the previous value

AttributeSetBase.h:

// Wrapper for a TMap. If you need thread safety, use another container or allocator.
class CachePreviousDataFromReplication
{
    TMap<FName, FGameplayAttributeData> CachedPreviousData;
public:
    void Add(const FName, const FGameplayAttributeData&);
    auto Find(const FName) const -> const FGameplayAttributeData*;
};
class YOUR_API UAttributeSetBase : public UAttributeSet
{
    // ...
private:
    UFUNCTION() void OnRep_MyAttribute(const FGameplayAttributeData& Previous);
    // ...
private:
    CachePreviousDataFromReplication CachedDataFromReplication;
public:
    // \param[in]   AttributeName   Use GET_MEMBER_NAME_CHECKED() to retrieve the name.
    auto GetPreviousDataFromReplication(const FName AttributeName) const -> const FGameplayAttributeData*;
}

AttributeSetBase.cpp:

void CachePreviousDataFromReplication::Add(const FName AttributeName, const FGameplayAttributeData& AttributeData)
{
    this->CachedPreviousData.Add(AttributeName, AttributeData);
}

auto CachePreviousDataFromReplication::Find(const FName AttributeName) const -> const FGameplayAttributeData*
{
    return CachedPreviousData.Find(AttributeName);
}

void UAttributeSetBase::OnRep_MyAttribute(const FGameplayAttributeData& Previous)
{
    CachedDataFromReplication.Add(GET_MEMBER_NAME_CHECKED(UAttributeSetBase, MyAttribute), Previous); // Add this to every OnRep function.
    GAMEPLAYATTRIBUTE_REPNOTIFY(UAttributeSetBase, MyAttribute);
}

auto UAttributeSetBase::GetPreviousDataFromReplication(const FName AttributeName) const -> const FGameplayAttributeData*
{
    return CachedDataFromReplication.Find(AttributeName);
}

Accessing the previous value in the delegate

ACharacterBase.h:

class YOUR_API ACharacterBase : public ACharacter, public IAbilitySystemInterface
{
    // ...
    void OnMyAttributeValueChange(const FOnAttributeChangeData& Data); // The callback to be registered within GAS.
    // ...
}

ACharacterBase.cpp:

void ACharacterBase::OnMyAttributeValueChange(const FOnAttributeChangeData& Data)
{
    // This delegate is fired either from
    // 1. `SetBaseAttributeValueFromReplication` or from
    // 2. `InternalUpdateNumericalAttribute`
    // #1 is called on clients, after the attribute has changed its value. This implies,
    // that the previous value is not present on the client anymore. Therefore, the
    // value of `Data.OldValue` is erroneously identical to `Data.NewValue`.
    // In that case (and only in that case), the previous value is retrieved from a cache
    // in the AttributeSet. This cache will be only present on client, after it had
    // received an update from replication.
    auto deltaValue = 0.f;
    if (Data.NewValue == Data.OldValue)
    {
        const auto attributeName = GET_MEMBER_NAME_CHECKED(UAttributeSetBase, MyAttribute);
        if (auto previousData = AttributeSetComponent->GetPreviousDataFromReplication(attributeName))
        {
            // This will be called on the client, when coming from replication.
            deltaValue = Data.NewValue - previousData->GetCurrentValue();
        }
    }
    else
    {
        // This might be called on the server or clients, when coming from
        // `InternalUpdateNumericalAttribute`.
        deltaValue = Data.NewValue - Data.OldValue;
    }
    // Use deltaValue as you like.
}
Roi Danton
  • 7,933
  • 6
  • 68
  • 80