跳到主要内容
信息
文章中可能会出现一些错误,希望大佬们可以在评论区指出错误,感谢支持!

04 - Gameplay Effect

本文主要说明了UE5中Gameplay技能系统(Gameplay Ability System, GAS)中有关Gameplay Effect的相关内容。

概述

GAS使用GE来改变目标Actor的Attribute值或Gameplay Tags,它由一组改变Actor属性的函数库组成,包括一些立即效果(如施加伤害)和持续性效果(如中毒)。Gameplay Effect(后文简称GE)属于UE5中的静态资产,因此不能在运行时改变它的相关配置;而Gameplay Effect Spec(后文简称GESpec)则是GE的运行时版本,它是封装GE和相关工具函数的实例,在运行时很有用。

配置项

GE的常用配置项如下:

Duration

该配置项可设置GE的持续时间,是否可重复以及重复周期。具体可设置成如下三项:

  1. Instant:让GE立即生效;
  2. Has Duration:让GE持续生效一段时间,时间到后去除;
  3. Infinite:让GE无限持续生效,只有主动移除它才会失效;

其中Instant选项会永久改变受影响Attribute的基础值;后两个选项则是会暂时改变受影响Attribute的当前值,且可撤回,只有修改周期Period后才永久改变受影响Attribute的基础值。

Component

组件可赋予GE各种各样的行为,具体可添加的组件如下:

  • UChanceToApplyGameplayEffectComponent:让GE变成概率施加;
  • UBlockAbilityTagsGameplayEffectComponent:负责阻止基于Gameplay Tags激活的Gameplay Ability,针对拥有GE的目标Actor;
  • UAssetTagsGameplayEffectComponent:让GE资产拥有Tags,不会转移至任何Actor;
  • UAdditionalEffectsGameplayEffectComponent:添加附加的GEs,它们可在特定条件下激活(或无条件);
  • UTargetTagsGameplayEffectComponent:将Tags授予GE的目标或拥有者Actor;
  • UTargetTagRequirementsGameplayEffectComponent:特定对目标(GE的拥有者)Tags的需求,并据此决定该GE是否施加或继续执行;
  • URemoveOtherGameplayEffectComponent:基于特定条件,移除其他GE;
  • UCustomCanApplyGameplayEffectComponent:负责配置CustomApplicationRequirement函数,确认该GE是否应被施加;
  • UImmunityGameplayEffectComponent:负责阻止其他GESpec的施加;

Modifier

GE通过Modifier来改变目标Actor属性值,它的配置项主要有计算符号Modifier Op和计算类型Modifier Magnitude

计算符号 Modifier Op

计算符号如下所示:

  • Add:加法
  • Multiply:乘法
  • Divide:除法
  • Override:直接覆盖

计算类型 Modifier Magnitude

计算类型则如下所示:

  • Scalable Float:硬编码值或曲线上某点的值
  • Attribute Based:基于Source或Target的属性计算值
  • Custom Calculation Class:继承UGameplayModMagnitudeCalculation类,自定义值的计算
  • Set by Caller:在运行时由GESpec对值进行设置

我们常用的是第二和第三种计算类型:

Attribute Based

该计算类型可以基于Source或Target的属性值计算要修改属性的值,计算公式为:

Value=Coefficient×(PreMultiplyAdditiveValue + 要参考的属性值) + PostMultiplyAdditiveValue\nonumber \mathrm{Value =Coefficient \times (PreMultiplyAdditiveValue \ + \ \text{要参考的属性值}) \ + \ PostMultiplyAdditiveValue}

该计算类型主要有如下配置项:

  • Coefficient、PreMulplyAdditiveValue 和 PostMultiplyAdditiveValue:参与上述计算公式的配置项,可以是一个值,也可以是一个曲线;
  • Backing Attribute:此项是要参考的属性的相关设置,包含属性名,属性来源对象和是否快照snapshot。如果需要快照,那么参考的属性值在该GE应用时及以后均被确定;如果不需要快照,那么参考的属性值将会实时读取;
Custom Calculation Class

如果基于Actor属性的计算无法满足需求,可以选择该计算类型,它通过继承UGameplayModMagnitudeCalculation类(后文简称MMC类),重写float CalculateBaseMagnitude_Implementation()函数以自定义属性值的计算(例如获取其他Actor的属性和它的变量来计算该属性值)。

例如在Aura项目中,主角Aura的MaxHealth属性基于如下公式计算:

MaxHealth=80.0+2.5×Vigor+10.0×PlayerLevel\nonumber \mathrm{MaxHealth = 80.0 + 2.5 \times Vigor + 10.0 \times PlayerLevel}

可以发现,MaxHealth属性依赖于参考属性Vigor和玩家等级变量PlayerLevel,适合通过该计算类型计算。相关MMC类代码如下:

MaxHealthMMC.h点击展开/折叠代码
UCLASS()
class AURA_API UMaxHealthMMC : public UGameplayModMagnitudeCalculation
{
GENERATED_BODY()

public:
UMaxHealthMMC();

virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;

private:
FGameplayEffectAttributeCaptureDefinition VigorDef;
};

MaxHealthMMC.cpp点击展开/折叠代码
UMaxHealthMMC::UMaxHealthMMC()
{
// 1.初始化要捕获的Attribute信息:
VigorDef.AttributeToCapture = UAuraAttributeSet::GetVigorAttribute();
VigorDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
VigorDef.bSnapshot = false;
// 2.将该Attribute信息加入要捕获的Attribute数组中
RelevantAttributesToCapture.Add(VigorDef);
}

float UMaxHealthMMC::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
// 获取Source Actor和Target Actor的GameplayTags待用
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

// 用于获取参考Attributes值函数的参数
FAggregatorEvaluateParameters EvalParams;
EvalParams.SourceTags = SourceTags;
EvalParams.TargetTags = TargetTags;

// 获取目标Attribute值
float TargetVigorValue = 0.f;
GetCapturedAttributeMagnitude(VigorDef, Spec, EvalParams, TargetVigorValue);

// 自定义计算: MaxHealth = 80.0 + 2.5 * Vigor + 10.0 * PlayerLevel
TargetVigorValue = FMath::Max<float>(TargetVigorValue, 0.f);
float MaxHealth = 0.f;
if (UObject* Player = Spec.GetContext().GetSourceObject();
Player && Player->Implements<UCombatInterface>())
{
const TScriptInterface<ICombatInterface> PlayerCombatInterface = TScriptInterface<ICombatInterface>(Player);
const int32 PlayerLevel = PlayerCombatInterface->GetActorLevel();
MaxHealth = 80.f + 2.5f * TargetVigorValue + 10.f * PlayerLevel;
}
else
{
UE_LOG(LogTemp, Error, TEXT("[%hs] Target actor is not player (Not implements ICombatInterface)"), __FUNCTION__);
}

return MaxHealth;
}



其中:

  • 为了获取Aura参考属性Vigor,要定义并初始化属性描述相关信息FGameplayEffectAttributeCaptureDefinition,并将其加入要捕获Attribute数组RelevantAttributesToCapture中;
  • 在MMC计算函数中,通过GetCapturedAttributeMagnitude()获取要参考的Attribute信息,最终返回计算好的float类型的MaxHealth值;

实现MMC类后,就能在编辑器中编辑相关GE了:

多个Modifier的执行顺序

如果一个GE中有多个Modifier,执行顺序为该数组的索引顺序(从小到大)。

Execution

GE通过Execution实现更多自定义逻辑,这需要我们自行编写GameplayEffectExecutionCalculation逻辑。

Gameplay Cues

GE还可触发一些Gameplay Cues,有关Gameplay Cues详见 文章(TODO)。

Stacking

负责多个GE生效时的堆叠处理,可通过Limit Count设置该类GE的最大堆叠上限,通过Stacking Type设置堆叠种类。后者的有两个选项:

  1. Aggregate by Source:多个Source Actor为当前Target Actor施加GE时,为每个Source Actor持有Stack计数。即同一Source Actor对任意Target Actor施加超过当前计数的GE时,超过计数的GE暂时不生效。
  2. Aggregate by Target:多个Source Actor为当前Targe t Actor施加GE时,为当前Target Actor持有Stack计数。即任意Source Actor对同一Target Actor施加超过计数的GE时,超过计数的GE暂时不生效。

常用操作

对目标Actor施加GE

要想对目标Actor施加GE,需要完成如下步骤:

  1. 获取目标Actor自己的ASC;
  2. 通过TargetASC->MakeEffectContext(),创建FGameplayEffectContexxtHandle(封装GEContext和工具函数),并通过它的AddSourceXXX()成员函数添加Source Actor或其他Source UObject类;
  3. 通过TargetASC->MakeOutgoingSpec()创建FGameplayEffectSpecHandle(封装GESpec和工具函数),将GE实例化;
  4. 通过TargetASC->ApplayGameplayEffectSpecToSelf(),对目标Actor的ASC施加GE;
  5. 【可选】对于持续时间无限的GE,需要存储激活它的FActiveGameplayEffectHandle,以便后续通过TargetASC->RemoveActiveGameplayEffect()移除;

例如在Aura项目中,Source Actor 对 Target Actor 施加 GE 的通用函数如下:

AuraEffectActor.cpp点击展开/折叠代码
void AAuraEffectActor::ApplyEffectToTarget(AActor* InTargetActor, const FEffectActorGE& InEffectActorGE)
{
if (InTargetActor->Implements<UAbilitySystemInterface>())
{
checkf(InEffectActorGE.GEClass, TEXT("[%hs] GEClass is empty, please fill out in editor!"), __FUNCTION__);

// 1. 获取Target自己的ASC
const TScriptInterface<IAbilitySystemInterface> ASCInterface = TScriptInterface<IAbilitySystemInterface>(
InTargetActor);
UAbilitySystemComponent* TargetActorAsc = ASCInterface->GetAbilitySystemComponent();
// 2. 创建GEContextHandle(封装GEContext + 工具函数)
FGameplayEffectContextHandle GEContextHandle = TargetActorAsc->MakeEffectContext();
GEContextHandle.AddSourceObject(this);
// 3. 创建GESpecHandle(封装GESpec + 工具函数)
const FGameplayEffectSpecHandle GESpecHandle = TargetActorAsc->MakeOutgoingSpec(
InEffectActorGE.GEClass, InEffectActorGE.GELevel, GEContextHandle);
// 4. 对Target自己的ASC施加GE
const FActiveGameplayEffectHandle ActiveGEHandle = TargetActorAsc->ApplyGameplayEffectSpecToSelf(
*GESpecHandle.Data);

// 5. 对于Infinite类GE, 需要存储激活它的FActiveGameplayEffectHandle, 以便后续移除它
if (GESpecHandle.Data->Def->DurationPolicy == EGameplayEffectDurationType::Infinite &&
InEffectActorGE.GERemovalPolicy == EEffectRemovalPolicy::EERP_RemoveOnEndOverlap)
{
ActiveInfiniteGEHandles.Add(ActiveGEHandle, TargetActorAsc);
}
}

// 获取ASC的另一个方法:
// UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Target);
}

参考资料