UE5.8 Gameplay Ability System 专题系列

UE5.8 GAS 专题(三):GameplayAbility 生命周期与输入激活

从 GiveAbility、AbilitySpec、TryActivateAbility、CanActivateAbility、CommitAbility 到 EndAbility,梳理 Ability 的完整执行链路与输入绑定方式。

总览

UGameplayAbility 是 GAS 里最像“玩法代码”的地方,但它不应该变成巨型脚本。一个 Ability 的职责是组织一次行为流程:能不能激活、什么时候提交消耗、等待什么异步事件、对哪些目标应用什么 GE、什么时候结束。

本篇按 UE5.8 源码链路讲清一次 Ability 从授予到结束的过程,并给出输入、实例策略、网络策略和项目基类建议。

UE5.8 GAS 专题(三):GameplayAbility 生命周期与输入激活 配图
Ability 激活主线:输入触发、ASC 查找 Spec、做权限和预测检查,Ability 内部再提交消耗和结束。

源码依据

关键文件:

  • GameplayAbility.h
  • GameplayAbilityTypes.h
  • GameplayAbilitySpec.h
  • AbilitySystemComponent_Abilities.cpp

源码注释列出了 Ability 的主流程:CanActivateAbilityTryActivateAbilityCallActivateAbilityActivateAbilityCommitAbilityCancelAbilityEndAbility

ASC 里的执行链路大致是:

  1. GiveAbility 在服务器创建 FGameplayAbilitySpec
  2. 输入或代码调用 TryActivateAbility
  3. ASC 检查 Spec、ActorInfo、网络策略。
  4. InternalTryActivateAbility 调用 CanActivateAbility
  5. 本地预测 Ability 创建 FScopedPredictionWindow 并发 ServerTryActivateAbility
  6. 创建或获取 Ability 实例。
  7. 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 都按这个顺序设计:

  1. 写清 Ability Tag、Input Tag、Cooldown Tag。
  2. 选择 Instancing Policy。
  3. 选择 Net Execution Policy。
  4. 配置 Cost GE 和 Cooldown GE。
  5. CanActivateAbility 只放特殊条件,不重复 Cost/Cooldown。
  6. ActivateAbility 里先 CommitAbility
  7. 异步等待用 AbilityTask。
  8. 对目标应用 GE,不直接改属性。
  9. 明确每条路径都调用 EndAbility
  10. 在失败路径里区分 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.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