总览
UGameplayAbility 是 GAS 里最像“玩法代码”的地方,但它不应该变成巨型脚本。一个 Ability 的职责是组织一次行为流程:能不能激活、什么时候提交消耗、等待什么异步事件、对哪些目标应用什么 GE、什么时候结束。
本篇按 UE5.8 源码链路讲清一次 Ability 从授予到结束的过程,并给出输入、实例策略、网络策略和项目基类建议。
源码依据
关键文件:
GameplayAbility.hGameplayAbilityTypes.hGameplayAbilitySpec.hAbilitySystemComponent_Abilities.cpp
源码注释列出了 Ability 的主流程:CanActivateAbility、TryActivateAbility、CallActivateAbility、ActivateAbility、CommitAbility、CancelAbility、EndAbility。
ASC 里的执行链路大致是:
GiveAbility在服务器创建FGameplayAbilitySpec。- 输入或代码调用
TryActivateAbility。 - ASC 检查 Spec、ActorInfo、网络策略。
InternalTryActivateAbility调用CanActivateAbility。- 本地预测 Ability 创建
FScopedPredictionWindow并发ServerTryActivateAbility。 - 创建或获取 Ability 实例。
CallActivateAbility进入 Ability 的ActivateAbility。
AbilitySpec 是关键中间层
项目里经常说“角色有某个 Ability”,源码实际表达是“ASC 有一个 FGameplayAbilitySpec”。Spec 里包含:
| 字段 | 说明 |
|---|---|
| Ability | Ability CDO 或类 |
| Level | 能力等级 |
| InputID | 旧输入路径可用 |
| SourceObject | 武器、装备或授予来源 |
| Handle | 激活和查找用的稳定句柄 |
| ActiveCount | 当前激活次数 |
| DynamicAbilityTags | 运行时附加标签 |
| ReplicatedInstances | 复制实例 |
| NonReplicatedInstances | 非复制实例 |
| GameplayEffectHandle | 如果由 GE 授予,记录来源 |
这意味着武器授予技能时,不应该只知道 AbilityClass,还应该考虑 SourceObject。比如同一个 GA_FireWeapon 可以由不同武器授予,Ability 内通过 SourceObject 读取弹道、后坐力、伤害 GE。
FGameplayAbilitySpec Spec(FireAbilityClass, WeaponLevel);
Spec.SourceObject = EquippedWeapon;
AbilitySystem->GiveAbility(Spec);
授予能力
授予能力必须在服务器执行。客户端输入只能请求激活,不能创建权威 Spec。
void AYourCharacter::GrantStartupAbilities()
{
if (!HasAuthority() || !AbilitySystem)
{
return;
}
for (const TSubclassOf<UGameplayAbility> AbilityClass : StartupAbilities)
{
if (!AbilityClass)
{
continue;
}
FGameplayAbilitySpec Spec(AbilityClass, 1);
AbilitySystem->GiveAbility(Spec);
}
}
如果 Ability 来自装备或 Buff,建议集中在装备组件或 GE Component 里授予和移除,不要让角色类到处 GiveAbility。
激活方式
ASC 支持多种激活方式:
| 方式 | 用法 | 场景 |
|---|---|---|
| By Handle | TryActivateAbility(Handle) |
UI 或系统保存了 SpecHandle |
| By Class | TryActivateAbilityByClass |
单例技能、测试命令 |
| By Tag | TryActivateAbilitiesByTag |
输入 Tag、行为分类 |
| GameplayEvent | Ability Trigger | 命中、交互、动画事件 |
新项目推荐使用 Input Tag,而不是旧式 InputID。Enhanced Input 收到输入后,把输入 Tag 传给 ASC,再用 Ability 的动态或资产 Tag 匹配。
void UYourAbilityInputComponent::AbilityInputPressed(const FGameplayTag& InputTag)
{
if (!AbilitySystem || !InputTag.IsValid())
{
return;
}
FGameplayTagContainer Tags;
Tags.AddTag(InputTag);
AbilitySystem->TryActivateAbilitiesByTag(Tags);
}
输入层不应该知道具体 AbilityClass。它只说“玩家按下了 Input.Ability.Primary”,由 ASC 和 Ability 标签决定激活谁。
Instancing Policy
UE5.8 仍能看到三种 Instancing Policy:
| 策略 | 说明 | 建议 |
|---|---|---|
InstancedPerActor |
每个 ASC 一个实例,可保存状态 | 默认推荐 |
InstancedPerExecution |
每次激活一个新实例 | 短生命周期、完全独立技能 |
NonInstanced |
不创建实例,共用 CDO | UE5.5 起 deprecated,新项目避免 |
InstancedPerActor 最适合绝大多数项目。它允许 Ability 保存轻量运行时状态,比如当前 Combo 段、缓存的 Montage Task、最近目标句柄。InstancedPerExecution 要注意实例数量和复制策略;源码里对某些预测复制组合有明确警告。
Net Execution Policy
| 策略 | 执行位置 | 适合 |
|---|---|---|
LocalPredicted |
客户端先执行,服务器确认 | 翻滚、开火、近战起手 |
LocalOnly |
只在本地 | UI、纯表现 |
ServerInitiated |
服务器发起,可让客户端表现 | 怪物技能、服务器事件 |
ServerOnly |
只在服务器 | 权威结算、管理型能力 |
手感强的技能用 LocalPredicted,但结果敏感的部分仍由服务器确认。比如射击可以本地播动画和枪口火光,但命中伤害必须服务器校验。
Ability 内部结构
推荐项目 Ability 基类提供统一工具函数:
UCLASS(Abstract)
class UYourGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
UYourGameplayAbility();
protected:
UPROPERTY(EditDefaultsOnly, Category = "Ability")
FGameplayTag InputTag;
UPROPERTY(EditDefaultsOnly, Category = "Ability")
FGameplayTagContainer ActivationRequiredTags;
bool ApplySetByCallerEffectToTarget(
const FGameplayAbilityTargetDataHandle& TargetData,
TSubclassOf<UGameplayEffect> EffectClass,
FGameplayTag DataTag,
float Magnitude);
};
Ability 子类只写当前行为独有流程。通用的取 ASC、取 Avatar、读 SourceObject、构建 Spec、错误结束、日志都放到基类。
CommitAbility 的意义
CommitAbility 不是可有可无的一行。它会统一处理:
- 冷却检查。
- 消耗检查。
- 应用 Cost GE。
- 应用 Cooldown GE。
- 广播 Commit 结果。
void UGA_Dash::ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
StartDashMovement();
PlayDashMontage();
}
不要在每个 Ability 里手写“Mana 是否够、冷却是否结束”。这会让 UI 查询、服务器拒绝和预测失败都分裂。
项目落地流程
建议每个 Ability 都按这个顺序设计:
- 写清 Ability Tag、Input Tag、Cooldown Tag。
- 选择 Instancing Policy。
- 选择 Net Execution Policy。
- 配置 Cost GE 和 Cooldown GE。
- 在
CanActivateAbility只放特殊条件,不重复 Cost/Cooldown。 ActivateAbility里先CommitAbility。- 异步等待用 AbilityTask。
- 对目标应用 GE,不直接改属性。
- 明确每条路径都调用
EndAbility。 - 在失败路径里区分 Cancel 和 ReplicateEndAbility。
常见坑
- Ability 不 End。 ActiveCount 不归零,后续激活或取消会异常。
- Ability 里写永久状态。 状态应进 GE、AttributeSet 或外部组件,Ability 负责流程。
- Cost/Cooldown 手写。 UI、预测和服务器检查无法统一。
- 输入直接绑定 AbilityClass。 后续换技能、装备授予、动态技能栏会很难做。
- 滥用 NonInstanced。 5.5 起已 deprecated,新项目不要依赖。
- 客户端授予 Ability。 本地看起来有,服务器没有,激活必然出问题。
本篇结论:Ability 是流程编排器,不是所有玩法逻辑的垃圾桶。把授予、输入、Commit、Task、GE 和 End 的边界固定下来,技能数量变多后仍然能维护。
源码路径索引
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