原课程:点击查看
课程资源:点击查看
适用对象:
- 有一定
UE5
基础,希望深入掌握GAS(Gameplay Ability System
)的开发者- 对动作
RPG/ARPG
开发感兴趣,需构建复杂技能、属性和战斗系统的游戏开发者- 需要了解
多人联机同步机制
和高效数据管理
的进阶学习者
一、Introduction(简介)#
1.核心内容模块#
**基础架构:**学习Ability System Component(ASC)、AttributeSet(属性集)、Gameplay Effects(技能效果)的核心原理,涵盖属性访问器、效果叠加策略(如持续效果、周期效果) 。
**实战应用:**通过近战连击系统、伤害计算模块,掌握如何利用GAS实现动态技能组合与状态标签管理(如连击跳转标签、技能冷却标签) 。
**角色与战斗系统:**从基础角色类构建到动画蓝图设计,覆盖增强输入(Enhanced Input)、敌人高亮交互、武器碰撞检测等实战技术
**属性与伤害体系:**通过数据表(Data Table)初始化角色属性,结合曲线表(Curve Table)实现可扩展的伤害计算逻辑,支持攻击力、防御力、生命值等动态调整
**UI与数据交互:**使用Widget Controller架构动态更新血条/蓝条,通过属性广播和回调机制实现UI与游戏逻辑的解耦 。
**多人游戏支持:**探讨GAS在多人模式下的复制策略(Replication Mode),解决同步延迟和预测问题
案例驱动:从零构建完整的Top Down RPG项目,涵盖角色控制器、敌人AI、技能特效(如高亮材质、粒子系统)、关卡设计等模块
工具链整合:结合Quixel Megascans资源库与虚幻商城素材,提升开发效率
二、Project Creation(项目创建)#
2.项目设置#
- 打开带有资产的项目文件
Aura.uproject
。 - 编辑→编辑器偏好设置→通用→源代码→设置使用image.webp的代码编辑器为
Rider
- 编辑→编辑器偏好设置→通用→实时代码编写→
关闭
实时代码编写 - 编辑→编辑器偏好设置→通用→关闭
Automatically Compile Newly Added C++Classes
(自动编译添加C++文件) - 编辑→编辑器偏好设置→通用→外观→设置
Asset Editor Open Location
(资产编辑器打开位置)为Main Window(主窗口) - 编辑→插件→开启插件
Rider Integration
(Rider集成) - 编辑→项目设置→项目→描述→设置或清空
Copyright Notice
(著作权信息,在Cpp文件首行生成)
3.设置版本控制(可选)#
GItHub创建新仓库GameplayAbilitySystem_Aura。
在项目根目录创建.gitignore忽略文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Binaries DerivedDataCache Intermediate Saved Build .vscode .vs *.VC.db *.opensdf *.opendb *.sdf *.s1n *.Suo *.xcodeproj *.xcworkspace
添加提交项目文件到版本仓库(视频中使用Git命令),建议可以用GitHubDesktop,SourceTree等Git可视化软件进行版本控制,或者使用SVN,Perforce等其他方案进行版本控制。
1 2 3 4 5 6 7 8 9 10 11
echo "GameplayAbilitySystem_Aura" >> README.md git init //初始化后可以用 git status //查看状态 git add . //add文件 git status //查看状态 git log //查看log git add README.md git commit -m "first commit" git branch -M main git remote add origin https://github.com/DruidMech/GameplayAbilitySystem_Aura.git //换成自己的仓库 git push -u origin main
4.创建角色基类#
- 创建
C++
角色基类,继承Character
,名为AuraCharacterBase
,权限为Public
,文件夹位置为Character
- 自动打开
Rider
,编辑器弹出是否接管Git版本控制,作者选不在提示。(可自行抉择) - 用VS打开
项目文件.sln
,解决方案提示需要安装一些东西,点安装。 - 作者说有大量调试运行可以学到,要去引擎安装Debug模式。
Rider
设置Debug模式
运行编辑器,VS中也可以运行。- 删除
项目.sln文件
,提交版本控制。
5.玩家和敌人角色#
- 编写刚创建的
AuraCharacterBase.h
- 在类名上一行添加
UCLASS(Abstract)
,防止该类被直接拖入关卡中。 - 删除
Tick
函数和SetupPlayeInputComponent
函数(因为敌人的AI控制器不需要设置输入,我们这个Base基类同时是玩家和敌人的基类)。 - 删除不需要的注释。
- 在类名上一行添加
- 编写刚创建的
AuraCharacterBase.cpp
- 构造函数中编写
PrimaryActorTick.bCanEverTick = false;
(关闭Tick) 将AuraCharacterBase
放到Character
目录下,并修改cpp的include(之前创建的时候做了可不做)
- 构造函数中编写
- 创建
AuraCharacter
类,继承AuraCharacterBase
类,其他设置不特别说明都是Public,路径Character,创建后提示需要编译先取消 - 创建
AuraEnemy
类,继承AuraCharacterBase
类
- 小结
- 涉及代码比较少,不列出具体代码
- 此时有4个C++角色类,给出他们的继承关系
- AuraCharacterBase
- AuraCharacter
- AuraEnemy
- AuraCharacter
- AuraCharacterBase
6.角色动画蓝图#
为角色基类
AuraCharacterBase
添加武器骨骼AuraCharacterBase.h
添加TObjectPtr
骨骼网格体组件1 2
UPROPERTY(EditAnywhere,Category="Combat") TObjectPtr<USkeletalMeshComponent> Weapon;//不需要加*,因为我们知道是一个指针
AuraCharacterBase.cpp
的构造函数编写1 2 3
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>("Weapon");//<>里是返回类型,参数是FName,不需要TEXT()宏,而FString需要该宏,因为需要 Weapon->SetupAttachment(InParent:GetMesh(), FName("WeaponHandSocket"));//附加到骨骼网格体的插槽上 Weapon->SetCollisionEnabled( NewType:ECollisionEnabled::NoCollision);
创建
Blueprint/Character/Aura
文件夹,创建BP_AuraCharacter
蓝图类继承AuraCharacter
类,并设置角色的网格体,设置位置和旋转的Z轴为-90
。
7.动画蓝图#
- 在
Blueprint/Character/Aura
文件夹中,创建动画蓝图ABP_Aura
,添加状态机Main States
,连接默认插槽DefaultSlot
用于播放蒙太奇。 Main States
添加状态机IdleWalkRun
,状态内部添加混合空间IdleWalkRun
。- 复写蓝图初始化动画事件
Event Blueprint Initialize Animation
,用尝试获得Pawn拥有者Try Get Pawn Owner
节点拿到并转换成BP_AuraCharacter
,提升为变量BP_Aure_Character
,拿到移动组件提升为变量CharacterMovement
。 - 覆写蓝图更新动画事件
Event Blueprint Update Animation
(如果更复杂的动画蓝图可以覆写蓝图线程安全更新动画),有效获取BP_Aura_Character
,在移动组件变量中获取速度Velocity
,获得向量长度XYVector Length XY
(角色横向移动的速度),提升为变量GroundSpeed
,用GroundSpeed
驱动之前的混合空间IdleWalkRun
- 在角色蓝图
BP_AuraCharacter
中为骨骼网格体设置动画蓝图ABP_Aura
- 给敌人设置动画蓝图(敌人有很多种骨骼网格图,需要考虑到通用性)
- 在Character目录下创建
ABP_Enemy
,创建时选择模板 - 添加
Main States
状态机,连接DeaultSlot
插槽 - 状态机添加状态
IdleWalkRun
,连接混合空间播放器Blendspace Player
- 用相同方式从敌人类中拿到水平速度驱动动画。
- 在Blueprint/Character/Goblin_Spear文件夹下创建动画蓝图,继承
ABP_Enemy
,网格体选SKM_Goblin_Spear
,命名为ABP_Goblin_Spear
- 在资产覆盖窗口
Asset Override
,找到那个混合空间播放器,设置长矛的那个混合空间。 - 为
BP_Goblin_Spear
设置动画蓝图 - 同样的方法,创建哥布林弹弓
Goblin_Slingshot,蓝图,动画蓝图等
- 在Character目录下创建
8.增强输入#
- 创建
Input/InputActions
文件夹,创建IA_Move
,创建ICM_AuraContext
,设置WSAD移动。- 移动是二维动作前后左右,所以值类型为
Axis2D
- 三维空间中,角色的向前通常视为X轴,但是移动操作是两回事,ws是y轴,ad是x轴
- 对于左右AD,D为正,A为负,所以A需要添加修改器
Negate
否定X轴,只否定X轴,其他不需要。 - 对于前后WS,W为正,S为负, 但是WS因为是修改Y轴,即使是W也要添加修改器
Swizzle Input Axis Values
,将Y轴优先级提前YXZ
,不然默认是修改X轴,S则添加上面两个修改器Swizzle Input Axis Values
和Negate
。
- 移动是二维动作前后左右,所以值类型为
- 创建玩家控制器Cpp类,/Player目录,
AuraPlayerController
9.玩家控制器#
- .h添加
构造函数
(Public),BeginPlay
函数(Protected) ,增强输入上下文(private,需要在Build.cs
添加EnhanceInput
模块) - 设置复制
- 增强输入子系统添加操作映射上下文
- 设置鼠标显示和光标样式默认
- 设置输入模式为Game和UI,并设置一些属性,窗口锁定,捕获时不隐藏光标
10.移动输入#
覆盖
SetupInputConponent()
函数(protected)拿到增强输入组件,然后绑定
ActionMove
和回调函数Move
在回调函数中编写移动逻辑
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
// Learn by Zmingu #pragma once #include "CoreMinimal.h" #include "GameFramework/PlayerController.h" #include "AuraPlayerController.generated.h" class UInputAction; class UInputMappingContext; struct FInputActionValue; /** * */ UCLASS() class AURA_API AAuraPlayerController : public APlayerController { GENERATED_BODY() public: AAuraPlayerController(); protected: virtual void BeginPlay() override; virtual void SetupInputComponent() override; private: UPROPERTY(EditAnywhere,Category="Input") TObjectPtr<UInputMappingContext> AuraContext; UPROPERTY(EditAnywhere,Category="Input") TObjectPtr<UInputAction> MoveAction; //这里的参数是一个结构体,这是为什么是引用呢,因为结构体是值类型,如果不加引用,那么在函数内部对结构体的修改是不会影响到外部的 //如果加了引用,那么在函数内部对结构体的修改会影响到外部的(存疑) void Move(const struct FInputActionValue& InputActionValue); };
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
// Learn by Zmingu #include "Player/AuraPlayerController.h" #include "EnhancedInputSubsystems.h" #include "EnhancedInputComponent.h" AAuraPlayerController::AAuraPlayerController() { bReplicates = true; } void AAuraPlayerController::BeginPlay() { Super::BeginPlay(); check(AuraContext);//检查上下文是否为空,为空则报错 UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()); check(Subsystem);//检查子系统是否为空,为空则报错 Subsystem->AddMappingContext(AuraContext,0);//添加映射上下文,第二个参数是优先级 bShowMouseCursor = true;//显示鼠标 DefaultMouseCursor = EMouseCursor::Default;//设置鼠标样式为默认 FInputModeGameAndUI InputModeData;//创建一个输入模式为游戏和UI InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);//设置鼠标锁定模式为不锁定 InputModeData.SetHideCursorDuringCapture(false);//设置捕获时隐藏鼠标为false SetInputMode(InputModeData);//设置输入模式 } void AAuraPlayerController::SetupInputComponent() { Super::SetupInputComponent(); //用CastChecked转换为增强输入组件,这里的InputComponent是APlayerController的成员变量,是一个UInputComponent类型的指针,这里转换为UEnhancedInputComponent类型的指针 UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent); EnhancedInputComponent->BindAction(MoveAction,ETriggerEvent::Triggered,this,&AAuraPlayerController::Move);//绑定移动函数 } void AAuraPlayerController::Move(const FInputActionValue& InputActionValue) { const FVector2D InputAxisVector = InputActionValue.Get<FVector2d>();//InputActionValue是一个结构体,这里调用Get方法获取一个FVector2d类型的值 //这个InputAxisVector是一个二维向量,表示输入的方向,比如(1,0)表示向右,(-1,0)表示向左,(0,1)表示向上,(0,-1)表示向下 //但是这个变量后面是作为移动量的大小来使用 const FRotator Rotation = GetControlRotation();//获取控制器的旋转 const FRotator YawRotation(0,Rotation.Yaw,0);//只保留Yaw轴的旋转,这里的Yaw轴是Z轴 const FVector ForwardVector = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);//获取前向向量,这里的X轴是前向,这里的GetUnitAxis方法是获取单位轴 const FVector RightVector = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);//获取右向向量,这里的Y轴是右向 if (APawn* ControlledPawn = GetPawn<APawn>()) { ControlledPawn->AddMovementInput(ForwardVector,InputAxisVector.Y);//添加前向移动,这里的InputAxisVector.Y是前向移动的大小 ControlledPawn->AddMovementInput(RightVector,InputAxisVector.X);//添加右向移动,这里的InputAxisVector.X是右向移动的大小 } }
创建继承该cpp类的控制器蓝图,
BP_AuraPlayerController
,设置IMC
和IA
11.游戏模式#
创建基于
GameModeBase
的cpp类:AuraGameModeBase
,文件夹为/Public/Game创建父类为
AuraGameModeBase
的蓝图类:BP_AuraGameMode
,文件夹为/Blueprint/Game- 设置控制器
BP_AuraPlayerController
和角色类BP_Aura
- 设置控制器
在
BP_Aura
的胶囊体下添加相机和弹簧臂,设置弹簧臂长度和俯视视角- 不要勾选使用Pawn控制旋转,因为是俯视角游戏,希望相机固定视角,而不会跟着转
- 勾选
Enable Camera Lag
相机滞后效果 - 现在运行游戏看到角色没用朝向移动方向,去修改一下
在
AuraCharacter
类中设置1 2 3 4 5 6 7 8 9 10 11
AAuraCharacter::AAuraCharacter() { GetCharacterMovement()->bOrientRotationToMovement = true;//设置角色移动时是否朝向移动方向 GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f);//设置角色旋转速度 GetCharacterMovement()->bConstrainToPlane = true;//设置角色是否限制在平面上移动 GetCharacterMovement()->bSnapToPlaneAtStart = true;//设置角色是否在开始时就限制在平面上移动 bUseControllerRotationPitch = false;//设置是否使用控制器旋转俯仰角,意思是控制器是否影响相机的俯仰角 bUseControllerRotationYaw = false;//设置是否使用控制器旋转偏航角,意思是控制器是否影响相机的偏航角 bUseControllerRotationRoll = false;//设置是否使用控制器旋转滚动角,意思是控制器是否影响相机的滚动角 }
现在角色会朝向移动方向,但是相机也跟着变了,需要在相机臂上取消勾选 Inherit Pich、Yaw、Roll
现在有个问题,角色一停下来就开始摇头,我们去设置动画蓝图
- 添加一个状态
Idle
,动画设置为Idle并设置循环,将之前状态改为Running
- 添加一个布尔变量
ShouldMove
在获取移动速度那里赋值,速度>3时可以移动,将这个布尔值作为两个状态改变的条件
- 添加一个状态
12.敌人接口#
选择敌人的突出效果,玩家控制器下有查看鼠标光标下的事件
多个不同敌人都需要突出,用接口实现多样性 ,当鼠标悬停在Actor上,查看是否实现了敌人接口类,如果实现了就调用该接口函数
这样PC就不需要知道发生了什么,不同敌人类实现不同的高亮功能
创建C++类,继承
UnrealInterface
类,名称为EnemyInterface
,文件夹为public/Interaction
创建两个纯虚函数(意味着该类没用这个函数的定义,是抽象类)
1 2 3
public: virtual void HighlightActor() = 0; virtual void UnHighlightActor() = 0;
在
AuraEnemy
头文件,include上面的接口并继承,重写两个纯虚函数。1 2 3 4 5 6 7
void AAuraEnemy::Highlight() { } void AAuraEnemy::Unhighlight() { }
13.高亮敌人#
AuraPlayerController
重写Tick()
函数,public访问符创建CursorTrace()函数,private访问符,在tick函数中调用,并实现该函数
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
void AAuraPlayerController::CursorTrace() { FHitResult CursorHit; GetHitResultUnderCursor(ECC_Visibility,false,CursorHit);// 获取鼠标下的碰撞,并存储到CursorHit中 if (!CursorHit.bBlockingHit) return;// 如果没有碰撞到物体,直接返回。bBlockingHit表示是否碰撞到物体 // LastActor = ThisActor;//将ThisActor赋值给LastActor // ThisActor = Cast<IEnemyInterface>(CursorHit.GetActor());//将碰撞到的物体转换为IEnemyInterface接口 // 用脚本接口代替,不需要强制转换 LastActor = ThisActor; ThisActor = CursorHit.GetActor(); /* * 从光标发射射线追踪,有5种结果 * A.LastActor为空,ThisActor为空,既鼠标一直没有碰到敌人,这种情况下不需要做任何操作 * B.LastActor为空,ThisActor不为空,既鼠标第一次没有碰到敌人,然后碰到了敌人,这种情况下需要高亮当前碰到的敌人 * C.LastActor不为空,ThisActor为空,既鼠标第一次碰到敌人,然后没有碰到敌人,这种情况下需要取消高亮上一个碰到的敌人 * D.LastActor不为空,ThisActor不为空,但是这两个敌人不是同一个敌人,这种情况下需要取消高亮上一个碰到的敌人,然后高亮当前碰到的敌人 * E.LastActor不为空,ThisActor不为空,这两个敌人是同一个敌人,这种情况下不需要做任何操作 */ if (LastActor == nullptr) { if (ThisActor != nullptr) { // 情况B ThisActor->Highlight(); } else { // 情况A,什么都不做 } } else // 上一个敌人是有效的 { if (ThisActor == nullptr) { // 情况C LastActor->Unhighlight(); } else // 两者都是有效的 { if (LastActor != ThisActor) { // 情况D LastActor->Unhighlight(); ThisActor->Highlight(); } else { // 情况E,什么都不做 } } } }
在PC头文件创建两个指向接口的指针,分别代表当前敌人,和上一个敌人。这里有两种做法,使用第二种脚本接口更妥善,而不是原始指针
1 2 3 4 5 6 7
//TScriptInterface 是 Unreal Engine 中一个封装了指向对象的智能指针,它能够让你更安全地处理接口类型的对象。它的主要目的是通过避免直接操作原始指针,减少潜在的内存泄漏问题,同时让接口可以在蓝图和C++之间互操作。 // IEnemyInterface* LastActor; // IEnemyInterface* ThisActor; // 用脚本接口代替原生函数,而不是直接使用原始指针,好处是可以避免内存泄漏 // TScriptInterface 是 Unreal Engine 中一个封装了指向对象的智能指针,它能够让你更安全地处理接口类型的对象。它的主要目的是通过避免直接操作原始指针,减少潜在的内存泄漏问题,同时让接口可以在蓝图和C++之间互操作。 TScriptInterface<IEnemyInterface> LastActor; TScriptInterface<IEnemyInterface> ThisActor;
在敌人类上,创建一个布尔值,用于标识是否被高亮,蓝图可读,默认值为false,并在高亮函数中设置为真,取消高亮设置为false
1 2 3 4 5 6 7 8 9 10 11 12 13
UPROPERTY(BlueprintReadOnly) bool bIsHighlighted = false; void AAuraEnemy::Highlight() { bIsHighlighted = true; } void AAuraEnemy::Unhighlight() { bIsHighlighted = false; }
在敌人蓝图tick函数判断该变量,并绘制调制球,确保敌人Mesh设置碰撞预设\为自定义,将可见性设置为阻止,控制台:show coll可显示碰撞
14.后期处理-高亮#
- 创建
BP_EnemyBase
蓝图类,继承EnemyBase
类,将两个哥布林父类设置为它。 - 拖入后期处理体积。设置无线范围,搜索
infinite
。添加后期处理材质,文件搜PP_Highlight
- 进入项目设置,设置
custom depth Stencil Pass
为Enabled with Stencil
- 深度模板值为250的将会被突出显示
- 选中敌人网格,custom depth 启用,并设置自定义深度模板值为250。需要调高亮线条粗度,调材质的1.6值
- 在
AuraEnemy
敌人基类C++类中的高亮函数中设置启动和关闭网格体上的自定义深度删除bHighLight布尔值
在
Aura.h
中设置全局常亮2501
#define CUSTOM_DEPTH_RED 250
在两个函数设置启用高亮功能。
1 2 3 4 5 6 7 8 9 10 11 12 13
void AAuraEnemy::Highlight() { GetMesh()->SetRenderCustomDepth(true); GetMesh()->SetCustomDepthStencilValue(CUSTOM_DEPTH_RED); Weapon->SetRenderCustomDepth(true); Weapon->SetCustomDepthStencilValue(CUSTOM_DEPTH_RED); } void AAuraEnemy::Unhighlight() { GetMesh()->SetRenderCustomDepth(false); Weapon->SetRenderCustomDepth(false); }
在敌人C++基类,创建构造函数,设置碰撞预设的可见性为阻挡。
1 2 3 4 5
AAuraEnemy::AAuraEnemy() { // 会自动将碰撞预设设置为自定义 GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block); }
三、Intro to the Gameplay Ability System#
15.游戏能力系统#
介绍属性,技能,增益,debuff
16.GAS主要部件#
ASC,AS,GA,AT,GE,GC,Game Tag
ASC和AS的两种添加方法:添加到Pawn或者PlayerState
如果放在Pawn上,如果Pawn死了,ASC和AS就没了,重新生成角色那些数据是新的。
如果放在PlayerState上,就没有这个问题,可以切换Pawn。
本项目的添加方法:
所以接下来要创建一些PlayerState类,ASC类,AS类
17.玩家状态#
- 在Player文件夹下创建
AuraPlayerState
类,继承PlayerState C++类 - 创建构造函数,设置更新频率为100,
NetupdateFrequency = 100.f;
- 创建BP_AuraPlayerState,继承
AuraPlayerState
- 在GM中设置PS
18.能力系统组件和属性集#
- 插件中启用GAS:
Gameplay Abilities
- 创建C++类,继承
AbilitySystemComponent
,名为AuraAbilitySystemComponent
,目录为/public/AbilitySystem - 创建C++类,继承
AttributeSet
,名为AuraAttributeSet
,目录为/public/AbilitySystem - 在
.Build.cs
文件添加需要的模块Private:"GameplayAbilities","GameplayTags","GameplayTasks"
19.多人游戏中的GAS#
介绍
!
注意:每个客户端都有自己的HUD类,并且仅在该客户端上存在,所以专有服务器上没用HUD,监听服务器上的HUD是该本地玩家的HUD
变量复制只在服务器向客户端复制。而客户端想到服务器就只有用RPC了
20.构建能力系统组件和属性集#
AuraCharacterBase
中创建T对象指针,ASC和AS。但是因为只在敌人角色中有这两个,而玩家角色需要放在PlayerState中,所以要到敌人中创建。1 2 3 4 5 6 7
class UAbilitySystemComponent; class UAttributeSet; UPROPERTY() TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent; UPROPERTY() TObjectPtr<UAttributeSet> AttributeSet;
继承
IAbilitySystemInterface
接口,该接口有个获取能力系统组件的纯虚函数GetAbilitySystemComponent()
,我们实现并用来验证或获取能力系统组件1 2 3 4 5 6 7
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; UAbilitySystemComponent* AAuraCharacterBase::GetAbilitySystemComponent() const { return AbilitySystemComponent; }
创建获取属性集的函数
GetAttributeSet()
1
UAttributeSet* GetAttributeSet() const{return AttributeSet;}
在
AuraEnemy
中将父类声明的两个指针变量赋值:创建ASC并设置为复制。创建AS。注意创建的是Aura开头的ASC和AS1 2 3 4 5
AbilitySystemComponent = CreateDefaultSubobject<UAuraAbilitySystemComponent>(TEXT("AbilitySystemComponent")); AbilitySystemComponent->SetIsReplicated(true); AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);//Minimal适用于AI控制的角色 AttributeSet = CreateDefaultSubobject<UAuraAttributeSet>(TEXT("AttributeSet"));
在
AuraPlayerState
中声明这两个变量并创建,实现IAbilitySystemInterface
接口的纯虚函数,和创建GetAttributeSet()
函数.同上
21.复制模式#
设置
AuraPlayerState
和EnemyBase
类上的ASC的复制模式。- 经验而谈:玩家控制的设置为
Mixed
,AI控制的设置为Minimal
1
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
- 经验而谈:玩家控制的设置为
GE的复制模式
GE是什么,是对玩家造成伤害或治疗类似的
22.初始化ASC的拥有者和代表着#
介绍
什么时候调用呢
- 如图
在AuraCharacter中创建私有函数,用于初始化ASC拥有者和代表者,同时在玩家角色类中赋值ASC和AS指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
oid AAuraCharacter::PossessedBy(AController* NewController) { Super::PossessedBy(NewController); // 在服务器中初始化AbilityActorInfo InitAbilityActorInfo(); } void AAuraCharacter::OnRep_PlayerState() { Super::OnRep_PlayerState(); // 在客户端中初始化AbilityActorInfo InitAbilityActorInfo(); } void AAuraCharacter::InitAbilityActorInfo() { AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>(); check(AuraPlayerState); AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState,this); AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent(); AttributeSet = AuraPlayerState->GetAttributeSet(); }
注意一点:
对于混合复制模式:ASC的OwnerActor的所有者必须是控制者。
对于Pawn,这在PossessedBy()中自动设置 对于玩家状态,它的所有者自动设置为控制器。
如果是其他,需要手动设置ASC的OwnerActor的所有者为控制器
四、Attributes#
23.属性#
属性集AS可以有多个,但是该项目只使用一个。
预测:相当于客户端不需要等到服务器确认更改,先自行更改,然后再通知服务器,服务器再判断是否有效,无效的话就让客户端回滚操作
什么是属性值:由基础值和当前值构成,当前值代表临时值
24.生命和魔法#
AuraAttributeSet类添加构造函数
- 添加生命属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
public: UAuraAttributeSet(); //获取生命周期中需要复制的属性 virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override; //该变量的值发生变化时,调用OnRep_Health函数 UPROPERTY(BlueprintReadOnly,ReplicatedUsing = OnRep_Health,Category = "Vital Attributes") FGameplayAttributeData Health; UPROPERTY(BlueprintReadOnly,ReplicatedUsing = OnRep_MaxHealth,Category = "Vital Attributes") FGameplayAttributeData MaxHealth; UFUNCTION() void OnRep_Health(const FGameplayAttributeData& OldHealth) const; UFUNCTION() void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth) const;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); //Health属性的复制条件为:COND_None,意味着不需要复制条件,只要属性发生变化就会复制,REPNOTIFY_Always表示属性发生变化时总是会调用OnRep_Health函数 DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always); DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always); } void UAuraAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth) const { // 这是一个辅助宏,可以在RepNotify函数中使用,以处理客户端可能会预测性修改的属性。 // OldHealth是属性变化之前的值,Health是变化之后的值,旧值用于回滚。 GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Health, OldHealth); } void UAuraAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth) const { GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, MaxHealth, OldMaxHealth); }
- 上面添加属性的步骤是模板步骤,继续添加魔法和最大魔法属性
25.属性访问器#
将.cpp中引入的ASC,剪切到.h中,因为要使用属性访问器
AS源代码中到底属性访问器宏的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/** * 这定义了一组用于访问和初始化属性的辅助函数,以避免手动编写这些函数。 * 它会为属性 Health 创建如下函数: * * static FGameplayAttribute UMyHealthSet::GetHealthAttribute(); * FORCEINLINE float UMyHealthSet::GetHealth() const; * FORCEINLINE void UMyHealthSet::SetHealth(float NewVal); * FORCEINLINE void UMyHealthSet::InitHealth(float NewVal); * * 在你的游戏中使用这些函数时,你可以这样定义,并根据需要添加游戏特定的函数: * * #define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \ * GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ * GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ * GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \ * GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) * * ATTRIBUTE_ACCESSORS(UMyHealthSet, Health) */
在
AuraAttributeSet
中定义该宏,并在构造函数中调用InitHealth函数设置默认值验证修改:showdebug abilitysystem
重复上面做法,将剩下属性也设置
26.效果Actor#
创建继承
Actor
的C++类,AuraEffectActor
,目录public/Actor- 删除tick函数,设置tick为false;
- 创建私有变量,球体组件Sphere
- 构造函数中为Sphere创建对象,并附加到根组件
- 创建私有变量,静态网格组件Mesh
- 构造函数总为Mesh创建对象,并设置为根组件。
- 创建公共函数,重写OnOverlap重叠函数
- BeginPlay中设置Sphere组件重叠委托,绑定为OnOverlap函数
- 创建公共函数,重写EndOverlap结束重叠函数。
- BeginPlay中设置Sphere组件结束重叠委托,绑定到EndOverlap函数
- 编写代码实现改变属性
- **Tip:**如何快速创建委托的回调函数呢,在蓝图中我们可以快速创建,并帮我们把参数设置好了。但在C++中,我们直接去Sphere->
OnComponentBeginOverlap
.AddDynamic,委托里面复制声明的委托参数,然后删掉不需要的委托变量名和不需要的逗号。
- **Tip:**如何快速创建委托的回调函数呢,在蓝图中我们可以快速创建,并帮我们把参数设置好了。但在C++中,我们直接去Sphere->
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
// Learn by Zmingu #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "AuraEffectActor.generated.h" class USphereComponent; UCLASS() class AURA_API AAuraEffectActor : public AActor { GENERATED_BODY() public: AAuraEffectActor(); UFUNCTION() virtual void OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); UFUNCTION() virtual void EndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex); protected: virtual void BeginPlay() override; private: UPROPERTY(VisibleAnywhere) TObjectPtr<UStaticMeshComponent> Mesh; UPROPERTY(VisibleAnywhere) TObjectPtr<USphereComponent> Sphere; };
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
// Learn by Zmingu #include "Actor/AuraEffectActor.h" #include "AbilitySystemComponent.h" #include "AbilitySystemInterface.h" #include "AbilitySystem/AuraAttributeSet.h" #include "Components/SphereComponent.h" // Sets default values AAuraEffectActor::AAuraEffectActor() { PrimaryActorTick.bCanEverTick = false; Mesh = CreateDefaultSubobject<UStaticMeshComponent>("Mesh"); SetRootComponent(Mesh); Sphere = CreateDefaultSubobject<USphereComponent>("Sphere"); Sphere->SetupAttachment(RootComponent); } void AAuraEffectActor::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { //T0D0: Change this to apply a Gameplay Effect. For now, using const_cast as a hack! // 如果重叠的Actor实现了IAbilitySystemInterface接口 if (IAbilitySystemInterface* AbilitySystemInterface = Cast<IAbilitySystemInterface>(OtherActor)) { const UAuraAttributeSet* AuraAttributeSet = Cast<UAuraAttributeSet>(AbilitySystemInterface->GetAbilitySystemComponent()->GetAttributeSet(UAuraAttributeSet::StaticClass())); UAuraAttributeSet* MutableAttributeSet = const_cast<UAuraAttributeSet*>(AuraAttributeSet); MutableAttributeSet->SetHealth(AuraAttributeSet->GetHealth() + 10.f); Destroy(); } } void AAuraEffectActor::EndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { } void AAuraEffectActor::BeginPlay() { Super::BeginPlay(); Sphere->OnComponentBeginOverlap.AddDynamic(this, &AAuraEffectActor::OnOverlap); Sphere->OnComponentEndOverlap.AddDynamic(this,&AAuraEffectActor::EndOverlap); }
五、RPG Game Ul#
27.游戏UI框架#
- MVC架构,用一个类来获取数据并广播给UI
28.Aura用户控件和控件控制器#
创建C++类,继承UserWidget,文件夹Public/UI/Widget,名为
AuraUserWidget
创建C++类,继承UObject,文件夹Public/UI/WidgetController,名为
AuraWidgetController
控件知道控制器,但是控制器不知道有哪些控件
在
AuraUserWidget
创建公共UObject变量WidgetController
,并创建一个可以在蓝图设置该变量的函数SetWidgetControllen
。再创建保护的蓝图实现的WidgetControllerSet函数,在SetWidgetControllen
中调用。1 2 3 4 5 6
void UAuraUserWidget::SetWidgetController(UObject* InWidgetController) { WidgetController = InWidgetController; WidgetControllerSet(); } //这个方法的作用是给WidgetController赋值
在
AuraWidgetController
中创建四个变量,因为控件控制器需要到处拿数据1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
class AURA_API UAuraWidgetController : public UObject { GENERATED_BODY() protected: UPROPERTY(BlueprintReadOnly, Category = "Widget Controller") TObjectPtr<APlayerController> PlayerController; UPROPERTY(BlueprintReadOnly, Category = "Widget Controller") TObjectPtr<APlayerState> PlayerState; UPROPERTY(BlueprintReadOnly,Category = "Widget Controller") TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent; UPROPERTY(BlueprintReadOnly, Category = "Widget Controller") TObjectPtr<UAttributeSet> AttributeSet; };
29.球形进度条#
- 我们需要创建一个球形进度条父类,他的大小可以被子类重写,如何做呢
- 在目录
/Blueprints/UI/ProgressBar
中创建控件,继承AuraUserWidget
,命名为WBP_GlobeProgressBar
- 设置大小为所需,添加
SizeBox
设置为变量并命名SizeBox_Root
,宽高250, - 创建浮点变量
BoxWidth
,BoxHeight
默认值250
- 在预构造中设置
SizeBox
的高度,宽度覆盖。并折叠成函数UpdateBoxSize
- 在目录
- 添加覆层
Overlay_Root
- 添加图像
Image_Background
,设置为水平垂直填充,设置为变量- 创建
BackgroundBrush
变量,类型为Slate Brush,默认值为GlobeRing
- 创建分类
GlobeProperties
,将自己创建的变量移入 - 预构造中给图 片设置Brush,折叠成函数
UpdateBackgroundBrush
- 创建
- 添加进度条
ProgressBar_Globe
,设置填满,设置为变量,设置Fill Image为MI_HealthGlobe
,设置Draw As为Image
,设置 Fill Color and opacity为白色
- 设置Background Image为透明
- 设置Bar Fill Type为
Bottom to Top
- 预构造函数中设置进度条的样式,背景图像的Tint颜色为透明。填充图像Fill Image提升为变量
ProgressBarFilllmage
,折叠成函数UpdateGlobelmage
- 预构造中设置进度条的Padding:需要调用
Slot as Overlay Slot
再调用Set Padding
。提升变量GlobePadding
设置为10,折叠成函数UpdateGlobePadding
- 球形进度条快为空的时候,球体的玻璃材质效果
- 拖入图片
Image_Glass
,设置填充,设置为变量 - 创建
Image_Glass
画刷变量,默认值为MI_EmptyGlobe
,为Image_Glass
设置上画笔 - 为
Image_Glass
设置边距,使用之前的边距 - 设置
Image_Glass
的透明度为0.5 - 折叠成函数
UpdateGlassBrush
,和UpdateGlassPadding
- 拖入图片
30.生命球#
创建继承
WBP_GlobeProgressBar
的控件WBP_HealthGlobe
,点击显示继承的变量找到球的图片,换成生命值的图片。
在
UI/Overlay
下创建继承AuraUserWidget
的控件WBP_Overlay
- 画布面板耗费比较高,添加画布面板
- 添加
WBP_HealthGlobe
,设置锚点为底部中心。 - 添加到视口看看效果。
- 为了能方便设置生命球的大小和样式,可以把这些变量暴露出来
同样方法设置魔法球UI,并添加到
WBP_Overlay
中
31.Aura HUD#
- 创建
C++
类,继承HUD
,位置UI/HUD
,命名为AuraHUD
- 将
WBP_Overlap
添加到视口创建公共
TObjectPtr
的UAuraUserWidget
类的变量,OverlayWidget
创建私有
TSubclassOfUAuraUserWidget
类变量,OverlayWidgetClass
重写BeginPlay函数。
1 2
UUserWidget* Widget = CreateWidget<UUserWidget>( owningObject:GetWorld(), 0verlayWidgetclass); Widget->AddToViewport();
创建
BP_AuraHud
蓝图类,细节面板设置Overlay Widget Class
类,设置GM的HUD为该蓝图类
32.Overlay 控件控制器#
一个继承与
UObject
的类,用来控制小部件,这个类中需要拿到PC,PS,ASC,AS
等属性,这些属性弄在一个结构体里面,外部调用SetWidgetControllerParams
函数并传入结构体来把这些属性设置到这个类中。该类作为父类,OverlayWidgetController
为该类的子类。
在
AuraWidgetController
中创建结构体FWidgetControllerParams
创建默认构造函数,创建带四个参数的构造函数初始化列表,
创建公共蓝图可调用函数
SetWidgetControllerParams
,传入结构体,并将结构体中的值设置给类中的变量1 2 3 4 5 6 7
void UAuraWidgetController::SetWidgetControllerParams(FWidgetControllerParams WCParams) { PlayerController = WCParams.PlayerController; PlayerState = WCParams.PlayerState; AbilitySystemComponent = WCParams.AbilitySystemComponent; AttributeSet = WCParams.AttributeSet; }
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
class UAttributeSet; class UAbilitySystemComponent; /** * */ USTRUCT(BlueprintType) struct FWidgetControllerParams { GENERATED_BODY() //结构体中的构造函数都需要声明并定义,空的构造函数也需要定义 FWidgetControllerParams(){}; //这是一个构造函数,用于初始化结构体的成员变量,这里的初始化是将所有的成员变量都初始化为nullptr FWidgetControllerParams(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS): PlayerController(PC), PlayerState(PS), AbilitySystemComponent(ASC), AttributeSet(AS) {} UPROPERTY(EditAnywhere,BlueprintReadWrite) TObjectPtr<APlayerController> PlayerController = nullptr; UPROPERTY(EditAnywhere,BlueprintReadWrite) TObjectPtr<APlayerState> PlayerState = nullptr; UPROPERTY(BlueprintReadWrite,BlueprintReadWrite) TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent = nullptr; UPROPERTY(BlueprintReadWrite,BlueprintReadWrite) TObjectPtr<UAttributeSet> AttributeSet = nullptr; }; UCLASS() class AURA_API UAuraWidgetController : public UObject { GENERATED_BODY() public: //这个函数用于将传入的结构体的成员变量赋值给当前类的成员变量 UFUNCTION(BlueprintCallable) void SetWidgetControllerParams(FWidgetControllerParams WCParams); protected: UPROPERTY(BlueprintReadOnly, Category = "Widget Controller") TObjectPtr<APlayerController> PlayerController; UPROPERTY(BlueprintReadOnly, Category = "Widget Controller") TObjectPtr<APlayerState> PlayerState; UPROPERTY(BlueprintReadOnly,Category = "Widget Controller") TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent; UPROPERTY(BlueprintReadOnly, Category = "Widget Controller") TObjectPtr<UAttributeSet> AttributeSet; };
创建基于
AuraWidgetController
的OverlayWidgetController
类,暂时没有逻辑.在
AuraHUD
类中得到UI控制器类:声明UI控制器类指针
1 2
TObjectPtr<UOverlayWidgetController> OverlayWidgetController; //非 UProperty 对象成员可能会在垃圾收集期间被销毁,导致指针过时
声明UI控制器类
1 2
UPROPERTY(EditAnywhere) TSubclassOf<UOverlayWidgetController> OverlayWidgetControllerClass;
在AuraHUD类中创建
GetOverlayWidgetController
函数,传入参数为结构体FWidgetControllerParam
,用于初始化UI控制器中的PC,PS,ASC和AS属性.1 2 3 4 5 6 7 8 9 10
UOverlayWidgetController* AAuraHUD::GetOverlayWidgetController(const FWidgetControllerParams WCParams) { if(OverlayWidgetController == nullptr) { OverlayWidgetController = NewObject<UOverlayWidgetController>(this, OverlayWidgetControllerClass); OverlayWidgetController->SetWidgetControllerParams(WCParams); return OverlayWidgetController; } return OverlayWidgetController; }
删除
BeginPlayer
函数, 创建InitOverLay
函数1
void InitOverlay(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS); //初始化UI控制器和UI
1 2 3 4 5 6 7 8 9 10 11 12 13 14
void AAuraHUD::InitOverlay(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS) { checkf(OverlayWidgetClass,TEXT("Overlay Widget class uninitialized, please fill out BP_AuraHuD")); checkf(OverlayWidgetControllerClass,TEXT("Overlay Widget Controller class uninitialized, please fill out BP_AuraHUD")); UUserWidget* Widget = CreateWidget<UUserWidget>(GetWorld(), OverlayWidgetClass);//将创建的UI转换为UAuraUserWidget OverlayWidget = Cast<UAuraUserWidget>(Widget); const FWidgetControllerParams WidgetControllerParams(PC,PS,ASC,AS);//创建一个结构体,用于传递给UI控制器 UOverlayWidgetController* WidgetController = GetOverlayWidgetController(WidgetControllerParams);//获取UI控制器 OverlayWidget->SetWidgetController(WidgetController);//为UI设置UI控制器 Widget->AddToViewport(); }
什么时候调用
InitOverlay
函数呢, PC, PS, ASC, AS确保这些都初始化, 并确保服务端和客户端AuraCharacter
中的InitAbilityActorInfo
中设置, 要拿到HUD就要用玩家控制器1 2 3 4 5 6 7
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController())) { if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(AuraPlayerController->GetHUD())) { AuraHUD->InitOverlay(AuraPlayerController,AuraPlayerState,AbilitySystemComponent,AttributeSet); } }
小插曲: Aura玩家控制器中的check(Subsystem);也许需要考虑多人游戏
33.Broadcasting Initial Values#
在父类UI控制器
AuraWidgetController
中创建虚函数BroadcastInitialValues()
用来广播初始值,并定义为空函数。1 2
public: virtual void BroadcastInitialValues();//用来广播初始值
1 2 3 4
void UAuraWidgetController::BroadcastInitialValues() { //在父类中没有实现任何功能,所以这个函数是空的,需要在子类中实现 }
在子类UI控制器
OverlayWidgetController
中重写该函数,并创建两个动态多播委托,在BroadcastInitialValues(
)函数中广播从AS中获得的生命值和最大生命值。接下来考虑在哪里绑定这两个委托。1 2 3 4 5 6 7 8
public: virtual void BroadcastInitialValues() override; UPROPERTY(BlueprintAssignable,Category = "GAS|Attributes") FOnHealthChangedSignature OnHealthChanged; UPROPERTY(BlueprintAssignable,Category = "GAS|Attributes") FOnMaxHealthChangedSignature OnMaxHealthChanged;
1 2 3 4 5 6 7
void UOverlayWidgetController::BroadcastInitialValues() { UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet); OnHealthChanged.Broadcast(AuraAttributeSet->GetHealth()); OnMaxHealthChanged.Broadcast(AuraAttributeSet->GetMaxHealth()); }
为生命值UI,法力值UI设置上UI控制器
在
WBP_Overlay
蓝图类中,为两个子UI生命球和法力值球设置UI控制器。将
UOverlayWidgetController
类设置为蓝图可访问和蓝图类型,这是为了使生命值球UI中的UI控制器能够转换成OverlayWidgetController
控制器,这样才能绑定到委托,但是我们希望能在蓝图中处理绑定的事件。1 2 3 4 5
**UCLASS(BlueprintType, Blueprintable)** class AURA_API UOverlayWidgetController : public UAuraWidgetController { ...... }
我们干脆创建
/Blueprint/UI/WidgetController
文件夹,创建继承OverlayWidgetController
的BP_OverlayWidgetController
,并在BP_HUD
中设置OverlayWidgetControllerClass
为该蓝图类回到生命值球UI蓝图中,拿到父类为自己设置的UI控制器,转换成子控制器后同时绑定回调事件,拿到新的生命值和最大生命值
在生命值UI父类创建为进度条设置百分比的函数
Set Progress Bar Percent
!
在生命值球UI中为生命值和最大生命值设置百分比
运行游戏查看效果,控制台输入showdebug abilitysystem,查看数值是否正确
优化蓝图
34.Listening for Attribute Changes#
现在考虑属性变化的监听,GAS自带一个属性变化:AbilitySystemComponent->GetGameplayAttributeValueChangeDelegat
在UI控制器父类创建绑定回调函数的函数,并生成空定义。
1
virtual void BindCallbacksToDependencies();//用来绑定回调函数到依赖项
在子类UI控制器中实现该函数。
- 新建两个回调函数,该函数的参数需要特定,因为要被GAS中的xx绑定
- 在依赖函数中添加绑定
- 在绑定的回调函数中广播新的变化值
- 修改
AuraAttributeSet
类中的初始生命值为50,运行捡起生命药水查看效果
任务
绑定回调 1.调用 BindCallbacksToDependencies()
2.这应该在哪里调用? 3.进行测试。捡起一个生命药水
在HUD获取UI控制器的地方调用绑定回调的函数 BindCallbacksToDependencies
1 2 3 4 5 6 7 8 9 10 11
UOverlayWidgetController* AAuraHUD::GetOverlayWidgetController(const FWidgetControllerParams WCParams) { if(OverlayWidgetController == nullptr) { OverlayWidgetController = NewObject<UOverlayWidgetController>(this, OverlayWidgetControllerClass); OverlayWidgetController->SetWidgetControllerParams(WCParams); **OverlayWidgetController->BindCallbacksToDependencies();** return OverlayWidgetController; } return OverlayWidgetController; }
运行游戏,捡起药水查看UI变化
控制台输入ShowDebug abilitysystem查看数值变化
35.Callbacks for Mana Changes#
任务
- 法力值变化的回调 1.创建回调函数 2.绑定回调程序 3.更改效果Actor,使其从法力中扣除 4.测试游戏。
用生命值回顾下流程,GAS中的AS属性变化时,自带一个委托,用来绑定回调函数
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(该AS属性)
从该属性中获得委托,.AddUObject(this,&UOverlayWidgetController::HealthChanged)
,绑定HealthChanged()
回调函数,HealthChanged()
函数广播我们自己创建的委托OnHealthChanged.Broadcast(Data.NewValue)
然后我们在UI中绑定我们创建的委托,所以每次AS属性变化时,我们的UI就能拿到变化后的值NewValue
所以我们先创建两个回调函数,给GAS自带的委托绑定
1 2 3 4 5 6 7 8 9 10 11 12
void ManaChanged(const FOnAttributeChangeData& Data) const; void MaxManaChanged(const FOnAttributeChangeData& Data) const; void UOverlayWidgetController::ManaChanged(const FOnAttributeChangeData& Data) const { } void UOverlayWidgetController::MaxManaChanged(const FOnAttributeChangeData& Data) const { }
给GAS自带的委托绑定上回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
void UOverlayWidgetController::BindCallbacksToDependencies() { UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet); AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetHealthAttribute()) .AddUObject(this,&UOverlayWidgetController::HealthChanged); AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxHealthAttribute()) .AddUObject(this,&UOverlayWidgetController::MaxHealthChanged); **AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetManaAttribute()) .AddUObject(this,&UOverlayWidgetController::ManaChanged); AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxManaAttribute()) .AddUObject(this,&UOverlayWidgetController::MaxManaChanged);** }
然后自定义两个动态多播委托,用来把改变后的数据传递给UI
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(FOnManaChangedSignature,float,NewMana); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMaxManaChangedSignature,float,NewMana); public: UPROPERTY(BlueprintAssignable,Category = "GAS|Attributes") FOnManaChangedSignature OnManaChanged; UPROPERTY(BlueprintAssignable,Category = "GAS|Attributes") FOnMaxManaChangedSignature OnMaxManaChanged; void UOverlayWidgetController::ManaChanged(const FOnAttributeChangeData& Data) const { OnManaChanged.Broadcast(Data.NewValue); } void UOverlayWidgetController::MaxManaChanged(const FOnAttributeChangeData& Data) const { OnMaxHealthChanged.Broadcast(Data.NewValue); }
现在AS属性改变的时候,会调用ManaChanged函数,该函数把AS改变后的属性值通过
OnManaChanged
委托广播出去了。接下来就是去UI中绑定这个委托。在去UI中绑定委托前,别忘了法力值的初始值的广播
1 2 3 4 5 6 7 8 9 10
void UOverlayWidgetController::BroadcastInitialValues() { UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet); OnHealthChanged.Broadcast(AuraAttributeSet->GetHealth()); OnMaxHealthChanged.Broadcast(AuraAttributeSet->GetMaxHealth()); **OnManaChanged.Broadcast(AuraAttributeSet->GetMana()); OnMaxManaChanged.Broadcast(AuraAttributeSet->GetMaxMana());** }
我们将之前用到的血瓶改点参数用来测试法力值的改变是否生效
在AuraEffectActor类中,临时修改重叠事件中测试用的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14
void AAuraEffectActor::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { //T0D0: Change this to apply a Gameplay Effect. For now, using const_cast as a hack! // 如果重叠的Actor实现了IAbilitySystemInterface接口 if (IAbilitySystemInterface* AbilitySystemInterface = Cast<IAbilitySystemInterface>(OtherActor)) { const UAuraAttributeSet* AuraAttributeSet = Cast<UAuraAttributeSet>(AbilitySystemInterface->GetAbilitySystemComponent()->GetAttributeSet(UAuraAttributeSet::StaticClass())); UAuraAttributeSet* MutableAttributeSet = const_cast<UAuraAttributeSet*>(AuraAttributeSet); MutableAttributeSet->SetHealth(AuraAttributeSet->GetHealth() + 10.f); MutableAttributeSet->SetMana(AuraAttributeSet->GetMana() - 25.f); Destroy(); } }
36.Gameplay Effects#
开始涉及GE(游戏效果),GE用来改变AS(属性)和Tag(标签)
GE只是数据,不涉及逻辑
GE没有子类,而是创建继承GE的蓝图类
GE通过修饰符
Modifiers
和执行Executions
来调整属性Modifiers
- 修饰符包括
Add
,Multiply
,Divide
,Override
(加减乘除覆盖) - 修饰符操作值,这个值称为幅度
Magnitude
,对幅度进行减乘除后就是对属性修改的结果 - 幅度可以是很多种值,比如幅度计算类型
Magnitude Calculation Type
包括Scalable Float
可伸缩的浮点值Arrtibute Based
另一个属性的基础值(比如你打了我一拳,我扣的血的值是你的力量值)Custom Calculation Class (MMC)
自定义计算类,专门拿一个类来计算- Set by Caller
- 甚至可以用一个GE的等级表来修改属性,GE拥有等级(比如GE等级越高,扣的血越多)
- 修饰符包括
Executions
- 先空着,没看懂
GE生效方式之时间间隔有三种方式
- 生效一下就消失
- 生效一段时间后消失
- 生效后不消失(除非通过其他手段
GE可以叠加,通过Tag或给予能力
GE的规范
37.Effect Actor Improved#
之前的
AuraEffectActor
代码中对属性的修改是临时的,我们从这里开始修改删除网格体,重叠事件相关代码。
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
// Learn by Zmingu #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "AuraEffectActor.generated.h" ~~class USphereComponent;~~ UCLASS() class AURA_API AAuraEffectActor : public AActor { GENERATED_BODY() public: AAuraEffectActor(); ~~UFUNCTION() virtual void OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); UFUNCTION() virtual void EndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);~~ protected: virtual void BeginPlay() override; private: ~~UPROPERTY(VisibleAnywhere) TObjectPtr<UStaticMeshComponent> Mesh; UPROPERTY(VisibleAnywhere) TObjectPtr<USphereComponent> Sphere;~~ };
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
// Learn by Zmingu #include "Actor/AuraEffectActor.h" ~~#include "AbilitySystemComponent.h" #include "AbilitySystemInterface.h" #include "AbilitySystem/AuraAttributeSet.h" #include "Components/SphereComponent.h"~~ // Sets default values AAuraEffectActor::AAuraEffectActor() { PrimaryActorTick.bCanEverTick = false; ~~Mesh = CreateDefaultSubobject<UStaticMeshComponent>("Mesh"); SetRootComponent(Mesh); Sphere = CreateDefaultSubobject<USphereComponent>("Sphere"); Sphere->SetupAttachment(RootComponent);~~ } ~~void AAuraEffectActor::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { //T0D0: Change this to apply a Gameplay Effect. For now, using const_cast as a hack! // 如果重叠的Actor实现了IAbilitySystemInterface接口 if (IAbilitySystemInterface* AbilitySystemInterface = Cast<IAbilitySystemInterface>(OtherActor)) { const UAuraAttributeSet* AuraAttributeSet = Cast<UAuraAttributeSet>(AbilitySystemInterface->GetAbilitySystemComponent()->GetAttributeSet(UAuraAttributeSet::StaticClass())); UAuraAttributeSet* MutableAttributeSet = const_cast<UAuraAttributeSet*>(AuraAttributeSet);//临时强制修改const属性 MutableAttributeSet->SetHealth(AuraAttributeSet->GetHealth() + 10.f); MutableAttributeSet->SetMana(AuraAttributeSet->GetMana() - 25.f); Destroy(); } } void AAuraEffectActor::EndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { }~~ void AAuraEffectActor::BeginPlay() { Super::BeginPlay(); ~~Sphere->OnComponentBeginOverlap.AddDynamic(this, &AAuraEffectActor::OnOverlap); Sphere->OnComponentEndOverlap.AddDynamic(this,&AAuraEffectActor::EndOverlap);~~ }
C++类中创建场景组件设置为根组件
1 2 3 4 5 6 7 8 9
//.h //.cpp AAuraEffectActor::AAuraEffectActor() { PrimaryActorTick.bCanEverTick = false; SetRootComponent(CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"))); }
蓝图类中添加网格体,设置上生命药水SM_PotionBottle,网格体设置整体缩放为0.2,碰撞预设为无碰撞,
添加球体碰撞,实现重叠事件(上面这些操作相当于把C++的逻辑弄到蓝图里,方便设计师操作一些蓝图
在C++类中声明GE类变量和应用GE的函数
声明GE类变量和应用GE的函数
1 2 3 4 5 6
protected: UPROPERTY(EditAnywhere,Category = "Applied Effects") TSubclassOf<UGameplayEffect> InstantGameplayEffectClass; UFUNCTION(BlueprintCallable, Category = "Applied Effects") void ApplyEffectToTarget(AActor* Target, TSubclassOf<UGameplayEffect> GameplayEffectClass);
应用GE的函数中,先要拿到目标的ASC组件,有两种方法,1是通过该Actor实现的接口获取,2是通过GAS的全局蓝图函数库获取
1 2 3 4
~~//第二种的原理其实和第一种一样,做了个封装 IAbilitySystemInterface* ASCInterface = Cast<IAbilitySystemInterface>(Target);if(ASCInterface) ASCInterface->GetAbilitySystemComponent();~~ UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Target);
ASC组件中有应用效果的一些方法,但使用这些方法前我们要去看下他需要的参数
d.最终代码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
void AAuraEffectActor::ApplyEffectToTarget(AActor* Target, TSubclassOf<UGameplayEffect> GameplayEffectClass) { UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Target); if (TargetASC == nullptr) return;//如果目标没有ASC,直接返回 check(GameplayEffectClass); // 1. 创建GE上下文 FGameplayEffectContextHandle GEContextHandle = TargetASC->MakeEffectContext(); // 2. 添加源对象(效果的施放者) GEContextHandle.AddSourceObject(this); // 3. 创建GE规格(Spec) FGameplayEffectSpecHandle GESpecHandle = TargetASC->MakeOutgoingSpec(GameplayEffectClass,1.f,GEContextHandle);//创建GE规格(Spec) // 4. 将GE应用到目标自己身上 TargetASC->ApplyGameplayEffectSpecToSelf(*GESpecHandle.Data.Get()); }
MakeEffectContext()
创建一个新的游戏效果上下文包含了效果产生时的相关信息,比如:- 效果的来源(Source)
- 目标(Target)
- 触发效果时的位置
- 其他相关数据
- 返回一个
FGameplayEffectContextHandle
,这是一个智能指针包装器
GEContextHandle.AddSourceObject(this);
AddSourceObject
设置效果的来源对象this
指向当前的AuraEffectActor
- 这告诉系统这个效果是由谁产生的
- 对于伤害追踪、效果可视化等很重要
TargetASC->**MakeOutgoingSpec(GameplayEffectClass, 1.f, GEContextHandle);**
- 创建一个
GameplayEffect
的具体实例(Spec
) - 参数解析:
GameplayEffectClass
: 要应用的效果类型- 1.f: 效果等级(Level),影响效果强度
GEContextHandle
: 前面创建的上下文
- 返回一个
FGameplayEffectSpecHandle
(智能指针包装器) Spec
包含了效果的所有具体信息:- 效果类型
- 等级
- 修改器
- 持续时间
- 堆叠规则等
- 创建一个
TargetASC->ApplyGameplayEffectSpecToSelf(*GESpecHandle.Data.Get());
- 将创建好的效果实际应用到目标身上
GESpecHandle.Data.Get()
获取原始指针- 解引用获取实际的 Spec
ApplyGameplayEffectSpecToSelf
将效果应用到拥有这个ASC的角色上- 这会触发:
- 属性修改
- 效果应用事件
- 网络复制(如果需要)
- 相关的游戏逻辑
- 应用GE流程总结
- 创建上下文(记录效果产生的环境信息)
- 设置效果来源(谁产生的效果)
- 创建效果规格(具体的效果实例)
- 将效果应用到目标
- 这是UE的GameplayAbility系统中标准的效果应用流程,确保了效果应用的可预测性、可复制性和可扩展性
38.Instant Gameplay Effects#
回到
BP_HealthPotion
蓝图类添加球体碰撞开始重叠事件,调用刚刚父类中写的应用GE到目标的函数
新建GE类
GE_PotionHeal
继承GE基类,放在/Blueprints/Actor/Potion
目录下,把刚刚的生命药水蓝图也放进来设置GE类
GE_PotionHeal
给药水蓝图设置GE类和调用
- 回到C++类
AAuraEffectActor
将InstantGameplayEffectClass
变量设置为蓝图可读 - 将
ApplyEffectToTarget
函数的形参Target
改为TargetActor
,避免和蓝图中调用的函数的Target混淆
- 回到C++类
在蓝图将
InstantGameplayEffectClass
类默认值设置为GE_PotionHeal
任务:制作魔法药水,略
39.Duration Gameplay Effects#
在蓝图中实现应用GE(学习用,后面还是用之前的实现方法
任务:做一个持续两秒增加最大生命值的水晶球
AAuraEffectActor
类中新声明一个用于有作用间隔的GE类变量1 2
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category = "Applied Effects") TSubclassOf<UGameplayEffect> DurationGameplayEffectClass;
创建并设置
GE_CrystalHealth
其他和之前设置的一样,略,最后测试
40.Periodic Gameplay Effects#
1
修改生命水晶,使其可以周期性缓慢增加生命值
这样设置意思每0.1秒增加1,增加2秒
任务:创建持续增加法力值的法力水晶,略
41.Effect Stacking#
介绍堆叠
- 按来源堆叠
- 按目标堆叠
Stacking Type(堆叠类型)
- None - 不堆叠 -
- 新效果会替换旧效果 - 重置持续时间
- AggregateBySource - 按来源堆叠
- 同一个来源的效果会堆叠
- 不同来源的效果分别计算
- 例:玩家A的DOT和玩家B的DOT分别堆叠
- 3. AggregateByTarget - 按目标堆叠
- 不管来源是谁,效果都会堆叠
- 例:多个玩家的DOT效果会在目标身上合并堆叠
- None - 不堆叠 -
Stack Limit Count(堆叠数量限制)
- 设置最大可堆叠层数
- 0 = 无限制
- 正整数 = 最大堆叠层数
- 达到限制后的行为
取决于其他堆叠策略
Stack Duration Refresh Policy(持续时间刷新策略)
Refresh on Successful Applicatio
n - 成功应用时刷新- 新效果成功应用时刷新持续时间
- 最常用的选项
Never Refresh
- 从不刷新- 持续时间保持不变
- 适用于需要精确计时的效果
Stack Period Reset Policy(周期重置策略)
Reset on Successful Application
- 成功应用时重置- 当新效果应用时重置周期计时
- 适用于周期性效果(如DOT)
Never Reset
- 从不重置- 保持原有周期
- 适用于需要固定周期的效果
Stack Expiration Policy(过期策略)
ClearEntireStack - 清除整个堆叠
- 当一个效果过期时,移除所有堆叠层数
- 适用于"全有或全无"的效果
RemoveSingleStackAndRefreshDuration - 移除单层并刷新持续时间
- 每次只移除一层堆叠
- 刷新剩余堆叠的持续时间
- 适用于需要逐层衰减的效果
按视频设置:
!
按视频设置的结果
连续捡起3个的效果过程:
- 捡起第一个:
- 效果成功应用
- 堆叠数:1
- 开始计时(假设持续时间为10秒)
- 捡起第二个(比如2秒后):
- 由于 Stack Limit Count = 1,不会增加新的堆叠
- 但因为 Duration Refresh = 成功应用时刷新,所以:
- 持续时间重置为新的10秒
- 如果有周期性效果,也会重置周期计时
- 捡起第三个(比如再过2秒):
- 同第二个的效果
- 再次刷新持续时间为新的10秒
- 再次重置周期计时
总结效果:
- 始终只会有一层效果(因为 Stack Limit Count = 1)
- 每次捡起都会刷新持续时间
- 不会叠加效果强度
- 当持续时间结束时,效果完全消失
这种配置适用于:
- 不需要堆叠的增益/减益效果
- 希望可以刷新持续时间的效果
- 例如:速度提升、护盾效果等
42.Infinite Gameplay Effects#
将之前的水晶放到新的文件夹
Crystal
新建Area目录,新建
BP_FireArea
继承AuraE类- 添加碰撞盒
- 添加奶瓜粒子,设置火特效
在GEActor类中新建永久应用的GE变量类
InfiniteGameplayEffectClass
1 2
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category = "Applied Effects") TSubclassOf<UGameplayEffect> InfiniteGameplayEffectClass;
在工目录创建
GE_FireArea,
设置类型为无限Infinite
,设置周期每秒扣五滴血按照之前方法实现碰到火应用GE
来到
AuraEffectActor
类中声明枚举变量特效应用配置
EEffectApplicationPolicy
,并声明三个三种GE类型的枚举变量并设置上默认值。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
UENUM(BlueprintType) enum class EEffectApplicationPolicy//使用enum class的好处是可以避免命名冲突和类型安全 { ApplyOnOverlap, ApplyOnEndOverlap, DoNotApply }; UPROPERTY(EditAnywhere,BlueprintReadOnly,Category = "Applied Effects") EEffectApplicationPolicy InstantEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply; UPROPERTY(EditAnywhere,BlueprintReadOnly,Category = "Applied Effects") EEffectApplicationPolicy DurationEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply; UPROPERTY(EditAnywhere,BlueprintReadOnly,Category = "Applied Effects") EEffectApplicationPolicy InfiniteEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
新建枚举变量特效移除配置
EEffectRemovalPolicy
,并只需要创建无限类型
的GE枚举变量(因为其他两种会自动移除,并设置上默认值。1 2 3 4 5 6 7 8 9
UENUM(BlueprintType) enum class EEffectRemovalPolicy { RemoveOnEndOverlap, DoNotRemove }; UPROPERTY(EditAnywhere,BlueprintReadOnly,Category = "Applied Effects") EEffectRemovalPolicy InfiniteEffectRemovalPolicy = EEffectRemovalPolicy::RemoveOnEndOverlap;
创建布尔变量
bDestroyOnEffectRemoval
默认值为false创建开始重叠和结束重叠函数
1 2 3 4 5
UFUNCTION(BlueprintCallable) void OnOverlap(AActor* TargetActor); UFUNCTION(BlueprintCallable) void OnEndOverlap(AActor* TargetActor);
43.Instant and Duration Application Policy#
编写开始重叠和结束重叠时判断即时GE和持续GE应用的策略
1 2 3 4 5 6 7 8 9 10 11
void AAuraEffectActor::OnOverlap(AActor* TargetActor) { InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap ? ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass) : nullptr; DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap ? ApplyEffectToTarget(TargetActor,DurationGameplayEffectClass) : nullptr; } void AAuraEffectActor::OnEnd0verlap(AActor* TargetActor) { InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap ? ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass) : nullptr; DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap ? ApplyEffectToTarget(TargetActor,DurationGameplayEffectClass) : nullptr; }
44.Infinite Effect Application and Removal#
TargetASC->ApplyGameplayEffectSpecToSelf
这个函数会返回GE句柄
- 当应用了无限的GE时,我们需要存起来,同时存储这个
GE
对应的Actor
,使两者对应起来,方便在应用多个无限GE时能准确的移除在
ApplyEffectToTarget
函数中拿到ActiveEffectHandle
激活的GE句柄,这是由TargetASC->ApplyGameplayEffectSpecToSelf(*GESpecHandle.Data.Get());
返回的1
FActiveGameplayEffectHandle ActiveGEHandle = TargetASC->ApplyGameplayEffectSpecToSelf(*GESpecHandle.Data.Get());
然后判断该应用的
GE是不是无限的
,是不是需要在结束重叠移除的
,是的话需要存起来1 2 3 4 5 6 7 8 9
// 4. 将GE应用到目标自己身上 FActiveGameplayEffectHandle ActiveGEHandle = TargetASC->ApplyGameplayEffectSpecToSelf(*GESpecHandle.Data.Get()); bool bIsInfinite = GESpecHandle.Data.Get()->Def.Get()->DurationPolicy == EGameplayEffectDurationType::Infinite; if (bIsInfinite && InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap) { }
创建能一一对应
激活的GE句柄
和Actor里面的ASC
的Map1
TMap<FActiveGameplayEffectHandle,UAbilitySystemComponent*> ActiveGameplayEffects;
存起激活的
激活的GE句柄
h和对应的ASC1
ActiveGameplayEffects.Add(ActiveGEHandle,TargetASC);
添加
应用无限GE
的两个策略,1 2 3 4 5 6 7 8 9 10 11 12 13
void AAuraEffectActor::OnOverlap(AActor* TargetActor) { if (InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass); if (DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap) ApplyEffectToTarget(TargetActor,DurationGameplayEffectClass); if (InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap) ApplyEffectToTarget(TargetActor,InfiniteGameplayEffectClass); } void AAuraEffectActor::OnEndOverlap(AActor* TargetActor) { if (InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap) ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass); if (DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap) ApplyEffectToTarget(TargetActor,DurationGameplayEffectClass); if (InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap) ApplyEffectToTarget(TargetActor,InfiniteGameplayEffectClass); }
在结束重叠的地方判断键值对中的ASC是否等于当前Actor的ASC,是的话移除对应GE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
void AAuraEffectActor::OnEndOverlap(AActor* TargetActor) { InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap ? ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass) : nullptr; DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap ? ApplyEffectToTarget(TargetActor,DurationGameplayEffectClass) : nullptr; InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap ? ApplyEffectToTarget(TargetActor,InfiniteGameplayEffectClass) : nullptr; if (InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap) { UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor); for (auto HandlePair :ActiveGEHandles) { if (TargetASC == HandlePair.Value) { TargetASC->RemoveActiveGameplayEffect(HandlePair.Key); } } } }
同时移除Maps里相应的键值对
- 创建临时数组存需要在Map中移除的
激活的GE句柄
- 遍历移除
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
void AAuraEffectActor::OnEndOverlap(AActor* TargetActor) { InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap ? ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass) : nullptr; DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap ? ApplyEffectToTarget(TargetActor,DurationGameplayEffectClass) : nullptr; InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap ? ApplyEffectToTarget(TargetActor,InfiniteGameplayEffectClass) : nullptr; if (InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap) { UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor); if (TargetASC == nullptr) return; TArray<FActiveGameplayEffectHandle> ActiveGEHandlesToRemove; for (auto HandlePair :ActiveGEHandles) { if (TargetASC == HandlePair.Value) { TargetASC->RemoveActiveGameplayEffect(HandlePair.Key); ActiveGEHandlesToRemove.Add(HandlePair.Key);//临时存储要删除的Handle } } for (auto Handle : ActiveGEHandlesToRemove) { ActiveGEHandles.FindAndRemoveChecked(Handle);//这是安全的删除方式 } } }
- 创建临时数组存需要在Map中移除的
蓝图中设置并编写
设置GE
现在在多个重叠的火堆中,从其中一个出来,就会把所有都移除
一次只移除一个Stack的修改
1
TargetASC->RemoveActiveGameplayEffect(HandlePair.Key,1);