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();
}
启用网络同步
要想给一个属性启用网络同步,需要完成如下步骤:
- 在
UPROPERTY()中添加ReplicatedUsing = OnRep_XXXX,并声明签名为void OnRep_XXXX(const FGameplayAttributeData& OldValue);的函数; - 实现上述函数,其中包含
GAMEPLAYATTRIBUTE_REPNOTIFY(UAttributeSet, XXXX, OldValue);,以负责该属性在GAS中的网络同步; - 重写
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,具体步骤如下:
- 定义值改变的回调函数
void OnXXXChanged(const FOnAttributeChangeData& Data); - 在对应ASC中获取委托
ASC->GetGameplayAttributeValueChangeDelegate(AttributeSet->GetXXXAttribute()); - 给获取到的委托绑定该回调函数;
例如在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);
}
常用函数
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的Source Actor和Target Actor:
AuraAttributeSet.cpp点击展开/折叠代码
void UAuraAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取Source和Target的相关信息
FEffectProperties EffectProperties;
SetEffectProperties(Data, EffectProperties);
}
void UAuraAttributeSet::SetEffectProperties(const struct FGameplayEffectModCallbackData& Data, FEffectProperties& Props)
{
Props.EffectContextHandle = Data.EffectSpec.GetContext();
// Source: 引发GE的Actor
Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
if (IsValid(Props.SourceASC) &&
Props.SourceASC->AbilityActorInfo.IsValid() &&
Props.SourceASC->AbilityActorInfo->AvatarActor.IsValid())
{
Props.SourceAvatarActor = Props.SourceASC->GetAvatarActor();
Props.SourceController = Props.SourceASC->AbilityActorInfo->PlayerController.Get();
if (Props.SourceController == nullptr && Props.SourceAvatarActor != nullptr)
{
if (const APawn* SourcePawn = Cast<APawn>(Props.SourceAvatarActor))
{
Props.SourceController = SourcePawn->GetController();
}
}
Props.SourceCharacter = Props.SourceController->GetCharacter();
}
// Target: 被GE施加效果的Actor
if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
{
Props.TargetAvatarActor = Data.Target.GetAvatarActor();
Props.TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
Props.TargetCharacter = Props.TargetController->GetCharacter();
Props.TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Props.TargetAvatarActor);
}
}


