UE5.8 Gameplay Ability System 专题系列

UE5.8 GAS 专题(五):伤害、治疗、Execution 与 SetByCaller

把伤害公式从 Ability 中拆出来,系统讲解 SetByCaller、Attribute Capture、ExecutionCalculation、Meta Attribute 和服务器权威结算。

总览

伤害系统是 GAS 最常见也最容易写乱的部分。简单项目里可以在 Ability 里算出伤害,然后直接扣 Health;但一旦有护甲、暴击、等级差、元素抗性、命中部位、伤害来源、吸血、护盾和服务器校验,这种写法很快失控。

GAS 更推荐的方向是:Ability 负责发起攻击和提供上下文,GameplayEffectSpec 携带本次伤害数据,ExecutionCalculation 做复杂结算,AttributeSet 消费 Meta Attribute 并修改最终属性。

UE5.8 GAS 专题(五):伤害、治疗、Execution 与 SetByCaller 配图
复杂伤害适合用 ExecutionCalculation 结算,再把结果写入 Damage 这类 Meta Attribute。

源码依据

关键文件:

  • GameplayEffect.h
  • GameplayEffectTypes.h
  • AttributeSet.h
  • AbilitySystemComponent.cpp

FGameplayEffectSpec 支持 SetByCaller Magnitude。Execution 能读取 Spec、Source Tags、Target Tags、EffectContext 和捕获属性。AttributeSet 的 PostGameplayEffectExecute 是处理 Instant GE 结果的关键位置。

源码预测说明里也有边界:复杂 Execution 结果通常不做客户端预测。客户端可以预测动画、Cue 和临时反馈,最终伤害以服务器权威复制为准。

伤害数据从哪里来

一次伤害通常由多层数据组成:

来源 示例 推荐位置
Ability 等级 火球 3 级 AbilitySpec Level
武器数据 基础伤害 80 SourceObject 或 DataAsset
运行时参数 蓄力倍率 1.6 SetByCaller
攻击者属性 AttackPower、CritChance Source Attribute Capture
目标属性 Armor、FireResistance Target Attribute Capture
命中信息 Headshot、HitBone EffectContext 或 TargetData
状态标签 Status.Burning、Shielded Source/Target Tags

Ability 不应该独自完成所有计算。它最多收集本次攻击的上下文,把数据放进 Spec,然后让 GE/Execution 处理结算。

SetByCaller 的职责

SetByCaller 适合存本次施放才知道的数值:

  • 蓄力时间。
  • 武器随机出来的基础伤害。
  • 距离衰减后的倍率。
  • 连击段数倍率。
  • 命中部位倍率。
  • 外部系统传来的治疗量。
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(DamageEffectClass, GetAbilityLevel());
if (SpecHandle.IsValid())
{
    static const FGameplayTag DamageTag = FGameplayTag::RequestGameplayTag(TEXT("Data.Damage"));
    static const FGameplayTag HeadshotTag = FGameplayTag::RequestGameplayTag(TEXT("Data.HeadshotMultiplier"));

    SpecHandle.Data->SetSetByCallerMagnitude(DamageTag, BaseDamage);
    SpecHandle.Data->SetSetByCallerMagnitude(HeadshotTag, bHeadshot ? 2.0f : 1.0f);

    ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, SpecHandle, TargetData);
}

Tag 命名建议统一放在 Data.* 命名空间。不要用散乱 FName,否则内容资产和 C++ 很难互相检查。

ExecutionCalculation 骨架

复杂伤害建议使用 UGameplayEffectExecutionCalculation。属性捕获声明通常写成静态结构。

struct FDamageStatics
{
    DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
    DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower);

    FDamageStatics()
    {
        DEFINE_ATTRIBUTE_CAPTUREDEF(UCombatAttributeSet, Armor, Target, false);
        DEFINE_ATTRIBUTE_CAPTUREDEF(UCombatAttributeSet, AttackPower, Source, false);
    }
};

static const FDamageStatics& DamageStatics()
{
    static FDamageStatics Statics;
    return Statics;
}

Execution 中读取 SetByCaller 和捕获属性:

void UExecCalc_Damage::Execute_Implementation(
    const FGameplayEffectCustomExecutionParameters& ExecutionParams,
    FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
    const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();

    static const FGameplayTag DamageTag = FGameplayTag::RequestGameplayTag(TEXT("Data.Damage"));
    static const FGameplayTag HeadshotTag = FGameplayTag::RequestGameplayTag(TEXT("Data.HeadshotMultiplier"));

    const float BaseDamage = Spec.GetSetByCallerMagnitude(DamageTag, false, 0.0f);
    const float HeadshotMultiplier = Spec.GetSetByCallerMagnitude(HeadshotTag, false, 1.0f);

    FAggregatorEvaluateParameters Params;
    Params.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
    Params.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

    float Armor = 0.0f;
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, Params, Armor);

    float AttackPower = 0.0f;
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().AttackPowerDef, Params, AttackPower);

    const float RawDamage = (BaseDamage + AttackPower) * HeadshotMultiplier;
    const float FinalDamage = FMath::Max(0.0f, RawDamage - Armor);

    OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(
        UCombatAttributeSet::GetDamageAttribute(),
        EGameplayModOp::Additive,
        FinalDamage));
}

这里输出的是 Damage Meta Attribute,而不是直接改 Health。这样 AttributeSet 可以统一处理护盾、死亡、受击事件、溢出和 UI 通知。

AttributeSet 消费 Damage

void UCombatAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
    Super::PostGameplayEffectExecute(Data);

    if (Data.EvaluatedData.Attribute == GetDamageAttribute())
    {
        const float LocalDamage = GetDamage();
        SetDamage(0.0f);

        if (LocalDamage <= 0.0f)
        {
            return;
        }

        const float NewHealth = FMath::Clamp(GetHealth() - LocalDamage, 0.0f, GetMaxHealth());
        SetHealth(NewHealth);

        if (NewHealth <= 0.0f)
        {
            // 通知角色或死亡组件,不要在 AttributeSet 里直接销毁 Actor。
        }
    }
}

AttributeSet 可以判断 Health 为 0,但不建议直接播放动画、销毁 Actor、发奖励。它应该广播事件或调用受控接口,让死亡系统处理后续流程。

治疗和护盾

治疗可以复用类似结构:

  • SetByCaller 写入 Data.Heal
  • Execution 根据治疗强度、治疗加成、禁疗标签计算最终值。
  • 输出到 Healing Meta Attribute。
  • AttributeSet 把 Healing 加到 Health,并 clamp 到 MaxHealth。

护盾有两种常见设计:

方案 说明 适合
Shield Attribute 护盾是属性,Damage 先扣 Shield 再扣 Health 数值护盾
Shield GE 护盾是 Duration GE,提供标签和吸收逻辑 临时状态、可驱散

项目早期建议先用 Shield Attribute,逻辑直观。后期需要护盾来源、可驱散、可叠层时,再结合 GE 和 Execution 扩展。

服务器权威与预测边界

伤害结算一般服务器权威。客户端可以预测以下内容:

  • 开火动画。
  • 枪口火光。
  • 命中火花。
  • 本地准星反馈。
  • 轻量的临时 UI 反馈。

不要让客户端决定最终伤害、击杀、掉落和积分。即使 LocalPredicted Ability 本地先执行,服务器也必须重新校验 TargetData、距离、视线和时间,再应用权威 GE。

项目落地清单

建议
伤害输入 用 SetByCaller 放运行时数值
公式位置 简单用 Modifier,复杂用 Execution
属性捕获 Source 捕获攻击者,Target 捕获防御者
输出 写入 Damage Meta Attribute
扣血 AttributeSet 的 PostExecute 统一处理
表现 用 GameplayCue,不在 Execution 里播特效
日志 记录 Source、Target、RawDamage、FinalDamage、Tags

常见坑

  • Ability 里直接扣 Health。 绕过 GE 和 AttributeSet,后期很难加护甲、标签和调试。
  • Execution 里播放表现。 Execution 是结算,不是表现层。
  • Damage 不清零。 Meta Attribute 不清理会污染后续结算。
  • 捕获属性方向写错。 Armor 应该从 Target 捕获,AttackPower 通常从 Source 捕获。
  • 客户端权威伤害。 多人项目必须服务器校验并应用最终 GE。
  • 所有伤害共用一个巨大 Execution。 可以先统一,复杂后按伤害类型拆分。

本篇结论:伤害系统要把“发起、携带、计算、消费、表现”分层。Ability 发起,Spec 携带,Execution 计算,AttributeSet 消费,Cue 表现。这样伤害越复杂,结构反而越清晰。

源码路径索引

  • Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemComponent.h
  • Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent.cpp
  • Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent_Abilities.cpp
  • Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/Abilities/GameplayAbility.h
  • Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayEffect.h
  • Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AttributeSet.h
  • Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayPrediction.h