在这篇文章开始前,先分享一个惨痛的经历,就因为在虚幻四的源码中加了两句注释,项目的编译就走向了拥有3000+ Errors的不归路 T T,这是啥原理啊。
这次我们要实现的功能是角色的冲刺奔跑,操作就是点击shift后角色的移动速度会增加。这个能力的实现应该是挺简单的,但是我会扩展一部分的GAS源码,深入一下GAS的Attribute,希望能够帮助到一部分读者。有问题也希望大家可以在评论或者私信告诉我。
接下来进入正题,首先还是讲解一下加速跑的实现过程:
打开AttributeSetBase.h/cpp,然后添加MoveSpeed Attribute。这个操作流程已经重复无数次啦,相信能看到这里的读者应该都掌握了,我就不重复了。
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Attributes", ReplicatedUsing=OnRep_MoveSpeed)
FGameplayAttributeData MoveSpeed;
ATTRIBUTE_ACCESSORS(UAttributeSetBase, MoveSpeed);
UFUNCTION()
virtual void OnRep_MoveSpeed(const FGameplayAttributeData& OldMoveSpeed);
但是我们创建了这么多次的Attibute,还不了解它的数据结构是咋样的,打开AttributeSetBase的父类AttributeSet.h。我删除了其它代码,重要的是下面的部分。它表明一个Attribute中有两个float值Base Value,和Current Value。
USTRUCT(BlueprintType)
struct GAMEPLAYABILITIES_API FGameplayAttributeData
{
protected:
UPROPERTY(BlueprintReadOnly, Category = "Attribute")
float BaseValue;
UPROPERTY(BlueprintReadOnly, Category = "Attribute")
float CurrentValue;
};
为什么属性需要Base和Current两个值呢?根据大佬的文档https://github.com/tranek/GASDocumentation#concepts-asc, Base Value的值属性的基础,它是永久(permanent)的,Current Value相反,它是临时的。
打个比方,比如我们要实现的加速奔跑,它改变的是实际是Current Value,当加速效果结束后,Current Value就会降低回默认值。类似的效果还有某种buff会暂时提高角色的生命值或者护甲等等,这个buff改变的就是这种属性的Current Value。
相反,当角色的生命值被攻击后扣除,它改变的就是Health属性的Base Value,可以把攻击造成的伤害当作是永久改变的,毕竟没有其它影响角色的生命值就不会发生改变了。
那么哪些操作会改变Base值,哪些会改变Current值呢?打开一个Gameplay Effect,然后点击Duration Policy,可以看到有三种模式,其中Instant改变的是Base值,Infinite和Has Duration改变的是Current Value,因为某种意义上这两个效果是有持续时间的。但是Duration policy的特殊情况,当它存在period的时候,它改变的同样是Base值,因为Period Duration可以理解为每一个period触发一次的instant。
创建完毕后,记得需要给予MoveSpeed Attribute一个初值。
创建Gameplay Ability命名为GA_Sprint,作为加速跑技能。创建Gameplay Effect命名为GE_Sprint_SpeedUp负责提高移动速度。
打开GE_Sprint_SpeedUp。技能效果为永久(Infinite)提高MoveSpeed属性2000,同时需要给该GE添加Tag为Ability.Sprint.SpeedUp
然后打开GA_Sprint。Sprint的流程为对拥有该技能的角色申请一个GE,就是刚才我们创建的那个。然后等待Gameplay Event,这个事件带有标签Ability.Sprint.EndAbility。接收到事件后,移除带有Ability.Sprint.SpeedUp标签的GE,然后结束能力。
打开角色蓝图。
添加能力(Give Ability),绑定输入。这里当shift松开后,会向自己发送一个Gameplay Event,带有标签Ability.Sprint.EndSprint。这就是在能力中等待的事件。
打开CharacterBase.h/cpp,然后创建function
void OnMoveSpeedAttributeChanged(const FOnAttributeChangeData& Data);
实现,当接收到发生改变的Attribute后,改变MovementComponent的MaxWalkSpeed,注意,这里不能直接通过GetCharacterMovement()修改移动速度。
void ACharacterBase::OnMoveSpeedAttributeChanged(const FOnAttributeChangeData& Data)
{
UCharacterMovementComponent *MovementPtr = Cast<UCharacterMovementComponent>(GetCharacterMovement());
MovementPtr->MaxWalkSpeed = Data.NewValue;
}
然后在BeginPlay中将该function与MoveSpeed的Change Delegate绑定。
void ACharacterBase::BeginPlay()
{
Super::BeginPlay();
if(AbilitySystem)
{
AbilitySystem->GetGameplayAttributeValueChangeDelegate(UAttributeSetBase::GetMoveSpeedAttribute())
.AddUObject(this, &ACharacterBase::OnMoveSpeedAttributeChanged);
}
}
刚才实现的相应Attribute改变实际上发生在属性值已经改变后,但是我们处理最大生命值,生命值不能小于0等事件不适合在角色代码里实现,实际上GAS系统提供了接口可以在属性值发生改变前进行处理。
重写两个方法。PreAttributeChange负责在属性值的Current Value发生改变前进行处理。PostGameplayEffectExecute发生在Base Value发生改变前。
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
我们可以在这里设置属性值的范围。比如下图的处理就可以让生命值保持在0到100之间。
void UAttributeSetBase::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
SetHealth(FMath::Clamp(GetHealth(), 0.0f, 100.0f));
}
}