04 AttributeSet 与属性系统
目标:掌握属性的定义方式、BaseValue 与 CurrentValue 的本质区别、Meta Attribute 的伤害流水线、属性变化监听、派生属性、AttributeSet 设计模式、三个关键回调的准确用途、OnAttributeAggregatorCreated,以及属性的网络复制与 ACCESSORS 宏原理。
4.1 Attribute 是什么
Attribute(属性) 是 GAS 中一个浮点数值,由结构体 FGameplayAttributeData 承载。它可以代表任何与游戏逻辑相关的数值——生命值、法力值、攻击力、暴击率、弹药、冷却缩减、等级、经验……
判断标准:如果一个数值属于某个 Actor 且会影响玩法,就应该考虑用 Attribute 表示。
Attribute 的核心特征
- 只能在 C++ 中定义,且必须定义在
UAttributeSet子类头文件里; - 应该仅通过 GameplayEffect 修改(确保 ASC 能预测变化、能聚合);
- 由 AttributeSet 持有并负责网络复制;
- 内部引用格式为
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,接收端)的逻辑分离。
元属性特征
- 临时性:每次新 GE 覆盖旧值,不持久;
- 不复制:不标记
ReplicatedUsing,不进GetLifetimeReplicatedProps; - 仅服务端有意义:伤害计算在服务端,客户端通过 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 移动速度案例。