AttributeSet和Attribute

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。

我们有三种实现的方式

  1. 在物品中简单使用浮点值
  2. 在物品中使用单独的AttributeSet。
  3. 在物品中使用单独的ASC

使用单独的浮点值

在武器的实力中存储浮点值而不使用Attribute,对于武器,我们可以存储同步的浮点数(COND_OwrnerOnly),例如最大弹匣量,弹匣中的子弹量,剩余子弹量。如果武器需要共享剩余弹药量,我们可以将剩余弹药量移动到Character中的AttributeSet中作为一个Attribute。

在射击游戏中,会出现抢先反向同步弹药量使得本地弹药量出现跳变,即由于延迟很高并且在一些射速高的五其中,客户端的子弹来不及和服务器进行同步,造成子弹数量减少又增多的情况,这个时候可以使用客户端预测(Blaster中),也可以在玩家开火的时候直接禁止同步,本质上就是客户端做自己的本地预测。

有点:

  • 避免了Attribute的局限。

缺点:

  • 不能使用GE
  • 要求重写UGameplayAbility中的关键函数来检查和应用枪械中浮点数的花销(Cost).

在物体中使用AttributeSet

AttributeSet存于除了OwnerActor之外的对象上时(对于某个武器来说), 会得到一些关于AttributeSet的编译错误, 解决办法是在BeginPlay()中构建AttributeSet而不是在构造函数中, 并在武器类中实现IAbilitySystemInterface(当你添加武器到玩家Inventory时设置ASC的指针).

好处:

  • 可以使用已有的GameplayAbilityGameplayEffect工作流(弹药使用的Cost GEs等等).

  • 对于很小的物品集可以快速设置

局限:

  • 必须为每个武器类型创建新的AttributeSet类, ASC实际上只能有一个该类的AttributeSet实例, 因为对Attribute的修改会在ASC的SpawnedAttribute数组中寻找其第一个AttributeSet类实例, 其他相同的AttributeSet类实例则会被忽略.

  • 和第1条同样的原因(每个AttributeSet类一个AttributeSet实例), 在玩家的Inventory中每种武器类型只能有一个.

  • 移除AttributeSet是很危险的. 在GASShooter中, 如果玩家因为火箭弹而自杀, 玩家会立即从其Inventory中移除火箭弹发射器(包括其在ASC中的AttributeSet), 当服务端同步火箭弹发射器的弹药Attribute改变时, 由于AttributeSet在客户端ASC上不复存在而使游戏崩溃.

在物体中使用单独的ASC

这种方式会有未知的开发成本,以及似乎没有人这么做。

好处:

  • 可以使用已有的GameplayAbilityGameplayEffect工作流(弹药使用的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)GameplayEffectAttribute的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 AttributeGameplayEffect之间不是持久化的, 并且可以被任何一方重写. Meta Attribute一般是不可同步的.

响应Attribute的变化:

监听Attribute的变化我们可以使用UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute),该函数返回一个Delegate,可以在这个委托绑定需要调用的函数,这个委托提供一个FOnAttributeChangeData参数, 其中有NewValue, OldValueFGameplayEffectModCallbackData.

Note: FGameplayEffectModCallbackData只能在服务端上设置.

AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged);

virtual void HealthChanged(const FOnAttributeChangeData& Data);

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值