AttributeSet
AttributeSet是用于管理Attribute的注册,保存以及修改。我们需要在OwnerActor的构造山书中穿件AttributeSet,其会自动注册到ASC,这必须在C++中完成。
一个ASC可能有一个或者多个AttributeSet,AttributeSet的开销很小,因此取决于开发人员自己定义使用多少AttribteSet。有一种解决方法是这只一个巨大的AttributeSet
,共享于游戏中所有Actor,并且只使用需要的Attribute,而忽略不需要的。ASC可以拥有多个AttributeSet,但是同一个ASC不能拥有多个同类的AttributeSet,ASC将会不知道使用哪一个而随机挑选。
Attribute
在AttributeSet中我们将会单独使用Attribute,例如一些ACT游戏中对于角色的伤害是分部位的(怪物猎人之类的),那么我们可以将多个生命值Attribute放入到一个AttributeSet中,如果子组件可以分离被其他玩家使用,例如武器可以被其他玩家捡到,那么我们最好不要使用Attribute而是转为普通的float值。
在多人游戏中AttributeSet可以在ASC上添加或者移除,但是这么做是非常危险的,例如,某个AttributeSet在客户端上别移除,这一动作早于服务器,而服务器中这个AttributeSet中的某个Attribute发生了改变同步到了客户端,那么Attribute就会因为找不到AttributeSet而使游戏崩溃。
Item Attribute
有一些装备中的某些属性,我们同样可以使用Attribute来描述,例如一把武器,我们可以将它的子弹数量以及伤害作为Attribute,防御性的盔甲,我们可以将它的防御值以及耐久值作为Attribute。
我们有三种实现的方式
- 在物品中简单使用浮点值
- 在物品中使用单独的AttributeSet。
- 在物品中使用单独的ASC
使用单独的浮点值
在武器的实力中存储浮点值而不使用Attribute,对于武器,我们可以存储同步的浮点数(COND_OwrnerOnly),例如最大弹匣量,弹匣中的子弹量,剩余子弹量。如果武器需要共享剩余弹药量,我们可以将剩余弹药量移动到Character中的AttributeSet中作为一个Attribute。
在射击游戏中,会出现抢先反向同步弹药量使得本地弹药量出现跳变,即由于延迟很高并且在一些射速高的五其中,客户端的子弹来不及和服务器进行同步,造成子弹数量减少又增多的情况,这个时候可以使用客户端预测(Blaster中),也可以在玩家开火的时候直接禁止同步,本质上就是客户端做自己的本地预测。
有点:
- 避免了Attribute的局限。
缺点:
- 不能使用GE
- 要求重写UGameplayAbility中的关键函数来检查和应用枪械中浮点数的花销(Cost).
在物体中使用AttributeSet
当AttributeSet
存于除了OwnerActor之外的对象上时(对于某个武器来说), 会得到一些关于AttributeSet
的编译错误, 解决办法是在BeginPlay()中构建AttributeSet
而不是在构造函数中, 并在武器类中实现IAbilitySystemInterface
(当你添加武器到玩家Inventory时设置ASC
的指针).
好处:
-
可以使用已有的
GameplayAbility
和GameplayEffect
工作流(弹药使用的Cost GEs等等). -
对于很小的物品集可以快速设置
局限:
-
必须为每个武器类型创建新的
AttributeSet
类,ASC
实际上只能有一个该类的AttributeSet
实例, 因为对Attribute
的修改会在ASC
的SpawnedAttribute数组中寻找其第一个AttributeSet
类实例, 其他相同的AttributeSet
类实例则会被忽略. -
和第1条同样的原因(每个
AttributeSet
类一个AttributeSet
实例), 在玩家的Inventory中每种武器类型只能有一个. -
移除
AttributeSet
是很危险的. 在GASShooter中, 如果玩家因为火箭弹而自杀, 玩家会立即从其Inventory中移除火箭弹发射器(包括其在ASC
中的AttributeSet
), 当服务端同步火箭弹发射器的弹药Attribute
改变时, 由于AttributeSet
在客户端ASC
上不复存在而使游戏崩溃.
在物体中使用单独的ASC
这种方式会有未知的开发成本,以及似乎没有人这么做。
好处:
-
可以使用已有的
GameplayAbility
和GameplayEffect
工作流(弹药使用的Cost GEs等等). -
可以复用
AttributeSet
类(每个武器的ASC
中各一个).
局限:
-
未知的开发成本.
-
甚至方案可行么?
定义AttributeSet
Attribute只能使用C++在Attribute在Attribute头文件定义,我们可以通过宏加到每个AttributeSet的顶部,会自动为每个Attribute生成Getter、Setter以及Init函数。
/*
* 增加下面这段宏,UE将会自动为Attribute创建Init、Get和Set函数
* 但是在Attribute后面需要添加ATTRIBUTE_ACCESSORS宏
*/
#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(UFuseAttributeSetBase, Health)
然后定义OnRep函数
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
在AttributeSet的cpp文件中,我们定义OnRep函数,我们使用预测系统的GAMEPLAYATTRIBUTE_REPNOTIFY填充OnRep函数
void UFuseAttributeSetBase::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UFuseAttributeSetBase, Health, OldHealth);
}
最后, Attribute
需要添加到GetLifetimeReplicatedProps
:
void UGDAttributeSetBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UGDAttributeSetBase, Health, COND_None, REPNOTIFY_Always);
}
初始化Attribute
有多种方法初始化Attribute,Epic建议使用Instant GameplayEffect。如果在定义Attribute是使用了ATTRIBUTE_ACCESSOR的宏,那么Attribute将会自动为每个Attribute生成一个初始化函数。
AttributeSet->InitHealth(100.f);
PreAttributeChange()
PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
是AttributeSet
中的主要函数之一, 其在修改发生前响应Attribute
的CurrentValue变化, 其是通过引用参数NewValue限制(Clamp)CurrentValue即将进行的修改的理想位置.
例如像样例项目那样限制移动速度Modifier
:
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()
函数是由我们在AttributeSet.h
中添加的宏块创建的.
PreAttributeChange()
可以被Attribute
的任何修改触发, 无论是使用Attribute
的setter(由AttributeSet.h
中的宏块定义还是使用GameplayEffect
Note: 在这里做的任何限制都不会永久性地修改ASC
中的Modifier
, 只会修改查询Modifier
的返回值, 这意味着像GameplayEffectExecutionCalculations和ModifierMagnitudeCalculations这种自所有Modifier
重新计算CurrentValue的函数需要再次执行限制(Clamp)操作.
Note: Epic对于PreAttributeChange()的注释说明不要将该函数用于游戏逻辑事件, 而主要在其中做限制操作.
/*
* 这个函数在Attribute的CurrentValue发生变化之前响应,那么对于引入的NewValue,我们需要对其进行限制
* Epic对于PreAttributeChange()的注释说明不要将该函数用于游戏逻辑事件, 而主要在其中做限制操作
*/
void UFuseAttributeSetBase::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
//这个函数会在真正改变Attribute之前调用
Super::PreAttributeChange(Attribute, NewValue);
//如果我们改变了MaxHealth的值,那么我们需要同样改变Health的值,使得其百分比没有改变。
if (Attribute == GetMaxHealthAttribute())
{
ClampMaxAttributeRage(Health, MaxHealth, NewValue, Attribute);
}
else if (Attribute == GetMaxRageAttribute())
{
ClampMaxAttributeRage(Rage, MaxRage, NewValue, Attribute);
}
else if (Attribute == GetMaxShieldAttribute())
{
ClampMaxAttributeRage(Shield, MaxShield, NewValue, Attribute);
}
else if (Attribute == GetMaxStaminaAttribute())
{
ClampMaxAttributeRage(Stamina, MaxStamina, NewValue, Attribute);
}
else
{
NewValue = FMath::Clamp<float>(NewValue, 200.f, 1000.f);
}
}
PostGameplayEffectExecute()
PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
仅在即刻(Instant)GameplayEffect
对Attribute
的BaseValue修改之后触发, 当GameplayEffect对其修改时, 这就是一个处理更多Attribute
操作的有效位置.
例如, 在样例项目中, 我们在这里从生命值Attribute
中减去了最终的伤害值Meta Attribute
, 如果有护盾值Attribute
的话, 我们也会在减除生命值之前从护盾值中减除伤害值. 样例项目也在这里应用了被击打反应动画, 显示浮动的伤害数值和为击杀者分配经验值和赏金. 通过设计, 伤害值Meta Attribute
总是会传递给即刻(Instant)GameplayEffect
而不是Attribute Setter.
其他只会由即刻(Instant)GameplayEffect
修改BaseValue的Attribute
, 像魔法值和耐力值, 也可以在这里被限制为其相应的最大值Attribute
.
Note: 当PostGameplayEffectExecute()被调用时, 对Attribute
的修改已经发生, 但是还没有被同步回客户端, 因此在这里限制值不会造成对客户端的二次同步, 客户端只会接收到限制后的值.
Attribute
Attribute
是由FGameplayAttributeData结构体定义的浮点值,起可以表示角色生命值,角色等级或者一瓶药水的恢复量,如果某个数值属于某个Actor,那么就需要考虑使用Attribute。Attribute
一般智能通过GameplayEffect修改,这样ASC
才能预测其改变。
Attribute
也可以由AttributeSet
定义并存于其中,AttributeSet用于同步那些被标记为Replicated的Attribute。
BaseValue和CurrentValue:
一个Attribute是由两个值组成——一个BaseValue和一个CurrentValue,BaseValue是Attribute的永久值,CurrentValue是BaseValue加上GamplayEffect给定的临时修改值后得到的、例如一个Character拥有一个BaseValue为600的移动速度,当前GamplayEffect并没有改变Attribute,因此CurrentValue的值为600,当Character获得了一个50的速度buff,那么BaseValue仍然为600,但是CurrentValue的值为600+50=650,当buff小时后,CurrentValue将会回到600.
需要注意的一点是,BaseValue并不是Attribute的最大值,对于某个值的最大值我们应该设置单独的Attribute,我们应该在Ablilty或UI中单独定义最大值,并且通过Clamp函数限制Attribute的大小。
(Instant)GameplayEffect
可以永久性的修改BaseValue,而(Duration)
和(Infinite)GameplayEffect
可以修改CurrentValue,(Periodic)GameplayEffect
被视为即刻GameplayEffect,并且可以修改BaseValue。
元(Meta)Attribute:
有一些Attribute作为占位符,用于预计和Attribute交互的临时值,我们通常将伤害值定义为Meta Attribute,使用伤害值Attribute作为占位符,而不是通过GameplayEffect直接修改生命值Attribute,使用Meta Attribute,伤害可以通过GameplayEffectExecutionCalculation中有buff和debuff修改,并且在AttributeSet中进一步操作, 例如, 在最终将生命值减去伤害值之前, 要将伤害值减去当前的护盾值. 伤害值Meta Attribute
在GameplayEffect
之间不是持久化的, 并且可以被任何一方重写. Meta Attribute
一般是不可同步的.
响应Attribute的变化:
监听Attribute的变化我们可以使用UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute)
,该函数返回一个Delegate,可以在这个委托绑定需要调用的函数,这个委托提供一个FOnAttributeChangeData
参数, 其中有NewValue
, OldValue
和FGameplayEffectModCallbackData
.
Note: FGameplayEffectModCallbackData
只能在服务端上设置.
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged);
virtual void HealthChanged(const FOnAttributeChangeData& Data);