【UE5】GAS学习记录 —— 入门

视频教程:https://www.bilibili.com/video/BV1Jx4y1Z7ig/,根据自己的理解和想法进行了一定优化和更改。

本文主要聚焦于GAS框架下内容的学习记录,项目中其它内容,如敌人AI、动画、UI等不进行详细描述。

1.基础知识

目前UE引擎已经支持直接为Actor添加能力系统组件,比以前方便了很多。通过GAS系统,我们可以用更加合理和方便的方法去实现角色的各种能力(攻击,技能等)以及属性(生命,魔法等),包括各种技能的消耗、冷却、使用条件等。

想要使用GAS组件,需要先开启Gameplay Abilities插件。

image-20250526144722909

2.普攻

这里将普攻视作一个技能实现来对GAS系统进行初步的了解。

2.1 创建技能

首先要做的,就是创建Gameplay技能蓝图“普攻”。

image-20250521175253219

因为普攻是玩家角色和敌人角色都有的能力,所以将其调用在角色基类中实现。在BeginPlay函数中将普攻技能给予角色,并且创建一个普攻函数,当调用这个函数时就激活普攻技能。

image-20250521175535399

这里将激活能力写成了一个函数,具体逻辑如下:根据函数传入的参数来访问GameplayTagContainer数组(角色的一个成员变量)的对应索引,得到了GameplayTagContainer,通过能力系统的Try Activate Abilities by Tag节点来激活相应的能力。

image-20250521180719968

通过Tag访问能力是一个重要的特性,我们可以为技能蓝图设定标签,标签是全局可见的,接下来就可以在任何地方通过标签来指定想要使用或修改的能力。

image-20250521181347914

比如我们将角色的GameplayTagContainer数组加上一个Ability.Melee的值,之后通过这个标签来激活技能。

image-20250521181526215

普攻技能的逻辑如下图:普攻的动画蒙太奇中有两个片段,激活技能时随机播放一个。

image-20250521175635109

上图中有一个提交技能冷却的部分,接下来讲解技能的冷却是如何实现的。

技能蓝图中有许多这个技能的属性,冷却就包含在其中,它和其它许多技能属性一样,是通过技能效果来实现的。

image-20250521180109925

创建GameplayEffect,命名为GE_Melee_CD,为其添加持续时间,并添加用标签阻止能力的效果。

image-20250521180509255

这样一来,技能就有了0.5秒的CD。

2.2 实现普攻逻辑

这里通过胶囊体检测来实现普攻的检测。

为角色添加胶囊体,将其附着在角色的武器插槽上,并调整大小为武器大小。

image-20250521181918810

因为我们只需要胶囊体在角色攻击时进行检测,所以默认情况下将碰撞全部设置为忽略。

image-20250521182235658

在动画蒙太奇中想进行检测的部分添加通知AttackStart和AttackEnd。

image-20250521182321125

在攻击开始后设置碰撞检测,攻击结束后改为忽略碰撞。

image-20250521182342962

检测到碰撞后先检测是不是自己(防止自己打到自己)或者和自己相同类的角色(防止敌人之间互相攻击,如果想要敌人之间误伤就不需要判断),如果都不是,就对敌人造成伤害。

image-20250521182114483

造成伤害这部分也是通过GAS来实现的,下一部分将进行讲解。

2.3 角色属性

除了实现各种技能外,GAS也可以方便地实现角色属性的计算,包括计算防御后的伤害等。

首先创建一个属性集类,在里面定义一系列属性。

image-20250521184333875

注意这里使用了辅助宏,可以为每个属性自动生成初始化方法,并用数据表来初始化这些数据。

创建一个数据表来初始化这些数据。

image-20250521184720719

在角色的能力系统组件中就可以用这个数据表来进行角色属性的初始化。

image-20250521184923697

接下来创建一个普攻的GameplayEffect,在普攻检测处对攻击对象应用这个技能效果即可

image-20250521185754171

image-20250521185841216

这里可能存在伤害溢出的问题,可以使用辅助宏提供的函数来解决。

image-20250521191940816

2.4 伤害配表

战斗策划可以通过csv表的方式快速填写数值

image-20250522145813594

导入表格

image-20250522150043277

就可以在伤害的技能效果里直接应用了

image-20250522150356042

如果原表发生修改,可以直接通过重新导入来更新数据表

image-20250522150457421

这样,在应用伤害效果处就可以指定不同的等级来造成不同的伤害了

image-20250522150558134

3.生命条和技能信息结构体

3.1 生命条(委托机制)

游戏中需要显示敌人或玩家角色的生命条,而获取生命变动属性的方法是委托。

首先定义一个OnHealthAttributeChanged函数,参数是FOnAttributeChangeData。

1
2
//当HP属性变化时触发的回调函数
void OnHealthAttributeChanged(const FOnAttributeChangeData& Data);

获取到角色的能力系统组件,将这个函数绑定到能力系统组件下的属性值变化的委托,这样当角色的生命值发生变化时会自动调用OnHealthAttributeChanged函数。

1
2
3
4
5
6
7
8
9
void ABaseCharacter::BeginPlay()
{
Super::BeginPlay();
TObjectPtr<UAbilitySystemComponent> MyAbilityStystemComponent = this->FindComponentByClass<UAbilitySystemComponent>();
if (MyAbilityStystemComponent) {
//将OnHealthAttributeChanged函数绑定到属性值变化的委托
MyAbilityStystemComponent->GetGameplayAttributeValueChangeDelegate(UBaseAttributeSet::GetHPAttribute()).AddUObject(this, &ABaseCharacter::OnHealthAttributeChanged);
}
}

在BaseCharacter中定义一个多态多播委托(一个参数),并声明相应的事件HPChangeEvent。当OnHealthAttributeChanged函数函数被调用时,广播这个事件,所有绑定了这个事件的函数都会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//声明动态多播委托,用于广播生命值变化事件(参数为新生命值)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChangeEvent, float, NewValue);

UCLASS()
class GAS_API ABaseCharacter : public ACharacter
{
GENERATED_BODY()
public:
...

protected:
...
//声明HP值变化的事件
UPROPERTY(BlueprintAssignable, Category = "Ability")
FOnHealthChangeEvent HPChangeEvent;

...

void OnHealthAttributeChanged(const FOnAttributeChangeData& Data);
};
1
2
3
4
5
void ABaseCharacter::OnHealthAttributeChanged(const FOnAttributeChangeData& Data)
{
// 当HP变化时,广播事件(参数为新值)
HPChangeEvent.Broadcast(Data.NewValue);
}

这样一来,当HP值变化时,会自动调用绑定了HPChangeEvent事件的函数,用于实现血量变化相关的功能,如设置敌人和玩家的血条等。

image-20250524171249486

image-20250524171334718

3.2 构建技能信息结构体

有时候我们需要获取技能的各种信息(CD,消耗等)来进行一些处理,因此需要构建一个技能信息结构体来存储这些信息。

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
//枚举,技能消耗的类型
UENUM(BlueprintType)
enum class ECostType :uint8
{
HP,
SP,
MP
};

//技能信息的结构体
USTRUCT(BlueprintType)
struct FGameplayAbilityInfo
{
GENERATED_BODY()

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AbilityInfo")
float CD; //技能CD
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AbilityInfo")
ECostType CostType; //消耗类型
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AbilityInfo")
float CostValue; //消耗值
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AbilityInfo")
UMaterialInstance* IconMaterial; //图标材质
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AbilityInfo")
TSubclassOf<UBaseGameplayAbility> AbilityClass; //技能类

FGameplayAbilityInfo(); //默认构造函数
FGameplayAbilityInfo(float CD, ECostType CostType, float CostValue, UMaterialInstance* IconMaterial, TSubclassOf<UBaseGameplayAbility> AbilityClass); //带参构造函数
};

UCLASS()
class GAS_API UBaseGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()

public:
//图片材质在编辑器中直接指定
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AbilityInfo")
UMaterialInstance* IconMaterial;

//接口函数,用于获取技能信息
FGameplayAbilityInfo GetAbilityInfo(int level);
};

其中的技能图标材质是主UI中用到的图标材质。

构造函数和接口函数定义如下:

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
FGameplayAbilityInfo::FGameplayAbilityInfo():CD(0),CostType(ECostType::MP),CostValue(0),IconMaterial(nullptr),AbilityClass(nullptr)
{

}

FGameplayAbilityInfo::FGameplayAbilityInfo(float CD, ECostType CostType, float CostValue, UMaterialInstance* IconMaterial, TSubclassOf<UBaseGameplayAbility> AbilityClass):
CD(CD), CostType(CostType), CostValue(CostValue), IconMaterial(IconMaterial), AbilityClass(AbilityClass)
{

}

FGameplayAbilityInfo UBaseGameplayAbility::GetAbilityInfo(int level)
{
//获得冷却技能效果
UGameplayEffect* CDEffect = GetCooldownGameplayEffect();
//获得消耗技能效果
UGameplayEffect* CostEffect = GetCostGameplayEffect();
float CD = 0;
float CostValue = 0;
ECostType CostType = ECostType::MP;
if (CDEffect && CostEffect) {
//通过冷却技能效果获得技能CD
CDEffect->DurationMagnitude.GetStaticMagnitudeIfPossible(level, CD);
//通过消耗技能效果获得技能消耗
if (CostEffect->Modifiers.Num() > 0) {
//获得消耗技能效果的第一个修饰器
FGameplayModifierInfo CostEffectModifierInfo = CostEffect->Modifiers[0];
//获得消耗值
CostEffectModifierInfo.ModifierMagnitude.GetStaticMagnitudeIfPossible(level, CostValue);
//通过获得的消耗类型名称来确定结构体中的消耗类型
FString CostTypeName = CostEffectModifierInfo.Attribute.AttributeName;
if (CostTypeName == "HP") {
CostType = ECostType::HP;
}
if (CostTypeName == "MP") {
CostType = ECostType::MP;
}
if (CostTypeName == "SP") {
CostType = ECostType::SP;
}
}

//返回技能信息结构体
return FGameplayAbilityInfo(CD, CostType, CostValue, IconMaterial, GetClass());
}

return FGameplayAbilityInfo();
}

在角色类中定义获取技能信息的接口。

1
2
3
4
5
6
7
8
9
10
11
FGameplayAbilityInfo ABaseCharacter::GameplayAbilityInfo(TSubclassOf<UBaseGameplayAbility> AbilityClass, int level)
{
UAbilitySystemComponent* MyAbilitySystemComponent = this->FindComponentByClass<UAbilitySystemComponent>(); //获取能力系统组件
UBaseGameplayAbility* AbilityInstance = AbilityClass->GetDefaultObject<UBaseGameplayAbility>(); //获取技能实例
if (MyAbilitySystemComponent && AbilityInstance) {
//通过角色能力类中的接口获取技能信息
return AbilityInstance->GetAbilityInfo(level);
}

return FGameplayAbilityInfo();
}

使所有的技能都继承自BaseGameplayAbility类,这样就可以直接通过角色来获取技能信息了。

4.技能1 回血(制作技能的基本流程)

在完成了上面一系列基础框架的搭建后,制作(简单的)技能就是一件简单的事了。这里用回血这个最简单的技能来过一遍制作技能的流程。

首先创建回血技能,创建后要设定其Tag,它是GAS系统中用于各处通信的十分重要且方便的方法

image-20250525162157021

绑定操作键。这里用1键来触发技能,调用角色的ActivateAbility函数。

image-20250525003743789

ActivateAbility函数的逻辑在实现普攻的时候有介绍。需要注意,在实现普攻技能时是在基类调用的这个函数,其中的GameplayTagContainer数组只有一个默认值Ability.Melee,而回血技能是玩家独有的能力,因此在调用继承而来的ActivateAbility函数之前要先赋予玩家技能并将Ability.HPRegen Tag添加到玩家角色类继承而来的GameplayTagContainer数组中。

image-20250525004212859

玩家角色有一个技能类的数组,BeginPlay函数中会遍历这个数组并根据技能类将数组中的技能都赋予玩家。

image-20250525004336651

在赋予玩家技能的同时会执行UI的初始化,大致功能为初始化UI中技能插槽所需要的CD、技能图标等信息,这里不具体展示。

作为一个简单的技能,回血技能有三个效果:

回血效果。恢复玩家的生命值。通过一个修饰符实现。

image-20250525005203364

消耗效果。这个技能会消耗MP。通过一个修饰符实现。

image-20250525005253692

冷却效果。这个技能有一定的冷却时间。通过一个组件实现。

image-20250525005422423

与此同时,这个技能会有一个特效。可以通过Gameplay提示通知来实现。

image-20250525005623324

在消耗MP的Gameplay效果处可以加上这个提示通知,当修改器成功时就会显示特效了。

image-20250525005659223

最后时技能主体的实现。通过CommitAbility节点提交选定的消耗效果和冷却效果,然后对技能拥有者应用回血效果,播放动画蒙太奇,调用UI进入CD的函数,之后技能结束。

image-20250525005826141

5.技能2 冲刺(通过事件触发技能效果)

首先依旧是上面的流程,绑定输入,添加技能到玩家的技能类数组和GameplayTagContainer数组,创建技能蓝图,消耗效果和冷却效果。

冲刺技能的效果是进行冲刺,并对路径上的敌人造成伤害、击飞和眩晕效果。因此玩家需要一个球体触发器来检测击中的敌人。在游戏开始时可以将触发器对Pawn的碰撞响应设置为忽略。

image-20250525155300862

技能对于玩家角色本身的基本逻辑包括将角色的地面摩擦力设为0、将玩家胶囊体对Pawn的碰撞设为忽略(使玩家冲刺时可以穿过敌人)、清空存储碰撞到的敌人的数组、将球体触发器对Pawn的碰撞响应设置为重叠、对玩家施加一个向前的冲击力、播放动画蒙太奇。

image-20250525155629568

球体碰撞检测的逻辑如下,大致为当检测到Actor后尝试转换为敌人,转换成功后检查是否在敌人数组中(防止对同一个敌人重复触发),然后对检测到的敌人执行相应的逻辑。

image-20250525160237539

需要注意的是,这里可以直接对检测到的敌人进行应用伤害效果、击飞、眩晕的效果,但是技能效果的实现最好都在技能蓝图中去完成,从而防止GAS框架的解耦。这里实现将对检测到的敌人执行一些操作的功能转移到技能蓝图中的方法是GameplayEnent。当检测到敌人后,我们向玩家发送一个Gameplay事件,包含事件的Tag和一个负载,负载包括了事件的发起者和目标Actor等信息。

在播放蓝图的同时,技能蓝图中会开始监听带有这个Tag的事件。当接收到事件后就可以对负载中的敌人Actor进行相应的操作(伤害、击飞、眩晕)。

image-20250525161822646

当蒙太奇播放完成后技能也就释放完成了,需要恢复玩家的地面摩擦力、将球体触发器对Pawn的碰撞检测设为忽略,最后结束碰撞。

image-20250525162308751

6.技能3 激光(持续性技能)

6.1 技能实现

作为一个持续技能,激光在可以持续对敌人造成伤害的同时也会持续消耗MP,当MP耗尽时需要取消技能。

创建技能的流程和之前相同,需要注意的是,这个技能的消耗效果是永久存在的,并且有生效周期。

image-20250525193410557

在绑定输入操作时需要根据当前是否正在释放激光技能来决定是激活技能还是取消技能。这里取消技能是通过调用角色的事件实现的。

image-20250525193610672

激活技能后将角色的ED_CancelLaser事件绑定到取消技能节点,然后提交技能消耗,这里没有提交技能冷却,因为通常这种技能都是当技能结束才开始计算CD的。接下来就是将bLasering的值设为true,并播放动画蒙太奇。

image-20250525194645354

动画有一个抬手动作,因此播放动画蒙太奇后要延迟0.5秒,然后将角色的朝向锁定到摄像机朝向(玩家角色的成员函数),接下来生成激光Actor并附着到玩家上。

image-20250525195218375

激光Actor是用来实现激光技能的一个Actor。它实现了激光外形的表现和攻击敌人的功能的实现。

image-20250525195731665

激光在射到敌人或其它物体时会被挡住,这里在实现这个功能时用了一个取巧的方法:在激光Actor上附着一个摄像机臂,并开启碰撞测试,摄像机臂在碰到物体时会自动缩短,在摄像机臂的终点附着一个球形触发器,就可以通过触发器得到激光正在接触的物体;于此同时,在动画蓝图中获得球形触发器的位置并将激光骨骼网格体的End骨骼缩放到触发器位置,就可以实现激光在外形上的长度变化。

image-20250525200228460

image-20250525200243010

在激光Actor中设置每0.25秒调用LaserEffect事件,将获取到的击中的敌人数组转换为Target Data并作为负载,发送GameplayEvent。

image-20250525200405601

当球形触发器接触到敌人时将其加入数组,不再重叠时将其移出数组。

image-20250525200914503

继续技能蓝图的编写。接收到事件后判断能量(或者其它类型的消耗)是否足够,如果不够就直接取消技能,否则就对负载中的敌人数组应用技能效果,并遍历数组对每个敌人应用眩晕和击飞效果。

image-20250525201130269

6.2 技能之间的互相打断

技能有可以被打断和不能被打断之分,这里可以通过标签来实现。将技能基类中“取消带标签的能力”标签设为所有技能的根标签,这样执行任意一个技能都会取消正在执行的技能。

image-20250525201615447

对于不能被打断的技能,如冲刺技能,则可以将“激活阻止标签”设为所有技能的根标签,这样当前技能被激活时该标签下除了自身以外的其它技能都不会被激活。这个功能的优先级比“取消带标签的能力”功能的优先级高,通过这两个功能可以轻松设定技能之间的打断关系。

image-20250525201813517

7.技能4 爆裂(选取范围并确认)

这个技能的主要特点是:按下后会进行范围选取,可以按左键释放技能击飞范围里的敌人或者按右键。

先写一个类用于选取地面上的目标。

(这部分有点复杂,我也还在消化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UCLASS()
class GAS_API AGroundSelectTargetActor : public AGameplayAbilityTargetActor
{
GENERATED_BODY()
public:
//重写StartTargeting方法,初始化目标选择逻辑
virtual void StartTargeting(UGameplayAbility* Ability) override;

//重写ConfirmTargetingAndContinue方法,确定目标并广播结果
virtual void ConfirmTargetingAndContinue() override;

UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ExposeOnSpawn = "true"), Category = "GroundSelect")
float SelectRadius;

//获取玩家的视角击中点
UFUNCTION(BlueprintCallable, Category = "GroundSelect")
bool GetPlayerLookAtPoint(FVector& Out_LookPoint);

};
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
88
89
90
91
92
93
94
95
96
97
98
99
#include "Abilities/GameplayAbility.h"
#include "Abilities/GameplayAbilityTargetActor.h"
#include "Engine/OverlapResult.h"
#include "GroundSelectTargetActor.h"

void AGroundSelectTargetActor::StartTargeting(UGameplayAbility* Ability)
{
Super::StartTargeting(Ability);

//获取玩家控制器
PrimaryPC = Cast<APlayerController>(Ability->GetOwningActorFromActorInfo()->GetInstigatorController());
}

void AGroundSelectTargetActor::ConfirmTargetingAndContinue()
{
FVector LookPoint; //用于存储玩家的视角击中点
//计算玩家的视角击中点
GetPlayerLookAtPoint(LookPoint);
TArray<FOverlapResult> OverlapResults; //存储与选择范围重叠的物体
TArray<TWeakObjectPtr<AActor>> OverlapActors; //存储重叠的敌人

//设置碰撞查询参数
FCollisionQueryParams QueryParms;
QueryParms.bTraceComplex = false;
QueryParms.bReturnPhysicalMaterial = false;
APawn* SelfPawn = PrimaryPC->GetPawn();

//如果角色存在,将其添加到忽略列表
if (SelfPawn) {
QueryParms.AddIgnoredActor(SelfPawn);
}

//执行碰撞查询,查找与选择范围重叠的物体
bool QueryResult = GetWorld()->OverlapMultiByObjectType(OverlapResults, LookPoint, FQuat::Identity, FCollisionObjectQueryParams(ECollisionChannel::ECC_Pawn), FCollisionShape::MakeSphere(SelectRadius), QueryParms);

//如果查询成功,遍历查询结果,尝试将重叠物体转换为敌人角色,如果转换成功且敌人未被添加到OverlapActors,将敌人添加到OverlapActors
if (QueryResult)
{
for (int i = 0;i < OverlapResults.Num();i++) {
APawn* Enemy = Cast<APawn>(OverlapResults[i].GetActor());
if (Enemy && !OverlapActors.Contains(Enemy)) {
OverlapActors.Add(Enemy);
}
}
}

FGameplayAbilityTargetDataHandle TargetDataHandle; //创建目标数据句柄

//创建中心位置目标数据
FGameplayAbilityTargetData_LocationInfo* CenterLoc = new FGameplayAbilityTargetData_LocationInfo();
CenterLoc->TargetLocation.LiteralTransform = FTransform(LookPoint);
CenterLoc->TargetLocation.LocationType = EGameplayAbilityTargetingLocationType::LiteralTransform;

TargetDataHandle.Add(CenterLoc); //将中心位置目标数据添加到目标数据句柄

//如果有重叠的敌人,创建敌人数组目标数据,将敌人数组目标数据添加到目标数据句柄
if(OverlapActors.Num() > 0) {
FGameplayAbilityTargetData_ActorArray* ActorArray = new FGameplayAbilityTargetData_ActorArray();
ActorArray->SetActors(OverlapActors);
TargetDataHandle.Add(ActorArray);
}

check(ShouldProduceTargetData()); //检查是否应该生成目标数据

//如果允许确认目标,广播目标数据
if (IsConfirmTargetingAllowed()) {
TargetDataReadyDelegate.Broadcast(TargetDataHandle);
}
}

bool AGroundSelectTargetActor::GetPlayerLookAtPoint(FVector& Out_LookPoint)
{
//玩家的视角位置和旋转
FVector ViewLoc;
FRotator ViewRot;
//获取玩家的视角位置和旋转
PrimaryPC->GetPlayerViewPoint(ViewLoc, ViewRot);

FHitResult HitResult; //存储碰撞结果
//设置碰撞查询参数
FCollisionQueryParams QueryParms;
QueryParms.bTraceComplex = true;
APawn* SelfPawn = PrimaryPC->GetPawn(); //获取玩家控制的角色

//如果角色存在,将其添加到忽略列表
if (SelfPawn) {
QueryParms.AddIgnoredActor(SelfPawn);
}

//执行射线检测,计算玩家的视角击中点
bool TraceResult = GetWorld()->LineTraceSingleByChannel(HitResult, ViewLoc, ViewLoc + ViewRot.Vector() * 5000.0f, ECollisionChannel::ECC_Visibility, QueryParms);

//如果检测到碰撞,就获取碰撞点并返回检测结果
if (TraceResult) {
Out_LookPoint = HitResult.ImpactPoint;
}

return TraceResult;
}

创建技能,技能消耗,技能冷却等……

激活技能后判断消耗是否足够,如果不够就之间结束技能,否则进入选取范围状态。

技能结束后记得解除摄像机锁定并设正在选取状态的布尔值为false。

image-20250526100122842

在角色控制器中设置右键取消目标,而左键需要判断bReadyToBlast变量是否为真,为真则确认目标,否则进行普攻。

image-20250526100523814

玩家技能蓝图中会等待目标数据,如果接收到目标取消就结束技能,反之则释放技能。

image-20250526100916269

得到的目标数据是可以和数组互相转化的,可以直接对目标数据应用技能效果并对数组中的每个敌人执行击飞和眩晕效果。

image-20250526101321870

完成。

结语

在阅读UE官方文档的时候了解到了这个强大的系统,于是决定停下来花一段时间去了解(虽然现在正是急着拓宽知识面的时间点),学习完入门知识后感觉确实收获颇丰,而且学习过程相当愉快。当我刚决定走向游戏开发这条路的那段时间里会时不时感到恐惧,会想:我真的可能掌握如此繁杂且庞大的知识,成为能独当一面的游戏程序吗?而现在,我已经知道,只要能坚持以这样的热情和毅力去学习,总有一天我也能站在曾经我只能仰望的山巅。

(虽然但是,秋招没实习怎么办啊)