跳到主要内容
信息
· 文章中可能会出现一些错误,希望大佬们可以在评论区指出;

03 - Attribute Set

本文主要说明了UE5中Gameplay技能系统(Gameplay Ability System, GAS)中属性Attribute的相关内容。

概述

GAS使用游戏属性(Gameplay Attributes)计算和修改与游戏相关的浮点值。GAS中的Actor将它们的游戏属性存储在一个属性集(Attribute Set)中,该属性集有助于管理各属性与GAS中其他组件之间的交互(例如限定数值范围,值变更等),并将自身注册到Actor的ASC中。

常用操作

定义Attribute

通过FGameplayAttributeData类保存游戏属性,它主要由两个值描述:

  • 默认值 Base Value:在较长时间内保持固定的默认值;
  • 当前值 Current Value:大多数计算和逻辑中会使用的当前值;

此外,GAS提供了一些访问器宏以简化单个游戏属性的访问操作。以Aura项目为例,一个游戏属性的定义如下:

AuraAttributeSet.h点击展开/折叠代码
// ...

// 游戏属性访问器宏
#define AURA_ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

// ...

UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
// ...
public:
UPROPERTY()
FGameplayAttributeData Health;
AURA_ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Health)
// ...
}

这样就能在外部调用GetHealthAttribute()Get/Set/InitHealth()了。

初始化Attribute

通过访问器宏

上文定义的访问器宏ATTRIBUTE_ACCESSORS中提供了Attribute的初始化函数InitXXX(),可以使用该函数为Attribute进行简单的初始化。

通过Data Table

还可通过Data Table来初始化Attribute,这需要我们创建一个行类型为AttributeMetaData的Data Table,填写相关初始化信息,并最终在编辑器里目标Actor的ASC中进行绑定。

首先看看表初始化信息的填写,它主要填写如下内容:

  • Row Name:要初始化的属性名,填写属性集名.属性名
  • Base Value:该属性的默认值;
  • Min/Max Value:该属性的最小/最大值;
  • Can Stack:该属性是否可堆叠;

Ps:可以通过导入.csv表格的方式快速填写初始化信息。

然后就是编辑器上对ASC的操作,需要选中目标Actor的ASC,在Attribute Test -> Default Starting Data中添加属性集和它对应的Data Table:

通过GE

还能通过GE初始化Attribute,这需要我们定义一个用于初始化Attribute的GE,并在Ability System初始化后将GE施加到目标Actor上。

例如在Aura项目中,首先定义用于初始化主角Aura首要属性的GEGE_AuraPrimaryAttributes,它通过Modifier初始化主角的4个属性:

然后在Ability System初始化后对主角施加该GE即可:

AuraCharacter.cpp点击展开/折叠代码
// AuraCharacterBase 是 AuraCharacter 的基类, 这里直接把实现搬过来
void AAuraCharacterBase::InitPrimaryAttributes() const
{
checkf(IsValid(GetAbilitySystemComponent()), TEXT("[%hs] ASC is null!"), __FUNCTION__);
checkf(IsValid(InitPrimaryAttrGEClass), TEXT("[%hs] InitPrimaryAttrGEClass is null, plz fill it in editor"),
__FUNCTION__);

const FGameplayEffectContextHandle GEContextHandle = GetAbilitySystemComponent()->MakeEffectContext();
const FGameplayEffectSpecHandle GESpecHandle = GetAbilitySystemComponent()->MakeOutgoingSpec(
InitPrimaryAttrGEClass, 1.f, GEContextHandle);
GetAbilitySystemComponent()->ApplyGameplayEffectSpecToTarget(*GESpecHandle.Data, GetAbilitySystemComponent());
}

void AAuraCharacter::InitAbilitySystem()
{
Super::InitAbilitySystem();

AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
checkf(AuraPlayerState, TEXT("Can't get AuraPlayerState !!!"));
AuraAbilitySystemComponent = CastChecked<UAuraAbilitySystemComponent>(AuraPlayerState->GetAbilitySystemComponent());
AuraAbilitySystemComponent->InitAbilityActorInfo(AuraPlayerState, this);
AuraAbilitySystemComponent->OnAbilityActorInfoSet();

AuraAttributeSet = CastChecked<UAuraAttributeSet>(AuraPlayerState->GetAttributeSet());

InitPrimaryAttributes();
}

启用网络同步

要想给一个属性启用网络同步,需要完成如下步骤:

  1. UPROPERTY()中添加ReplicatedUsing = OnRep_XXXX,并声明签名为void OnRep_XXXX(const FGameplayAttributeData& OldValue);的函数;
  2. 实现上述函数,其中包含GAMEPLAYATTRIBUTE_REPNOTIFY(UAttributeSet, XXXX, OldValue);,以负责该属性在GAS中的网络同步;
  3. 重写GetLifetimeReplicatedProps()函数,其中包含DOREPLIFETIME_CONDITION_NOTIFY(UAttributeSet, XXXX, COND_None, REPNOTIFY_Always),以负责该属性的网络同步;

例如在Aura中,属性Health的网络同步实现如下:

AuraAttributeSet.h点击展开/折叠代码
UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
public:
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Vital Attributes")
FGameplayAttributeData Health;
AURA_ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Health)

virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;

UFUNCTION()
void OnRep_Health(const FGameplayAttributeData& OldHealth) const;
}
AuraAttributeSet.cpp点击展开/折叠代码
void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);

// _NOTIFY 会触发对应变量的OnRep_函数
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always);
}

void UAuraAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Health, OldHealth);
}

定义Attribute值改变时的回调函数

有时需要自定义Attribute值改变时的逻辑,这需要我们实现ASC的委托GameplayAttributeValueChangeDelegate,具体步骤如下:

  1. 定义值改变的回调函数void OnXXXChanged(const FOnAttributeChangeData& Data)
  2. 在对应ASC中获取委托ASC->GetGameplayAttributeValueChangeDelegate(AttributeSet->GetXXXAttribute())
  3. 给获取到的委托绑定该回调函数;

例如在Aura中,在属性Health的值改变时需要通知UI进行对应变化,它的回调函数实现如下:

AuraOverlayWidgetController.h点击展开/折叠代码
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAttributeChangedSignature, float, NewValue);

UCLASS(Blueprintable, BlueprintType)
class AURA_API UAuraOverlayWidgetController : public UAuraWidgetController
{
GENERATED_BODY()

public:
UPROPERTY(BlueprintAssignable, Category = "GAS|Attributes")
FOnAttributeChangedSignature OnHealthChanged;

virtual void BindDelegateCallbackFunctions() override;

protected:
void OnHealthChangedCallback(const FOnAttributeChangeData& Data) const;
};
AuraOverlayWidgetController.cpp点击展开/折叠代码
void UAuraOverlayWidgetController::BindDelegateCallbackFunctions()
{
AuraAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetHealthAttribute()).
AddUObject(this, &UAuraOverlayWidgetController::OnHealthChangedCallback);
}

void UAuraOverlayWidgetController::OnHealthChangedCallback(const FOnAttributeChangeData& Data) const
{
OnHealthChanged.Broadcast(Data.NewValue);
}

Meta Attribute

在 GAS 中,Meta Attribute 是一种用于传递过程数据的临时属性。它本身不代表角色的真实状态,通常也不参与网络同步,而是在本地作为“计算通道”使用。其核心作用是:在 GameplayEffect(GE)与 AttributeSet 之间传递中间数据,并由 AttributeSet 统一处理最终结果

引入 Meta Attribute 的主要意义在于:

  • 解耦 GE 与最终属性(如 Health)之间的直接修改
  • 将数值计算逻辑集中在 AttributeSet 中统一处理
  • 便于扩展复杂机制(护盾、抗性、暴击等)

以受到的伤害Damage为例,它可被看作一个Meta Attribute:

GE → Damage(Meta Attribute)
→ AttributeSet 统一处理(护盾 / 抗性 / 修正)
→ Health 减少

从上图中可以发现,Meta Attribute Damage 用于临时传递GE造成的伤害,并交由AttributeSet统一处理(根据角色护盾、抗性等Attribute进一步计算),最终影响生命值Health这一Attribute。

Aura项目中的伤害处理机制和上述例子类似:

  1. 角色通过GE受到伤害,其中的GameplayTag是Attribute.Meta.IncomingDamage

  2. 在AttributeSet的PostGameplayEffectExecute()函数中实现伤害处理逻辑:

    AuraAttributeSet.cpp点击展开/折叠代码
    void UAuraAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
    {
    // ...

    // 处理IncomingDamage
    if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
    {
    // 在使用IncomingDamage后马上将其归零
    const float LocalIncomingDamage = GetIncomingDamage();
    SetIncomingDamage(0.f);

    // 中间可以对IncomingDamage进行一些护盾/抗性处理
    // ...

    // 角色仍能受到伤害
    if (LocalIncomingDamage > 0.f)
    {
    const float NewHealth = GetHealth() - LocalIncomingDamage;
    SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));

    const bool bIsDead = NewHealth <= 0.f;
    if (bIsDead)
    {
    // 角色死亡逻辑
    AActor* TargetAvatarActor = EffectProperties.TargetAvatarActor;
    if (TargetAvatarActor->Implements<UCombatInterface>())
    {
    const TScriptInterface<ICombatInterface> CombatInterface = TScriptInterface<ICombatInterface>(TargetAvatarActor);
    CombatInterface->Die();
    }
    }
    else
    {
    // 角色受击逻辑
    FGameplayTagContainer TagContainer;
    TagContainer.AddTag(AuraGameplayTags::GE::HitReact.GetTag());
    EffectProperties.TargetASC->TryActivateAbilitiesByTag(TagContainer);
    }
    }
    }
    }

常用函数

PreAttributeChange()

可重写函数void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue),该函数在Attribute的当前值改变前触发,可实现在该Attribute的当前值改变前的一些操作,最常见的就是Clamp操作。

例如在Aura项目中,该函数主要用于属性值在改变前的Clamp操作:

AuraAttributeSet.cpp点击展开/折叠代码
void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);

// 在Attribute的Current Value改变前进行Clamp操作
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
}
}

PostGameplayEffectExecute()

可重写函数void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data),该函数在GE执行后调用,可以在Data中获取许多GAS中的信息。

例如在Aura项目中,主要通过该函数进行一些GE执行后的数据获取和处理:

AuraAttributeSet.cpp点击展开/折叠代码
void UAuraAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);

// 获取Source和Target的相关信息
FEffectProperties EffectProperties;
SetEffectProperties(Data, EffectProperties);

// 在GE之后对Current Value再次Clamp, 防止在PreAttributeChange()中Clamp错误的NewValue
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}

// 处理IncomingDamage
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);

if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
}
}
}

参考资料