README.md 24 KB

GAS

思考

关于 GE (Gameplay Effect) 有一些想法

一个 Effect 需要考虑哪些功能呢?

  • 配置项
    • 周期触发配置项
    • 叠层配置项
    • 冲突、免疫配置项
    • 冲突:该 Effect 生效时,哪些已有的 Buff 会失去效果
    • 免疫:该 Effect 生效时,哪些还未添加的 Buff 无法添加或不能生效
    • 上下文信息
    • Caster:施加者。可以为空,比如场地效果
    • Parent:被施加者。不可为空
    • Ability:通过 技能 施加。可以为空,比如场地效果,以 技能 形式添加
    • 当前 Effect 的 Tag
    • 当前 Effect 所处的状态
    • BeforeInit
    • Init
    • Exeution
    • Finished
    • Destroy
    • Caster 被销毁时,是否保持该 Effect
    • Parent 被销毁时,是否保持该 Effect
    • 对属性进行的操作
    • 是否在 UI 上显示
    • 阶段触发事件
    • 通知 Parent/Caster/Ability 当前 Buff 所处阶段
  • 操作配置
    • 优先级
    • 目标属性
    • 操作配置:加、乘、除、覆盖
    • 数值配置
  • 触发周期配置
    • 触发类型
    • 直接触发
    • 周期性触发,持续一段时间
    • 周期性触发,永久存在
    • 触发周期间隔时间
    • 持续时间
    • 是否立刻触发:如果是周期性触发,是否在生效时立刻触发一次
  • 叠层机制配置
    • 能否叠层
    • 最大叠层数限制
    • 叠层方案
    • 不叠层,刷新当前 buff 的持续时间
    • 叠层,不刷新当前 buff 的持续时间,一层一层减少
    • 叠层,刷新当前 buff的持续时间,并且更改数值,一层一层减少
    • 叠层,刷新当前 buff 的持续时间,并且更改数值,一次性扣完所有层数
  • 数值配置
    • 配置固定值
    • 基于属性百分比
    • 数值来源
      • Caster
      • Parent
    • 是否限制取值范围
    • 最大值
    • 最小值
    • 自定义计算流程
    • 是否动态计算,也就是运行时计算,还是一直使用添加时的数值
    • 简单来说就是,是否锁面板

Attribute

AttributeSet

  • 作为属性容器,存储游戏实体的所有属性
  • 作为行为控制器,通过生命周期钩子函数控制属性修改逻辑
  • 实现网络同步基础
  • 执行数值钳制、伤害计算等游戏规则
虚函数 作用 返回值
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

关于属性的思考

  • 效果 A:是 Duration 类型,持续时间是 3s,Period 为 0,作用是 Add HP 50%
  • 效果 B:是 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 的时间周期分类

于是乎,我们想到一种解决方案 以血量为例,除开 HPMaxHP 之外,再额外加上 ExtraHPDamage 对血量的操作并不直接作用于 HP,而是作用于 ExtraHP

  1. 假设初始血量为 40
  2. 使用效果 A,增加血量 50%,此时 ExtraHP 值为 20
  3. 使用效果 B,减少血量 5点,此时 Damage 值为 5
  4. 计算得到 HP 的 CurrentValue 为 55,即 HP + ExtraHP - Damage
  5. 效果 A 时间结束,结束时根据 ExtraHP 的值动态修改 Damage 值,此时 ExtraHP 为 0,Damage 值为 0(因为 20 > 5,如果 Damage 值为 30,那么效果 A 结束时 Damage 更新为 10)
  6. 计算得到 HP 的 CurrentValueHP + ExtraHP - Damage = 4

临时性 HP 修改全部作用于 ExtraHP,比如 效果A,或者其他 Buff、Debuff
直接扣除 HP 的全部作用于 Damage
增加当前 HP 的全部作用于 HP

关于属性的最大最小值

  1. 限制一个属性的取值范围,需要自己处理
  2. 一个属性的最大值可能是另一个属性的值,比如血量最大血量,最大血量会随着角色等级增加

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 然后强行修改对应属性,应对这种状态也有对应函数接口

PreAttributeBaseChangePreAttributeChange 中,将将要设置的属性值进行修改,保证值在允许的区间范围内

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 的时间配置如上图所示,分为三种类型

周期类型 作用
Instant 立即执行一次
Infinite 永久持续
Has Duration 持续一段时间

Period 用于配置执行周期

根据周期类型的不同,Period 数值的不同,会出现几种不同的情况

Duration 配置 效果 解释
DurationPolicy: Instant 修改 BaseValue(current也跟着变)
DurationPolicy: Infinite
Period: 0.0
只修改 CurrentValue,BaseValue 不变
DurationPolicy: Infinite
Period: 1.0
修改 BaseValue(current也跟着变)
DurationPolicy: HasDuration
ScalableFloatMagnitude: 2.0
Period: 0.0
修改了CurrentValue,BaseValue不变,2s 后GE消除,CurrentValue 恢复到 100
DurationPolicy: HasDuration
ScalableFloatMagnitude: 2.0
Period: 1.0
直接修改了 BaseValue (current也跟着变)

分析来看,这是合理的

  • 如果一个 Effect 是周期性的,但是 Period 为 0,表示这是一个增益性的 Buff,在持续时间结束后需要清除,所以修改 CurrentValue 更为合适
  • 如果一个 Effect 是周期性的,并且 Period 不为 0 或者这个 GE 是 Instant 的,那么生效时应该修改 BaseValue

但是这是一个隐式的计算过程,对外并不可见,可能会导致使用者的使用错误,所以需要添加额外说明

通过查看源码,可以发现对属性的所有修改操作,会封装成一个 Mod 的数组,基于 BaseValueMod 数组,最终计算得到 CurrentValue

ModModifier 的缩写

关于 UGameplayEffect、FGameplayEffectSpec 和 FActiveGameplayEffect

UGameplayEffect 用于配置,新建的 GE 的资产就是这个

FGameplayEffectSpec 包含 UGameplayEffect 和 对应的运行时信息,比如运行时计算得到的 DurationPeriod

FActiveGameplayEffect 包含 UGameplayEffect,当一个 GE 是周期性时,会加入到 FActiveGameplayEffectsContainer 容器中,需要记录 GE 执行的 开始时间、下一次执行时间 等信息

综上,不同的配置用于的不同的情况,但是无论如何,一个 GE 只要被应用了,就会创建对应的 FGameplayEffectSpec 实例

GE 的添加流程

BlueprintCallableBP_ApplyGameplayEffectToSelf 作为入口

  1. 通过传入的 TSubclassOf<UGameplayEffect> 创建 GE 对象
  2. 调用 ApplyGameplayEffectToTarget
UGameplayEffect* GameplayEffect = GameplayEffectClass->GetDefaultObject<UGameplayEffect>();
return ApplyGameplayEffectToTarget(GameplayEffect, Target, Level, Context);	注意这里是通过 GetDefaultObject 获取目标的 CDO 对象,也就是说所有 

GE 在这里得到的 GameplayEffect 都是同一个对象,这为后面判断堆叠提供了帮助 在 ApplyGameplayEffectToTarget

  1. 通过传入参数创建 FGameplayEffectSpec
  2. 调用 ApplyGameplayEffectSpecToTarget
FGameplayEffectSpec	Spec(GameplayEffect, Context, Level);
return ApplyGameplayEffectSpecToTarget(Spec, Target, PredictionKey);在 ApplyGameplayEffectSpecToTarget 中
  1. 判断是否需要预测,以此来清空预测键 PredictionKey
  2. 调用 Target->ApplyGameplayEffectSpecToSelf 获得 GE 的 Handle 接下来所有代码都不考虑网络同步和预测的问题 在 Target->ApplyGameplayEffectSpecToSelf
  3. 检查
    • 通过 GameplayEffectApplicationQueries 检查当前要添加的 GE 是否有效
    • 通过 ActiveGameplayEffects 和当前要添加的 GE 检查能否添加
      • 通过 GEComponent 来判断的
      • 例如:AssetTagsGameplayEffectComponentAbilitiesGameplayEffectComponentGEComponent 配置
    • 检查当前准备添加的 GE 是否配置了有效的 Attribute
  4. 判断是否是立即执行的 GE
    • 如果是立即执行的 GE:ExecuteGameplayEffect(*OurCopyOfSpec, PredictionKey) 直接执行 GE
    • 如果是持续时间的 GE:ActiveGameplayEffects.ApplyGameplayEffectSpec 添加 GE 到容器中

ActiveGameplayEffects.ApplyGameplayEffectSpec

  1. 判断堆叠
    • 如果堆叠:更新叠层计数,根据条件刷新持续时间、重置计数器等操作,使用现有的 FActiveGameplayEffect 实例
    • 如果不堆叠:创建 FActiveGameplayEffect 实例
  2. 重新计算 FActiveGameplayEffect 数值,收集其绑定的属性
  3. 计算持续时间和周期,绑定 Timer 到 FActiveGameplayEffect 实例上
  4. 根据是否堆叠触发不同的事件
    • 如果堆叠:触发 OnStackCountChange
    • 如果不堆叠:触发 InternalOnActiveGameplayEffectAdded

在代码中有两个地方使用了 TimeManager

  • 第一个 Timer 是在持续时间结束后触发,用于标记效果过期(可以移除效果),只触发一次所以 bLoop 设置为 false
  • 第二个 Timer 是固定时间执行周期性效果(每秒造成1点伤害),需要循环触发所以设置为 true
    • 如果 GE 的 bExecutePeriodicEffectOnApplication 为 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 函数中

  1. 会根据 GEComponent 来判断当前 GE 能否被激活或者说是否被抑制(bIsInhibited)
  2. 调用 UAbilitySystemComponent::InhibitActiveGameplayEffect

UAbilitySystemComponent::InhibitActiveGameplayEffect 函数中

  1. 根据 GE 是否被 抑制
    • 被抑制:执行 RemoveActiveGameplayEffectGrantedTagsAndModifiers
    • 不被抑制:执行 AddActiveGameplayEffectGrantedTagsAndModifiers
  2. 触发 OnInhibitionChanged 事件
  3. FScopedAggregatorOnDirtyBatch 对象析构的时候触发
    • ActiveGameplayEffects.OnMagnitudeDependencyChange 属性变化事件
    • OnDirty 脏数据事件触发

流程图

代码执行过程

GE的添加流程

GE 的执行流程

FTimerDelegate Delegate = FTimerDelegate::CreateUObject(Owner, &UAbilitySystemComponent::ExecutePeriodicEffect, AppliedActiveGE->Handle);

根据上面的代码,直接定位执行位置是 UAbilitySystemComponent::ExecutePeriodicEffect

通过函数调用,真正执行的代码在 FActiveGameplayEffectsContainer::ExecuteActiveEffectsFrom 函数中

  1. 设置 TargetTagsOwner 标记上的 GameplayTags
  2. 计算 GEModifiers
  3. 通过循环调用 InternalExecuteMod 应用 Modifier
  4. 遍历 Executions 数组,计算得到 ConditionalEffectSpecs 将要附加的 GE
  5. 触发 GC
  6. 应用 ConditionalEffectSpecs
  7. 最后触发事件

重点在于两个函数 CalculateModifierMagnitudesInternalExecuteMod

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

基本流程也很简单

  1. 调用 PreGameplayEffectExecute 判断能否触发
  2. 添加对应属性修改 ApplyModToAttribute
  3. 添加属性的修改记录
  4. 设置累加值
  5. 触发事件 PostGameplayEffectExecute

GE的执行流程

UGameplayEffectExecutionCalculation

UGameplayEffectExecutionCalculationUGameplayModMagnitudeCalculation 类似,都是可以自定义属性的计算过程,不同的是 UGameplayEffectExecutionCalculation 可以对多个属性进行操作

Lyra 项目中的 ULyraDamageExecution 为例

Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput)

首先观察参数

  • ExecutionParamsconst &,表示作为输入
  • 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);

收集 SourceTarget 身上的 Tag,通过 AttemptCalculateCapturedAttributeMagnitude 将捕获的属性 以及 收集的信息 进行计算得到 BaseDamage

捕获的属性是 ULyraCombatSet::GetBaseDamageAttribute()

这一步进行计算,得到属性值

接下来就是进行一系列计算:距离衰减、友伤判定、护甲衰减等,得到最终的数值 DamageDone

最后将 DamageDown 应用到属性上

OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(ULyraHealthSet::GetDamageAttribute(), EGameplayModOp::Additive, DamageDone));

注意,最开始获取伤害是通过 ULyraCombatSet::GetBaseDamageAttribute 属性,最后应用伤害是在 ULyraHealthSet::GetDamageAttribute

这也是 UGameplayEffectCalculation 的优势点

UGameplayEffectComponent

为了方便配置、提升可扩展性,将 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 的时候删除本次添加的 GE
  • OnGameplayEffectApplied 通过 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 的配置更加方便、简单、易读