GAS 系统深度学习指南UE 5.4 / 5.5

04 AttributeSet 与属性系统

目标:掌握属性的定义方式、BaseValue 与 CurrentValue 的本质区别、Meta Attribute 的伤害流水线、属性变化监听、派生属性、AttributeSet 设计模式、三个关键回调的准确用途、OnAttributeAggregatorCreated,以及属性的网络复制与 ACCESSORS 宏原理。


4.1 Attribute 是什么

Attribute(属性) 是 GAS 中一个浮点数值,由结构体 FGameplayAttributeData 承载。它可以代表任何与游戏逻辑相关的数值——生命值、法力值、攻击力、暴击率、弹药、冷却缩减、等级、经验……

判断标准:如果一个数值属于某个 Actor 且会影响玩法,就应该考虑用 Attribute 表示。

Attribute 的核心特征

  1. 只能在 C++ 中定义,且必须定义在 UAttributeSet 子类头文件里;
  2. 应该仅通过 GameplayEffect 修改(确保 ASC 能预测变化、能聚合);
  3. 由 AttributeSet 持有并负责网络复制;
  4. 内部引用格式为 AttributeSetClassName.AttributeName(如 UMyAttributeSet.Health)。
// 属性定义(AttributeSet 头文件中)
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Health)

若不想让某个属性出现在编辑器 Details 面板,加 Meta = (HideInDetailsView)(Meta 属性常用)。

4.2 BaseValue vs CurrentValue(核心中的核心)

每个属性由两个值组成:

值 含义 谁修改
BaseValue 永久值,属性的"真实"数值 Instant GE、属性初始化
CurrentValue BaseValue + 所有临时 Modifier 的叠加 Duration/Infinite GE

银行账户类比:

  • BaseValue = 基本工资(月薪 10000,永久)
  • CurrentValue = 当月实际收入(基本工资 + 奖金 - 扣款)
  • 奖金/扣款是临时的(Duration/Infinite GE),这个月有下个月可能没有
  • 升职加薪 = Instant GE 改 BaseValue

移动速度完整示例

初始:        Base=600   Current=600
+10s 加速BUFF(+50):     Base=600   Current=650    (Duration GE)
+Infinite 减速(-100):   Base=600   Current=550    (Infinite GE)
10s 后 BUFF 到期:       Base=600   Current=500    (+50 自动消失)
+Instant 永久加速(+20): Base=620   Current=520    (BaseValue 永久改变)
移除减速 Debuff:        Base=620   Current=620    (回到 BaseValue)

权威结论(官方文档口径):

  • Instant(及 Periodic)GE 永久修改属性的 BaseValue;
  • Duration / Infinite GE 通过聚合器影响 CurrentValue(GE 移除后贡献自动消失);
  • CurrentValue = BaseValue + 所有活跃临时 Modifier 的贡献。

常见误区:把 BaseValue 当"最大值"用是错误做法。最大值(MaxHealth)应建模为独立属性,在回调中钳制。

聚合公式(底层机制)

Duration/Infinite 修改器在 FAggregatorModChannel::EvaluateWithBase 中聚合(GameplayEffectAggregator.cpp)。准确公式(含引擎"偏移相加"细节):

FinalValue = ((BaseValue + Additive) × Multiplicitive) ÷ Division

其中(对应引擎 SumMods 的 Bias 机制,Bias: Add=0, Mult=1, Div=1, Override=0):

  • Additive = Σ 所有 Add 修饰器的值(先求和);
  • Multiplicitive = 1 + Σ(Magnitudeᵢ − 1)——多个 Multiply 不是相乘,而是相对 1 的偏移量相加;
  • Division = 1 + Σ(Magnitudeᵢ − 1)——同理;
  • Override 是特例:所有 Override 相加,直接令 FinalValue = 该值(多个 Override 取最后结果)。

重要提醒:当只有一个 Multiply(如 ×1.5)时,1+(1.5-1)=1.5,与直觉的乘法一致;但当有多个 Multiply 时(如 1.5 与 1.5),引擎结果是 1+0.5+0.5=2.0,而不是 1.5×1.5=2.25。多个 <1 的值(如两个 0.5)甚至会归零。详见第 05 章 5.6 节的完整分析。

举例:Base=50,装备 +20(Add),单个 Buff ×1.5(Mult)→ Final = (50+20)×1.5 = 105。

4.3 Meta Attributes(元属性)

Meta Attribute 不表示持久状态,而是临时占位符,用于在 GE 和 AttributeSet 之间传递中间计算结果。最经典的是 Damage(伤害)。

为什么需要元属性

没有元属性时:GE 直接改 Health → GE/Execution 必须知道目标有没有护盾、护甲怎么算——逻辑臃肿、难扩展。

有了元属性:

第一步:Execution 计算最终伤害 → 写入 Damage 元属性(GE → Damage = 80)
第二步:PostGameplayEffectExecute 接管
  - 读 Damage(80)→ 护盾吸收 30 → 剩余 50
  - 护甲减免 20% → 剩余 40
  - 最终扣血:Health - 40
  - 清零 Damage:SetDamage(0)

元属性实现了"造成多少伤害"(GE/Execution,输出端)与"如何处理伤害"(AttributeSet,接收端)的逻辑分离。

元属性特征

  1. 临时性:每次新 GE 覆盖旧值,不持久;
  2. 不复制:不标记 ReplicatedUsing,不进 GetLifetimeReplicatedProps;
  3. 仅服务端有意义:伤害计算在服务端,客户端通过 Health 变化感知结果。
// 元属性定义(注意:没有 ReplicatedUsing)
UPROPERTY(BlueprintReadOnly, Category = "Damage")
FGameplayAttributeData Damage;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Damage)

最佳实践:任何涉及伤害/治疗的系统都建议用元属性。以后要加"护盾",只需改 PostGameplayEffectExecute 的分摊逻辑,不用动任何 Execution。

4.4 响应属性变化(UI 与逻辑)

委托绑定

// 绑定 Health 变化回调
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(
    AttributeSet->GetHealthAttribute()
).AddUObject(this, &AMyPlayerState::HealthChanged);

回调参数 FOnAttributeChangeData

void HealthChanged(const FOnAttributeChangeData& Data)
{
    // Data.NewValue / Data.OldValue —— 新旧值
    // Data.GEModData —— FGameplayEffectModCallbackData*(仅服务器端有效!)
    float Delta = Data.NewValue - Data.OldValue;
    if (Data.NewValue <= 0.0f && Data.OldValue > 0.0f && HasAuthority())
    {
        HandleDeath();
    }
}

易错:Data.GEModData 只在服务端有效,客户端访问会空指针——务必判空或 HasAuthority() 检查。

Blueprint 异步监听(AsyncTaskAttributeChanged)

GASDocumentation 提供 UAsyncTaskAttributeChanged(UBlueprintAsyncActionBase 子类)封装属性监听为蓝图节点,可直接在 UMG Widget 使用:

UCLASS()
class UAsyncTaskAttributeChanged : public UBlueprintAsyncActionBase
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
    static UAsyncTaskAttributeChanged* ListenForAttributeChange(
        UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute);

    UPROPERTY(BlueprintAssignable) FOnAttributeChangedDelegate OnAttributeChanged;
    UFUNCTION(BlueprintCallable) void EndTask();
};

最佳实践:Widget 的 Destruct 中务必调用 EndTask(),否则异步任务存活导致泄漏。

4.5 派生属性(Derived Attributes)

派生属性的值部分/完全依赖其他属性,被依赖属性变化时自动重算。实现:一个 Infinite GE,Modifier 用 AttributeBased 或 MMC 引用其他属性。

场景:RPG 攻击力派生

BaseAttackPower = 100,Strength = 50,WeaponMultiplier = 1.5
派生攻击力 = (BaseAttackPower + Strength) × WeaponMultiplier = 225
实现:Infinite GE
  Modifier[0] Additive → MMC 计算 (BaseAttackPower + Strength)
  Modifier[1] Multiply → WeaponMultiplier 属性

MMC 完整示例(捕获属性)

UCLASS()
class UMMC_DerivedAttackPower : public UGameplayModMagnitudeCalculation
{
    GENERATED_BODY()
public:
    UMMC_DerivedAttackPower()
    {
        StrengthDef.AttributeToCapture = UMyAttributeSet::GetStrengthAttribute();
        StrengthDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Source;
        StrengthDef.bSnapshot = true;                       // 快照
        RelevantAttributesToCapture.Add(StrengthDef);

        BaseAttackPowerDef.AttributeToCapture = UMyAttributeSet::GetBaseAttackPowerAttribute();
        BaseAttackPowerDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Source;
        BaseAttackPowerDef.bSnapshot = false;               // 实时
        RelevantAttributesToCapture.Add(BaseAttackPowerDef);
    }

    virtual float CalculateBaseMagnitude_Implementation(
        const FGameplayEffectSpec& Spec) const override
    {
        const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
        const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
        FAggregatorEvaluateParameters Eval;
        Eval.SourceTags = SourceTags;
        Eval.TargetTags = TargetTags;

        float Strength = 0.f, BaseAttack = 0.f;
        GetCapturedAttributeMagnitude(StrengthDef, Spec, Eval, Strength);
        GetCapturedAttributeMagnitude(BaseAttackPowerDef, Spec, Eval, BaseAttack);
        return BaseAttack + Strength;    // Multiply 部分在 GE 的另一 Modifier 配
    }
private:
    FGameplayEffectAttributeCaptureDefinition StrengthDef;
    FGameplayEffectAttributeCaptureDefinition BaseAttackPowerDef;
};

PIE 多客户端测试派生属性时,建议禁用 "Run Under One Process",否则除第一个客户端外派生属性可能不自动更新(GASDocumentation 注意点)。

4.6 属性的网络复制

属性复制由 AttributeSet 负责,每个要复制的属性需要三件套:

// 1. UPROPERTY 标记 ReplicatedUsing
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;

// 2. OnRep 回调(内部用 GAMEPLAYATTRIBUTE_REPNOTIFY 宏)
void UMyAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
    GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldHealth);
}

// 3. GetLifetimeReplicatedProps 注册
void UMyAttributeSet::GetLifetimeReplicatedProps(
    TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Always);
}

为什么必须 REPNOTIFY_Always

REPNOTIFY_Always 让引擎即使本地值已等于同步值(如因预测)也始终触发 OnRep。GAS 属性可能被客户端预测修改过,若不 Always 触发,预测系统的内部表示会不同步。

常见陷阱:漏用 GAMEPLAYATTRIBUTE_REPNOTIFY 宏 → 预测系统内部表示不同步,客户端与服务端属性值不一致。

4.7 三个关键回调(准确口径)

PreAttributeChange

  • 时机:属性 CurrentValue 即将变化之前(聚合器求值出新 CurrentValue 时)。
  • 用途:对 CurrentValue 做钳制(Clamp)、最大值变化时按比例调整当前值。
  • 注意:对 BaseValue 变化(Instant GE)不生效。
void UMyAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
    Super::PreAttributeChange(Attribute, NewValue);

    // 钳制移速
    if (Attribute == GetMoveSpeedAttribute())
    {
        NewValue = FMath::Clamp<float>(NewValue, 150.f, 1000.f);
    }

    // 最大值变化时按比例调整当前值(MaxHealth 翻倍 → Health 也翻倍)
    if (Attribute == GetMaxHealthAttribute())
    {
        AdjustAttributeForMaxChange(Health, MaxHealth, NewValue, GetHealthAttribute());
    }
}

void UMyAttributeSet::AdjustAttributeForMaxChange(
    FGameplayAttributeData& AffectedAttribute,
    const FGameplayAttributeData& MaxAttribute,
    float NewMaxValue,
    const FGameplayAttribute& AffectedAttributeProperty)
{
    UAbilitySystemComponent* ASC = GetOwningAbilitySystemComponent();
    const float CurrentMaxValue = MaxAttribute.GetCurrentValue();
    if (!FMath::IsNearlyEqual(CurrentMaxValue, NewMaxValue) && ASC)
    {
        const float CurrentValue = AffectedAttribute.GetCurrentValue();
        const float NewDelta = (CurrentMaxValue > 0.f)
            ? (CurrentValue * NewMaxValue / CurrentMaxValue) - CurrentValue
            : NewMaxValue;
        ASC->ApplyModToAttributeUnsafe(AffectedAttributeProperty,
            EGameplayModOp::Additive, NewDelta);
    }
}

重要限制:PreAttributeChange 中的钳制只改变查询返回值,不永久修改 Modifier 本身。Execution/MMC 自行聚合 Modifier 时拿到的仍是未钳制的原始值——因此 Execution/MMC 中需要自行实现钳制。

PreAttributeBaseChange

  • 时机:BaseValue 即将变化之前。
  • 用途:对 BaseValue 做限制;使用频率低于 PreAttributeChange。

PostGameplayEffectExecute(重点)

  • 时机:Instant(及 Periodic)GE 修改 BaseValue 之后(服务端权威执行)。
  • 用途:结算收尾——Clamp、死亡判定、吸血/护盾/减伤、触发表现与事件、发放奖励。
  • 调用时属性变化已发生但尚未复制,此时钳制不会产生两次网络更新。
void UMyAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
    Super::PostGameplayEffectExecute(Data);

    // 取来源信息
    FGameplayEffectContextHandle Context = Data.EffectSpec.GetContext();
    UAbilitySystemComponent* SourceASC = Context.GetOriginalInstigatorAbilitySystemComponent();

    if (Data.EvaluatedData.Attribute == GetDamageAttribute())
    {
        const float LocalDamage = GetDamage();
        SetDamage(0.f);                                        // Meta 用后清零

        if (LocalDamage > 0.f)
        {
            // 伤害分摊:护甲减伤 → 扣血 → 死亡判定
            const float FinalDamage = FMath::Max(LocalDamage - GetDefense() * 0.5f, 0.f);
            SetHealth(FMath::Clamp(GetHealth() - FinalDamage, 0.f, GetMaxHealth()));

            if (GetHealth() <= 0.f)
            {
                // 死亡:施加 GE_Death(Infinite, Granted Tag: State.Dead)或广播事件
                // 击杀奖励:给 SourceASC 施加动态 Instant GE(经验/金币)
            }
            // 触发受击表现(GameplayCue)
        }
    }
    else if (Data.EvaluatedData.Attribute == GetHealthAttribute())
    {
        SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
    }
    else if (Data.EvaluatedData.Attribute == GetManaAttribute())
    {
        SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
    }
}

设计哲学:Data.EvaluatedData.Attribute 判断顺序先检查 Damage 元属性、再检查 Health/Mana——确保伤害流水线先算、再统一扣血。

击杀奖励的动态 Instant GE(经典模式)

// 动态创建 Instant GE 发放经验/金币(只有 Instant 能运行时创建)
UGameplayEffect* GEBounty = NewObject<UGameplayEffect>(GetTransientPackage(), TEXT("Bounty"));
GEBounty->DurationPolicy = EGameplayEffectDurationType::Instant;
int32 Idx = GEBounty->Modifiers.Num();
GEBounty->Modifiers.SetNum(Idx + 2);
GEBounty->Modifiers[Idx].ModifierMagnitude = FScalableFloat(GetXPBounty());
GEBounty->Modifiers[Idx].ModifierOp = EGameplayModOp::Additive;
GEBounty->Modifiers[Idx].Attribute = UMyAttributeSet::GetXPAttribute();
// ... 金币同理
SourceASC->ApplyGameplayEffectToSelf(GEBounty, 1.f, SourceASC->MakeEffectContext());

4.8 OnAttributeAggregatorCreated(高级)

OnAttributeAggregatorCreated() 在属性 Aggregator 创建时触发。通过它可自定义 FAggregatorEvaluateMetaData,控制哪些 Modifier 有资格参与聚合。

经典案例(Paragon 移动速度):多个减速只取最强的那个,所有加速全部叠加 → 用 FAggregatorEvaluateMetaDataLibrary::MostNegativeMod_AllPositiveMods。

void UMyAttributeSet::OnAttributeAggregatorCreated(
    const FGameplayAttribute& Attribute,
    FAggregator* NewAggregator) const
{
    Super::OnAttributeAggregatorCreated(Attribute, NewAggregator);

    if (Attribute == GetMoveSpeedAttribute())
    {
        // 减速(负向)只取最强者,加速(正向)全部叠加
        NewAggregator->EvaluationMetaData =
            &FAggregatorEvaluateMetaDataLibrary::MostNegativeMod_AllPositiveMods;
    }
}

场景:玩家同时受 3 个减速(-200 / -150 / -100)和 1 个加速(+50): - 默认:Base + (-200-150-100) + 50 - MostNegativeMod_AllPositiveMods:Base + (-200) + 50(只取最强减速 + 所有正向)

4.9 AttributeSet 设计模式(三种)

模式一:单一大属性集(Monolithic)

所有属性放一个 AttributeSet 类,所有 Actor 共享。

UCLASS()
class UMyAttributeSet : public UAttributeSet
{
    GENERATED_BODY()
public:
    FGameplayAttributeData Health, MaxHealth, HealthRegenRate;
    FGameplayAttributeData Mana, MaxMana, ManaRegenRate;
    FGameplayAttributeData Stamina, MaxStamina;
    FGameplayAttributeData AttackPower, Armor, MoveSpeed;
    FGameplayAttributeData Gold, XP;
    FGameplayAttributeData Damage;   // 元属性
};
  • 优点:实现简单、引用统一、无需管理多个 AS 生命周期;
  • 缺点:逻辑混杂、文件膨胀、所有 Actor 携带所有属性;
  • 适用:小型项目、原型。

模式二:多个分组属性集(推荐)

按功能分组到不同 AttributeSet。

UCLASS() class UHealthAttributeSet : public UAttributeSet
{ FGameplayAttributeData Health, MaxHealth, HealthRegenRate, Damage; };
UCLASS() class UManaAttributeSet : public UAttributeSet
{ FGameplayAttributeData Mana, MaxMana, ManaRegenRate; };
UCLASS() class UCombatAttributeSet : public UAttributeSet
{ FGameplayAttributeData AttackPower, CritChance, Armor, MoveSpeed; };
  • 优点:职责清晰、按需赋予(小兵不需要 Mana 就不挂 ManaAttributeSet)、伤害处理集中;
  • 缺点:需管理多个 AS 的创建注册;
  • 适用:中大型项目(MOBA:英雄有法力、小兵没有)。

模式三:子类化属性集(Inheritance)

UCLASS() class UBaseAttributeSet : public UAttributeSet
{ FGameplayAttributeData Health, MaxHealth, MoveSpeed, Damage; };
UCLASS() class UCasterAttributeSet : public UBaseAttributeSet
{ FGameplayAttributeData Mana, MaxMana, SpellPower; };   // 继承 Health...
UCLASS() class UWarriorAttributeSet : public UCasterAttributeSet
{ FGameplayAttributeData Stamina, MaxStamina, Armor; };

重要细节:父类属性仍保留父类名前缀(UBaseAttributeSet.Health),子类新增属性用子类名(UCasterAttributeSet.Mana)。

  • 优点:符合 OO 继承、角色类型自然映射;
  • 缺点:前缀不随类名变易混淆、深层继承难维护;
  • 适用:中型项目。
维度 单一 分组 子类化
实现复杂度 低 中 中
可维护性 差 好 中
灵活性 低 高 中
推荐度 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐

最佳实践:大多数项目用多个分组属性集;单一大属性集只适合快速原型。

4.10 子组件属性(可破坏部件/槽位)

可破坏部件数量已知时,用一个 AttributeSet 预定义多个"槽位"属性:

UCLASS()
class UArmorAttributeSet : public UAttributeSet
{
    GENERATED_BODY()
public:
    UPROPERTY(ReplicatedUsing = OnRep_ArmorHealth0) FGameplayAttributeData ArmorHealth0;
    ATTRIBUTE_ACCESSORS(UArmorAttributeSet, ArmorHealth0)
    UPROPERTY(ReplicatedUsing = OnRep_ArmorMaxHealth0) FGameplayAttributeData ArmorMaxHealth0;
    ATTRIBUTE_ACCESSORS(UArmorAttributeSet, ArmorMaxHealth0)
    // ... ArmorHealth1/2/3 同理(最多 4 槽)
};

组件用 SlotIndex 指定自己用哪个槽位;伤害处理按槽位读取对应属性。

限制:若每个部件需要大量属性、数量不可预知、或部件可脱离被他人使用 → 改用普通浮点数。

4.11 运行时增删 AttributeSet(⚠️ 危险操作)

// 添加(物品/武器 AttributeSet 注册到玩家 ASC)
AbilitySystemComponent->GetSpawnedAttributes_Mutable().AddUnique(WeaponAS);
AbilitySystemComponent->ForceReplication();

// 移除(危险!)
AbilitySystemComponent->GetSpawnedAttributes_Mutable().Remove(WeaponAS);
AbilitySystemComponent->ForceReplication();

严重警告:运行时移除 AttributeSet 可能导致客户端崩溃——服务端移除后若还有待复制的属性变化引用该 AttributeSet,客户端收到时找不到对象就崩。GASShooter 早期版本踩过此坑。

最佳实践:尽量避免运行时移除 AttributeSet;若必须,确保没有待处理的属性变化;必要时用普通浮点数替代。

4.12 物品属性(武器弹药)的三种方案

维度 普通浮点数(推荐) 物品 AttributeSet 物品 ASC
实现复杂度 低 中 极高
GE Cost 支持 ❌ 需覆盖函数 ✅ ✅
同类型多件 ✅ ❌ 只能一件 ✅
移除安全 ✅ ⚠️ 有崩溃风险 ⚠️
推荐度 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐

方案一(Fortnite/GASShooter 实际做法):武器弹药用普通浮点数 + Replicated,配合 PreReplication + DOREPLIFETIME_ACTIVE_OVERRIDE 防止射击期间服务器旧值覆盖本地预测:

void AGSWeapon::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
{
    Super::PreReplication(ChangedPropertyTracker);
    DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, PrimaryClipAmmo,
        (IsValid(AbilitySystemComponent) &&
         !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag)));
}

关键要点:需要共享的备用弹药应放角色的共享 AttributeSet;只有弹夹弹药放武器浮点数。Epic 的 Dave Ratti 也认可"Pawn 与武器各自独立 ASC"有各自语义,但同 Owner 多 ASC 会很棘手——尽量避免。

4.13 属性初始化

// 方式一:代码初始化(简单原型)
AttributeSet->InitHealth(100.f);
AttributeSet->InitMaxHealth(100.f);

// 方式二(推荐,数据驱动):Instant GE 初始属性
FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(GE_HeroAttributes, 1.f, Context);
ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get());

4.14 本章验证

  • [ ] 定义好 Health/MaxHealth/Mana/MaxMana/AttackPower/Defense/MoveSpeed + Damage(Meta);
  • [ ] 能解释 Instant GE 与 Duration GE 分别改 BaseValue 还是 CurrentValue;
  • [ ] 用 Meta Attribute + PostGameplayEffectExecute 实现带护甲减伤的伤害结算;
  • [ ] 用 GetGameplayAttributeValueChangeDelegate 驱动 UI 血条;
  • [ ] 属性正确复制(REPNOTIFY_Always + OnRep + GAMEPLAYATTRIBUTE_REPNOTIFY);
  • [ ] 能口述 ATTRIBUTE_ACCESSORS 展开的四组函数;
  • [ ] 能对比三种 AttributeSet 设计模式并给出选择理由;
  • [ ] 理解 OnAttributeAggregatorCreated 的 Paragon 移动速度案例。

下一章:05 GameplayEffects 效果系统