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 高级实战模式与性能优化