UE5-Aura笔记-GameplayEffect

GameplayEffects

GameplayEffects介绍

img

GameplayEffect 是UGameplayEffect类型的对象,我们使用GameplayEffect 来改变属性和Tags,

GameplayEffect 只用作于处理数据,而不执行逻辑,其通过ModifiersExecutions 来修改属性,通常设计者只需要创建UGameplayEffect的蓝图派生类

img

修改器(Modifiers)用于修改属性并且是属性修改预测的仅有方式。一个GameplayEffect 可以有0个或多个Modifiers。每一个修改器只能通过下述方式修改一个属性:Add,Mutiply,DIvide,Override

AttributeCurrentValue 是一系列Modifiers添加到BaseValue的聚合结果(就像Vue中的computed,每当Effect变化,都会重新执行该公式重新计算当前属性)。聚合Modifiers的公式如下所示(FAggregatorModChannel::EvaluateWithBase

((InlineBaseValue + Additive) * Multiplicitive) / Division

一共有四种Modifiers:Scalable Float, Attribute Based, Custom Calculation Class, and Set By Caller。这些全部通过浮点值和操作符改变ModifierAttribute

修改器类型描述
Scalable FloatFScalableFloats是一种能够指向Data Table(行表示变量,列表示等级)的结构。 Scalable Floats将根据当前技能等级(或者是在GameplayEffectSpec覆盖的等级)自动读取值。这个值可以根据系数进一步处理。如果没有指定数据表,值会被当作是1,需要硬编码系数作为实际的值(忽略等级)
Attribute BasedAttribute Based Modifiers 基于源(GameplayEffectSpec创建者)或目标(GameplayEffectSpec接收者)的支持属性的CurrentValue 或BaseValue并且可通过系数、Pre/Post Multiply Additive Value进一步处理。快照(Snapshot)意味着取GameplayEffectSpec被创建时属性的值,否则取GameplayEffectSpec被应用时属性的值。
Custom Calculation ClassCustom Calculation Class 是最灵活和最复杂的Modifiers。这个Modifier需要创建一个ModifierMagnitudeCalculation类并且可通过系数、Pre/Post Multiply Additive Value进一步处理。
Set By CallerSetByCaller Modifiers是在GameplayEffect之外由Ability在运行时设置或者由GameplayEffectSpec的创建者设置。 例如,当你想根据按钮按下的时间决定伤害大小时可以使用SetByCaller。SetByCallers本质是存在于GameplayEffectSpec上的TMap<FGameplayTag, float>,Modifier仅仅是告诉聚合器通过GameplayTag去检索值。 SetByCallers仅能使用GameplayTag不能使用FName。如果没有在GameplayEffectSpec中找到GameplayTag对应的值,游戏将会抛出一个运行时错误并且返回0。如果运算是除法你就悲剧了。 具体使用详见SetByCallers

GameplayEffectExecutionCalculations (ExecutionCalculation, Execution(在插件代码中经常会看到这个术语), or ExecCalc) 是GameplayEffects改变ASC的一种最有力的方式。与ModifierMagnitudeCalculations类似,ExecCalc可以获取Attributes。 与MMCs不同的是,ExecCalc可以改变多个属性并且高效的处理任何事情。

img

GameplayEffects有三种持续类型:立即(Instant),持续( Duration),和无限(Infinite)。

后面我们再逐一使用他们来构建Gameplay

改进之前的EffectActor

立即效果

在之前有一个AuraEffectActor,我们把它当作生命药水来使用

//TODO Change this to apply Gameplay Effect. But now,using const_cast as a hack

我们有这样一个TODO,现在我们就要使用GameplayEffect来改进它

我们清空整个EffectActor,我们想要整个Actor在蓝图中很灵活且方便控制,建立一个Root根组件,可以在蓝图中配置子项

//AuraEffectActor.cpp
AAuraEffectActor::AAuraEffectActor()
{
    PrimaryActorTick.bCanEverTick = true;
    SetRootComponent(CreateDefaultSubobject<USceneComponent>("SceneRoot"));
}

然后在蓝图中配置BP_HealthPotion

img

img

然后添加一个sphere,大小调整,然后设置Mesh中的碰撞为NoCollision

img

然后我们在蓝图事件中

放置一个节点,组件开始重叠时,我们就要为目标添加一个Effect,回到C++中

//AAuraEffectActor.h
//...
protected:
    virtual void BeginPlay() override;
    //要对目标添加的EffectClass
    UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
    TSubclassOf<UGameplayEffect> InstantGameplayEffectClass;
    //对目标添加Effects的执行逻辑
    UFUNCTION(BlueprintCallable)
    void ApplyEffectToTarget(AActor* Target,TSubclassOf<UGameplayEffect> GameplayEffectClass);
};
//AAuraEffectActor.cpp
void AAuraEffectActor::ApplyEffectToTarget(AActor* Target, TSubclassOf<UGameplayEffect> GameplayEffectClass)
{
    //这个函数是工具库,可以获取实现了IAbilitySystem接口的Actor的AbilitySystem
    UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Target);
    if(!TargetASC) return;
    //检查是否Class不为null
    check(GameplayEffectClass)
    //获取ASC的EffectContext
    FGameplayEffectContextHandle EffectContextHandle = TargetASC->MakeEffectContext();
    //为ContextHandle添加源(就是这个效果的添加者)
    EffectContextHandle.AddSourceObject(this);
    //制作一个Spec
    FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(GameplayEffectClass,1.f,EffectContextHandle);
    //为Target添加效果
    TargetASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
}
//...

然后在蓝图中,创建GE_PotionHeal,作为GameplayEffect的子类,我们在其中设置治疗效果

img

然后设置InstantGameplayEffectClass,并在重叠时调用ApplyEffectToTarge t

img

在网络模式下也正常

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Quest:制作Mana药水

img

现在生命药水和法力药水都做好了

持续时间效果

创建一个基于持续时间的效果,在10秒内增加自己的最大生命值100,10秒后撤回

img

在AuraEffectActor.h中添加一个DurationGameplayEffectClass

//AuraEffectActor.h
//...
protected:
//...
    UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
    TSubclassOf<UGameplayEffect> DurationGameplayEffectClass;
//...

然后直接在蓝图中设置

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

img

img

周期性持续时间效果

周期性效果,在10秒内,每秒回复10血量(在之前持续效果的水晶中修改)

img

  • Period 周期,每隔一段时间后会触发
  • Exectute Periodic Effect on Application 立即执行第一个周期的效果,否则等待一个周期后执行

img

然后就可以发现血量每隔一秒回复一次

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

无限持续时间效果

我们想要做一个火堆,在角色踩上时,会一直造成伤害

img

火堆

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

事件

img

Effect

img

但是在游戏中,我们想要离开火堆时,也会持续永久造成伤害

所以,我们想要设置一些ApplicationPolicy来规范化代码

//AuraEffectActor.h
UENUM(BlueprintType)
//在Overlap开始时还是结束时应用Effect效果
enum class EEffectApplicationPolicy
{
    ApplyOnOverlap,
    ApplyOnEndOverlap,
    DoNotApply
};

//在Overlap结束时是否移除Effect效果(无限时长的Effect)
UENUM(BlueprintType)
enum class EEffectRemovalPolicy
{
    RemoveOnEndOverlap,
    DoNotRemove
};
//...

protected:
    UFUNCTION(BlueprintCallable)
    void OnOverlap(AActor* TargetActor);
    
    UFUNCTION(BlueprintCallable)
    void OnEndOverlap(AActor* TargetActor);

//...
    //是否在效果移除时摧毁自身
    UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
    bool bDestroyOnEffectRemoval = false;
//...
    UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
    EEffectApplicationPolicy InstantEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
    
    UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
    EEffectApplicationPolicy DurationEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
    UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
    EEffectApplicationPolicy InfiniteEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
    UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
    EEffectRemovalPolicy InfiniteEffectRemovalPolicy = EEffectRemovalPolicy::DoNotRemove;

我们想要经过一系列判断,来确定某个Effect类是在Overlap开始时调用还是结束时调用还是不调用,或者某个无限时长的Effect类是否要在Overlap结束时移除

//AuraEffectActor.cpp
void AAuraEffectActor::OnOverlap(AActor* TargetActor)
{
    if(InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)
    {
       ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass);
    }
    if(DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)
    {
       ApplyEffectToTarget(TargetActor,DurationGameplayEffectClass);
    }
    if(InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)
    {
       ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass);
    }
}

void AAuraEffectActor::OnEndOverlap(AActor* TargetActor)
{
    if(InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
    {
       ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass);
    }
    if(DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
    {
        ApplyEffectToTarget(TargetActor,DurationGameplayEffectClass);
    }
    if(InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
    {
        ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass);
    }
    if(InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
    {
       //去除InfiniteEffect
    }
}

但是,我们现在还不知道如何去除InfiniteEffect

//去除附着在Actor上的GameplayEffect函数
UAbilitySystemComponent::RemoveActiveGameplayEffect(FActiveGameplayEffectHandle Handle);

有了这个函数,我们就可以做到去除InfiniteEffect了,首先我们要先获取到FActiveGameplayEffectHandle

AAuraEffectActor::ApplyEffectToTarget(...){
//...
    //为Target添加效果,然后获取FActiveGameplayEffectHandle 
    FActiveGameplayEffectHandle ActiveEffectHandle = 
        TargetASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
    //如果这个Effect为无限时长的类,且当前无限时长类的policy为RemovedOnEndOverlap,
    //则需要存储ActiveEffectHandle,以便后续删除
    bool bIsInfinite = false;
    if(EffectSpecHandle.Data->Def.Get()->DurationPolicy == EGameplayEffectDurationType::Infinite)
    {
        bIsInfinite = true;
    }
    if(bIsInfinite && InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
    {
        ActiveEffectHandles.Add(ActiveEffectHandle,TargetASC);
    }
//...
AAuraEffectActor::OnEndOverlap(...){
//...

    if(InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
    {
        //去除InfiniteEffect
        UAbilitySystemComponent* TargetASC = 
            UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
        TArray<FActiveGameplayEffectHandle> RemovedKeys;
        for(TTuple<FActiveGameplayEffectHandle, UAbilitySystemComponent*>& Tuple:ActiveEffectHandles)
        {
           if(TargetASC == Tuple.Value)
           {
              TargetASC->RemoveActiveGameplayEffect(Tuple.Key);
              RemovedKeys.Add(Tuple.Key);
           }
        }
        for(auto& Key:RemovedKeys)
        {
           ActiveEffectHandles.Remove(Key);
        }
    }
 }
//AuraEffectActor.h
//...
    TMap<FActiveGameplayEffectHandle ,UAbilitySystemComponent*> ActiveEffectHandles;
  • ActiveEffectHandles

键值对**ActiveEffectHandles** 是用来存储FActiveGameplayEffectHandle的,为什么要以FActiveGameplayEffectHandle 为key呢?只有FActiveGameplayEffectHandle 不会造成key冲突,如果以UAbilitySystemComponent或者AActor为key的话,都有可能造成冲突,导致value被更改;

最后就是修改蓝图,将InfiniteEffectRemovalPolicy改为RemovedOnEndOverlap

此时,我们就达成了目的

img

如果将3个火堆堆在一起,然后stackSize设为3,当我们从3个火堆的区域进入到2个火堆区域时,所有的stack都消除了,这是因为RemoveActiveGameplayEffect函数有第二个参数,stack,默认为-1 UAbilitySystemComponent::RemoveActiveGameplayEffect(FActiveGameplayEffectHandle Handle,int32 StacksToRemove =-1);

-1就代表将所有stack清空;当我们将第二个参数设为1后,3个火堆退出到2个火堆时,就不会出现stack全部消除的情况了

PreAttributeChange

在之前的扣血时,我们发现,血量可以被扣到了负数,但是这不应该发生,所以我们应该加一个扣血前的回调函数,让血量不会下降到0以下,或者上升到MaxHealth以上

我们应该对其进行限制,其方法就是一个虚函数PreAttributeChange ,官方文档是如下解释的:

//AttributeSet.h
/**
 *  Called just before any modification happens to an attribute. This is lower level than PreAttributeModify/PostAttribute modify.
 *  There is no additional context provided here since anything can trigger this. Executed effects, duration based effects, effects being removed, immunity being applied, stacking rules changing, etc.
 *  This function is meant to enforce things like "Health = Clamp(Health, 0, MaxHealth)" and NOT things like "trigger this extra thing if damage is applied, etc".
 *  
 *  NewValue is a mutable reference so you are able to clamp the newly applied value as well.
 */
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) { }
  • 文档释义
    • 这个函数会在任何属性被修改之前调用。属性可以是游戏角色的状态(例如 健康值法力值力量 等)。

    • 它比其他修改属性的回调(如 PreAttributeModifyPostAttributeModify)更低层次。即,这个函数是在更底层的情况下运行的,发生在更早的阶段。

    • 调用此函数时,没有提供任何额外的上下文信息,因为任何操作都可能触发它。比如,可能是因为效果应用、持续时间影响、效果移除、免疫应用、叠加规则改变等。

    • 它可以在以下几种场景中被触发:

    • 执行的效果(例如:增加伤害、治疗等)

    • 基于持续时间的效果(例如:随着时间的推移逐渐增加或减少属性)

    • 效果被移除(例如:移除一个增益或减益效果)

    • 免疫应用(例如:免疫某些效果的影响)

    • 叠加规则的变化(例如:属性叠加的规则发生改变)

    • 这个函数的主要作用是强制执行某些约束或规则,比如限制某个属性的值在一个特定范围内。

    • 举例来说,如果是生命值(Health),可以在这个函数中强制将其限定在一个范围,比如:

      • 不能低于 0(防止出现负数生命值)
      • 不能高于最大生命值(MaxHealth
    • 类似 Health = Clamp(Health, 0, MaxHealth) 的操作。

    • 此函数 应该用于触发额外的操作,比如检测是否应用了伤害并触发额外效果。它的目的是对属性进行简单的约束,而不是执行复杂的逻辑。

    • 在属性发生改变之前,你可以对即将应用的新值(NewValue)进行处理或限制,例如对其进行 Clamp 操作(将其限制在某个范围内)。

如上所说的Clamp 是虚幻中FMath工具类中的方法,将NewValue限制到某个范围内,所以我们现在就可以开始改代码了,我们对Health和Mana进行限制

//AuraAttributeSet.h
//...
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
//AuraAttributeSet.cpp
//...
void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
    Super::PreAttributeChange(Attribute, NewValue);
    if(Attribute == GetHealthAttribute())
    {
       NewValue = FMath::Clamp(NewValue,0.f,GetMaxHealth());
    }
    if(Attribute == GetManaAttribute())
    {
       NewValue = FMath::Clamp(NewValue,0.f,GetMaxMana());
    }
}

但是,NewValue 指的是当前的属性值,这个值可以受到多个修改器的叠加影响。

PreAttributeChange 里,你可以影响即将应用的新值,但 它不会永久性地改变任何修改器,也就是说它不改变修改器的基础值,只是暂时调整 返回的属性值

之后,系统会从所有影响这个属性的 修改器(Modifiers)重新计算 ,并将其作为最终的属性值。因此,任何变化都可能会被叠加或再次修改。

比如,我们修改PotionHeal中的Modifiers,当前比如95滴血,满血100

img

喝到生命药水后是92滴血还是97滴呢?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

97滴血,这就证明了,虽然在PreAttributeChange 里Clamp了NewValue,还是经过了又一次的Modifier计算,然后最终为97滴而不是先加10后减8的92滴血,所以,PreAttributeChange 不是最终结果,我们不能在里面去进行一些效果比如"反伤刺甲",在受到Effect的时候去进行其他操作

那么有什么方法能够得到应用某个Effect之后最终的Attribute值呢?

PostGameplayEffectExecute

PostGameplayEffectExecute 是一种常用于自定义处理游戏效果执行后的逻辑的方法。它通常在派生自 UGameplayEffectExecutionCalculationUGameplayAbility 类的类中重载,用于在游戏效果执行后进行进一步处理。

我们可以在PostGameplayEffectExecute 中获取关于源、目标和Effect的所有信息

//AuraAttributeSet.h
//...

//能够在PostGameplayEffectExecute中获取到的信息,放到一个结构体中方便获取
USTRUCT()
struct FEffectProperties
{
    GENERATED_BODY()
    FEffectProperties(){}
    FGameplayEffectContextHandle EffectContextHandle;
    //Source 代表这个Effect是从哪个Actor释放的,Target代表自身(拥有该AttributeSet的角色)
    
    UPROPERTY()
    UAbilitySystemComponent* SourceASC=nullptr;
    UPROPERTY()
    AActor* SourceAvatarActor=nullptr;
    UPROPERTY()
    AController* SourceController=nullptr;
    UPROPERTY()
    ACharacter* SourceCharacter=nullptr;
    UPROPERTY()
    UAbilitySystemComponent* TargetASC=nullptr;
    UPROPERTY()
    AActor* TargetAvatarActor=nullptr;
    UPROPERTY()
    AController* TargetController=nullptr;
    UPROPERTY()
    ACharacter* TargetCharacter=nullptr;
};

//...

public:
    //在GameplayEffect被添加时的回调
    virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) override;
    
private:
    //从PostGameplayEffectExecute的Data中获取所有的属性,包括ContextHandle,Actor,Character等等
    void SetEffectProperties(const FGameplayEffectModCallbackData& Data,FEffectProperties& Props);
//AuraAttributeSet.cpp
//...

void UAuraAttributeSet::SetEffectProperties(const struct FGameplayEffectModCallbackData& Data,FEffectProperties& Props)
{
    Props.EffectContextHandle = Data.EffectSpec.GetContext();
    Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
    if(IsValid(Props.SourceASC) && Props.SourceASC->AbilityActorInfo.IsValid()
       && Props.SourceASC->AbilityActorInfo->AvatarActor.IsValid())
    {
       Props.SourceAvatarActor = Props.SourceASC->AbilityActorInfo->AvatarActor.Get();
       Props.SourceController = Props.SourceASC->AbilityActorInfo->PlayerController.Get();
       //如果GetPlayerController为null,可能该Controller不是PlayerController
       if(Props.SourceController == nullptr && Props.SourceAvatarActor != nullptr)
       {
          if(const APawn* Pawn = Cast<APawn>(Props.SourceAvatarActor))
          {
             Props.SourceController = Pawn->GetController();
          }
       }
       if(Props.SourceController)
       {
          Props.SourceCharacter = Cast<ACharacter>(Props.SourceController->GetPawn());
       }
    }


    if(Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
    {
       Props.TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
       Props.TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
       Props.TargetCharacter = Cast<ACharacter>(Props.TargetAvatarActor);
       Props.TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Props.TargetAvatarActor);
    }
}

void UAuraAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
    Super::PostGameplayEffectExecute(Data);
    FEffectProperties Props;
    //在这之后可以使用Props来进行各种操作了
}
//...

CurveTable

img

在Effect的设置中,可以看到,除了使用固定值,还可以使用CurveTable曲线表)来去设置修改值

我们创建一个曲线表来试试

img

然后进入表格,点击三角形来添加点和数据

img

然后点击图表就可以看到整个曲线了

img

然后在PotionHeal中添加该CurveTable

img

我们发现实际修改值是我们的静态数字乘以CurveTable的当前值

但是,CurveTable中的键,在EffectActor中并不存在,所以我们需要在EffectActor中添加一些"药水等级",来让它当作CurveTable的键

在ApplyEffectToTarget方法中,在创建Spec的时候我们其实传递了一个level 1.f,所以当我们捡起药水时,它其实会将level = 1 来当作CurveTable的键

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们要添加一个 ActorLevel 而不是硬编码这个level

//AuraEffectActor.h
//...
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
float ActorLevel = 1.f;
//...


//AuraEffectActor.cpp
//...ApplyEffectToTarget
//制作一个Spec
FGameplayEffectSpecHandle EffectSpecHandle = 
    TargetASC->MakeOutgoingSpec(GameplayEffectClass,ActorLevel,EffectContextHandle);
//...

在这里就可以设置ActorLevel了

img

而且,ActorLevel是float,也就意味着可以为小数,小数对应着的就是图表线上的点

我们可以添加多个曲线,然后让法力值也可以运用这个曲线

img

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值