前言

GAS(Gameplay Ability System)是UE5中用于快速搭建MOBA(比如英雄联盟)、FPS(比如穿越火线、CS)等游戏技能系统的强大框架。

为什么需要GAS?

如果你已经能用UE5的蓝图和动画系统做出一个能跑能跳、会放几个简单技能的角色,那你肯定遇到过下面这些“糟心事儿”。

  • 比如,想给角色加个“生命值”属性,结果发现这个值要在UI上显示、要被技能伤害扣除、还要被治疗技能恢复,不得不在蓝图的各个角落里写上一堆“Set Health”、“Get Health”的节点,稍微改点逻辑就得满世界找。
  • 再比如,想做个“火球术”技能,要有施法动作、飞行轨迹、碰撞伤害,还得有个冷却时间,可能得用一个状态机、一堆定时器、再加上事件分发器才能勉强拼出来,代码(或者蓝图)乱得像一团麻。

刚开始开发的时候,都是通过基类硬写的。如果项目里的技能数量上了两位数,各种状态效果纠缠在一起,才意识到,是时候找个“管家”了。这个管家,就是GameplayAbilitySystem,我们通常叫它GAS。

可以把GAS理解为游戏中所有“规则”和“能力”的中央管理系统。它帮管好角色的属性(比如生命、魔法、攻击力),管好角色的技能(比如普攻、火球、闪现),还管好技能带来的效果(比如中毒掉血、加速Buff)。最重要的是,它天生就为网络同步设计好了。这意味着,费老大劲在本地调好的技能,在多人联机时,大概率不会出现“我打中了你,你却没事”这种灵异事件。

介绍原理

GAS框架主要由以下几个模块构成

1. AbilitySystemComponent(ASC):能力系统的组件,用于管理玩家拥有哪些能力,并通过该组件尝试使用对应能力
2. GameplayAbility(GA):具体的能力,每种能力对应一种或多种效果
3. GameplayAttribute:持有能力组件的拥有者的属性,比如最大生命值,生命值,攻击力等
4. AttributeSet(AS):在C++中修改,是GameplayAttribute的集合,放置所有属性
5. GameplayEffect(GE):技能效果,可以由ASC或GA释放,通常用于为玩家添加标签,或者修改玩家的属性等
6. GameplayCue(GC):用于播放特效,音效,镜头摇晃等视觉效果
7. AbilityTask:UE内置了许多任务,在GA中可以用到,比如移动当前玩家位置,播放动画蒙太奇等
8. GameplayTags:本身并不属于GAS系统,但是GAS系统大量用到,本质是一个可以动态增删的标签,可以起到类似枚举的作用,冷却,条件判断都可以用到

准备阶段

1.创建C++第三人称项目(不能用纯蓝图)
GAS的核心功能是通过 C++ 类暴露给蓝图的,纯蓝图项目无法使用GAS的全部威力(尤其是网络部分)。如果现在还是纯蓝图项目,建议立刻把它升级为 C++ 项目,很简单,在编辑器里选择“工具”->“新建C++类…”,随便创建一个类(比如MyGameMode),UE5就会自动为你生成所需的VS项目文件。

2.在插件设置中启用GameplayAbilities插件
在Build.cs文件中添加"GameplayTags",“GameplayTasks”,“GameplayAbilities”,三个模块依赖。

在顶部菜单栏找到 “编辑” -> “插件”。会弹出一个庞大的插件窗口。
在右上角的搜索框里,输入 “Gameplay”。

三个关键的插件:

  • Gameplay Abilities
  • Gameplay Tags
  • Gameplay Tasks

确保这三个插件旁边的复选框都是已启用(打勾)状态。如果之前没启用,勾选后,编辑器会提示需要重启。放心点击重启,这是标准操作。

注意:Gameplay Tags(游戏标签)是GAS的“语言系统”,技能、状态、效果都用它来标识和查询,至关重要。Gameplay Tasks则用于管理一些异步任务

3.在C++项目中添加模块依赖

插件启用只是让编辑器认识GAS了,要让C++代码能调用GAS的类,还需要在编译层面建立依赖关系。

找到项目源代码文件夹。通常在项目根目录下,有一个Source文件夹,里面有项目名的子文件夹(例如MyGasProject)。
在这个文件夹里,找到一个名为 [项目名].Build.cs 的文件。比如我的项目叫GasDemo,那这个文件就是GasDemo.Build.cs。
用文本编辑器(或VS Code)打开它。会看到类似下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnrealBuildTool;

public class GasDemo : ModuleRules
{
public GasDemo(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

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

PrivateDependencyModuleNames.AddRange(new string[] { });

// ... 其他代码
}
}

需要修改的是PublicDependencyModuleNames.AddRange这一行。把刚才在插件窗口看到的三个模块名字加进去:

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

创建核心组件:ASC与AttributeSet

GAS有两个基石般的组件:Ability System Component (ASC) 和 Attribute Set (AS)。可以把ASC想象成角色的“技能大脑”,所有技能的激活、冷却、状态管理都由它负责。而Attribute Set则是角色的“属性仓库”,专门存放生命值、魔法值、力量、敏捷这些数值。

4.1 创建自定义的ASC(可选但推荐)

虽然可以直接把引擎自带的UAbilitySystemComponent挂到角色上,但我强烈建议角色创建一个子类。这样未来可以很方便地添加一些自定义逻辑,比如记录角色拥有的技能列表,或者覆写一些网络同步的回调函数。

在内容浏览器的 C++ 类文件夹上右键,选择“新建C++类”。在搜索框里输入AbilitySystemComponent,选择它作为父类,给类起个名字,比如MyAbilitySystemComponent。创建完成后,暂时不需要在里面写任何代码,它代表了“这是我的技能系统组件”。

4.2 创建属性集(Attribute Set)

属性集是定义游戏数值的地方。比如创建一个最基础的,只包含生命值和最大生命值。

同样,右键新建C++类,这次父类选择AttributeSet,类名可以叫MyAttributeSet

打开生成的MyAttributeSet.h头文件,我们需要在里面定义属性。GAS的属性不是普通的float变量,而是用特殊的宏UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = “Attribute”)来声明,并且需要配套的Getter/Setter函数和复制回调函数。

下面是一个定义了Health(当前生命值)和MaxHealth(最大生命值)的属性集头文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// MyAttributeSet.h
#pragma once

#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "MyAttributeSet.generated.h"

// 这个宏用于定义属性的Getter和Setter,是GAS的约定
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

UCLASS()
class GASDEMO_API UMyAttributeSet : public UAttributeSet
{
GENERATED_BODY()

public:
UMyAttributeSet();

// 生命值属性
UPROPERTY(BlueprintReadOnly, Category = "Attributes|Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Health); // 这会生成 GetHealth(), SetHealth() 等方法

// 最大生命值属性
UPROPERTY(BlueprintReadOnly, Category = "Attributes|Health", ReplicatedUsing = OnRep_MaxHealth)
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxHealth);

protected:
// 当生命值在服务器端改变并复制到客户端时,这个函数会被调用
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);

// 当最大生命值在服务器端改变并复制到客户端时,这个函数会被调用
UFUNCTION()
virtual void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth);

// 重写此函数,用于处理属性改变前的逻辑(如 clamping,确保生命值不超过最大值)
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;

// 网络复制
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};

在对应的.cpp文件中,需要实现这些函数,特别是网络复制部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// MyAttributeSet.cpp
#include "MyAttributeSet.h"
#include "Net/UnrealNetwork.h"

UMyAttributeSet::UMyAttributeSet()
{
// 可以在这里初始化默认值
InitHealth(100.0f);
InitMaxHealth(100.0f);
}

void UMyAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);

// 告诉引擎,Health和MaxHealth属性需要从服务器复制到客户端
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
}

void UMyAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
// 这个宏是处理属性复制通知的标准做法
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldHealth);
}

void UMyAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, MaxHealth, OldMaxHealth);
}

void UMyAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);

// 如果正在改变的是生命值,确保它不会超过最大生命值,也不会低于0
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
}
}

关键点:属性用FGameplayAttributeData类型声明,并用ATTRIBUTE_ACCESSORS宏来生成便捷函数,同时必须处理好网络复制(ReplicatedUsing和GetLifetimeReplicatedProps)。

4.3 将组件挂载到角色

想为玩家添加ASC组件,需要修改角色类(通常是继承自ACharacter的C++类),除了为它添加一个UPROPERTY标记的蓝图可见的UAbilitySystemComponent属性外,还需要让该类继承自IAbilitySystemInterface接口并实现对应接口。
打开角色类头文件(例如MyCharacter.h),添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// MyCharacter.h
#include "AbilitySystemInterface.h"
#include "MyAbilitySystemComponent.h"
#include "MyAttributeSet.h"

UCLASS()
class GASDEMO_API AMyCharacter : public ACharacter, public IAbilitySystemInterface
{
GENERATED_BODY()

public:
AMyCharacter();

// 实现 IAbilitySystemInterface 接口
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;

// 公开获取属性集的函数,方便蓝图或其他代码访问
UMyAttributeSet* GetAttributeSet() const { return AttributeSet; }

protected:
// 我们的自定义ASC组件
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities")
TObjectPtr<UMyAbilitySystemComponent> AbilitySystemComponent;

// 我们的属性集
UPROPERTY()
TObjectPtr<UMyAttributeSet> AttributeSet;

virtual void BeginPlay() override;
};

在角色类的实现文件(.cpp)中,需要在构造函数或BeginPlay中创建并初始化这些组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// MyCharacter.cpp
AMyCharacter::AMyCharacter()
{
// 创建ASC组件。注意:这里创建的是我们自定义的UMyAbilitySystemComponent
AbilitySystemComponent = CreateDefaultSubobject<UMyAbilitySystemComponent>("AbilitySystemComponent");
// ASC的复制模式通常设置为Mixed(混合),即预测某些事情,服务器校正其他事情。对于入门,可以先设为Full(完全由服务器复制)。
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);

// 创建属性集
AttributeSet = CreateDefaultSubobject<UMyAttributeSet>("AttributeSet");
}

UAbilitySystemComponent* AMyCharacter::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}

void AMyCharacter::BeginPlay()
{
Super::BeginPlay();

// 非常重要:将属性集注册到ASC
if (AbilitySystemComponent && AttributeSet)
{
// 这行代码建立了ASC和AttributeSet之间的关联
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
}

至此,角色已经拥有了GAS的核心骨架。编译项目,如果没有错误,最基础也是最繁琐的配置工作已经完成了!

实现第一个技能:GameplayAbility

技能(GameplayAbility,简称GA)是GAS中代表一个可激活能力的单位。一个火球术、一次跳跃、一次格挡,都可以是一个GA。我们来创建一个最简单的火球术GA,它需要做几件事:检查能否释放(比如魔法值够不够)、播放施法动画、生成一个投射物、进入冷却。

1 创建GameplayAbility类

右键新建 C++ 类,父类选择GameplayAbility,起名例如GA_Firebolt。GAS的GA类通常以GA_前缀命名,方便管理。

打开GA_Firebolt.h,会看到它继承自UGameplayAbility。暂时不需要添加新的变量,但需要了解几个关键的可重写函数:

  • CanActivateAbility:检查技能是否可以释放。这里可以检查角色是否死亡、是否处于眩晕状态等。
  • ActivateAbility:技能激活时调用的主函数。在这里编写技能的主要逻辑。
  • EndAbility:技能结束时调用(无论成功、取消还是失败)。
  • InputPressed / InputReleased:处理技能按键输入。

对于火球术技能,主要关注ActivateAbility。想让它在服务器上执行核心逻辑(生成投射物),在客户端上播放动画和音效(表现)。

2 编写火球术逻辑( C++部分)

先在头文件里声明一个UFUNCTION,用于在服务器上生成火球投射物。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// GA_Firebolt.h
UCLASS()
class GASDEMO_API UGA_Firebolt : public UGameplayAbility
{
GENERATED_BODY()

public:
UGA_Firebolt();

// 技能激活的主入口
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;

protected:
// 一个函数,用于在服务器上生成火球投射物
UFUNCTION(BlueprintCallable, Category = "Firebolt")
void SpawnFireboltProjectile();
};

在.cpp文件中,实现它。为了简化,假设已经有一个蓝图类BP_FireboltProjectile,它继承自Actor或Projectile移动组件,负责飞出去并检测碰撞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// GA_Firebolt.cpp
#include "GA_Firebolt.h"
#include "AbilitySystemComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "YourProject/Projectiles/BP_FireboltProjectile.h" // 你的投射物蓝图类头文件

UGA_Firebolt::UGA_Firebolt()
{
// 设置此技能的实例化策略。通常使用`InstancedPerActor`,每个角色拥有此技能的一个独立实例。
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
// 设置网络执行策略。这个技能需要在服务器上执行(Authority),在客户端上预测执行(Predicted)。
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
}

void UGA_Firebolt::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
// 首先,一定要调用父类的ActivateAbility
if (!HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
{
return;
}

// 检查技能是否可以激活(可选,父类会调用CanActivateAbility)
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
// 如果Commit失败(例如冷却未结束、资源不足),则结束技能
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}

// **关键步骤**:播放一个蒙太奇动画(施法动作)。
// 假设你有一个动画蒙太奇资源叫`AM_CastFirebolt`
UAnimMontage* CastMontage = ...; // 这里需要通过某种方式加载或引用你的蒙太奇
if (CastMontage && ActorInfo->AnimInstance.IsValid())
{
// 创建一个任务来播放蒙太奇并等待它结束
FGameplayAbilityTask_PlayMontageAndWait* PlayMontageTask = FGameplayAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, NAME_None, CastMontage);
// 当蒙太奇播放完成时,调用SpawnFireboltProjectile函数
PlayMontageTask->OnCompleted.AddDynamic(this, &UGA_Firebolt::SpawnFireboltProjectile);
PlayMontageTask->OnInterrupted.AddDynamic(this, &UGA_Firebolt::EndAbility); // 被打断则结束技能
PlayMontageTask->ReadyForActivation();
}
else
{
// 如果没有蒙太奇,直接生成火球
SpawnFireboltProjectile();
}
}

void UGA_Firebolt::SpawnFireboltProjectile()
{
// 这个函数应该在服务器上被调用
if (!HasAuthority(&CurrentActivationInfo))
{
return;
}

// 获取技能拥有者(即施法者)的位置和旋转
AActor* AvatarActor = GetAvatarActorFromActorInfo();
if (!AvatarActor)
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
return;
}

FVector SpawnLocation = AvatarActor->GetActorLocation() + AvatarActor->GetActorForwardVector() * 100.0f; // 在角色前方100单位生成
FRotator SpawnRotation = AvatarActor->GetActorRotation();

// 生成火球投射物
UWorld* World = GetWorld();
if (World && BP_FireboltProjectileClass) // 需要提前设置BP_FireboltProjectileClass这个变量
{
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = AvatarActor;
SpawnParams.Instigator = Cast<APawn>(AvatarActor);

AActor* Projectile = World->SpawnActor<AActor>(BP_FireboltProjectileClass, SpawnLocation, SpawnRotation, SpawnParams);
if (Projectile)
{
// 可以在这里对投射物进行初始化,比如设置伤害值(从AttributeSet读取)
// ...
}
}

// 技能逻辑执行完毕,结束技能。这也会触发冷却时间。
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, false, false);
}

以上代码是一个高度简化的示例,重点展示了GA的结构:在ActivateAbility中检查条件、播放动画、通过任务等待动画完成、然后在服务器上生成投射物。在实际项目中,BP_FireboltProjectileClass需要通过UPROPERTY(EditDefaultsOnly)在编辑器中指定,动画蒙太奇也需要妥善引用。

3 在蓝图中配置技能并赋予角色

C++ 类写好后,编译项目。然后在内容浏览器中右键 -> 蓝图类 -> 所有类中搜索 GA_Firebolt,基于它创建一个蓝图,例如BP_GA_Firebolt。

在这个蓝图中,可以做很多可视化配置:

  • 冷却时间(Cooldown):添加一个GameplayEffect(后面会讲)来定义技能冷却。
  • 消耗(Cost):添加另一个GameplayEffect来定义释放技能消耗的魔法值。
  • 技能标签(Ability Tags):给技能打上标签,比如Ability.Spell.Fire,方便其他系统识别。

接下来,需要把这个技能赋予给角色。
有几种方式,最简单的是在角色的蓝图里,在BeginPlay事件中,调用其AbilitySystemComponentGiveAbility函数,将BP_GA_Firebolt的类赋予给它。同时,还需要在角色的输入设置中,绑定一个按键(如鼠标左键)来触发这个技能(通过调用AbilitySystemComponent->TryActivateAbilityByClass)。

为技能添加效果:GameplayEffect与GameplayCue

技能放出去了,但它现在只是个“空壳”。需要两个东西来让它完整:GameplayEffect (GE)来定义它的“规则”(比如伤害多少、冷却多久),和GameplayCue (GC)来定义它的“表现”(比如命中时的爆炸特效和音效)。

1 创建冷却与消耗的GameplayEffect

GameplayEffect是GAS中修改属性和应用状态的核心数据资产。它不包含逻辑,只包含配置。为火球术创建两个GE:

  • 冷却GE:一个Duration(持续时长)类型的GE,持续时间为5秒。它本身不修改任何属性,但拥有一个Cooldown标签(如Cooldown.Firebolt)。当这个GE在技能上激活时,技能在5秒内无法再次使用。
  • 消耗GE:一个Instant(瞬时)类型的GE,在技能释放时立即扣除角色的魔法值(Mana)。这需要在AttributeSet中先定义好Mana属性。

创建GE非常简单,在内容浏览器中右键 -> 蓝图类 -> 所有类中搜索 GameplayEffect,然后选择InstantDuration类型创建即可。在GE的蓝图中,可以:

  • 在Modifiers(修饰器)列表中添加一条,选择AttributeSet中的Mana属性,设置Modifier Op为Add(相加),Magnitude为-20.0(表示扣除20点)。
  • 在Granted Tags(授予标签)或Gameplay Effect Tags(GE标签)中添加冷却标签。

然后,回到技能蓝图BP_GA_Firebolt,在Ability Details面板中,找到Cooldown和Cost选项,分别将创建的冷却GE和消耗GE拖进去。这样,GAS框架就会在技能激活时自动应用这些效果。

2 创建命中的GameplayCue

GameplayCue用于处理与游戏逻辑无关的视听表现。火球命中目标时,想播放一个爆炸粒子和音效。

首先,需要创建一个GameplayCue通知类。在C++中,新建一个类继承自UGameplayCueNotify_Static(一次性效果)或UGameplayCueNotify_Actor(持续效果,需要附着到Actor)。对于爆炸,我们用Static。

1
2
3
4
5
6
7
8
9
// GCN_FireboltHit.h
UCLASS()
class GASDEMO_API UGCN_FireboltHit : public UGameplayCueNotify_Static
{
GENERATED_BODY()

public:
virtual bool OnExecute_Implementation(AActor* MyTarget, const FGameplayCueParameters& Parameters) const override;
};
1
2
3
4
5
6
7
8
9
10
11
// GCN_FireboltHit.cpp
bool UGCN_FireboltHit::OnExecute_Implementation(AActor* MyTarget, const FGameplayCueParameters& Parameters) const
{
// 在这里播放粒子效果和音效
if (MyTarget)
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionParticle, Parameters.Location, Parameters.Normal.Rotation());
UGameplayStatics::PlaySoundAtLocation(GetWorld(), ExplosionSound, Parameters.Location);
}
return true;
}

同样,编译后,在内容浏览器中基于此类创建蓝图BP_GCN_FireboltHit,并在蓝图中指定具体的粒子系统ExplosionParticle和声音ExplosionSound。

3 在投射物中触发GameplayCue

最后,修改火球投射物蓝图BP_FireboltProjectile。在其碰撞事件(OnHit)中,在造成伤害(通过ApplyGameplayEffectToTarget应用一个瞬时伤害GE)的同时,执行GameplayCue。

在蓝图中,可以使用Execute Gameplay Cue on Actor或Execute Gameplay Cue at Location节点。需要传递一个GameplayCueTag参数,这个标签需要和在BP_GCN_FireboltHit蓝图中设置的标签匹配(例如Cue.Firebolt.Impact)。

标签的管理是另一个话题,可以在项目设置中定义GameplayTag,然后在蓝图中通过Make Gameplay Tag节点来引用它。

当火球击中目标时,服务器会应用伤害GE,并广播一个“执行GameplayCue”的事件。所有客户端(包括施法者和旁观者)收到这个事件后,会根据标签找到对应的BP_GCN_FireboltHit蓝图,并在击中位置播放预设好的爆炸特效和音效。这样,一个完整的、带表现的火球术技能闭环就完成了。

测试、调试与下一步

完成以上所有步骤后,强烈建议在一个简单的场景中进行测试。确保角色蓝图正确挂载了ASC和AttributeSet,在BeginPlay时赋予了GA_Firebolt技能,并且输入按键已绑定。

按下按键,应该能看到角色播放施法动画,发射出一个火球投射物。当火球击中场景中的物体(或其他角色)时,应该播放爆炸特效和音效。同时,打开角色的属性界面(如果你做了UI),应该能看到魔法值被扣除,并且技能图标进入冷却状态。

如果遇到问题,UE5编辑器的**输出日志(Output Log) **。GAS会输出大量详细的日志信息,告诉技能激活失败的原因(CanActivateAbility失败)、属性修改的数值等。另外,可以打开 “~” 控制台,输入 showdebug abilitysystem 来在屏幕上显示当前选中角色的GAS状态,包括激活的技能、拥有的效果和当前的属性值,这对于调试至关重要。

走到这里,已经成功搭建了GAS最核心的流程。但这只是冰山一角。GAS的强大之处在于它的扩展性:

  • 更复杂的技能:可以使用AbilityTask来实现蓄力、引导、通道等持续施法技能。
  • 状态效果:利用GameplayEffect的Period(周期)属性,可以轻松实现“每秒钟掉血”的中毒效果,或者“持续10秒增加攻击力”的增益效果。
  • 属性计算:通过GameplayEffectExecutionCalculation(GEEC)类,可以编写复杂的公式来计算伤害,比如(基础攻击力 + 力量*系数 - 目标防御力)。
  • 预测(Prediction):为了让客户端操作更流畅,GAS支持预测某些动作(如移动、消耗资源),这需要更深入的理解。

建议你在掌握这个基础框架后,尝试着去实现一个治疗技能,或者一个给自己加攻击Buff的技能。GAS是一个框架,它提供了一套强大的工具和严谨的规则,但如何用这些工具搭建出你想象中的游戏世界,还需要不断地尝试和创造。