## UGameplayEffect UGameplayEffect在框架中主要负责各种数值上的效果,如果技能cd、类似黑魂中的异常效果堆叠与buff,甚至连角色升级时的属性点添加都可以使用它来实现。 因为大多数逻辑都是设置数据子类的操作,所以对于这个类,本人推荐使用蓝图来进行操作。 ## 简单使用教程 通过继承UGameplayEffect来创建一个新的GameplayEffect类,并在构造函数中对相应的属性进行设置。之后在Ability类中调用ApplyGameplayEffectToOwner函数让GameplayEffect生效。 ``` if (CommitAbility(Handle, ActorInfo, ActivationInfo)) // ..then commit the ability... { // Then do more stuff... const UGameplayEffect* GameplayEffect = NewObject(); ApplyGameplayEffectToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, GameplayEffect, 5, 1); K2_EndAbility(); } ``` 具体操作可以参考ActionRPG模板,或者是我的项目代码。 ## Modifiers 本质是一个FGameplayModifierInfo结构体数组用于存储所有数值修改信息。FGameplayModifierInfo包含以下属性: - Attribute 修改的目标属性集中的属性。 - ModifierOp 修改方式。例如Override、Add、Multiply。 - Magnitude(已被废弃) - ModifierMagnitude 修改的数值与类型,可以配置数据表。 - EvaluationChannelSettings (不知道为什么没在编辑器中显示,而且代码中只有一处调用,所以直接跳过) - SourceTags 本身标签行为(Effect生效所需或者忽略的标签) - TargetTags 目标标签行为(Effect生效所需或者忽略的标签) 可以看得出ModifierMagnitude才是Modifiers的关键,而它的本质是FGameplayEffectModifierMagnitude结构体。但是我们只需要学会初始化它即可。它具有以下四种类型: - ScalableFloat 较为简单的使用方式,使用ScalableFloat进行计算 - AttributeBased 基于属性执行计算。 - CustomCalculationClass 能够捕获多个属性进行自定义计算 - SetByCaller 被蓝图或者代码显式设置 ### ScalableFloat的调用示例 ScalableFloat类型是用于设置固定值的简单方式,同时它也支持通过CurveTable配合技能等级设置倍率。(最后结果=固定值*倍率)当然如果你向完全通过CurveTable来控制参数,那就把固定值设置为1即可。 ``` FGameplayModifierInfo info; info.Attribute = FGameplayAttribute(FindFieldChecked(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health))); info.ModifierOp = EGameplayModOp::Additive; //固定值 //info.ModifierMagnitude = FGameplayEffectModifierMagnitude(FScalableFloat(100.0)); //CurveTable控制倍率 FScalableFloat damageValue = {1.0}; FCurveTableRowHandle damageCurve; static ConstructorHelpers::FObjectFinder curveAsset(TEXT("/Game/ActionRPG/DataTable/Damage")); damageCurve.CurveTable = curveAsset.Object; damageCurve.RowName = FName("Damage"); damageValue.Curve = damageCurve; info.ModifierMagnitude = FGameplayEffectModifierMagnitude(damageValue); Modifiers.Add(info); ``` PS.技能等级在ApplyGameplayEffectToOwner函数中设置。 ### AttributeBased的调用示例 最终计算过程可以在CalculateMagnitude函数中找到。 1. 如果尝试捕获到数值(不为None),则将赋值给AttribValue。 2. 判断AttributeCalculationType,来计算对应的AttribValue。(我不太了解代码中channel的概念,如果channel不存在,AttribValue为原本值) 3. 如果AttributeCurve存在,则将AttribValue作为x轴值来查找y轴值,并进行插值计算,最后将结果赋值给AttribValue。 4. 最终计算公式:`$((Coefficient * (AttribValue + PreMultiplyAdditiveValue)) + PostMultiplyAdditiveValue)$` ### BackingAttribute 为GameplayEffect捕获GameplayAttribute的选项。(你可以理解为Lambda表达式的捕获) - AttributeToCapture:捕获属性 - AttributeSource:捕获的目标(自身还是目标对象) - bSnapshot:属性是否需要被快照(没仔细看,如果为false,每次都会重新获取吧) ### AttributeCalculationType 默认值为AttributeMagnitude。 - AttributeMagnitude:使用最后通过属性计算出来的级数 - AttributeBaseValue:使用属性基础值 - AttributeBonusMagnitude:使用(最后计算值-基础值) - AttributeMagnitudeEvaluatedUpToChannel:不清楚使用方法,关键是在编辑器中,这个选项默认是不显示的 ``` FGameplayModifierInfo info; info.Attribute = FGameplayAttribute(FindFieldChecked(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health))); info.ModifierOp = EGameplayModOp::Additive; FAttributeBasedFloat damageValue; damageValue.Coefficient = { 1.2f }; damageValue.PreMultiplyAdditiveValue = { 1.0f }; damageValue.PostMultiplyAdditiveValue = { 2.0f }; damageValue.BackingAttribute = FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health))) , EGameplayEffectAttributeCaptureSource::Source, false); FCurveTableRowHandle damageCurve; static ConstructorHelpers::FObjectFinder curveAsset(TEXT("/Game/ActionRPG/DataTable/Damage")); damageCurve.CurveTable = curveAsset.Object; damageCurve.RowName = FName("Damage"); damageValue.AttributeCurve = damageCurve; damageValue.AttributeCalculationType = EAttributeBasedFloatCalculationType::AttributeMagnitude; info.ModifierMagnitude = damageValue; Modifiers.Add(info); ``` ### CustomCalculationClass的调用示例 与AttributeCalculationType相比,少了属性捕获,多了CalculationClassMagnitude(UGameplayModMagnitudeCalculation类)。 ``` FGameplayModifierInfo info; info.Attribute = FGameplayAttribute(FindFieldChecked(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health))); info.ModifierOp = EGameplayModOp::Additive; FCustomCalculationBasedFloat damageValue; damageValue.CalculationClassMagnitude = UDamageMagnitudeCalculation::StaticClass(); damageValue.Coefficient = { 1.2f }; damageValue.PreMultiplyAdditiveValue = { 1.0f }; damageValue.PostMultiplyAdditiveValue = { 2.0f }; info.ModifierMagnitude = damageValue; Modifiers.Add(info); ``` PS.如果这个计算过程还取决于外部非GameplayAbility框架的条件,那么你可能需要重写GetExternalModifierDependencyMulticast()函数,以获得FOnExternalGameplayModifierDependencyChange委托。从而实现:当外部条件发生改变时,及时更新计算结果。 ### UGameplayModMagnitudeCalculation 你可以通过继承UGameplayModMagnitudeCalculation来创建自定义的Calculation类。所需实现步骤如下: 1. 在构造函数中,向RelevantAttributesToCapture数组添加需要捕获的属性。 2. 实现CalculateBaseMagnitude事件。(因为BlueprintNativeEvent类型,所以既可以在c++里实现也可以在蓝图中实现,关于两者结合可以参考UGameplayAbility类中ActivateAbility()的写法。 案例代码如下: ``` UCLASS(BlueprintType, Blueprintable, Abstract) class ACTIONRPG_API UDamageMagnitudeCalculation : public UGameplayModMagnitudeCalculation { GENERATED_BODY() public: UDamageMagnitudeCalculation(); float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override; }; ``` ``` UDamageMagnitudeCalculation::UDamageMagnitudeCalculation() { RelevantAttributesToCapture.Add(FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health))) , EGameplayEffectAttributeCaptureSource::Source, false)); } float UDamageMagnitudeCalculation::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const { float damage{ 0.0f}; FAggregatorEvaluateParameters InEvalParams; //捕获失败的容错语句 if (!GetCapturedAttributeMagnitude(FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health))) , EGameplayEffectAttributeCaptureSource::Source, false), Spec, InEvalParams, damage)) { //如果这个变量会作为除数的话,不能为0 damage = 1.0f; } return damage; } ``` ## Executions Executions更为简单,而且更加自由。只需要编写Calculation Class即可。它与Modifiers的不同之处在于:一个Modifiers只能修改一个属性,而Executions可以同时改动多个属性。 ### UGameplayEffectExecutionCalculation 这里我就直接复制actionRPG模板的代码了。 开头的RPGDamageStatics结构体与DamageStatics函数,可以减少后面的代码量。可以算是FGameplayEffectAttributeCaptureDefinition的语法糖吧。 ``` UCLASS() class ACTIONRPG_API UDamageExecutionCalculation : public UGameplayEffectExecutionCalculation { GENERATED_BODY() public: UDamageExecutionCalculation(); virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override; }; ``` ``` struct RPGDamageStatics { DECLARE_ATTRIBUTE_CAPTUREDEF(DefensePower); DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower); DECLARE_ATTRIBUTE_CAPTUREDEF(Damage); RPGDamageStatics() { // Capture the Target's DefensePower attribute. Do not snapshot it, because we want to use the health value at the moment we apply the execution. DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DefensePower, Target, false); // Capture the Source's AttackPower. We do want to snapshot this at the moment we create the GameplayEffectSpec that will execute the damage. // (imagine we fire a projectile: we create the GE Spec when the projectile is fired. When it hits the target, we want to use the AttackPower at the moment // the projectile was launched, not when it hits). DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, AttackPower, Source, true); // Also capture the source's raw Damage, which is normally passed in directly via the execution DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, Damage, Source, true); } }; static const RPGDamageStatics& DamageStatics() { static RPGDamageStatics DmgStatics; return DmgStatics; } UDamageExecutionCalculation::UDamageExecutionCalculation() { RelevantAttributesToCapture.Add(DamageStatics().DefensePowerDef); RelevantAttributesToCapture.Add(DamageStatics().AttackPowerDef); RelevantAttributesToCapture.Add(DamageStatics().DamageDef); } void UDamageExecutionCalculation::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(); // 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; // -------------------------------------- // Damage Done = Damage * AttackPower / DefensePower // If DefensePower is 0, it is treated as 1.0 // -------------------------------------- //计算捕获属性的数值。 float DefensePower = 0.f; ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DefensePowerDef, EvaluationParameters, DefensePower); //因为要做除数所以需要加入容错语句 if (DefensePower == 0.0f) { DefensePower = 1.0f; } float AttackPower = 0.f; ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().AttackPowerDef, EvaluationParameters, AttackPower); float Damage = 0.f; ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage); //伤害计算公式 float DamageDone = Damage * AttackPower / DefensePower; if (DamageDone > 0.f) { //这里可以修改多个属性 OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().DamageProperty, EGameplayModOp::Additive, DamageDone)); } } ``` ## Period Period指的是周期,一般用于制作周期性技能。 ``` //持续类型,只有设置为HasDuration,技能才能变成周期性的 DurationPolicy = EGameplayEffectDurationType::HasDuration; //持续时间 DurationMagnitude = FGameplayEffectModifierMagnitude(1.0); //周期,技能生效次数=持续时间/周期 Period = 2.0; ``` # FGameplayEffectContainer与Spec EPIC实现了这个结构体 调用URPGGameplayAbility::MakeEffectContainerSpecFromContainer 使用FGameplayEffectContainer生成FGameplayEffectContainerSpec结构体。 Spec是实例版本,存储TargetDataHandle与EffectSpecHandle。通过MakeEffectContainerSpecFromContainer进行实例化(但本质是通过Spec的AddTarget进行数据填充)。 之后再通过 ``` ReturnSpec.TargetGameplayEffectSpecs.Add(MakeOutgoingGameplayEffectSpec(EffectClass, OverrideGameplayLevel)); ``` 填充EffectSpec数据。 **MakeEffectContainerSpec则是个快捷函数** 通过FGameplayTag寻找对应的Effect与Target数据。EventData则用于调用TargetType类的GetTarget函数,用于获取符合要求的目标(Actor)。 在ActionRPG中URPGTargetType_UseEventData的GetTarget用到了EventData。大致逻辑为首先寻找EventData里是否带有EventData.HitResult信息(可以在Send Event To Actor中设置),如果没有则返回EventData.Target信息。 ``` void URPGTargetType_UseEventData::GetTargets_Implementation(ARPGCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, TArray& OutHitResults, TArray& OutActors) const { const FHitResult* FoundHitResult = EventData.ContextHandle.GetHitResult(); if (FoundHitResult) { OutHitResults.Add(*FoundHitResult); } else if (EventData.Target) { OutActors.Add(const_cast(EventData.Target)); } } ``` ApplyEffectContainer则是个方便函数。 # 实现在AnimNotify中向指定目标引用GameplayEffect 从GameplayAbilityComponent或者从GameplayAbility中设置. MakeOutgoingGameplayEffectSpec=> ApplyGameplayEffectSpecToTarget 位于UGameplayAbility ApplyGameplayEffectToTarget GameplayEffectSpec.GetContext().AddTarget() RemoveGrantedByEffect()函数可以移除Ability中Instance类型的Effect。非常适合来清除翻滚免伤、技能硬直效果。 ``` FRPGGameplayEffectContainerSpec URPGBlueprintLibrary::AddTargetsToEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec, const TArray& HitResults, const TArray& TargetActors) { FRPGGameplayEffectContainerSpec NewSpec = ContainerSpec; NewSpec.AddTargets(HitResults, TargetActors); return NewSpec; } TArray URPGBlueprintLibrary::ApplyExternalEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec) { TArray AllEffects; // Iterate list of gameplay effects for (const FGameplayEffectSpecHandle& SpecHandle : ContainerSpec.TargetGameplayEffectSpecs) { if (SpecHandle.IsValid()) { // If effect is valid, iterate list of targets and apply to all for (TSharedPtr Data : ContainerSpec.TargetData.Data) { AllEffects.Append(Data->ApplyGameplayEffectSpec(*SpecHandle.Data.Get())); } } } return AllEffects; } ``` ``` FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(Handle, ActorInfo, ActivationInfo, GameplayEffectClass, GameplayEffectLevel); if (SpecHandle.Data.IsValid()) { SpecHandle.Data->StackCount = Stacks; SCOPE_CYCLE_UOBJECT(Source, SpecHandle.Data->GetContext().GetSourceObject()); EffectHandles.Append(ApplyGameplayEffectSpecToTarget(Handle, ActorInfo, ActivationInfo, SpecHandle, Target)); } ``` # 实用函数 Wait Input Release