总览
伤害系统是 GAS 最常见也最容易写乱的部分。简单项目里可以在 Ability 里算出伤害,然后直接扣 Health;但一旦有护甲、暴击、等级差、元素抗性、命中部位、伤害来源、吸血、护盾和服务器校验,这种写法很快失控。
GAS 更推荐的方向是:Ability 负责发起攻击和提供上下文,GameplayEffectSpec 携带本次伤害数据,ExecutionCalculation 做复杂结算,AttributeSet 消费 Meta Attribute 并修改最终属性。
源码依据
关键文件:
GameplayEffect.hGameplayEffectTypes.hAttributeSet.hAbilitySystemComponent.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 根据治疗强度、治疗加成、禁疗标签计算最终值。
- 输出到
HealingMeta 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.hEngine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent.cppEngine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent_Abilities.cppEngine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/Abilities/GameplayAbility.hEngine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayEffect.hEngine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AttributeSet.hEngine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayPrediction.h