首页 > 其他分享 >游戏效果(GameplayEffect)

游戏效果(GameplayEffect)

时间:2022-10-19 00:04:42浏览次数:59  
标签:const 游戏 效果 GameplayEffect float GE Tag GA Spec

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()); }

标签:const,游戏,效果,GameplayEffect,float,GE,Tag,GA,Spec
From: https://www.cnblogs.com/yutuerzf/p/16804675.html

相关文章