- Published on
GameplayAbilitySystem入门与实战(二):属性
- Authors

- Name
- 东哥
前言
前一篇已经完成了初始化,角色可以在服务端和客户端正确触发技能,那么这一篇我们开始研究如何声明我们GAS框架内的属性
暗黑3属性图镇楼

属性类
GAS常用的属性类是FGameplayAttribute和FGameplayAttributeData
FGameplayAttributeData
此结构体内有两个float变量, 表示Base和Current两个值, 一般情况下是一致的. 多数游戏中的生命值,魔法值等多数属性都可以使用此属性
这里请看如下关键代码
FAggregatorRef* RefPtr = AttributeAggregatorMap.Find(Attribute);
if (RefPtr)
{
// There is an aggregator for this attribute, so set the base value. The dirty callback chain
// will update the actual AttributeSet property value for us.
RefPtr->Get()->SetBaseValue(NewBaseValue);
}
// if there is no aggregator set the current value (base == current in this case)
else
{
InternalUpdateNumericalAttribute(Attribute, NewBaseValue, nullptr);
}
上面的代码是在GameplayEffect.cpp;中的SetAttributeBaseValue()函数体中的, 本函数设置了Base值,然后InternalUpdateNumericalAttribute()设置了与Base一样的Current值, 从除非你自己创建了FAggregatorRef类, 该类通过FActiveGameplayEffectsContainer::FindOrCreateAttributeAggregator()创建,此方法是私有的, 实测通过FActiveGameplayEffectsContainer::CaptureAttributeForGameplayEffect()可以在外部创建, 那么使用起来就有点略微麻烦了,而且如果使用默认的FAggregatorRef类还是会饶了一圈通过OnAttributeAggregatorDirty()再执行InternalUpdateNumericalAttribute()把Current值给设置为与Base值一样的值.
当然你强行拿到FGameplayAttributeData直接SetCurrentValue()是可以改变的, 但是无法通过GE来改到Current值,而且没有属性的事件响应
对此处没有过多研究,如有错误请执教
FGameplayAttribute
相比而言只有一个浮点值, 对于一些辅助属性或者UI之类的属性可以用此属性
- 事件响应
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &APlayerState::HealthChanged);
virtual void HealthChanged(const FOnAttributeChangeData& Data);
用上述两个方法来绑定生命属性更改以后的回调,我们可以在绑定的事件处理比如UI之类的逻辑
后面会创建一个下图所示的异步节点来监听属性改变, 方便在各个地方得到属性数据

属性集/AttributeSet
顾名思义,属性集就是多个属性的容器,通常在拥有类的构造函数中创建,当然目前必须用C++
例如我们也可以在CharacterBase的构造函数构造此类
UPROPERTY()
USRAttributeSetBase* AttributeSet;
AttributeSet = `CreateDefaultSubobject<USRAttributeSetBase>`(TEXT("AttributeSet"));
声明
- 辅助宏
AttributeSet.h中为我们提供了几个很有用的辅助宏, 用于方便我们在cpp中get,set,init等属性操作
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) \
我们再用ATTRIBUTE_ACCESSORS宏包裹4个宏
然后用如下方法申明一个属性
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(USRAttributeSetBase, Health)
测试可以在cpp中调用如下方法
this->InitHealth(100);
this->GetHealth();
this->SetHealth(1);
this->GetHealthAttribute();
注意: 这个是cpp中的函数,在蓝图中无法使用
在蓝图中可以通过如下方式获取, 不能Set

同步
如果属性设置了同步(Rep)
那么可以在实现的时候用宏GAMEPLAYATTRIBUTE_REPNOTIFY来处理属性的同步
void UVGAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UVGAttributeSet, Health, OldHealth);
}
因为
OnRep函数是UFUNCTION(),所以因为UE的限制, 没办法用宏来快速声明和定义, 所以属性很多的话只能手动一个个声明定义
最后,同步的属性需要在GetLifetimeReplicatedProps函数内用宏DOREPLIFETIME_CONDITION_NOTIFY处理同步,REPNOTIFY_Always属性意味着服务器值过来以后一定会走Rep函数, 默认情况如果值不变不用调用;
后面两个参数可以不填, 默认值是
COND_None,REPNOTIFY_OnChanged
如果是类似MetaAttribute类的非同步属性, 那么此函数也会取消同步
void UVGAttributeSet::GetLifetimeReplicatedProps(`TArray<FLifetimeProperty>`& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UVGAttributeSet, Health, COND_None, REPNOTIFY_Always);
}
动态添加/移除
属性集也可以动态的添加和移除
AbilitySystemComponent->SpawnedAttributes.AddUnique(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();
AbilitySystemComponent->SpawnedAttributes.Remove(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();
预处理/PreAttributeChange
属性在正在修改之前会到函数PreAttributeChange()
这里比较适合对NewValue进行Clamp处理,比如
if (Attribute == GetMoveSpeedAttribute())
{
NewValue = FMath::`Clamp<float>`(NewValue, 150, 1000);
}
或者我们在这里对修改生命和魔法的最大值做一个处理, 请思考一般Moba或RPG等游戏中最大值修改以后, 当前的值也会按照百分比修改
void USRAttributeSetBase::AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty)
{
UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent();
const float CurrentMaxValue = MaxAttribute.GetCurrentValue();
if (!FMath::IsNearlyEqual(CurrentMaxValue, NewMaxValue) && AbilityComp)
{
// 更改当前值以保持 Cur / Max 的百分比
const float CurrentValue = AffectedAttribute.GetCurrentValue();
float NewDelta = (CurrentMaxValue > 0.f) ? (CurrentValue * NewMaxValue / CurrentMaxValue) - CurrentValue : NewMaxValue;
AbilityComp->ApplyModToAttributeUnsafe(AffectedAttributeProperty, EGameplayModOp::Additive, NewDelta);
}
}
然后在预处理中执行
void USRAttributeSetBase::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
UE_LOG(SRLog, Log, TEXT("Attribute Pre Changed old = %s, new= %f"), Attribute.GetUProperty(), NewValue);
Super::PreAttributeBaseChange(Attribute, NewValue);
if (Attribute == GetMaxManaAttribute())
{
AdjustAttributeForMaxChange(Health, MaxHealth, NewValue, GetHealthAttribute());
}
else if (Attribute == GetMaxManaAttribute())
{
AdjustAttributeForMaxChange(Mana, MaxMana, NewValue, GetManaAttribute());
}
}
PostGameplayEffectExecute
PostGameplayEffectExecute()函数会在瞬间执行的GameplayEffect修改到属性的时候执行
这里可以做属性做一些额外的操作,比如对一些临时属性进行处理然后反馈到最终角色上, 我们后面会在这里做对临时的伤害值进行处理后, 修改角色的生命值等操作.
因为还涉及到很多后续的内容, 这里先不展开