首页 > 解决方案 > 如何获取客户端上 GameplayAbility 属性的增量值(属性值更改后)?

问题描述

在我使用GameplayAbilitySystem和默认服务器->客户端架构的虚幻引擎项目中,客户端会收到服务器上发生的属性值更改的通知。

此外,我不仅要获取新值,还要获取值更改的数量(delta = new value - old value)。这应该可以使用属性值更改委托,因为它包含FOnAttributeChangeData其成员NewValueOldValue

在服务器上,这两个值都是正确的。但是,在客户端上,FOnAttributeChangeData::NewValue == FOnAttributeChangeData::OldValue两者都具有与NewValue服务器上相同的值。

这是因为在复制发生后调用了委托......

UPROPERTY(ReplicatedUsing=OnRep_MyAttribute)
FGameplayAttributeData MyAttribute;

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

(这是ActionRPG的默认 GAS 设置)

...所以客户端不知道它在复制之前的价值。

  1. 我如何获取属性的值,它在服务器更新之前具有的值?
  2. 如何将此值转发给委托人?

标签: c++unreal-engine4unreal-gameplay-ability-system

解决方案


获取旧值(问题 1)

UnrealEngine OnRep 函数提供复制变量的先前状态作为函数中的第一个参数OnRep。所以添加参数

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

感谢来自 Unreal GAS discord 频道的@Dan。

将值转发给委托人(问题 2)

主意

当您的目标是不修改 UE4 源代码时,一种可能性是在属性集中缓存以前的值,以便您能够从外部访问它。

  1. OnRep为属性集函数中的每个属性缓存该值。
  2. 在委托中使用缓存的值,但前提是它是有效的。由于该值是在OnRep函数内分配的,因此它不会存在于服务器上。这很好,因为我们希望保留服务器上的行为,它使用FOnAttributeChangeData::OldValue(仅在服务器上具有正确的值)。

示例实现

缓存上一个值

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);
}

访问委托中的前一个值

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.
}

推荐阅读