GE的主要用途是通过改变目标或自身的Attribute或者Tags实现诸如造成伤害、治疗、强化、削弱等效果,GE也提供了Execution来执行逻辑,提供了相当大的灵活性。

(我个人觉得GE是整个GAS框架中基本逻辑最为复杂的部分,真的有好多好多功能=。=)

GE的数据结构

GE是一个纯数据蓝图,不能添加任何逻辑,它在执行时,会根据数据创建一个GameplayEffectSpec的实例用来产生效果。

GE通常不需要拓展,策划只需要创建UGameplayEffet的子类即可。

持续类型

GE有三种持续类型:瞬时(Instant)、持续(Duration)、永久(Infinite)

  • Instant:适用于产生一次性效果,如伤害、治疗等,InstantGE的Modifiers会立即永久改变Attribute的BaseValue。InstantGE无法向角色添加Tag。
  • Duration:持续一段时间的GE,持续时间在GE上配置。可以修改Atrribute的CurrentValue。可以向角色添加Tag,并在GE过期或被移除时自动移除。
  • Infinite:与Duration类似,但不会过期,必须手动移除。

 

GE有两种方式去修改属性,分别是修改器(Modifiers)和操作器(Executions)。

Modifiers(修改器)

一个GE可以配置多个修改器,每个修改器只能修改一个属性(Attribute)

修改器提供以下4种修改属性的方式(ModifierOp):

  • Add:加。
  • Multiply:乘。
  • Divide:初。
  • Override:覆盖。

多个对同一属性的修改,会通过聚合器叠加到属性的CurrentValue上,标准的聚合器是FAggregatorModChannel:EvaluateWithBase,其聚合公式如下:

(BaseValue+Add)*Mutiply/Divide

Override修改会直接覆盖最终值,如果有多个Override修改器,只有一个会生效。

标签过滤:修改器可以进行标签过滤,根据Source和Target身上的标签情况,决定修改器是否生效。可以做一些类似“目标中毒时,降低50%防御力”的需求。

Modifier Magnitude(修改值)

修改值方面,GE提供了4种方式:

  • ScalableFloat:写死一个浮点值。最简单的方式,不用多说。
  • AttributeBase:基于属性值算出一个值。(看到这我惊了,功能真特么强大)
    • 取一个属性Attribute
    • 可选属性来自Source还是Target。
    • 可选是取BaseValue,还是CurrentValue,还是CurrentValue-BaseValue的变化值。
    • 可选是否快照,快照会抓取GE添加时刻的属性值,不快照的话则会跟着变。
    • 用这个属性,按照(Value+PreMultiplyAdditiveValue)*Coeffcient+PostMultiplyAdditiveValue得出最终值,这三个值是可配的。
    • 这里的参数和属性,可以配置一个曲线表格,但我还没研究明白怎么玩。
  • CustomCalculationClass:适用于更加复杂灵活的修改,你需要创建一个ModifierMagnitudeCalculation(MMC)类,在其中计算出一个Float,然后通过Pre/Post/Coeffcient进一步修改。这个MMC类可以做很多奇怪的事情,或者说很多依赖Buff的奇怪的东西都适合写在这。
  • SetByCaller:这种方式,是在GE的Spec创建之后,再由Ability传入一个值,例如技能的蓄力时间越长,伤害越高。使用起来比较麻烦,在这里不做介绍。

Modifier Magnitude Calculation(MMC)

MMC很牛逼,单拿出来讨论一下。MMC也是ModifierMagnitude的一种,所以他需要返回一个float值,MMC通过CalculateBaseMagnitude_Implementation返回这个值,你的子类C++类或者蓝图应该重写这个方法。

这里贴一段示例项目中的代码,它实现了一个类似法力燃烧的效果。

UPAMMC_PoisonMana::UPAMMC_PoisonMana()
{
 
    //ManaDef defined in header FGameplayEffectAttributeCaptureDefinition ManaDef;
    ManaDef.AttributeToCapture = UPAAttributeSetBase::GetManaAttribute();
    ManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
    ManaDef.bSnapshot = false;
 
    //MaxManaDef defined in header FGameplayEffectAttributeCaptureDefinition MaxManaDef;
    MaxManaDef.AttributeToCapture = UPAAttributeSetBase::GetMaxManaAttribute();
    MaxManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
    MaxManaDef.bSnapshot = false;
 
    RelevantAttributesToCapture.Add(ManaDef);
    RelevantAttributesToCapture.Add(MaxManaDef);
}
 
float UPAMMC_PoisonMana::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
    // Gather the tags from the source and target as that can affect which buffs should be used
    const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
    const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
 
    FAggregatorEvaluateParameters EvaluationParameters;
    EvaluationParameters.SourceTags = SourceTags;
    EvaluationParameters.TargetTags = TargetTags;
 
    float Mana = 0.f;
    GetCapturedAttributeMagnitude(ManaDef, Spec, EvaluationParameters, Mana);
    Mana = FMath::Max<float>(Mana, 0.0f);
 
    float MaxMana = 0.f;
    GetCapturedAttributeMagnitude(MaxManaDef, Spec, EvaluationParameters, MaxMana);
    MaxMana = FMath::Max<float>(MaxMana, 1.0f); // Avoid divide by zero
 
    float Reduction = -20.0f;
    if (Mana / MaxMana > 0.5f)
    {
        //半蓝以上削蓝翻倍
        Reduction *= 2;
    }
     
    if (TargetTags->HasTagExact(FGameplayTag::RequestGameplayTag(FName("Status.WeakToPoisonMana"))))
    {
        //如果目标有削蓝强化,削蓝翻倍
        Reduction *= 2;
    }
     
    return Reduction;
}

 

ExecutionClac(操作器)

Execution仅能用于InstantGE或者Periodic,如果我想要在GE添加或间隔触发时做一些事情,比如一个Dot造成伤害时,如果目标的生命值少于30%,那么就立刻杀死他。这个逻辑就适合放在EC中去做。

你需要创建一个UGameplayEffectExecutionCalculation的子类,并且重写Execute_Implementation方法。

示例项目做了一个EC来处理伤害受到护甲削减,以及爆头的伤害增加以及标签,同样贴上源码:

void UGSDamageExecutionCalc::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
    UAbilitySystemComponent* TargetAbilitySystemComponent = ExecutionParams.GetTargetAbilitySystemComponent();
    UAbilitySystemComponent* SourceAbilitySystemComponent = ExecutionParams.GetSourceAbilitySystemComponent();
 
    AActor* SourceActor = SourceAbilitySystemComponent ? SourceAbilitySystemComponent->AvatarActor : nullptr;
    AActor* TargetActor = TargetAbilitySystemComponent ? TargetAbilitySystemComponent->AvatarActor : nullptr;
 
    const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
    FGameplayTagContainer AssetTags;
    Spec.GetAllAssetTags(AssetTags);
 
    // Gather the tags from the source and target as that can affect which buffs should be used
    const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
    const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
 
    FAggregatorEvaluateParameters EvaluationParameters;
    EvaluationParameters.SourceTags = SourceTags;
    EvaluationParameters.TargetTags = TargetTags;
 
    float Armor = 0.0f;
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, Armor);
    Armor = FMath::Max<float>(Armor, 0.0f);
 
    float Damage = 0.0f;
    // Capture optional damage value set on the damage GE as a CalculationModifier under the ExecutionCalculation
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);
    // Add SetByCaller damage if it exists
    Damage += FMath::Max<float>(Spec.GetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), false, -1.0f), 0.0f);
 
    float UnmitigatedDamage = Damage; // Can multiply any damage boosters here
 
    // Check for headshot. There's only one character mesh here, but you could have a function on your Character class to return the head bone name
    const FHitResult* Hit = Spec.GetContext().GetHitResult();//这里通过GE上下文拿到了命中数据,GE上下文里存了很多东西,下面会说。
    if (AssetTags.HasTagExact(FGameplayTag::RequestGameplayTag(FName("Effect.Damage.CanHeadShot"))) && Hit && Hit->BoneName == "b_head")
    {
        UnmitigatedDamage *= HeadShotMultiplier;
        FGameplayEffectSpec* MutableSpec = ExecutionParams.GetOwningSpecForPreExecuteMod();
        MutableSpec->DynamicAssetTags.AddTag(FGameplayTag::RequestGameplayTag(FName("Effect.Damage.HeadShot")));//他给这个GE动态加了一个爆头标签,这样待会处理伤害数字的时候,他就知道要显示一个暴击数字了
    }
 
    float MitigatedDamage = (UnmitigatedDamage) * (100 / (100 + Armor));
 
    if (MitigatedDamage > 0.f)
    {
        // Set the Target's damage meta attribute
        OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().DamageProperty, EGameplayModOp::Additive, MitigatedDamage));
    }
}

我感觉这个功能放在MMC中实现更合适一些,因为他最后把伤害值塞进了MMC中,然后用SetByCaller读取它。(也许MMC不支持某些特性?)

Period(间隔触发)

一个Duration或Infinite的GE可以间隔地执行Modifiers和Executions,要被看成是Instant,每次都会永久性地修改BaseValue。

周期:可以配置一个浮点值,或者一个曲线,作为触发的间隔时间。

ExecutePeriodicEffectOnApplication:是否在GE添加时立刻执行一次。

Periodic Inhibtion policy:被抑制之后的策略,当这个GE因为Tag条件等原因被禁止时,间隔触发应该怎么做?

  • Never Reset:不重置,间隔执行器仿佛没停一样,会在再次激活后,在本来会执行的时刻执行。
  • Reset Period:重置,间隔时间会在再次激活后的一个周期之后执行。
  • Execute And Reset Period:被禁用时立刻执行一次,然后重置。

Application(激活条件)

Change To Apply To Target:添加概率,可以配置一个Float值。

Application Requirement:这个可以认为是一个自定义激活条件,与Tag条件共存,可以配置若干个UGameplayEffectCustomApplicationRequirement的子类,可以简称GER

这个类只有一个bool CanApplyGameplayEffect(const UGameplayEffect* GameplayEffect, const FGameplayEffectSpec& Spec, UAbilitySystemComponent* ASC)接口,它返回一个布尔值,只有所有的GER都为true时,效果才能成功添加。

Stacking(叠层)

这里不得不提一下,在GE的持续时间类型上,将永久和有限持续作为静态互斥的类型区分,让叠层变得很便捷,不需要考虑有限和无限的GE叠层的时候,时间刷新的问题。

Stacking Type:叠层类型。

  • 按源聚合:每个施放者添加的GE会分别叠层。
  • 按目标聚合:所有施放者添加的GE都会叠层。

Statck Limit Count:叠层上限。

Stack Duration Refresh Policy:是否刷新持续时间。

Stack Period Reset Policy:是否在叠层时刷新间隔触发的周期。

Stack Expiration Policy:到期时候的策略。

  • 移除所有叠层:就直接移除。
  • 移除一层叠层,并且刷新持续时间。
  • 刷新持续时间:选这个会把GE变相搞成无限持续的。源码说,策划提了个需求,是每次到期之后,叠层变化的规则是不确定的,比如每次到期之后加1层,如果身上有XXTag就加2层,然后他们弄了这个类型,然后在OnStackCountChange中手动处理这些需求(如果选了这个类型,层数不会自动变,但是会触发回调)。

Overflow(溢出)

指的是叠层打到上限时,再上一个新的GE的时候,会触发溢出。

Overflow Effects:在一个会引起溢出GE尝试添加的时候,就会向Target添加的GE。注意:不管那个触发了溢出的GE是否添加成功,这些Effect都会添加!

Deny Overflow Application:禁用溢出。就是说一个新的GE加上来的时候,如果会引起溢出,就否决它,仿佛什么都没有发生。但会正常触发溢出,添加Overflow Effects。

Clear Stack On Overflow:清除所有叠层。只有开启了Deny Overflow Application之后才能勾选。

Expiration(过期处理)

Premature Expiration Effect Classes:过早移除时添加的GE。可以配若干个。

Routine Expiration Effect Classes:常规移除时添加的GE。可以配若干个。

Immunity(免疫)

GrantedApplicationImmunityTags:检查持有者是否符合指定的标签需求。

GrantedApplicationImmunityQuery:相当于上一条的强化版,除了Tags之外,还能检查很多其他的条件。

标签判断

有数个具备不同功能的标签容器,每个标签容器包括Add和Remove两部分,看源码的意思,貌似是能让策划更灵活的方式编辑标签组。

Gameplay Effect Asset Tag:这个GE的Tag,不是加给Actor的Tag。

GrantedTags:加给目标的标签。

Ongoing Tag Requiements:如果目标不满足这组Tags,GE将会被关闭,知道满足时会再次打开。

Application Tag Requirements:目标身上有这些标签时,GE才能被激活。

Removal Tag Requirements:如果遇到这些标签,GE将会被移除,如果目标身上有这些标签,GE也上不去。

Remove Gameplay Effects With Tags:当GE被激活时,如果目标身上的GE的AssetTags或者GrantedTags有这些标签,这些GE将会被移除。

 

显示

Require Modifier Success to Trigger Cues:至少有一个修改器成功时,才显示Cues。

Suppress Stacking Cues:如果是个叠层的GE,那么只有第一层显示Cues,如果不勾,那么每层都会创建一个Cues。

Gameplay Cues :配置Cues。

UIData:GE的UI相关的数据,一个UGameplayEffectUIData的子类,这是个空类,你需要自己实现并解析它。

 

赋予Ability

GE可以赋予目标新的GA,InstantGE不能赋予GA。

官方提到的一个典型的用例,是给目标添加击退、击飞等GA,并自动激活。

应用GameplayEffect

给角色应用一个GE的最基本方法,就是在其ASC上调用UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf(),GA也提供了多个方法来应用GE,但本质还是还是调上面那个接口。

GE是一个纯数据蓝图,本身没有任何逻辑,你需要先以GE类创建一个GameplayEffectSpec对象,然后再把它Apply到目标的ASC上。

如果你想通过子弹给目标添加GE,你可以先通过GA创建出一个GESpec对象,然后把它传给子弹Actor,然后在碰撞发生时Apply它。

GameplayEffectSpec(游戏效果细则)

GESpec是GE的实例化数据,是一个struct,它包含了GameplayEffect的信息,以及包括施放者,等级等实例所需的数据,它会在Apply的时候创建FActiveGameplayEffect,作为实时数据。

GESpec旨在从技能施放,到GE实际上产生效果这两个时间点期间,去收集所需的信息,比如炮弹的落点等。

 

GESpec通过UAbilityStstemComponent::MakeOutGoingSpec()以一个GE为模板所创建。并且在调用Apply之后,创建FActiveGameplayEffect(AGE)。

GESpec可以调整的内容:

  • GEClass
  • 持续时间
  • 等级
  • 周期间隔
  • 堆叠数
  • 上下文(包含GE的施放者、应用的目标等,源码说你可以拓展这个类,但是同时要改一大堆东西….)
  • 属性快照
  • 标签组
  • SetByCaller映射

移除GameplayEffect

除了利用GE本身的规则移除它之外,手动在目标身上调用RemoveActiveGameplayEffect也可以移除一个GE。

GA的冷却时间GE

GA可以设置一个GE来管理其冷却时间,这个GE必须是一个DurationGE。另外需要在CooldownTag中配置每个GA的位移GameplayTag。GA的运行时检查的,就是角色身上是否有这个Tag。

一般来说一个技能的冷却时间是定义在GA上的,所以如果用一般的方式,你得给每个GA都配置一个冷却GE。

想要用一个通用的GE解决冷却时间问题,可以通过GE的DurationMagnitude,用一个Tag版本的SetByCaller解决:

 

首先给GA定义一个float类型的冷却时间,以及一个FGameplayTagContainer 用来配置冷却标签。

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;
 
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;
 
// Temp container that we will return the pointer to in GetCooldownTags().
// This will be a union of our CooldownTags and the Cooldown GE's cooldown tags.
UPROPERTY()
FGameplayTagContainer TempCooldownTags;

 

然后重写GA的GetCooldownTags(),将配置的GE添加到MutableTaggs中。

const FGameplayTagContainer * UPGGameplayAbility::GetCooldownTags() const
{
    FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
    const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
    if (ParentTags)
    {
        MutableTags->AppendTags(*ParentTags);
    }
    MutableTags->AppendTags(CooldownTags);
    return MutableTags;
}

最后向SetByCaller中写入冷却时间

void UPGGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
    UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
    if (CooldownGE)
    {
        FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
        SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CooldownTags);
        SpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName(  OurSetByCallerTag  )), CooldownDuration.GetValueAtLevel(GetAbilityLevel()));
        ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
    }
}

查询剩余的冷却时间

bool APGPlayerState::GetCooldownRemainingForTag(FGameplayTagContainer CooldownTags, float & TimeRemaining, float & CooldownDuration)
{
    if (AbilitySystemComponent && CooldownTags.Num() > 0)
    {
        TimeRemaining = 0.f;
        CooldownDuration = 0.f;
 
        FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTags);
        TArray< TPair<floatfloat> > DurationAndTimeRemaining = AbilitySystemComponent->GetActiveEffectsTimeRemainingAndDuration(Query);
        if (DurationAndTimeRemaining.Num() > 0)
        {
            int32 BestIdx = 0;
            float LongestTime = DurationAndTimeRemaining[0].Key;
            for (int32 Idx = 1; Idx < DurationAndTimeRemaining.Num(); ++Idx)
            {
                if (DurationAndTimeRemaining[Idx].Key > LongestTime)
                {
                    LongestTime = DurationAndTimeRemaining[Idx].Key;
                    BestIdx = Idx;
                }
            }
 
            TimeRemaining = DurationAndTimeRemaining[BestIdx].Key;
            CooldownDuration = DurationAndTimeRemaining[BestIdx].Value;
 
            return true;
        }
    }
 
    return false;
}

要判断冷却开始和结束,这个我推荐监听冷却Tag的添加和移除,因为GE不一定复制,而Tag是统一复制的,监听Tag比较省心。

AbilitySystemComponent->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved)

操作冷却时间

要修改GESpec的Duration,然后要更新

1.StartServerWorldTime.

2.CachedStartServerWorldTime.

3.StartWorldTime.

然后调用CheckDuration()更新持续时间,然后手动调用GESpec的Broadcast()和ASC的OnGameplayEffectDurationChange();

bool UPAAbilitySystemComponent::SetGameplayEffectDurationHandle(FActiveGameplayEffectHandle Handle, float NewDuration)
{
    if (!Handle.IsValid())
    {
        return false;
    }
 
    const FActiveGameplayEffect* ActiveGameplayEffect = GetActiveGameplayEffect(Handle);
    if (!ActiveGameplayEffect)
    {
        return false;
    }
 
    FActiveGameplayEffect* AGE = const_cast<FActiveGameplayEffect*>(ActiveGameplayEffect);
    if (NewDuration > 0)
    {
        AGE->Spec.Duration = NewDuration;
    }
    else
    {
        AGE->Spec.Duration = 0.01f;
    }
 
    AGE->StartServerWorldTime = ActiveGameplayEffects.GetServerWorldTime();
    AGE->CachedStartServerWorldTime = AGE->StartServerWorldTime;
    AGE->StartWorldTime = ActiveGameplayEffects.GetWorldTime();
    ActiveGameplayEffects.MarkItemDirty(*AGE);
    ActiveGameplayEffects.CheckDuration(Handle);
 
    AGE->EventSet.OnTimeChanged.Broadcast(AGE->Handle, AGE->StartWorldTime, AGE->GetDuration());
    OnGameplayEffectDurationChange(*AGE);
 
    return true;
}

GA的消耗GE

跟冷却GE类似,可以实现一个MMC来从GA中读取消耗值。

float UPGMMC_HeroAbilityCost::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
    const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(Spec.GetContext().GetAbilityInstance_NotReplicated());
 
    if (!Ability)
    {
        return 0.0f;
    }
 
    return Ability->Cost.GetValueAtLevel(Ability->GetAbilityLevel());
}

原文地址:http://www.cnblogs.com/yutuerzf/p/16804675.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性