4.4 属性集 - Attribute Set

4.4.1 属性集的定义 - Attribute Set Definition

AttributeSet会负责Attributes的定义、保存和管理。开发者可以从UAttributeSet继承并进一步拓展。在OwnerActor的构造函数中创建AttributeSet会将他自动注册到OwnerActorASC中。这一步必须在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的可装备物品(武器弹药,防具耐久等)。所有这些东西都是把值直接存在物品上。这对于能够被多个玩家装备和使用的那些物品来说是必须的。

  1. 在物品上完全都用float来处理 (推荐)
  2. 为物品分别分配独立的AttributeSet
  3. 为物品分别分配独立的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()中有IsFiringGameplayTag时禁用掉复制功能。你也可以在这里实现你本地的预测。

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)));
}

优势:

  1. 避免了使用AttributeSets的局限性(后面会有详细内容)

限制:

  1. 无法使用现有的GameplayEffect的工作流(比如以Cost GEs来处理弹药的使用,等等)
  2. 需要进一步拓展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.

优势:

  1. 可以使用现有GameplayAbilityGameplayEffect的工作流 workflow(比如以Cost GEs来处理弹药的使用,等等)
  2. 在物品不多时比较容易设置

限制:

  1. 对于每种武器类型都需要去定制一个新的AttributeSet类。ASCs只能够保存一个AttributeSet类的实例,因为对某个Attribute的修改会去在ASCSpawnedAttributes数组中查找他们AttributeSet类的第一个实例。额外的同一个或者同源的AttributeSet类会被忽略掉。
  2. 出于上面的原因,那么同种类型的装备你就只能装备一把了。
  3. 移除掉某个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

优势:

  1. 可以使用现有的GameplayAbilityGameplayEffect的工作流(比如以Cost GEs来处理弹药的使用,等等)
  2. 可以重用AttributeSet类(在每个武器的ASC上重复使用)

限制:

  1. 工作量
  2. 可行性

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,那么OnRepGetLifetimeReplicatedProps这两步的设置是可以跳过的。

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 DataPOD)。在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的修改器。它改变的实际上只是通过对修改器的查询而返回的值。这意味着任何修改器(比如GameplayEffectExecutionCalculationsModifierMagnitudeCalculations)在重计算CurrentValue时都需要再实现截取的操作。

注意Epic的对PreAttributeChange()的注释提到,不要去使用它来处理游玩相关的事件,而只是把它用作数值的修正和处理。监听Attribute的变化而产生的和游玩相关的事件(译者注:比如说生命值、弹药数等属性的UI响应事件)的推荐的处理方案是使用UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute)Responding to Attribute Changes)。

4.4.6 PostGameplayEffectExecute()

PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)只是在由Instant类型的GameplayEffect对某个AttributeBaseValue修改之后才会触发。这里可以进一步做一些Attribute相关的操作。

例如,在示例项目中我们令生命值的Attribute减去最终伤害值的Meta Attribute。如果有护盾的Attribute的话,我们可以在这里先让护盾值减去伤害值,然后再把剩余伤害(如果还有的话)应用到生命值上。示例项目也在这个位置来应用受击动画,显示伤害飘字,并且为击杀者赋予经验和金币奖励。从设计上说,伤害值的Meta Attribute将始终通过Instant类型的GameplayEffect来设置,并且永远不需要通过Attribute的设置器(setter)来设置。

其他一些仅由Instant类型的GameplayEffect来改变其BaseValueAttributes,比如法力值和体力值,也可以在这里通过其最大值对应的Attributes来进行截取操作。

**注意:**当调用PostGameplayEffectExecute(),对Attribute的修改就已经生效了,但是还没有复制回客户端,所以在此处进行截取操作的话实际上不会进行两次值的复制。客户端仅收到截取过后的结果(最终值)。

4.4.7 OnAttributeAggregatorCreated()

OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator)会在AttributeSet中的某个Attribute的聚合器Aggregator创建时进行触发。这里可以自定义设置FAggregatorEvaluateMetaDataAggregator使用AggregatorEvaluateMetaData,基于所有应用到当前AttributeModifiers来计算该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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值