GAS 系统深度学习指南UE 5.4 / 5.5

13 实战:从零构建一个技能角色(跟随教程)

目标:把前面所有知识串成一个可运行的演示项目 GASTutorial。按下面顺序一步步做,每步都有"做什么 / 怎么做 / 看到什么"。


13.0 项目规划

  • 引擎:UE 5.4+(模板:第三人称)
  • 语言:C++
  • 模块:GameplayAbilities / GameplayTags / GameplayTasks / EnhancedInput
  • 玩法:一个 ARPG 英雄,拥有近战连击、火球术、冲刺、被动回血、治疗术
  • 网络:ASC 放 PlayerState,Mixed 复制模式,LocalPredicted 技能

文件清单(完成后应有)

Source/GASTutorial/
├── GASTutorial.Build.cs
├── GASTutorialCharacter.h/.cpp       # 角色:IAbilitySystemInterface、输入
├── GASTutorialPlayerState.h/.cpp     # PlayerState:持有 ASC + AttributeSet
├── GASTutorialPlayerController.h/.cpp# 输入绑定
├── GASTutorialGameMode.h/.cpp        # 默认 PlayerState/角色
├── AbilitySystem/
│   ├── GAS_AttributeSet.h/.cpp       # 属性集
│   ├── GAS_AbilityBase.h/.cpp        # 技能基类
│   ├── GAS_MeleeAttack.h/.cpp
│   ├── GAS_Fireball.h/.cpp
│   ├── GAS_Dash.h/.cpp
│   ├── GAS_PassiveRegen.h/.cpp
│   ├── GAS_Heal.h/.cpp
│   ├── GAS_ExecDamage.h/.cpp         # 伤害 Execution
│   ├── GAS_TargetActor_Fireball.h/.cpp
│   ├── GAS_Projectile.h/.cpp
│   └── GAS_GameplayCue_*(C++ 或蓝图)
Content/GASTutorial/
├── Abilities/Effects/  ...            # GE、Cue 蓝图资产
└── Characters/ ...

13.1 第一步:启用模块与全局初始化

1.1 模块依赖 GASTutorial.Build.cs:

PublicDependencyModuleNames.AddRange(new string[] {
    "Core", "CoreUObject", "Engine", "InputCore",
    "EnhancedInput", "GameplayAbilities", "GameplayTags", "GameplayTasks"
});

1.2 (仅 UE 5.2 及以下需要) 在 AssetManager 里手动调用 InitGlobalData();UE 5.3+ 引擎自动调用,可跳过。

1.3 项目设置 → Gameplay Tags 注册标签(也可直接写 Config/DefaultGameplayTags.ini):

[/Script/GameplayTags.GameplayTagsList]
GameplayTagList=(Tag="State.Dead",DevComment="死亡")
GameplayTagList=(Tag="State.Stunned",DevComment="眩晕")
GameplayTagList=(Tag="State.Combat",DevComment="战斗")
GameplayTagList=(Tag="Ability.Skill.Fireball",DevComment="火球术")
GameplayTagList=(Tag="Ability.Skill.MeleeAttack",DevComment="近战")
GameplayTagList=(Tag="Ability.Skill.Dash",DevComment="冲刺")
GameplayTagList=(Tag="Cooldown.Ability.Fireball",DevComment="火球冷却")
GameplayTagList=(Tag="Cooldown.Ability.Dash",DevComment="冲刺冷却")
GameplayTagList=(Tag="Data.Damage",DevComment="伤害数值(SetByCaller)")
GameplayTagList=(Tag="Data.Cooldown",DevComment="冷却时长(SetByCaller)")
GameplayTagList=(Tag="Event.MeleeHit",DevComment="近战命中")
GameplayTagList=(Tag="Event.Death",DevComment="死亡事件")
GameplayTagList=(Tag="Input.Attack",DevComment="攻击输入")
GameplayTagList=(Tag="Input.Fireball",DevComment="火球输入")
GameplayTagList=(Tag="Input.Dash",DevComment="冲刺输入")
GameplayTagList=(Tag="Input.Heal",DevComment="治疗输入")
GameplayTagList=(Tag="GameplayCue.Damage.Fire",DevComment="火焰伤害命中")
GameplayTagList=(Tag="GameplayCue.Ability.Dash",DevComment="冲刺拖影")
GameplayTagList=(Tag="GameplayCue.Buff.Speed",DevComment="加速Buff")

看到什么:项目能编译;编辑器内 Tag 列表出现上述标签。

13.2 第二步:AttributeSet

2.1 GAS_AttributeSet.h/.cpp:按第 04 章模板实现 Health/MaxHealth/Mana/MaxMana/AttackPower/Defense/MoveSpeed + Meta(Damage、Healing),带复制与 OnRep、PostGameplayEffectExecute 伤害结算、死亡事件触发。

// 关键片段(伤害结算 + 死亡事件)
void UGAS_AttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
    Super::PostGameplayEffectExecute(Data);
    if (Data.EvaluatedData.Attribute == GetDamageAttribute())
    {
        const float LocalDamage = GetDamage();
        SetDamage(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)
        {
            // 通过一个 Death GE(Infinite + Granted Tag State.Dead)进入死亡状态
            if (UAbilitySystemComponent* ASC = GetOwningAbilitySystemComponent())
            {
                FGameplayEffectContextHandle Ctx = ASC->MakeEffectContext();
                ASC->ApplyGameplayEffectToSelf(GE_Death, 1.f, Ctx);
            }
        }
    }
}

看到什么:属性在角色默认值可见;施加伤害 GE 后 HP 下降、受护甲减伤、HP=0 进死亡。

13.3 第三步:PlayerState + ASC

3.1 GASTutorialPlayerState:持有 UAbilitySystemComponent + UGAS_AttributeSet,实现 IAbilitySystemInterface,构造函数 CreateDefaultSubobject。

3.2 角色 GASTutorialCharacter:

// PossessedBy(服务端):初始化 + 授予技能
void AGASTutorialCharacter::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);
    if (AGASTutorialPlayerState* PS = GetPlayerState<AGASTutorialPlayerState>())
    {
        PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
        if (HasAuthority()) PS->GetAbilitySystemComponent()->SetReplicationMode(
            EGameplayEffectReplicationMode::Mixed);
    }
}

// OnRep_PlayerState(客户端)
void AGASTutorialCharacter::OnRep_PlayerState()
{
    Super::OnRep_PlayerState();
    if (AGASTutorialPlayerState* PS = GetPlayerState<AGASTutorialPlayerState>())
    {
        PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
    }
}

看到什么:双人联机两端都能通过 GetAbilitySystemComponent() 拿到 ASC。

13.4 第四步:三个基础 GE

在内容浏览器创建 GE 蓝图(在 Content/GASTutorial/Abilities/Effects):

GE Duration Modifier 说明
GE_InitAttributes Instant Health/MaxHealth/Mana/MaxMana Override、AttackPower/Defense/MoveSpeed Override 初始化属性
GE_Cost_Mana Instant Mana Add -25 火球耗蓝
GE_Cooldown_Fireball HasDuration(3s) — Granted Tags: Cooldown.Ability.Fireball
GE_Cooldown_Dash HasDuration(5s) — Granted Tags: Cooldown.Ability.Dash
GE_Damage Instant Damage(Meta) SetByCaller(Data.Damage) 通用伤害
GE_SpeedBuff HasDuration(5s) MoveSpeed Multiplicitive 1.3 加速 Buff
GE_Regen Infinite Health/Mana Add(Period 2s 各 +5) 被动回血
GE_Heal Instant Healing(Meta) SetByCaller(Data.Healing) 治疗
GE_Death Infinite — Granted Tags: State.Dead

看到什么:手动 Apply 这些 GE,属性按预期变化;到期自动回退(Duration);HP 到底进死亡。

13.5 第五步:技能基类

GAS_AbilityBase(继承 UGameplayAbility):

UCLASS()
class UGAS_AbilityBase : public UGameplayAbility
{
    GENERATED_BODY()
public:
    // 默认统一策略
    UGAS_AbilityBase()
    {
        InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerExecution;
        NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
        ActivationBlockedTags.AddTag(FGameplayTag::RequestGameplayTag(FName("State.Dead")));
    }
};

看到什么:派生技能都自动继承"死亡不能放技能"。

13.6 第六步:近战连击(Montage + 事件)

GAS_MeleeAttack:用 PlayMontageAndWait 播放攻击动画,动画通知发 Event.MeleeHit,技能监听 WaitGameplayEvent 后对面前扇形内敌人施加 GE_Damage。

void UGAS_MeleeAttack::ActivateAbility(...)
{
    if (!CommitAbility(...)) { EndAbility(...); return; }

    UAbilityTask_PlayMontageAndWait* MontageTask =
        UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
            this, NAME_None, AttackMontage);
    MontageTask->OnCompleted.AddDynamic(this, &UGAS_MeleeAttack::OnEnd);
    MontageTask->OnInterrupted.AddDynamic(this, &UGAS_MeleeAttack::OnEnd);
    MontageTask->ReadyForActivation();

    // 监听命中事件
    UAbilityTask_WaitGameplayEvent* EventTask =
        UAbilityTask_WaitGameplayEvent::CreateWaitGameplayEvent(
            this, FGameplayTag::RequestGameplayTag(FName("Event.MeleeHit")), nullptr, true);
    EventTask->EventReceived.AddDynamic(this, &UGAS_MeleeAttack::OnHitEvent);
    EventTask->ReadyForActivation();
}

看到什么:按键 → 播放攻击动画 → 动画中命中帧 → 敌人掉血。

13.7 第七步:火球术(TargetData + 投射物 + 冷却)

7.1 GAS_TargetActor_Fireball:SingleLineTrace 从镜头向前射线,返回目标点。

7.2 GAS_Fireball 逻辑:

void UGAS_Fireball::ActivateAbility(...)
{
    if (!CommitAbility(...)) { EndAbility(...); return; }

    UAbilityTask_WaitTargetData* TargetTask =
        UAbilityTask_WaitTargetData::CreateWaitTargetData(
            this, NAME_None, EGameplayTargetingConfirmation::UserConfirmed,
            TargetActorClass);
    TargetTask->ValidData.AddDynamic(this, &UGAS_Fireball::OnTargetDataReady);
    TargetTask->ReadyForActivation();
}

void UGAS_Fireball::OnTargetDataReady(const FGameplayAbilityTargetDataHandle& Data)
{
    // 生成投射物(携带伤害 Spec,SetByCaller 设置 Data.Damage = AttackPower*2)
    // 投射物命中目标 → 施加 GE_Damage
    // 触发 GameplayCue.Damage.Fire
    EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
}

看到什么:左键 → 出现瞄准指示 → 点击 → 火球飞向目标 → 命中爆炸 + 敌人掉血 + 冷却期间无法再放。

13.8 第八步:冲刺(RootMotion + 冷却)

GAS_Dash:用 UAbilityTask_ApplyRootMotionConstantForce(或蒙太奇根运动)向前冲一小段,配 GE_Cooldown_Dash,触发 GameplayCue.Ability.Dash。

看到什么:按键 → 角色瞬间前冲 → 带拖影 → 5 秒冷却。

13.9 第九步:被动回血 + 治疗

  • 被动:GAS_PassiveRegen 授予时用 GiveAbilityAndActivateOnce,激活后施加 GE_Regen(Infinite + Period)并 EndAbility(挂后台)。注意 GE_Regen 由服务端施加,客户端看到的是属性值变化。
  • 治疗:GAS_Heal 对自己施加 GE_Heal(SetByCaller Healing),PostGameplayEffectExecute 里加血。

看到什么:每 2 秒自动回 5 血 5 蓝;按治疗键立即回血。

13.10 第十步:GameplayCue

用蓝图/ C++ 建两个 Cue:

  • GC_Impact_Fire(Static):火球命中生成爆炸粒子 + 音效 + 飘字;
  • GC_Dash(Actor):冲刺拖影(可循环、跟随)。

在 GE/技能中用 K2_ExecuteGameplayCue / ExecuteGameplayCue 触发。

看到什么:命中/冲刺都有视觉反馈;双人联机时所有客户端都看到特效(Cue 多播)。

13.11 第十一步:输入绑定

11.0 先定义输入 ID 枚举(与授予技能时的 InputID 对应):

UENUM(BlueprintType)
enum class EAbilityInputID : uint8
{
    None,
    Attack,       // 近战
    Fireball,     // 火球
    Dash,         // 冲刺
    Heal,         // 治疗
    MAX
};

11.1 GASTutorialPlayerController:

void AGASTutorialPlayerController::SetupInputComponent()
{
    Super::SetupInputComponent();
    UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
    AGASTutorialCharacter* Char = Cast<AGASTutorialCharacter>(GetPawn());
    if (EIC && Char)
    {
        UAbilitySystemComponent* ASC = Char->GetAbilitySystemComponent();
        // 每个 InputAction 绑定 AbilityLocalInputPressed/Released + InputID
        EIC->BindAction(IA_Attack, ETriggerEvent::Started, ASC,
            &UAbilitySystemComponent::AbilityLocalInputPressed, (int32)EAbilityInputID::Attack);
        // ... Fireball / Dash / Heal 同理
    }
}

授予技能时给 Spec 设置对应 InputID。看到什么:按键触发技能。

13.12 第十二步:初始属性与授予(GameMode 或 Character)

void AGASTutorialCharacter::GrantDefaultAbilities()
{
    UAbilitySystemComponent* ASC = GetAbilitySystemComponent();
    // 1. 施加初始属性 GE
    FGameplayEffectContextHandle Ctx = ASC->MakeEffectContext();
    ASC->ApplyGameplayEffectToSelf(GE_InitAttributes, 1.f, Ctx);

    // 2. 授予技能(带 InputID)
    ASC->GiveAbility(FGameplayAbilitySpec(MeleeClass, 1, (int32)EAbilityInputID::Attack));
    ASC->GiveAbility(FGameplayAbilitySpec(FireballClass, 1, (int32)EAbilityInputID::Fireball));
    ASC->GiveAbility(FGameplayAbilitySpec(DashClass, 1, (int32)EAbilityInputID::Dash));
    ASC->GiveAbility(FGameplayAbilitySpec(HealClass, 1, (int32)EAbilityInputID::Heal));
    // 被动
    ASC->GiveAbilityAndActivateOnce(FGameplayAbilitySpec(RegenClass, 1));
}

看到什么:生成角色即有完整属性与技能列表,全部技能可正常使用。

13.13 完整验证清单(多人)

在 Listen Server 双开(PIE 多客户端):

  • [ ] 玩家 A 放火球 → 玩家 B 的机器上也看到火球飞行与爆炸特效(Cue 多播);
  • [ ] 玩家 A 的 HP 变化只有 A 自己与服务端可见(Mixed 属性复制);
  • [ ] 玩家 B 放冲刺 → A 看到 B 前冲 + 拖影(动画复制 + Cue);
  • [ ] 玩家 A 死亡 → 无法再放技能(ActivationBlockedTags: State.Dead);
  • [ ] 冷却期间技能按钮置灰(UI 用 GetCooldownTimeRemaining);
  • [ ] 打开 showdebug abilitysystem,分别查看自己与对方的标签/技能/效果状态。

13.14 常见卡点速查

现象 原因排查
技能点了没反应 InputID 没对上 / GA 没授予 / Tag 拦截(State.Dead)
客户端不播放动画 忘记 ReadyForActivation() / Montage 没配复制
伤害只在单人有效 GE 复制模式 / 客户端没走服务端权威路径
冷却没生效 冷却 GE 是 Instant(应为 HasDuration)/ Granted Tags 没配
死亡还能放技能 Death GE 没配 Granted Tags=State.Dead / BlockedTags 没加
"Can't activate LocalOnly or LocalPredicted ability when not local!" 客户端没调 InitAbilityActorInfo(OnRep_PlayerState 里补)
动画不复制(PlayMontage) GA 里别用 AnimInstance->Montage_Play,用 PlayMontageAndWait(经 ASC 复制)
AttributeSet 为 nullptr(复制蓝图 Actor) 引擎 Bug(UE-81109):别在头文件声明 AS 指针,PostInitializeComponents 里 AddSet<T>()
ScriptStructCache 错误断线 仅 UE 4.24~5.2:AssetManager::StartInitialLoading 里 InitGlobalData();5.3+ 自动
枚举名路径警告 C4996 BindAbilityActivationToInputComponent 用 FTopLevelAssetPath 代替 FString 枚举名
MarkPropertyDirty LNK2019 Build.cs 加 NetCore 模块

下一章:14 高级实战模式与性能优化