GameplayAbilitySystem入门与实战(二):属性

前言

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

暗黑3属性图镇楼

image-20201217111712682

属性类

GAS常用的属性类是FGameplayAttributeFGameplayAttributeData

  • FGameplayAttributeData

此结构体内有两个float变量, 表示BaseCurrent两个值, 一般情况下是一致的. 多数游戏中的生命值,魔法值等多数属性都可以使用此属性

这里请看如下关键代码

1
2
3
4
5
6
7
8
9
10
11
12
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之类的属性可以用此属性

  • 事件响应
1
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &APlayerState::HealthChanged);
1
virtual void HealthChanged(const FOnAttributeChangeData& Data);

用上述两个方法来绑定生命属性更改以后的回调,我们可以在绑定的事件处理比如UI之类的逻辑

后面会创建一个下图所示的异步节点来监听属性改变, 方便在各个地方得到属性数据

image-20201024195609117

属性集/AttributeSet

顾名思义,属性集就是多个属性的容器,通常在拥有类的构造函数中创建,当然目前必须用C++

例如我们也可以在CharacterBase的构造函数构造此类

1
2
UPROPERTY()
USRAttributeSetBase* AttributeSet;
1
AttributeSet = CreateDefaultSubobject<USRAttributeSetBase>(TEXT("AttributeSet"));

声明

  • 辅助宏

AttributeSet.h中为我们提供了几个很有用的辅助宏, 用于方便我们在cppget,set,init等属性操作

1
2
3
4
5
#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个宏


然后用如下方法申明一个属性

1
2
3
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(USRAttributeSetBase, Health)

测试可以在cpp中调用如下方法

1
2
3
4
this->InitHealth(100);
this->GetHealth();
this->SetHealth(1);
this->GetHealthAttribute();

注意: 这个是cpp中的函数,在蓝图中无法使用

在蓝图中可以通过如下方式获取, 不能Set

image-20200922134907045

同步

如果属性设置了同步(Rep)

那么可以在实现的时候用宏GAMEPLAYATTRIBUTE_REPNOTIFY来处理属性的同步

1
2
3
4
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类的非同步属性, 那么此函数也会取消同步

1
2
3
4
5
6
void UVGAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);

DOREPLIFETIME_CONDITION_NOTIFY(UVGAttributeSet, Health, COND_None, REPNOTIFY_Always);
}

动态添加/移除

属性集也可以动态的添加和移除

1
2
3
4
5
AbilitySystemComponent->SpawnedAttributes.AddUnique(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();

AbilitySystemComponent->SpawnedAttributes.Remove(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();

预处理/PreAttributeChange

属性在正在修改之前会到函数PreAttributeChange()

这里比较适合对NewValue进行Clamp处理,比如

1
2
3
4
if (Attribute == GetMoveSpeedAttribute())
{
NewValue = FMath::Clamp<float>(NewValue, 150, 1000);
}

或者我们在这里对修改生命和魔法的最大值做一个处理, 请思考一般Moba或RPG等游戏中最大值修改以后, 当前的值也会按照百分比修改

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}

然后在预处理中执行

1
2
3
4
5
6
7
8
9
10
11
12
13
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修改到属性的时候执行

这里可以做属性做一些额外的操作,比如对一些临时属性进行处理然后反馈到最终角色上, 我们后面会在这里做对临时的伤害值进行处理后, 修改角色的生命值等操作.

因为还涉及到很多后续的内容, 这里先不展开