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