|
|
преди 4 месеца | |
|---|---|---|
| .. | ||
| Image | преди 4 месеца | |
| README.md | преди 4 месеца | |
关于 GE (Gameplay Effect) 有一些想法
一个 Effect 需要考虑哪些功能呢?
| 虚函数 | 作用 | 返回值 |
|---|---|---|
| PreGameplayEffectExecute | 效果执行执行前触发 | 返回 false 可阻止效果应用 |
| PostGameplayEffectExecute | 处理效果后的状态更新 | |
| PreAttributeChange | 任何属性修改前调用(包括直接修改) | |
| PostAttributeChange | 属性修改后调用 | |
| PreAttributeBaseChange | 属性基础值修改前调用(Aggregator存在时) | |
| PostAttributeBaseChange | 属性基础值修改后调用 | |
| OnAttributeAggregatorCreated | 自定义属性聚合规则 |
在 UAbilitySystemComponent::InitializeComponent 的时候,会获取绑定对象上所有的 UAttributeSet 并将其保存在 SpawnedAttributes 属性中
void UAbilitySystemComponent::InitializeComponent()
{
Super::InitializeComponent();
// Do Something ...
TArray<UObject*> ChildObjects;
GetObjectsWithOuter(Owner, ChildObjects, false, RF_NoFlags, EInternalObjectFlags::Garbage);
for (UObject* Obj : ChildObjects)
{
UAttributeSet* Set = Cast<UAttributeSet>(Obj);
if (Set)
{
SpawnedAttributes.AddUnique(Set);
}
}
SetSpawnedAttributesListDirty();
}
所以在角色中需要定义 UAttributeSet 属性
class LYRAGAME_API ALyraCharacterWithAbilities : public ALyraCharacter
{
private:
UPROPERTY(VisibleAnywhere, Category = "Lyra|PlayerState")
TObjectPtr<ULyraAbilitySystemComponent> AbilitySystemComponent;
UPROPERTY()
TObjectPtr<const class ULyraHealthSet> HealthSet;
UPROPERTY()
TObjectPtr<const class ULyraCombatSet> CombatSet;
}
然后在构造函数中初始化 UAttributeSet 属性 和 技能系统组件 (ULyraAbilitySystemComponent)
ALyraCharacterWithAbilities::ALyraCharacterWithAbilities(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
AbilitySystemComponent = ObjectInitializer.CreateDefaultSubobject<ULyraAbilitySystemComponent>(this, TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
HealthSet = CreateDefaultSubobject<ULyraHealthSet>(TEXT("HealthSet"));
CombatSet = CreateDefaultSubobject<ULyraCombatSet>(TEXT("CombatSet"));
}
Lyra 继承
UAbilitySystemComponent实现了自己的ULyraAbilitySystemComponent
通过上面的代码,可以发现一个角色可以存在多个 UAttributeSet
Duration 类型,持续时间是 3s,Period 为 0,作用是 Add HP 50%Instant 类型,作用是 Add HP -5先执行 A,再执行 B
Current Value = 40.0 Base Value = 40.0
Current Value = 60.0 Base Value = 40.0
Current Value = 52.5 Base Value = 35.0
Current Value = 35.0 Base Value = 35.0 // 3s 之后
很明显,由于 Instant 是直接修改 BaseValue,导致效果 A 基于 BaseValue 增加 50% 后计算得到值是 52.5
从玩家体验来看,理论上来说应该扣除 5 点血量,剩余 55 血量,结果实际上是 52.5。对用户来说这就是明显的 Bug
这里关于
Instant属性修改,可以先看下面 GE 的时间周期分类
于是乎,我们想到一种解决方案
以血量为例,除开 HP、MaxHP 之外,再额外加上 ExtraHP 和 Damage
对血量的操作并不直接作用于 HP,而是作用于 ExtraHP
CurrentValue 为 55,即 HP + ExtraHP - DamageExtraHP 的值动态修改 Damage 值,此时 ExtraHP 为 0,Damage 值为 0(因为 20 > 5,如果 Damage 值为 30,那么效果 A 结束时 Damage 更新为 10)CurrentValue 为 HP + ExtraHP - Damage = 4临时性 HP 修改全部作用于 ExtraHP,比如 效果A,或者其他 Buff、Debuff
直接扣除 HP 的全部作用于 Damage
增加当前 HP 的全部作用于 HP
以 Lyra 为例
首先对生命值和最大生命值设置取值范围
void ULyraHealthSet::ClampAttribute(const FGameplayAttribute& Attribute, float& NewValue) const
{
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
}
else if (Attribute == GetMaxHealthAttribute())
{
NewValue = FMath::Max(NewValue, 1.0f);
}
}
剩下的就是在合适调用 ClampAttribute
通常在什么时候会对属性进行修改? GE 执行的时候
于是在 GE 执行之后,将属性值限制在范围内即可
void ULyraHealthSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
// ... do something
else if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
SetHealth(FMath::Clamp(GetHealth(), MinimumHealth, GetMaxHealth()));
}
else if (Data.EvaluatedData.Attribute == GetMaxHealthAttribute())
{
OnMaxHealthChanged.Broadcast(Instigator, Causer, &Data.EffectSpec, Data.EvaluatedData.Magnitude, MaxHealthBeforeAttributeChange, GetMaxHealth());
}
}
当然还有一些其他情况,比如说在 C++ 中拿到 AttributeSet 然后强行修改对应属性,应对这种状态也有对应函数接口
在 PreAttributeBaseChange 和 PreAttributeChange 中,将将要设置的属性值进行修改,保证值在允许的区间范围内
在 PostAttributeChange 函数中判断是否修改了最大血量,如果当前血量大于最大血量,限制当前血量的值
void ULyraHealthSet::PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const
{
Super::PreAttributeBaseChange(Attribute, NewValue);
ClampAttribute(Attribute, NewValue);
}
void ULyraHealthSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);
ClampAttribute(Attribute, NewValue);
}
void ULyraHealthSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
{
Super::PostAttributeChange(Attribute, OldValue, NewValue);
if (Attribute == GetMaxHealthAttribute())
{
// Make sure current health is not greater than the new max health.
if (GetHealth() > NewValue)
{
ULyraAbilitySystemComponent* LyraASC = GetLyraAbilitySystemComponent();
check(LyraASC);
LyraASC->ApplyModToAttribute(GetHealthAttribute(), EGameplayModOp::Override, NewValue);
}
}
if (bOutOfHealth && (GetHealth() > 0.0f))
{
bOutOfHealth = false;
}
}
聚合器,用于存储对一个属性的所有操作
由于一些 GE 是临时对属性进行修改,在 GE 到期或者因为其他 GE 而被抑制的时候,需要撤销对属性的修改
如果单独使用一个 float 来记录属性是不合适的,因为在 GE 撤销的时候无法对属性进行准确的还原
通常的做法是,记录一个 BaseValue,记录基于 BaseValue 进行一系列的操作 Mod,最后通过 BaseValue 和 Mod 可以得到 CurrentValue 也就是最终值
最终显示给玩家的,可能是多个属性集合起来的效果
USTRUCT(BlueprintType)
struct GAMEPLAYABILITIES_API FGameplayAttributeData
{
// Some Functions ...
protected:
UPROPERTY(BlueprintReadOnly, Category = "Attribute")
float BaseValue;
UPROPERTY(BlueprintReadOnly, Category = "Attribute")
float CurrentValue;
}
FAggregatorModInfo 用于存储对属性的一次操作的信息
struct GAMEPLAYABILITIES_API FAggregatorModInfo
{
EGameplayModEvaluationChannel Channel;
EGameplayModOp::Type Op;
const FAggregatorMod* Mod;
};
Op 就是 Operator,就是操作
UE 提供 四种 操作:加、乘、除、覆盖
| 类型 | 含义 |
|---|---|
| EGameplayModOp::Additive | 加 |
| EGameplayModOp::Multiplicitive | 乘 |
| EGameplayModOp::Division | 除 |
| EGameplayModOp::Override | 覆盖 |
| EGameplayModOp::Max | 无效操作 |
没有减法,因为加一个负数,就是减法
FAggregatorMod 存储的是具体操作的数值
struct GAMEPLAYABILITIES_API FAggregatorMod
{
// Some Functions ...
const FGameplayTagRequirements* SourceTagReqs; // 施加效果者的标签
const FGameplayTagRequirements* TargetTagReqs; // 被施加效果者的标签
float EvaluatedMagnitude; // 计算后的最终修改值
float StackCount; // 效果堆叠机制
FActiveGameplayEffectHandle ActiveHandle;
bool IsPredicted; // 是否是预测生成的
private:
mutable bool IsQualified; // 该 Mod 是否有效
}
不过这个结构体,通常是用于记录一些数据传递给外界,运行时本质存储并非如此
FAggregatorModChannel 用于存储一个 Channel 中的所有 Mod
Channel不知道怎么翻译,一般可以说是 通道
UENUM()
enum class EGameplayModEvaluationChannel : uint8
{
Channel0 UMETA(Hidden),
Channel1 UMETA(Hidden),
Channel2 UMETA(Hidden),
Channel3 UMETA(Hidden),
Channel4 UMETA(Hidden),
Channel5 UMETA(Hidden),
Channel6 UMETA(Hidden),
Channel7 UMETA(Hidden),
Channel8 UMETA(Hidden),
Channel9 UMETA(Hidden),
// Always keep last
Channel_MAX UMETA(Hidden)
};
虚幻给出了 10 个 Channel
至于 Channel 的作用就是将 Channel0 计算的值,作为基础值给 Channel1,再将 Channel1 的值作为基础值给 Channel2
for (auto& ChannelEntry : ModChannelsMap)
{
const FAggregatorModChannel& CurChannel = ChannelEntry.Value;
ComputedValue = CurChannel.EvaluateWithBase(ComputedValue, Parameters);
}
用于对不同权重 Mod 进行操作
比如,游戏中 伤害 通常是 (基础攻击力 + 装备攻击力) * 攻击倍率 * 伤害倍率 * 增伤倍率
可以将 基础攻击力 + 装备攻击力 记录在 Channel0 通道中,计算得到值
将 攻击倍率 记录在 Channel1 通道中,将 伤害倍率 记录在 Channel2 中
实际情况可能更加复杂,这里只是为了说明
Channel的使用,而举例
FAggregatorModChannel 结构体本身比较简单
struct GAMEPLAYABILITIES_API FAggregatorModChannel
{
public:
// 一些其他的函数(Some Function Else) ...
static float SumMods(const TArray<FAggregatorMod>& InMods, float Bias, const FAggregatorEvaluateParameters& Parameters);
private:
TArray<FAggregatorMod> Mods[EGameplayModOp::Max];
}
核心属性仅有一个 Mods,其本质是一个 二维数组,一维数组长度是 4,数组中各个元素对应的就是具体操作的数值
比如 EGameplayModOp::Additive 值为 0,那么 Mods[0] 存储的就是 加法 对应的所有操作数值
EGameplayModOp::Max值等于 4
以 EvaluateWithBase 函数为例,说明计算一个 Channel 的计算过程
float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
for (const FAggregatorMod& Mod : Mods[EGameplayModOp::Override])
{
if (Mod.Qualifies())
{
return Mod.EvaluatedMagnitude;
}
}
float Additive = SumMods(Mods[EGameplayModOp::Additive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Additive), Parameters);
float Multiplicitive = SumMods(Mods[EGameplayModOp::Multiplicitive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Multiplicitive), Parameters);
float Division = SumMods(Mods[EGameplayModOp::Division], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Division), Parameters);
if (FMath::IsNearlyZero(Division))
{
ABILITY_LOG(Warning, TEXT("Division summation was 0.0f in FAggregatorModChannel."));
Division = 1.f;
}
return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
}
首先获取 EGameplayModOp::Override 类型为 覆盖 操作的所有 Mod,找到第一个有效的 Mod,将其值作为返回值
很合理,覆盖 就是要覆盖其他操作计算的结果
接下来分别获取 加、乘、除 计算对应的值,并存储到对应的变量中 Additive、Multiplicitive、Division
这里顺便检查了一下
Division是否趋近于 0,预防除 0 错误
最后计算的值是 ((InlineBaseValue + Additive) * Multiplicitive) / Division
可以知道,GAS 默认的计算公式是 (Value + Add) * Multi / Division
SumMods 函数比较简单,用于计算一个 加、乘、除 各自操作的最终值
float FAggregatorModChannel::SumMods(const TArray<FAggregatorMod>& InMods, float Bias, const FAggregatorEvaluateParameters& Parameters)
{
float Sum = Bias;
for (const FAggregatorMod& Mod : InMods)
{
if (Mod.Qualifies())
{
Sum += (Mod.EvaluatedMagnitude - Bias);
}
}
return Sum;
}
虽然看着简单,但是 Bias 是什么?
Bias 具体的作用需要看到调用 SumMods 时的参数
float Additive = SumMods(Mods[EGameplayModOp::Additive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Additive), Parameters);
float GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Type ModOp)
{
static const float ModifierOpBiases[EGameplayModOp::Max] = {0.f, 1.f, 1.f, 0.f};
check(ModOp >= 0 && ModOp < EGameplayModOp::Max);
return ModifierOpBiases[ModOp];
}
GetModifierBiasByModifierOp 函数也很简单,对 加法 和 覆盖 操作返回 0;对 乘法 和 除法 返回 0
其实理解 Bias 的作用很简单,比如我希望血量增加 50%,那么 乘法 操作最后得到的值是 0.5,BaseValue * 0.5 很明显不符合我们的需求
对于乘法来说,期望增加 50% 其实是希望 BaseValue * 1.5,所以这里的 Bias 就是根据具体的操作,对基础计算值做一个修改
从另一个角度来说,如果没有乘法操作,如果得到的操作值是 0,最终值也是 0,也是个错
FAggregatorModChannelContainer 就是一个 通道容器,存储所有通道对应的 FAggregatorModChannel
struct GAMEPLAYABILITIES_API FAggregatorModChannelContainer
{
// 其他的一些计算函数(Some Functions Else)...
float EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const;
private:
TMap<EGameplayModEvaluationChannel, FAggregatorModChannel> ModChannelsMap;
}
基于给定的基础值,通过 EvaluateWithBase 可以得到最终的值
float FAggregatorModChannelContainer::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
float ComputedValue = InlineBaseValue;
for (auto& ChannelEntry : ModChannelsMap)
{
const FAggregatorModChannel& CurChannel = ChannelEntry.Value;
ComputedValue = CurChannel.EvaluateWithBase(ComputedValue, Parameters);
}
return ComputedValue;
}
GE 的时间配置如上图所示,分为三种类型
| 周期类型 | 作用 |
|---|---|
| Instant | 立即执行一次 |
| Infinite | 永久持续 |
| Has Duration | 持续一段时间 |
Period 用于配置执行周期
根据周期类型的不同,Period 数值的不同,会出现几种不同的情况
分析来看,这是合理的
Effect 是周期性的,但是 Period 为 0,表示这是一个增益性的 Buff,在持续时间结束后需要清除,所以修改 CurrentValue 更为合适Effect 是周期性的,并且 Period 不为 0 或者这个 GE 是 Instant 的,那么生效时应该修改 BaseValue但是这是一个隐式的计算过程,对外并不可见,可能会导致使用者的使用错误,所以需要添加额外说明
通过查看源码,可以发现对属性的所有修改操作,会封装成一个 Mod 的数组,基于 BaseValue 和 Mod 数组,最终计算得到 CurrentValue
Mod是Modifier的缩写
UGameplayEffect 用于配置,新建的 GE 的资产就是这个
FGameplayEffectSpec 包含 UGameplayEffect 和 对应的运行时信息,比如运行时计算得到的 Duration、Period 等
FActiveGameplayEffect 包含 UGameplayEffect,当一个 GE 是周期性时,会加入到 FActiveGameplayEffectsContainer 容器中,需要记录 GE 执行的 开始时间、下一次执行时间 等信息
综上,不同的配置用于的不同的情况,但是无论如何,一个 GE 只要被应用了,就会创建对应的 FGameplayEffectSpec 实例
以 BlueprintCallable 的 BP_ApplyGameplayEffectToSelf 作为入口
TSubclassOf<UGameplayEffect> 创建 GE 对象ApplyGameplayEffectToTargetUGameplayEffect* GameplayEffect = GameplayEffectClass->GetDefaultObject<UGameplayEffect>();
return ApplyGameplayEffectToTarget(GameplayEffect, Target, Level, Context); 注意这里是通过 GetDefaultObject 获取目标的 CDO 对象,也就是说所有
GE 在这里得到的 GameplayEffect 都是同一个对象,这为后面判断堆叠提供了帮助
在 ApplyGameplayEffectToTarget 中
FGameplayEffectSpecApplyGameplayEffectSpecToTargetFGameplayEffectSpec Spec(GameplayEffect, Context, Level);
return ApplyGameplayEffectSpecToTarget(Spec, Target, PredictionKey);在 ApplyGameplayEffectSpecToTarget 中
PredictionKeyTarget->ApplyGameplayEffectSpecToSelf 获得 GE 的 Handle
接下来所有代码都不考虑网络同步和预测的问题
在 Target->ApplyGameplayEffectSpecToSelf 中GameplayEffectApplicationQueries 检查当前要添加的 GE 是否有效ActiveGameplayEffects 和当前要添加的 GE 检查能否添加
GEComponent 来判断的AssetTagsGameplayEffectComponent、AbilitiesGameplayEffectComponent 等 GEComponent 配置AttributeExecuteGameplayEffect(*OurCopyOfSpec, PredictionKey) 直接执行 GEActiveGameplayEffects.ApplyGameplayEffectSpec 添加 GE 到容器中在 ActiveGameplayEffects.ApplyGameplayEffectSpec 中
FActiveGameplayEffect 实例FActiveGameplayEffect 实例FActiveGameplayEffect 数值,收集其绑定的属性FActiveGameplayEffect 实例上OnStackCountChangeInternalOnActiveGameplayEffectAdded在代码中有两个地方使用了 TimeManager
bLoop 设置为 falsebExecutePeriodicEffectOnApplication 为 true,表示 GE 添加时立刻执行一次,所以会额外注册一个 TimerManager.SetTimerForNextTick// Calculate Duration mods if we have a real duration
if (DurationBaseValue > 0.f)
{
float FinalDuration = AppliedEffectSpec.CalculateModifiedDuration();
// 一些判断和特殊数据处理
if (Owner && bSetDuration)
{
FTimerManager& TimerManager = Owner->GetWorld()->GetTimerManager();
FTimerDelegate Delegate = FTimerDelegate::CreateUObject(Owner, &UAbilitySystemComponent::CheckDurationExpired, AppliedActiveGE->Handle);
TimerManager.SetTimer(AppliedActiveGE->DurationHandle, Delegate, FinalDuration, false);
if (!ensureMsgf(AppliedActiveGE->DurationHandle.IsValid(), TEXT("Invalid Duration Handle after attempting to set duration for GE %s @ %.2f"),
*AppliedActiveGE->GetDebugString(), FinalDuration))
{
// Force this off next frame
TimerManager.SetTimerForNextTick(Delegate);
}
}
}
// Register period callbacks with the timer manager
if (bSetPeriod && Owner && (AppliedEffectSpec.GetPeriod() > UGameplayEffect::NO_PERIOD))
{
FTimerManager& TimerManager = Owner->GetWorld()->GetTimerManager();
FTimerDelegate Delegate = FTimerDelegate::CreateUObject(Owner, &UAbilitySystemComponent::ExecutePeriodicEffect, AppliedActiveGE->Handle);
// The timer manager moves things from the pending list to the active list after checking the active list on the first tick so we need to execute here
if (AppliedEffectSpec.Def->bExecutePeriodicEffectOnApplication)
{
TimerManager.SetTimerForNextTick(Delegate);
}
TimerManager.SetTimer(AppliedActiveGE->PeriodHandle, Delegate, AppliedEffectSpec.GetPeriod(), true);
}
在 InternalOnActiveGameplayEffectAdded 函数中
bIsInhibited)UAbilitySystemComponent::InhibitActiveGameplayEffect在 UAbilitySystemComponent::InhibitActiveGameplayEffect 函数中
RemoveActiveGameplayEffectGrantedTagsAndModifiersAddActiveGameplayEffectGrantedTagsAndModifiersOnInhibitionChanged 事件FScopedAggregatorOnDirtyBatch 对象析构的时候触发
ActiveGameplayEffects.OnMagnitudeDependencyChange 属性变化事件OnDirty 脏数据事件触发FTimerDelegate Delegate = FTimerDelegate::CreateUObject(Owner, &UAbilitySystemComponent::ExecutePeriodicEffect, AppliedActiveGE->Handle);
根据上面的代码,直接定位执行位置是 UAbilitySystemComponent::ExecutePeriodicEffect
通过函数调用,真正执行的代码在 FActiveGameplayEffectsContainer::ExecuteActiveEffectsFrom 函数中
TargetTags 即 Owner 标记上的 GameplayTagsGE 的 ModifiersInternalExecuteMod 应用 ModifierExecutions 数组,计算得到 ConditionalEffectSpecs 将要附加的 GEConditionalEffectSpecs重点在于两个函数 CalculateModifierMagnitudes 和 InternalExecuteMod
在 CalculateModifierMagnitudes 中
const FGameplayModifierInfo& ModDef = Def->Modifiers[ModIdx];
FModifierSpec& ModSpec = Modifiers[ModIdx];
if (ModDef.ModifierMagnitude.AttemptCalculateMagnitude(*this, ModSpec.EvaluatedMagnitude) == false)
{
ModSpec.EvaluatedMagnitude = 0.f;
ABILITY_LOG(Warning, TEXT("Modifier on spec: %s was asked to CalculateMagnitude and failed, falling back to 0."), *ToSimpleString());
}
很清晰,通过在 GE 中配置的 Def->Modifiers 计算出 ModSpec.EvaluatedMagnitude 的值
switch (MagnitudeCalculationType)
{
case EGameplayEffectMagnitudeCalculation::ScalableFloat:break;
case EGameplayEffectMagnitudeCalculation::AttributeBased:break;
case EGameplayEffectMagnitudeCalculation::CustomCalculationClass:break;
case EGameplayEffectMagnitudeCalculation::SetByCaller:break;
// ...
}
在 AttemptCalculateMagnitude 中通过枚举,来计算具体的值内容
在 InternalExecuteMod 函数中,核心代码如下
if (AttributeSet->PreGameplayEffectExecute(ExecuteData))
{
float OldValueOfProperty = Owner->GetNumericAttribute(ModEvalData.Attribute);
ApplyModToAttribute(ModEvalData.Attribute, ModEvalData.ModifierOp, ModEvalData.Magnitude, &ExecuteData);
FGameplayEffectModifiedAttribute* ModifiedAttribute = Spec.GetModifiedAttribute(ModEvalData.Attribute);
if (!ModifiedAttribute)
{
// If we haven't already created a modified attribute holder, create it
ModifiedAttribute = Spec.AddModifiedAttribute(ModEvalData.Attribute);
}
ModifiedAttribute->TotalMagnitude += ModEvalData.Magnitude;
{
SCOPE_CYCLE_COUNTER(STAT_PostGameplayEffectExecute);
/** This should apply 'gamewide' rules. Such as clamping Health to MaxHealth or granting +3 health for every point of strength, etc */
AttributeSet->PostGameplayEffectExecute(ExecuteData);
}
}
基本流程也很简单
PreGameplayEffectExecute 判断能否触发ApplyModToAttributePostGameplayEffectExecuteUGameplayEffectExecutionCalculation 与 UGameplayModMagnitudeCalculation 类似,都是可以自定义属性的计算过程,不同的是 UGameplayEffectExecutionCalculation 可以对多个属性进行操作
以 Lyra 项目中的 ULyraDamageExecution 为例
Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput)
首先观察参数
ExecutionParams 是 const &,表示作为输入OutExecutionOutput 是 &,表示作为输出通过 ExecutionParams 可以得到 FGameplayEffectSpec 这个 GE 的运行时实例
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluateParameters;
EvaluateParameters.SourceTags = SourceTags;
EvaluateParameters.TargetTags = TargetTags;
float BaseDamage = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BaseDamageDef, EvaluateParameters, BaseDamage);
收集 Source 和 Target 身上的 Tag,通过 AttemptCalculateCapturedAttributeMagnitude 将捕获的属性 以及 收集的信息 进行计算得到 BaseDamage
捕获的属性是
ULyraCombatSet::GetBaseDamageAttribute()这一步进行计算,得到属性值
接下来就是进行一系列计算:距离衰减、友伤判定、护甲衰减等,得到最终的数值 DamageDone
最后将 DamageDown 应用到属性上
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(ULyraHealthSet::GetDamageAttribute(), EGameplayModOp::Additive, DamageDone));
注意,最开始获取伤害是通过 ULyraCombatSet::GetBaseDamageAttribute 属性,最后应用伤害是在 ULyraHealthSet::GetDamageAttribute
这也是 UGameplayEffectCalculation 的优势点
为了方便配置、提升可扩展性,将 GE 执行的各个流程封装成成 UGameplayEffectComponent,根据各自的需求实现各自的功能
从源码来看,UGameplayEffectComponent 内容非常简单
| 函数 | 作用 |
|---|---|
| CanGameplayEffectApply | 检查 GE 是否可以应用到目标,必须所有的 GEComponent 都返回 True ,该 GE 才可以被应用 |
| OnActiveGameplayEffectAdded | 带持续时间的效果激活时,如果返回 False,表示该 GE 不能激活 |
| OnGameplayEffectExecuted | 在瞬时效果或周期效果的执行时触发 |
| OnGameplayEffectApplied | 效果首次应用或堆叠时的通用入口 |
| OnGameplayEffectChanged | 编辑器中对 GameplayEffect 的修改保存后 |
OnGameplayEffectChanged通常执行在 Editor 模式下
通过拆分 GE 的生命周期,根据各自的需求封装 GEComponent
以 UAdditionalEffectsGameplayEffectComponent 为例
UAdditionalEffectsGameplayEffectComponent 的作用是当 GE 被 Applied 的时候,添加新的 GE 到 Target 上
OnActiveGameplayEffectAdded 激活时绑定了 GE 的 Removed 事件,方便在 Removed 的时候删除本次添加的 GEOnGameplayEffectApplied 通过 ConditionalEffect 将能够添加的 GE 添加到 Target 上以 UAssetTagsGameplayEffectComponent 为例
UAssetTagsGameplayEffectComponent 的作用是为 GE 添加 Tags
OnGameplayEffectChanged 的时候,将 InheritableAssetTags 存储的内容添加到 GE 的 CachedAssetTags 属性中PostEditChangeProperty 的时候,同步更新 GE 中的 CachedAssetTags 属性
PostEditChangeProperty是 UObject 的函数,当资产更新的时候触发,这里就是修改AssetTag的时候触发
还有一个 UGameplayEffectUIData
UGameplayEffectUIData 本身没有 任何属性,也没有重写 任何函数
它的作用就是作为一个标记类,所有继承 UGameplayEffectUIData 的类都是用于 UI 显示的配置
比如 Lyra 项目中 UTopDownArenaPickupUIData,在项目中可以通过 GetGameplayEffectUIData 获取对应的继承 UGameplayEffectUIData 的配置项
上述中,获取到对应的
UGameplayEffectUIData后,获取配置中的特效、贴图等配置,进行后续操作
由于 UGameplayEffectComponent 的存在,现在 GE 的配置更加方便、简单、易读
这是什么?
这是一个存储激活的 GE (FActiveGameplayEffect) 的容器(Container)
当一个 GE 被实例化的时候会封装成 FGameplayEffectSpec,当 GE 有持续时间时会通过 ApplyGameplayEffectSpec 添加到容器中,并封装成 FActiveGameplayEffect
struct GAMEPLAYABILITIES_API FActiveGameplayEffectsContainer : public FFastArraySerializer
它继承自 FFastArraySerializer,是一种专为复制大型数组而设计的序列化方式
TArray 类型。
MarkItemDirty对修改的 Entity 标脏
FActiveGameplayEffectsContainer 维护一个数组 GameplayEffects_Internal 用于存储 Active GE 的实体
UPROPERTY()
TArray<FActiveGameplayEffect> GameplayEffects_Internal;
| 类型 | 属性 | 作用 |
|---|---|---|
| TMap | AttributeAggregatorMap | 每个被任何 Active GE 修改过的 FGameplayAttribute 维护一个 FAggregator |
| TMap | AttributeChangeDelegates | 注册当某个 FGameplayAttribute 的值发生变化时触发的回调委托 |
| TMap | AttributeValueChangeDelegates | 为每个 FGameplayAttribute 维护一个当该属性值发生有意义的变化(超出容差范围)时触发的回调委托 |
| TMap | CustomMagnitudeClassDependencies | 管理动态数值计算器 (UGameplayModMagnitudeCalculation 类) 的依赖关系 |
| TMap, TArray > | SourceStackingMap | 用于管理当这个 ASC 是 GE 的 Instigator (来源) 时,该 GE 在其他目标身上的堆叠情况 |
| mutable int32 | ScopedLockCount | 作用域锁 (Scoped Lock) 的计数器 |
| int32 | PendingRemoves | 一个简单的计数器,记录当前有多少个 FActiveGameplayEffect 被标记为待移除 |
| 堆叠策略 | 作用 | 执行操作 |
|---|---|---|
| ClearEntireStack | 清除整个堆叠 | 完全移除整个效果堆叠 |
| RemoveSingleStackAndRefreshDuration | 移除单个堆叠并刷新持续时间 | |
| RefreshDuration | 刷新持续时间 | 不减少堆叠数,只刷新效果的持续时间 |
根据前面策略,设置 StacksToRemove、RefreshStartTime、RefreshDurationTimer 的值,再根据值执行不同的逻辑
StacksToRemove 默认值为 -2 表示不做事,该值为 -1 时表示全部清除,为正数时清除指定层数
RefreshStartTime 为真,执行 RestartActiveGameplayEffectDuration,但是这一步只是修改 ActiveGE 的 StartTime 为当前时间,并触发 OnGameplayEffectDurationChange 事件
本质上,并没有真的改变持续时间本身
RefreshDurationTimer 为真,会真正的重新创建并绑定定时器委托
if (StacksToRemove >= -1)
{
InternalRemoveActiveGameplayEffect(ActiveGEIdx, StacksToRemove, false);
}
if (RefreshStartTime)
{
RestartActiveGameplayEffectDuration(Effect);
}
if (RefreshDurationTimer)
{
// ... 重新设置 Timer
}
记得 ActiveGE 的周期执行绑定的 Delegate 吗
FTimerDelegate Delegate = FTimerDelegate::CreateUObject(Owner, &UAbilitySystemComponent::ExecutePeriodicEffect, AppliedActiveGE->Handle);
void UAbilitySystemComponent::ExecutePeriodicEffect(FActiveGameplayEffectHandle Handle)
{
ActiveGameplayEffects.ExecutePeriodicGameplayEffect(Handle);
}
所以本质上,还是执行 FActiveGameplayEffectsContainer 容器的 ExecutePeriodicGameplayEffect 函数
void FActiveGameplayEffectsContainer::ExecutePeriodicGameplayEffect(FActiveGameplayEffectHandle Handle)
{
GAMEPLAYEFFECT_SCOPE_LOCK();
FActiveGameplayEffect* ActiveEffect = GetActiveGameplayEffect(Handle);
if (ActiveEffect != nullptr)
{
InternalExecutePeriodicGameplayEffect(*ActiveEffect);
}
}
根据下面的代码,可以分为两个部分
ExecuteActiveEffectsFromOwner 和 Source 的 事件广播void FActiveGameplayEffectsContainer::InternalExecutePeriodicGameplayEffect(FActiveGameplayEffect& ActiveEffect)
{
GAMEPLAYEFFECT_SCOPE_LOCK();
if (!ActiveEffect.bIsInhibited)
{
FScopeCurrentGameplayEffectBeingApplied ScopedGEApplication(&ActiveEffect.Spec, Owner);
// do something else ...
ActiveEffect.Spec.ModifiedAttributes.Empty();
ExecuteActiveEffectsFrom(ActiveEffect.Spec);
UAbilitySystemComponent* SourceASC = ActiveEffect.Spec.GetContext().GetInstigatorAbilitySystemComponent();
Owner->OnPeriodicGameplayEffectExecuteOnSelf(SourceASC, ActiveEffect.Spec, ActiveEffect.Handle);
if (SourceASC)
{
SourceASC->OnPeriodicGameplayEffectExecuteOnTarget(Owner, ActiveEffect.Spec, ActiveEffect.Handle);
}
}
}
周期性 GE 真正执行逻辑的地方
Modifier 具体数值SpecToUse.CapturedTargetTags.GetActorTags().Reset();
Owner->GetOwnedGameplayTags(SpecToUse.CapturedTargetTags.GetActorTags());
SpecToUse.CalculateModifierMagnitudes();
InternalExecuteMod 修改属性的 BaseValuefor (int32 ModIdx = 0; ModIdx < SpecToUse.Modifiers.Num(); ++ModIdx)
{
const FGameplayModifierInfo& ModDef = SpecToUse.Def->Modifiers[ModIdx];
FGameplayModifierEvaluatedData EvalData(ModDef.Attribute, ModDef.ModifierOp, SpecToUse.GetModifierMagnitude(ModIdx, true));
ModifierSuccessfullyExecuted |= InternalExecuteMod(SpecToUse, EvalData);
}
UGameplayEffectExecutionCalculationfor (const FGameplayEffectExecutionDefinition& CurExecDef : SpecToUse.Def->Executions)
{
// 得到 Execution 的 CDO
const UGameplayEffectExecutionCalculation* ExecCDO = CurExecDef.CalculationClass->GetDefaultObject<UGameplayEffectExecutionCalculation>();
// 执行 Execute
FGameplayEffectCustomExecutionParameters ExecutionParams(SpecToUse, CurExecDef.CalculationModifiers, Owner, CurExecDef.PassedInTags, PredictionKey);
FGameplayEffectCustomExecutionOutput ExecutionOutput;
ExecCDO->Execute(ExecutionParams, ExecutionOutput);
// 对 ExecutionOutput 进行操作,对属性进行修改
TArray<FGameplayModifierEvaluatedData>& OutModifiers = ExecutionOutput.GetOutputModifiersRef();
for (FGameplayModifierEvaluatedData& CurExecMod : OutModifiers)
{
if (bApplyStackCountToEmittedMods && SpecStackCount > 1)
{
CurExecMod.Magnitude = GameplayEffectUtilities::ComputeStackedModifierMagnitude(CurExecMod.Magnitude, SpecStackCount, CurExecMod.ModifierOp);
}
ModifierSuccessfullyExecuted |= InternalExecuteMod(SpecToUse, CurExecMod);
}
// 添加 Execution 中配置的根据条件添加的 GE
if (bRunConditionalEffects)
{
for (const FConditionalGameplayEffect& ConditionalEffect : CurExecDef.ConditionalGameplayEffects)
{
if (ConditionalEffect.CanApply(SpecToUse.CapturedSourceTags.GetActorTags(), SpecToUse.GetLevel()))
{
FGameplayEffectSpecHandle SpecHandle = ConditionalEffect.CreateSpec(SpecToUse.GetEffectContext(), SpecToUse.GetLevel());
if (SpecHandle.IsValid())
{
ConditionalEffectSpecs.Add(SpecHandle);
}
}
}
}
}
if (InvokeGameplayCueExecute && SpecToUse.Def->GameplayCues.Num())
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->InvokeGameplayCueExecuted_FromSpec(Owner, SpecToUse, PredictionKey);
}
for (const FGameplayEffectSpecHandle& TargetSpec : ConditionalEffectSpecs)
{
if (TargetSpec.IsValid())
{
Owner->ApplyGameplayEffectSpecToSelf(*TargetSpec.Data.Get(), PredictionKey);
}
}
Spec.Def->OnExecuted(*this, Spec, PredictionKey);
GA 中比较重要的几个函数
CanActivateAbility用于检查当前 GA 是否满足执行条件,在 C++ 中会检查 CD、Cost,可以通过重写 K2_CanActivateAbility 来添加自定义判定条件
TryActivateAbility定义在 ASC (技能组件) 中,由外界调用
会依次进行 CanActivateAbility 检查、处理 GA 的实例化、网络预测
并在最后调用 GA 的 CallActivateAbility 函数
CallActivateAbility依次执行 PreActivate 和 ActivateAbility
PreActivate 是虚函数,在 GA 执行之前执行,可以重写来添加自己的执行顺序
ActivateAbility执行 GA 逻辑的地方,是虚函数,可以子类重写
如果没有重写,会根据蓝图是否还有数据分别执行 K2_ActivateAbilityFromEvent 和 K2_ActivateAbility
如果蓝图中没有实现对应的执行函数,则报错,并且直接执行 EndAbility 结束 GA
CommitAbilityCommit 提交
用于提交资源,也就是扣除 蓝量、体力值,并且更新 CD 的地方
在 GA 执行过程中,需要自行根据执行时机调用该函数,也可以不调用
会通过 CommitCheck 检查一遍资源,之后通过 CommitExecute 设置 CD 和扣除 Cost
CommitAbility 是虚函数,可以自行重写
CommitExecute 也是虚函数,自行重写,对蓝图暴露 K2_CommitExecute 添加自定义资源消耗或者做其他事情
除此之外提供 CommitAbilityCooldown 只设置 CD 和 CommitAbilityCost 只扣除 Cost
但是只有 CommitAbility 函数会触发 ASC 的 AbilityCommittedCallbacks 事件
CancelAbility用于外部打断、取消 GA,比如停止 montage 或者 jump
参考 GameplayAbility_CharacterJump 的实现
会通过 CanBeCanceled 检查当前 GA 能否被 Cancel,可以重写该函数自定义逻辑
会触发 GA 的 OnGameplayAbilityCancelled 事件,并且调用 EndAbility 来停止 GA
EndAbility使用 K2_OnEndAbility 通知蓝图执行对应逻辑
清理所有相关的 Timer;清理所有的 Task;通知 ASC 清理相关的 GC 和 Tag
执行 OnGameplayAbilityEnded 和 OnGameplayAbilityEndedWithData 事件
通过前面这些函数,大概能够整理出一个 GA 执行的周期
与 GE 类似,用于配置一个 GA