目录
- 4.4.1 属性集的定义 - Attribute Set Definition
- 4.4.2 属性集的设计 - Attribute Set Design
- 4.4.3 定义属性 - Defining Attributes
- 4.4.4 初始化属性 - Initializing Attributes
- 4.4.5 PreAttributeChange()
- 4.4.6 PostGameplayEffectExecute()
- 4.4.7 OnAttributeAggregatorCreated()
4.4.1 属性集的定义 - Attribute Set Definition
AttributeSet
会负责Attributes
的定义、保存和管理。开发者可以从UAttributeSet
继承并进一步拓展。在OwnerActor
的构造函数中创建AttributeSet
会将他自动注册到OwnerActor
的ASC
中。这一步必须在C++中完成。
4.4.2 属性集的设计 - Attribute Set Design
4.4.2.1 具备独立属性的子组件 - Subcomponents with Individual Attributes
设想这样一种情形:某个Pawn
上面有很多个负责抵御伤害的组件(比如说多个独立的可被破坏的护甲),如果你知道Pawn
可拥有的护甲的最大数量,那么可以在该Pawn
上做一个AttributeSet
,其中包含着许多的生命值Attributes
,比如说DamageableCompHealth0,DamageableCompHealth1等等,来表示这些可以抵御伤害的组件的逻辑上的插槽(即建立护甲和生命值属性的逻辑关联)。在你的表示护甲的类的实例上,令表示对应的生命值的插槽Attribute
可以由GameplayAbilities
进行读取或者由Executions
来读取,从而可以知道某个护甲遭到伤害时,伤害应该结算到具体哪一个Attribute
上。即使某些Pawns
拥有的护甲数量比AttributeSet
预先设定的数量小也没有关系,因为可以不去使用对应的Attribute
,而这部分额外的内存消耗是微乎其微的。
如果你的子组件每个上面都需要很多个的Attributes
,或者这个数量是未知的,亦或者子组件会被从现有个体上卸载然后被其他玩家使用(比如说你的角色死亡后掉落的武器被别人拾取),总之不管什么原因前面提到的方法无法完全解决你的问题,我的建议是直接使用老办法,即不用Attributes
系统来做而改用老办法,即单独创建一些float类型的值之类的(译者注:因为此时情况的复杂性再使用Attributes
来强行拓展已经是弊大于利了)。参阅后面的小节Item Attributes。
4.4.2.2 运行时添加和删除属性集 - Adding and Removing AttributeSets at Runtime
可以在运行时从ASC
中添加和删除AttributeSets
,当然移除掉某些AttributeSets
的行为可能是很危险的。例如,如果某个AttributeSet
的移除在客户端上早于在服务器端,而恰巧此时有一个Attribute
值被复制到客户端,这样的话Attribute
就找不对对应的AttributeSet
从而导致游戏奔溃。
装备武器时:
AbilitySystemComponent->GetSpawnedAttributes_Mutable().AddUnique(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();
卸载武器时:
AbilitySystemComponent->GetSpawnedAttributes_Mutable().Remove(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();
4.4.2.3 物品的属性(武器弹药) - Item Attributes (Weapon Ammo)
有多种方式可以实现带有Attributes
的可装备物品(武器弹药,防具耐久等)。所有这些东西都是把值直接存在物品上。这对于能够被多个玩家装备和使用的那些物品来说是必须的。
- 在物品上完全都用float来处理 (推荐)
- 为物品分别分配独立的
AttributeSet
- 为物品分别分配独立的
ASC
4.4.2.3.1 在物品上完全都用浮点数来处理 - Plain Floats on the Item
即直接在物品类实例上使用浮点值而不是Attributes
。Fortnite和GASShooter都是用这种方式来处理弹药的。对于枪械,具体就是存储最大的弹夹数量,当前弹夹中的弹药数量,后备弹药等等,把这些都以支持复制的浮点数(COND_OwnerOnly
)形式存在枪械实例上。如果武器可以共享后备弹药,也就是说所有的武器都是使用的同一种弹药,那么你可以为Character
添加一个代表后备弹药的Attribute
及其相应的AttributeSet
(重新加载能力时可以使用一个Cost GE
来从后备弹药中抽取然后装填到枪械的弹夹中)。因为你的当前弹夹弹药并没有用Attributes
来表示,你可能需要重写UGameplayAbility
中的某些函数来检查和修改枪械上对应的float类型值。在授予技能时,令枪械作为GameplayAbilitySpec
中的SourceObject
,这样你才可以在技能中去访问枪械的相应数据。
为了防止枪械在快速自动开火中由于弹药数量的复制而搞乱本地的弹药数量,需要当玩家在PreReplication()
中有IsFiring
的GameplayTag
时禁用掉复制功能。你也可以在这里实现你本地的预测。
void AGSWeapon::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
{
Super::PreReplication(ChangedPropertyTracker);
DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, PrimaryClipAmmo, (IsValid(AbilitySystemComponent) && !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag)));
DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, SecondaryClipAmmo, (IsValid(AbilitySystemComponent) && !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag)));
}
优势:
- 避免了使用
AttributeSets
的局限性(后面会有详细内容)
限制:
- 无法使用现有的
GameplayEffect
的工作流(比如以Cost GEs
来处理弹药的使用,等等) - 需要进一步拓展
UGameplayAbility
(重写其中一些函数),来检查和处理弹药的消耗(从而应对float类型而非Attribute
)
4.4.2.3.2 为物品分别分配独立的属性集 - AttributeSet on the Item
在物品上使用独立的AttributeSet
,当玩家装备物品时将物品上的AttributeSet
添加到玩家的ASC
上也是可以的,但是这样的做法也会相应的带来一些问题。我在GASShooter的早期版本中就是用这种方式来处理弹药的。武器将一些Attributes
,比如最大弹夹数,当前弹夹中的弹药,后备弹药等等,存储到一个AttributeSet
放到武器类上。如果武器间共享后备弹药,你可以将后备弹药这个Attribute
移动到角色身上,用一个共享弹药的AttributeSet
来保管。当在服务器上玩家装备武器时,武器的AttributeSet
将会添加到玩家的ASC::SpawnedAttributes
中。服务器将这个复制到客户端。如果武器被卸载掉,再从ASC::SpawnedAttributes
中移除掉AttributeSet
。
当AttributeSet
保存在非OwnerActor
的什么东西上(比如说武器),你会发现在AttributeSet
上发现一些编译错误。解决方案是在BeginPlay()
中构造AttributeSet
,而非在构造函数中,然后还要在武器上实现实现IAbilitySystemInterface
接口(当你装备武器时设置一个指向ASC
的指针,就和之前在Character或者PlayerState上实现的这个接口类似)。
void AGSWeapon::BeginPlay()
{
if (!AttributeSet)
{
AttributeSet = NewObject<UGSWeaponAttributeSet>(this);
}
//...
}
参见 older version of GASShooter.
优势:
- 可以使用现有
GameplayAbility
和GameplayEffect
的工作流 workflow(比如以Cost GEs
来处理弹药的使用,等等) - 在物品不多时比较容易设置
限制:
- 对于每种武器类型都需要去定制一个新的
AttributeSet
类。ASCs
只能够保存一个AttributeSet
类的实例,因为对某个Attribute
的修改会去在ASC
的SpawnedAttributes
数组中查找他们AttributeSet
类的第一个实例。额外的同一个或者同源的AttributeSet
类会被忽略掉。 - 出于上面的原因,那么同种类型的装备你就只能装备一把了。
- 移除掉某个
AttributeSet
是危险的行为。比如说在GASShooter里,如果玩家用火箭筒杀掉自己,玩家会立即卸载掉火箭筒这件装备(并从ASC
中卸载AttributeSet
)。当服务器去复制火箭筒弹药这个Attribute
的变化时,客户端上的ASC
上已经没有AttributeSet
,这样游戏就奔溃了。
4.4.2.3.3 为物品分别分配独立的ASC - ASC on the Item
为每个物品都添加一个完整的AbilitySystemComponent
是一个极端的方法。这个我既没有自己实现过,更没见过。要实现这个方案需要大量额外的工作。
Is it viable to have several AbilitySystemComponents which have the same owner but different avatars (e.g. on pawn and weapon/items/projectiles with Owner set to PlayerState)?
The first problem I see there would be implementing the IGameplayTagAssetInterface and IAbilitySystemInterface on the owning actor. The former may be possible: just aggregate the tags from all all ASCs (but watch out -HasAllMatchingGameplayTags may be met only via cross ASC aggregation. It wouldn’t be enough to just forward that calls to each ASC and OR the results together). But the later is even trickier: which ASC is the authoritative one? If someone wants to apply a GE -which one should receive it? Maybe you can work these out but this side of the problem will be the hardest: owners will multiple ASCs beneath them.
Separate ASCs on the pawn and the weapon can make sense on its own though. E.g, distinguishing between tags the describe the weapon vs those that describe the owning pawn. Maybe it does make sense that tags granted to the weapon also “apply” to the owner and nothing else (E.g, attributes and GEs are independent but the owner will aggregate the owned tags like I describe above). This could work out, I am sure. But having multiple ASCs with the same owner may get dicey.
Dave Ratti from Epic’s answer to community questions #6
优势:
- 可以使用现有的
GameplayAbility
和GameplayEffect
的工作流(比如以Cost GEs
来处理弹药的使用,等等) - 可以重用
AttributeSet
类(在每个武器的ASC上重复使用)
限制:
- 工作量
- 可行性
4.4.3 定义属性 - Defining Attributes
Attributes
只能通过C++来定义 在AttributeSet
的头文件中。我建议是将这一段宏代码块儿添加到每个AttributeSet
的头文件里。它会帮我们自动生成对应Attributes
的访问器(getter方法)和修改器(setter方法)。
// Uses macros from AttributeSet.h
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
一个支持复制的生命值Attribute
可以像这样来定义:
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Health)
别忘记在头文件中定义OnRep
函数:
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
AttributeSet
的.cpp文件中应该在相应的OnRep
方法中添加GAMEPLAYATTRIBUTE_REPNOTIFY
宏,从而支持预测系统的一些内容:
void UGDAttributeSetBase::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGDAttributeSetBase, Health, OldHealth);
}
最后,需要把Attribute
添加到GetLifetimeReplicatedProps
:
void UGDAttributeSetBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UGDAttributeSetBase, Health, COND_None, REPNOTIFY_Always);
}
REPNOTIFY_Always
是告诉OnRep
函数当本地值和服务器下发的值相同的时候也去进行相应的触发。默认情况下(即这里不用REPNOTIFY_Always
的情况下)这两个值一样的时候是不会触发OnRep
函数的。
如果某个Attribute
不需要复制,类似Meta Attribute
,那么OnRep
和GetLifetimeReplicatedProps
这两步的设置是可以跳过的。
4.4.4 初始化属性 - Initializing Attributes
实际上存在很多种方法来去初始化Attributes
(设置BaseValue
以及CurrentValue
的初始值)。Epic推荐使用一个Instant
类型的GameplayEffect
来完成这一步初始化(译者注:即通过应用应用一个GameplayEffect
,这个GameplayEffect
的施加效果就是对Attributes
进行初始化)。这也是示例项目中使用的方法。
参考示例项目中的GE_HeroAttributes
蓝图,其中有关于如何使用一个Instant
类型的GameplayEffect
来去初始化Attributes
。这个GameplayEffect
的实际应用是在C++的。
如果你在定义Attributes
时使用了宏ATTRIBUTE_ACCESSORS
,它会帮助你自动为AttributeSet
里的每个Attribute
都生成一个初始化方法,在C++里可以放心大胆的使用。
// InitHealth(float InitialValue) is an automatically generated function for an Attribute 'Health' defined with the `ATTRIBUTE_ACCESSORS` macro
AttributeSet->InitHealth(100.0f);
更多的Attributes
的初始化方法可以进一步参阅AttributeSet.h
。
注意: 在版本4.42之前,FAttributeSetInitterDiscreteLevels
是和FGameplayAttributeData
无法协调的。它是在Attributes
还是原始浮点数时创建,并且嫌弃FGameplayAttributeData
不是Plain Old Data
(POD
)。在4.24版本之后这个问题就被修复掉了 https://issues.unrealengine.com/issue/UE-76557。
4.4.5 PreAttributeChange()
PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
是AttributeSet
中重要的函数方法之一,主要是响应Attribute
中的CurrentValue
的修改发生之前的调用。这里最好就是去做一些对输入的限制和调整,利用NewValue
来将可能会被应用到CurrentValue
上的修改限制到某个合理的区间范围。
比如示例项目中将移动速度的修改器的限制如下:
if (Attribute == GetMoveSpeedAttribute())
{
// Cannot slow less than 150 units/s and cannot boost more than 1000 units/s
NewValue = FMath::Clamp<float>(NewValue, 150, 1000);
}
其中GetMoveSpeedAttribute()
函数是前面我们提到的宏代码块生成的函数之一(Defining Attributes)。
任何对Attributes
的修改都会先调用这个方法,无论是使用Attribute
的设置器(setters)(Defining Attributes)亦或是使用GameplayEffects
。
**注意:**此处的截取操作并没有永久的修改ASC
的修改器。它改变的实际上只是通过对修改器的查询而返回的值。这意味着任何修改器(比如GameplayEffectExecutionCalculations
和ModifierMagnitudeCalculations
)在重计算CurrentValue
时都需要再实现截取的操作。
注意Epic的对PreAttributeChange()
的注释提到,不要去使用它来处理游玩相关的事件,而只是把它用作数值的修正和处理。监听Attribute
的变化而产生的和游玩相关的事件(译者注:比如说生命值、弹药数等属性的UI响应事件)的推荐的处理方案是使用UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute)
(Responding to Attribute Changes)。
4.4.6 PostGameplayEffectExecute()
PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
只是在由Instant
类型的GameplayEffect
对某个Attribute
的BaseValue
修改之后才会触发。这里可以进一步做一些Attribute
相关的操作。
例如,在示例项目中我们令生命值的Attribute
减去最终伤害值的Meta Attribute
。如果有护盾的Attribute
的话,我们可以在这里先让护盾值减去伤害值,然后再把剩余伤害(如果还有的话)应用到生命值上。示例项目也在这个位置来应用受击动画,显示伤害飘字,并且为击杀者赋予经验和金币奖励。从设计上说,伤害值的Meta Attribute
将始终通过Instant
类型的GameplayEffect
来设置,并且永远不需要通过Attribute
的设置器(setter)来设置。
其他一些仅由Instant
类型的GameplayEffect
来改变其BaseValue
的Attributes
,比如法力值和体力值,也可以在这里通过其最大值对应的Attributes
来进行截取操作。
**注意:**当调用PostGameplayEffectExecute()
,对Attribute
的修改就已经生效了,但是还没有复制回客户端,所以在此处进行截取操作的话实际上不会进行两次值的复制。客户端仅收到截取过后的结果(最终值)。
4.4.7 OnAttributeAggregatorCreated()
OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator)
会在AttributeSet
中的某个Attribute
的聚合器Aggregator
创建时进行触发。这里可以自定义设置FAggregatorEvaluateMetaData
。Aggregator
使用AggregatorEvaluateMetaData
,基于所有应用到当前Attribute
的Modifiers
来计算该Attribute
的的CurrentValue
。默认为情况下,Aggregator
使用AggregatorEvaluateMetaData
来确定哪些Modifiers
符合MostNegativeMod_AllPositiveMods
的要求,而MostNegativeMod_AllPositiveMods
允许所有的正面的Modifiers
后仅仅最负面的Modifiers
。Paragon(虚幻争霸)中使用的就是这种方式,对于负面的减速效果的话在某个具体的时间点不管施加多少个只应用最负面的那个,而所有的正面的加速效果则全盘应用。没有通过要求的Modifiers
仍然会存在于ASC
上,只是不会进一步汇总到CurrentValue
里。当某些情况发生变化后,他们可能又有可能性来通过验证,比如说之前影响最大的减速效果结束了,那么就会从还没超时的Modifier
(如果还有的话)中挑一个减速效果最强的应用。
上述例子中的AggregatorEvaluateMetaData的使用:
virtual void OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const override;
void UGSAttributeSetBase::OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const
{
Super::OnAttributeAggregatorCreated(Attribute, NewAggregator);
if (!NewAggregator)
{
return;
}
if (Attribute == GetMoveSpeedAttribute())
{
NewAggregator->EvaluationMetaData = &FAggregatorEvaluateMetaDataLibrary::MostNegativeMod_AllPositiveMods;
}
}
自定义的AggregatorEvaluateMetaData
限定符应该以静态变量的形式添加到FAggregatorEvaluateMetaDataLibrary
。