Open WangShuXian6 opened 10 months ago
基于 角色类创建角色基类 AuraCharacterBase 将抽象说明符添加到类宏中,这会阻止此类被拖入关卡。 角色基类不执行任何功能。 不设置玩家输入组件。
我们将为玩家角色类(我们将控制的角色类)配置输入。 完全删除角色基类的玩家输入组件。
AuraCharacterBase 放入 Public/Character Private/Character 文件夹下。
E:\Unreal Projects 532\Aura\Source\Aura\Public\Character\AuraCharacterBase.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AuraCharacterBase.generated.h"
UCLASS(Abstract)
class AURA_API AAuraCharacterBase : public ACharacter
{
GENERATED_BODY()
public:
AAuraCharacterBase();
protected:
virtual void BeginPlay() override;
};
E:\Unreal Projects 532\Aura\Source\Aura\Private\Character\AuraCharacterBase.cpp
#include "Character/AuraCharacterBase.h"
AAuraCharacterBase::AAuraCharacterBase()
{
PrimaryActorTick.bCanEverTick = false;
}
void AAuraCharacterBase::BeginPlay()
{
Super::BeginPlay();
}
基于 AAuraCharacterBase 创建C++类 AuraCharacter
E:\Unreal Projects 532\Aura\Source\Aura\Public\Character\AuraCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "Character/AuraCharacterBase.h"
#include "AuraCharacter.generated.h"
/**
*
*/
UCLASS()
class AURA_API AAuraCharacter : public AAuraCharacterBase
{
GENERATED_BODY()
};
E:\Unreal Projects 532\Aura\Source\Aura\Private\Character\AuraCharacter.cpp
#include "Character/AuraCharacter.h"
基于 AAuraCharacterBase 创建C++类 AuraEnemy
E:\Unreal Projects 532\Aura\Source\Aura\Public\Character\AuraEnemy.h
#pragma once
#include "CoreMinimal.h"
#include "Character/AuraCharacterBase.h"
#include "AuraEnemy.generated.h"
/**
*
*/
UCLASS()
class AURA_API AAuraEnemy : public AAuraCharacterBase
{
GENERATED_BODY()
};
E:\Unreal Projects 532\Aura\Source\Aura\Private\Character\AuraEnemy.cpp
#include "Character/AuraEnemy.h"
所有角色都拥有武器。 将武器附加到角色的骨骼网格体组件骨架插槽上。 角色骨架必须拥有指定插槽。
E:\Unreal Projects 532\Aura\Source\Aura\Public\Character\AuraCharacterBase.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AuraCharacterBase.generated.h"
UCLASS(Abstract)
class AURA_API AAuraCharacterBase : public ACharacter
{
GENERATED_BODY()
public:
AAuraCharacterBase();
protected:
virtual void BeginPlay() override;
//TObjectPtr 与原始指针相似 但有附加功能:访问跟踪指针,可选的延迟加载资产
UPROPERTY(EditAnywhere,Category="Combat")
TObjectPtr<USkeletalMeshComponent> Weapon;
};
E:\Unreal Projects 532\Aura\Source\Aura\Private\Character\AuraCharacterBase.cpp
#include "Character/AuraCharacterBase.h"
AAuraCharacterBase::AAuraCharacterBase()
{
PrimaryActorTick.bCanEverTick = false;
//初始化武器
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>("weapon");
//将武器附加到骨架插槽
Weapon->SetupAttachment(GetMesh(),FName("WeaponHandSocket"));
//武器不应有任何碰撞
Weapon->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
void AAuraCharacterBase::BeginPlay()
{
Super::BeginPlay();
}
E:/Unreal Projects 532/Aura/Content/Blueprints/Character/Aura/BP_AuraCharacter.uasset
网格体组件-细节-网格体-骨骼网格体资产-SKM_Aura 移动角色网格体使其面向前方
hand_l
上添加插槽 WeaponHandSocket
为 插槽 WeaponHandSocket
添加预览资产 SKM_Staff
法杖。
调整插槽 WeaponHandSocket
位置
预览场景设置-预览控制器-使用特定资产 预览场景设置-动画-Cast_FireBolt
Weapon组件-细节-网格体-骨骼网格体资产-SKM_Staff
E:/Unreal Projects 532/Aura/Content/Blueprints/Character/Goblin_Spear/BP_Goblin_Spear.uasset
打开 BP_Goblin_Spear
网格体组件-细节-网格体-骨骼网格体资产-SKM_Goblin
移动角色网格体使其面向前方
缩小胶囊体组件适应角色 胶囊体组件-细节-半高-
自带插槽 Hand-LSocket ,重命名为 WeaponHandSocket
Weapon组件-细节-网格体-骨骼网格体资产-SKM_Spear
删除角色。
打开 ABP_Aura 添加 state machine 动画-状态机 : Main States 从 Main States 拖出 slot"DefaultSlot" 插槽。 插槽用于播放蒙太奇。 插槽 输出至 output pose.
添加状态 IdleWalkRun
从 资产管理器 拖入 混合空间1D 动画 IdleWalkRun,输出至 output animation pose 输出动画。
重载函数-蓝图初始化动画
为 Try Get Pawn Owner 添加节点 cast to BP_AuraCharacter 获取角色, 提升为变量 BP_Aura_Character
然后从 BP_Aura_Character 获取 变量-角色-character movement 角色移动组件 将 character movement 组件 提升为变量 CharacterMovement 。 现在我们有了可以在蓝图更新动画中访问的角色移动组件。
动画被更新后执行,目标是动画实例。
如果我们想要一个更复杂的动画蓝图,我们可以使用蓝图线程安全更新动画。 但这是一个简单的动画,因此我们不需要使用多线程。 但我们会使用event blueprint update animaition 来代替蓝图更新动画来制作更复杂的动画蓝图。
拖出角色变量 BP_Aura_Character -右键-转换为有效的get
拖出角色移动变量 CharacterMovement 从 CharacterMovement 获取 速度-Velocity 从 Velocity 获取 数学-向量-Vector Length XY 将 Vector Length XY 的输出提升为变量 GroundSpeed 地面速度
现在有一个地面速度变量,用地面速度驱动混合空间动画。
拖入 GroundSpeed 地面速度 变量,输出至 混合空间动画 IdleWalkRun 的 Speed 节点 此时人物具由空闲动画。
BP_Aura_Character-网格体-细节-动画-动画模式-使用动画蓝图 动画-动画类-ABP_Aura
将会有多个敌人。 它们不会共享相同的骨骼网格体和骨架。 因此,为了使其更加通用并防止代码重复,敌人动画蓝图将使用模板动画蓝图。
打开 ABP_Enemy 所有敌人的动画蓝图都有共同点。 首先需要一个状态机。
从 Main States 拖出 slot"DefaultSlot" 插槽。 插槽用于播放蒙太奇。 插槽 输出至 output pose.
添加状态 IdleWalkRun
此处不会拖入任何特定的在闲走、跑步中混合空间。
将使用混合空间播放器。 添加 动画-混合空间-Blendspace Player
这是一个通用节点。 把它连接到输出动画姿势。 因此,当我们创建从此模板派生的子动画蓝图时,我们所要做的就是选择混合空间播放器。
还需要一个速度变量来驱动混合空间 x 轴。
重载函数-蓝图初始化动画
为 Try Get Pawn Owner 添加节点 工具-cast- cast to AuraEnemy 获取敌人角色, 提升为变量 BP_Aura_Enemy
然后从 BP_Aura_Enemy 获取 变量-角色-get character movement 敌人角色移动组件 将 character movement 组件 提升为变量 CharacterMovement 。 现在我们有了可以在蓝图更新动画中访问的敌人角色移动组件。
动画被更新后执行,目标是动画实例。
拖出敌人角色变量 BP_Aura_Enemy -右键-转换为有效的get
拖出敌人角色移动变量 CharacterMovement 从 CharacterMovement 获取 速度-Velocity 从 Velocity 获取 数学-向量-Vector Length XY 将 Vector Length XY 的输出提升为变量 GroundSpeed 地面速度
现在有一个地面速度变量,用地面速度驱动混合空间动画。
拖入 GroundSpeed 地面速度 变量,输出至 混合空间动画 IdleWalkRun 的 Speed 节点
根据这个动画蓝图模板为敌人创建一个子类。
右键-动画-动画蓝图-模板-骨骼选择 SK_Goblin ,父类选择 ABP_Enemy
名称:ABP_Goblin_Spear
E:/Unreal Projects 532/Aura/Content/Blueprints/Character/Goblin_Spear/ABP_Goblin_Spear.uasset
打开 ABP_Goblin_Spear-资产覆盖编辑器-展开列表: ABP_Enemy-AnimGraph-Main States-IdleWalkRun-混合空间播放器-选择 混合空间 BS_GoblinSpear_IdleRun 现在敌人具有了空闲动画
BP_Goblin_Spear-网格体-细节-动画-动画模式-使用动画蓝图 动画-动画类-ABP_Goblin_Spear
此时敌人哥布林魔法师处于空闲动画。
基于 AuraEnemy 新建 蓝图 BP_Goblin_Slingshot
E:/Unreal Projects 532/Aura/Content/Blueprints/Character/Goblin_Slingshot/BP_Goblin_Slingshot.uasset
打开 BP_Goblin_Slingshot 网格体组件-网格体-骨骼网格体组件-SKM_Goblin 调整 网格体位置和朝向 调整胶囊体组件-半高
Weapon组件-细节-网格体-骨骼网格体组件-SKM_Slingshot
右键-动画-动画蓝图-模板-骨骼选择 SK_Goblin ,父类选择 ABP_Enemy
名称:ABP_Goblin_Slingshot
E:/Unreal Projects 532/Aura/Content/Blueprints/Character/Goblin_Slingshot/ABP_Goblin_Slingshot.uasset
打开 ABP_Goblin_Slingshot-资产覆盖编辑器-展开列表: ABP_Enemy-AnimGraph-Main States-IdleWalkRun-混合空间播放器-选择 混合空间 BS_GoblinSlingshot_IdleRun
BP_Goblin_Slingshot-网格体组件-细节-动画-动画模式-使用动画蓝图 动画-动画类-ABP_Goblin_Slingshot
现在不需要进入动画蓝图并设置所有变量或类似的内容,因为我们正在使用动画蓝图模板,可以将其共享给敌人。 需要做的就是设置网格体组件并在动画蓝图更改使用的任何动画资源。
移动角色。
创建控制目录:Input
Input 下创建目录 InputActions 现在需要一个输入操作来增强输入,以便从某种设备获取输入,例如键盘.
输入操作是用户可执行操作的逻辑表示,例如”跳跃”或“蹲伏”。这些是你的Gameplay代码绑定的内容,以便监听输入状态的变化。多数情况下你的游戏代码应该监听输入操作的“已触发”事件。这将允许最有可扩展性和可定制性的输入配置,因为你可以在输入映射上下文中为每个键映射添加不同的触发器。 它们在概念上等同于旧版输入系统中的“操作”和“轴”映射名称 注意:这些是每个玩家实例化(通过FInputActioninstance)
右键-输入-输入操作 新建 IA_Move
E:/Unreal Projects 532/Aura/Content/Blueprints/Input/InputActions/IA_Move.uasset
打开 IA_Move
细节-操作-值类型-Axis2D(Vector2D)
这将允许我接受输入具有 X 和 y 两个轴的二维向量的形式,
将X视为左和右,Y视为前和后。
因为对于运动,我希望能够处理二维运动。
左右是第一维度,前后是第二维度。
将输入操作其链接到角色的方式将是通过输入映射上下文,我们可以在其中将键盘按键与要移动的输入操作关联起来。
UInputMappingContext:特定输入上下文的操作映射键的集合, 可用于: 存储预定义控制器映射(允许在控制器配置变体之间切换)。TODO:构建一个允许UnputMappingContexts重定向的系统来处理该问题。 每个载具控制点映射定义 定义上下文相关的映射(如我从枪(射击操作) 切换到抓钩(放出、收、断开操作). 定义覆层映射以应用到现有控件映射之上(如:MOBA游戏中英雄特定操作映射)
在 Input 目录右键 -输入-输入映射情景 名称:IMC_AuraContext 打开 IMC_AuraContext 细节-映射-映射-添加一个操作映射 选择 IA_Move 数据资产(输入操作)
点击 IA_Move 右侧加号 将多个控制绑定添加到操作映射
键盘D:向右移动
键盘A:向左移动 为了向左移动需要添加一个 negate 否定修改器 表示负数。 只启用X轴表示左右方向的移动。 作为二位映射,Z轴无效。
键盘W:向上移动 为了向上移动需要添加一个 拌合输入轴值 修改器 表示Y轴数。 现在排序为 YXZ,Y为首选。 拌合输入值的轴组件。 将1D输入映射到2D操作的Y轴上时非常有用。
键盘W:向下移动 为了向下移动需要添加一个 negate 否定修改器 和 拌合输入轴值 修改器 表示Y轴负数。
基于 PlayerController C++类新建 PlayerController C++类 在 Player 目录下
在 AuraPlayerController玩家 角色控制器中将增强的输入操作和输入映射情景与我们的角色相关联。
E:\Unreal Projects 532\Aura\Source\Aura\Aura.Build.cs
// Fill out your copyright notice in the Description page of Project Settings.
using UnrealBuildTool;
public class Aura : ModuleRules
{
public Aura(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" });
PrivateDependencyModuleNames.AddRange(new string[] { });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
Source/Aura/Public/Player/AuraPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "AuraPlayerController.generated.h"
class UInputMappingContext;
UCLASS()
class AURA_API AAuraPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AAuraPlayerController();
protected:
virtual void BeginPlay() override;
private:
//输入映射情景 在蓝图中指定
UPROPERTY(EditAnywhere, Category = "Input")
TObjectPtr<UInputMappingContext> AuraContext;
};
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "Player/AuraPlayerController.h"
#include "EnhancedInputSubsystems.h"
//https://docs.unrealengine.com/5.3/en-US/API/Plugins/EnhancedInput/UEnhancedInputLocalPlayerSubsyst-/
AAuraPlayerController::AAuraPlayerController()
{
//启用玩家控制器的网络复制
//多人游戏中的复制本质上是当服务器上的实体发生变化时。服务器上发生的更改将复制或发送到连接到的所有客户端.
bReplicates = true;
}
void AAuraPlayerController::BeginPlay()
{
Super::BeginPlay();
// 如果没有设置输入映射情景,返回false,游戏将崩溃,check 硬性断言
check(AuraContext);
//ULocalPlayer 本地玩家类
// 获取增强输入的本地玩家子系统,单例。用以添加输入映射情景
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(
GetLocalPlayer());
//如果为空,系统崩溃
check(Subsystem);
//添加输入映射情景,可以同时有多个输入映射情景
//当前只添加一个输入映射情景,优先级为0.
Subsystem->AddMappingContext(AuraContext, 0);
//显示光标
bShowMouseCursor = true;
//设置光标类型
DefaultMouseCursor = EMouseCursor::Default;
//设置输入模式 可使用鼠标和键盘,并且影响UI部件
FInputModeGameAndUI InputModeData;
// 锁定模式:不将鼠标锁定在视口上
InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
//获取输入模式数据并在捕获期间使用设置隐藏光标为 false。
//因此,一旦我们的光标被捕获到视口中,我们就不会隐藏光标。
InputModeData.SetHideCursorDuringCapture(false);
//为了使用此输入模式数据,我们使用玩家控制器函数设置输入模式传递
SetInputMode(InputModeData);
}
之后需要在蓝图中指定 输入映射情景,输入操作 数据资产。 需要项目配置使用 AuraPlayerController 玩家角色控制器
Source/Aura/Public/Player/AuraPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "InputActionValue.h"
#include "GameFramework/PlayerController.h"
#include "AuraPlayerController.generated.h"
class UInputMappingContext;
class UInputAction;
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;
// MoveAction 的回调函数响应
void Move(const FInputActionValue& InputActionValue);
};
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "Player/AuraPlayerController.h"
#include "EnhancedInputSubsystems.h"
//https://docs.unrealengine.com/5.3/en-US/API/Plugins/EnhancedInput/UEnhancedInputLocalPlayerSubsyst-/
#include "EnhancedInputComponent.h"
AAuraPlayerController::AAuraPlayerController()
{
//启用玩家控制器的网络复制
//多人游戏中的复制本质上是当服务器上的实体发生变化时。服务器上发生的更改将复制或发送到连接到的所有客户端.
bReplicates = true;
}
void AAuraPlayerController::BeginPlay()
{
Super::BeginPlay();
// 如果没有设置输入映射情景,返回false,游戏将崩溃,check 硬性断言
check(AuraContext);
//ULocalPlayer 本地玩家类
// 获取增强输入的本地玩家子系统,单例。用以添加输入映射情景
// #include "EnhancedInputSubsystems.h"
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(
GetLocalPlayer());
//如果为空,系统崩溃
check(Subsystem);
//添加输入映射情景,可以同时有多个输入映射情景
//当前只添加一个输入映射情景,优先级为0.
Subsystem->AddMappingContext(AuraContext, 0);
//显示光标
bShowMouseCursor = true;
//设置光标类型
DefaultMouseCursor = EMouseCursor::Default;
//设置输入模式 可使用鼠标和键盘,并且影响UI部件
FInputModeGameAndUI InputModeData;
// 锁定模式:不将鼠标锁定在视口上
InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
//获取输入模式数据并在捕获期间使用设置隐藏光标为 false。
//因此,一旦我们的光标被捕获到视口中,我们就不会隐藏光标。
InputModeData.SetHideCursorDuringCapture(false);
//为了使用此输入模式数据,我们使用玩家控制器函数设置输入模式传递
SetInputMode(InputModeData);
}
void AAuraPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
//强制转换输入组件为增强型输入组件 失败则程序崩溃 确保输入组件未被破坏
//#include "EnhancedInputComponent.h"
UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent);
// 将移动输入操作绑定到输入组件的Move回调,用以移动角色。
// 参数1:输入操作
// 参数2:否希望在输入操作开始时调用 move
// 参数3: 用户对象 控制器
// 参数4:回调函数
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AAuraPlayerController::Move);
}
void AAuraPlayerController::Move(const FInputActionValue& InputActionValue)
{
//获取输入操作的二维轴向量
const FVector2D InputAxisVector = InputActionValue.Get<FVector2D>();
//使用X 轴和 Y 轴
const FRotator Rotation = GetControlRotation();
const FRotator YawRotation(0.f, Rotation.Yaw, 0.f);
// 前进的方向
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
// 前进的右方向
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// 使用if是因为 Move 可能会在每一帧中被调用,因此在此之前调用它可能有点为时过早,所以不使用check
if (APawn* ControlledPawn = GetPawn<APawn>())
{
//WS绑定在输入操作Y轴
ControlledPawn->AddMovementInput(ForwardDirection, InputAxisVector.Y);
//AD绑定在输入操作X轴
ControlledPawn->AddMovementInput(RightDirection, InputAxisVector.X);
}
}
内容-Blueprints 下新建目录 Player
基于 AuraPlayerController C++ 类创建 BP_AuraPlayerController 玩家角色控制器蓝图
E:/Unreal Projects 532/Aura/Content/Blueprints/Player/BP_AuraPlayerController.uasset
打开 BP_AuraPlayerController 细节-输入-aura context- IMC_AuraContext 输入映射情景/数据资产(输入映射上下文) 细节-输入-move action-IA_Move 数据资产(输入操作)
通过游戏模式将 BP_AuraPlayerController 和玩家联系在一起。
Source/Aura/Public/Game/AuraGameModeBase.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "AuraGameModeBase.generated.h"
UCLASS()
class AURA_API AAuraGameModeBase : public AGameModeBase
{
GENERATED_BODY()
};
Source/Aura/Private/Game/AuraGameModeBase.cpp
#include "Game/AuraGameModeBase.h"
内容-Blueprints 下新建目录 Game
E:/Unreal Projects 532/Aura/Content/Blueprints/Game/BP_AuraGameMode.uasset
打开 BP_AuraGameMode 细节-高级-类-玩家控制器类-BP_AuraPlayerController 细节-高级-类-默认 Pawn 类-BP_AuraCharacter
进入关卡-世界场景设置-游戏模式-游戏模式重载-BP_AuraGameMode
添加玩家出生点 基础-玩家出生点 拖至关卡任意位置即可 此时可控制玩家行走 WSAD。
C++设置相机和弹簧臂没有性能优势。
打开 BP_AuraCharacter 选中 胶囊体组件,作为弹簧臂的父组件。 添加 SpringArm弹簧臂组件。 选中 SpringArm弹簧臂组件,作为相机父组件。 添加Camera摄像机组件.
SpringArm组件-细节-摄像机设置-使用Pawn控制旋转-不启用 我不想用控制器旋转弹簧臂,对于自上而下的拍摄,相机将被固定。
SpringArm组件-细节-摄像机-目标臂长度-500
相机延迟: SpringArm组件-细节-滞后-启用摄像机延迟-启用
这是一个很好的小效果,让相机有点滞后。
camera组件-细节-摄像机选项-使用Pawn控制旋转 -不启用
Source/Aura/Public/Character/AuraCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "Character/AuraCharacterBase.h"
#include "AuraCharacter.generated.h"
UCLASS()
class AURA_API AAuraCharacter : public AAuraCharacterBase
{
GENERATED_BODY()
public:
AAuraCharacter();
};
Source/Aura/Private/Character/AuraCharacter.cpp
#include "Character/AuraCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
AAuraCharacter::AAuraCharacter()
{
// 获取角色运动组件
// 启用:方向旋转到运动
GetCharacterMovement()->bOrientRotationToMovement = true;
// 可以通过获取角色移动旋转速率来控制旋转速率。
// 角色就会以这个速度400,在偏航旋转方向上运动,角色运动可以迫使我们将运动限制在一个平面上。
// yaw():航向,将物体绕Y轴旋转
GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f);
// 角色被捕捉到平面
GetCharacterMovement()->bConstrainToPlane = true;
// 在开始时捕捉到平面
GetCharacterMovement()->bSnapToPlaneAtStart = true;
// 角色本身不应该使用控制器的旋转
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
bUseControllerRotationYaw = false;
}
SpringArm组件-细节-摄像机设置-继承Pitch-不启用 SpringArm组件-细节-摄像机设置-继承Yaw-不启用 SpringArm组件-细节-摄像机设置-继承Roll-不启用
弹簧臂没有继承俯仰偏航或滚动。
打开 动画蓝图 ABP_Aura-Main states 添加 状态 Idle,
进入 状态 Idle,拖入动画序列 Idle
选中 Idle 动画序列-细节-设置-循环动画-启用 打开事件图表-添加布尔变量-ShouldMove 将 GroundSpeed 大于3 的比较值赋 ShouldMove 。
进入 ABP_Aura-Main states 设置 Idle 到 IdleWalkRun 的条件:ShouldMove 为真。 直接拖入变量 ShouldMove
设置 IdleWalkRun 到Idle 的条件:ShouldMove 为假。 直接拖入变量 ShouldMove ,拖入 Not Bool
使用高亮效果来告诉我们哪个敌人正在被选中瞄准。 玩家控制器类 AuraPlayerController有方法可以查看鼠标光标下方的内容。
创建 敌人接口类,当将鼠标悬停在Actor上时,我们可以检查它是否实现了这个接口。 如果是的话,我们就可以在该接口上调用接口函数。 将鼠标悬停在Actor上时。,玩家控制器不需要知道会发生什么。 它所知道的是,如果参与者实现了接口,它应该调用该接口函数。 参与者可以重写该函数并以任何他想要的方式实现它。 不同的敌人类别可以具有不同的突出显示功能。 我们可以将此界面添加到桶或门上,并以不同的方式突出显示这些对象。 在这种情况下,敌人接口这个名称可能不太适合这个名称。
Source/Aura/Public/Interaction/EnemyInterface.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "EnemyInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UEnemyInterface : public UInterface
{
GENERATED_BODY()
};
/**
*
*/
class AURA_API IEnemyInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
// 纯虚函数
// 突出显示选择的Actor
virtual void HighlightActor() =0;
virtual void UnHighlightActor() =0;
};
Source/Aura/Private/Interaction/EnemyInterface.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "Interaction/EnemyInterface.h"
// Add default functionality here for any IEnemyInterface functions that are not pure virtual.
Source/Aura/Public/Character/AuraEnemy.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Character/AuraCharacter.h"
#include "Interaction/EnemyInterface.h"
#include "AuraEnemy.generated.h"
/**
*
*/
UCLASS()
class AURA_API AAuraEnemy : public AAuraCharacter, public IEnemyInterface
{
GENERATED_BODY()
public:
// 突出显示选择的Actor
virtual void HighlightActor() override;
virtual void UnHighlightActor() override;
};
Source/Aura/Private/Character/AuraEnemy.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "Character/AuraEnemy.h"
void AAuraEnemy::HighlightActor()
{
}
void AAuraEnemy::UnHighlightActor()
{
}
Source/Aura/Public/Character/AuraEnemy.h
public:
// 突出显示选择的Actor
virtual void HighlightActor() override;
virtual void UnHighlightActor() override;
UPROPERTY(BlueprintReadOnly)
bool bHighlighted=false;
Source/Aura/Private/Character/AuraEnemy.cpp
#include "Character/AuraEnemy.h"
void AAuraEnemy::HighlightActor()
{
bHighlighted=true;
}
void AAuraEnemy::UnHighlightActor()
{
bHighlighted=false;
}
Source/Aura/Public/Player/AuraPlayerController.h
class IEnemyInterface;
private:
// 光标跟踪 用以高亮选中的Actor
void CursorTrace();
// 帧更新之前光标跟踪的Actor
IEnemyInterface* LastActor;
// 光标跟踪的当前Actor
IEnemyInterface* ThisActor;
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "Interaction/EnemyInterface.h"
void AAuraPlayerController::PlayerTick(float DeltaTime)
{
Super::PlayerTick(DeltaTime);
CursorTrace();
}
void AAuraPlayerController::CursorTrace()
{
FHitResult CursorHit;
// 光标命中的结果 使用 ECC_Visibility 通道进行跟踪 ,简单碰撞跟踪
GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
// 检查跟踪结果
if (!CursorHit.bBlockingHit) return;
LastActor = ThisActor;
ThisActor = Cast<IEnemyInterface>(CursorHit.GetActor());
/**
* Line trace from cursor. There are several scenarios:
* A. LastActor is null && ThisActor is null
* - Do nothing
* B. LastActor is null && ThisActor is valid
* - Highlight ThisActor
* C. LastActor is valid && ThisActor is null
* - UnHighlight LastActor
* D. Both actors are valid, but LastActor != ThisActor
* - UnHighlight LastActor, and Highlight ThisActor
* E. Both actors are valid, and are the same actor
* - Do nothing
*/
if (LastActor == nullptr)
{
if (ThisActor != nullptr)
{
// Case B
ThisActor->HighlightActor();
}
else
{
// Case A - both are null, do nothing
}
}
else // LastActor is valid
{
if (ThisActor == nullptr)
{
// Case C
LastActor->UnHighlightActor();
}
else // both actors are valid
{
if (LastActor != ThisActor)
{
// Case D
LastActor->UnHighlightActor();
ThisActor->HighlightActor();
}
else
{
// Case E - do nothing
}
}
}
}
事件图表 添加继承自C++的变量 bHighlighted ,蓝图中会省略 b 前缀 光标下的actor位置会画出红色调试球
BP_Goblin_Spear-网格体组件-细节-碰撞-碰撞预设-custom 网格体组件-细节-碰撞-碰撞响应-检测响应-Visibility-阻挡 使光标跟踪生效,因为光标跟踪Visibility通道。 BP_Goblin_Slingshot 使用同样的设置。
GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
关卡拖入一个 BP_Goblin_Spear 测试 但对 BP_Goblin_Slingshot 无效
使用后期处理效果通过轮廓突出显示敌人。
基于 C++ AuraEnemy 类新建蓝图 BP_EnemyBase
E:/Unreal Projects 532/Aura/Content/Blueprints/Character/BP_EnemyBase.uasset
如果我们在敌人基础蓝图中做了任何事情,那么所有其他敌人的蓝图都会继承该功能。
打开 BP_Goblin_Slingshot 蓝图 类设置-类选项-父类-BP_EnemyBase
打开 BP_Goblin_Spear 蓝图 类设置-类选项-父类-BP_EnemyBase
对于像轮廓这样的效果,我们需要一个后期处理体积,因为它将是一个后期处理效果。 添加 -体积-后期处理体积 到关卡
选择 PostProcessVolume-细节-后期处理体积设置-无限范围(未设定)-启用 启用:此体积覆盖整个场景 或不启用:只有其边界中的区域
PostProcessVolume-细节-渲染功能-后期处理材质-添加一组材质,选择-资产引用-再选择-PP_Highlight 材质
只有当我们将项目配置为使用自定义深度模板通道时,这才有效。
编辑-项目设置-【搜索 custom depth】-引擎-渲染-后期处理-自定义深度-模板通道-已按需求启用 首次使用时创建深度缓冲,可节约内存但会导致停转。已禁用模板。
关闭胶囊体组件的轮廓 BP_Goblin_Spear-胶囊体组件-细节-渲染-可视-不启用
现在我们的后期处理体积正在使用高光,这意味着任何设置其自定义的深度模板值为 250 的 网格将突出显示。
选择关卡的任意一个敌人: 细节-【搜索 custom depth】-渲染-mesh-渲染自定义深度通道-启用 细节-【搜索 custom depth】-渲染-mesh-自定义深度模板值-50
此时 的人的轮廓将高亮显示
调节PP_Highlight轮廓粗度
删除 AuraEnemy 的 变量 bHighlighted。
Source/Aura/Aura.h
#pragma once
#include "CoreMinimal.h"
// 自定义深度模板值
#define CUSTOM_DEPTH_RED 250
Source/Aura/Public/Character/AuraEnemy.h
#pragma once
#include "CoreMinimal.h"
#include "Character/AuraCharacter.h"
#include "Interaction/EnemyInterface.h"
#include "AuraEnemy.generated.h"
UCLASS()
class AURA_API AAuraEnemy : public AAuraCharacter, public IEnemyInterface
{
GENERATED_BODY()
public:
// 突出显示选择的Actor
virtual void HighlightActor() override;
virtual void UnHighlightActor() override;
};
Source/Aura/Private/Character/AuraEnemy.cpp
#include "Character/AuraEnemy.h"
#include "Aura/Aura.h"
void AAuraEnemy::HighlightActor()
{
// 廉价操作 不需要担心每帧渲染性能
// 启用 渲染自定义深度通道
GetMesh()->SetRenderCustomDepth(true);
// 设置 自定义深度模板值
GetMesh()->SetCustomDepthStencilValue(CUSTOM_DEPTH_RED);
Weapon->SetRenderCustomDepth(true);
Weapon->SetCustomDepthStencilValue(CUSTOM_DEPTH_RED);
}
void AAuraEnemy::UnHighlightActor()
{
// 关闭 渲染自定义深度通道
GetMesh()->SetRenderCustomDepth(false);
Weapon->SetRenderCustomDepth(false);
}
删除 BP_Goblin_Spear 蓝图事件图的 调试球事件
BP_Goblin_Slingshot 和 BP_Goblin_Spear-网格体组件-细节-碰撞-碰撞预设-CharacterMesh [默认值]。
对所有敌人执行【检测响应-Visibility-阻挡,使光标跟踪生效】操作,我们应该在基类上执行此操作,无论这是C++基类或基础蓝图类,我想将其设置在C++类中.
Source/Aura/Public/Character/AuraEnemy.h
#pragma once
#include "CoreMinimal.h"
#include "Character/AuraCharacter.h"
#include "Interaction/EnemyInterface.h"
#include "AuraEnemy.generated.h"
UCLASS()
class AURA_API AAuraEnemy : public AAuraCharacter, public IEnemyInterface
{
GENERATED_BODY()
public:
AAuraEnemy();
// 突出显示选择的Actor
virtual void HighlightActor() override;
virtual void UnHighlightActor() override;
};
Source/Aura/Private/Character/AuraEnemy.cpp
AAuraEnemy::AAuraEnemy()
{
// 设置敌人基类的网格体组件的碰撞预设为 custom,检测响应-Visibility-阻挡,
// 使光标跟踪生效,因为光标跟踪Visibility通道。
// GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
}
编译后,基础蓝图生效,但二级敌人蓝图可能部分生效,依然需要手动更改检测响应: 网格体组件-细节-碰撞-碰撞响应-检测响应-Visibility-阻挡
https://docs.unrealengine.com/5.3/zh-CN/gameplay-ability-system-for-unreal-engine/
Gameplay技能系统 是一个高度灵活的框架,可用于构建你可能会在RPG或MOBA游戏中看到的技能和属性类型。你可以构建可供游戏中的角色使用的动作或被动技能,使这些动作导致各种属性累积或损耗的状态效果,实现约束这些动作使用的"冷却"计时器或资源消耗,更改技能等级及每个技能等级的技能效果,激活粒子或音效,等等。简单来说,此系统可帮助你在任何现代RPG或MOBA游戏中设计、实现及高效关联各种游戏中的技能,既包括跳跃等简单技能,也包括你喜欢的角色的复杂技能集。
Gameplay技能 实为继承自 UGameplayAbility 类的C++类或蓝图类。其定义C++代码或蓝图脚本中技能的实际作用,并建立处理技能的元素,如复制和实例化行为。
在定义技能的逻辑过程中,gameplay技能中的逻辑通常会调用一系列被称为 技能任务 异步编译块。技能任务衍生自抽象 UAbilityTask 类,以C++编写。其完成操作时,会基于最终结果频繁调用委托(C++中)或输出执行引脚(蓝图中)(例如,需要目标的技能需进行"瞄准"任务,将调用一个委托或输出引脚确认目标,并调用另一引脚取消技能)。
除Gameplay技能外,Gameplay技能系统还支持 Gameplay属性 和 Gameplay效果。Gameplay属性是存储在 FGameplayAttribute 结构中的"浮点"值,将对游戏或Actor产生影响;其通常为生命值、体力、跳跃高度、攻击速度等值。Gameplay效果可即时或随时间改变Gameplay属性(通常称为"增益和减益")。例如,施魔法时减少魔法值,激活"冲刺"技能后提升移动速度,或在治疗药物的效力周期内逐渐恢复生命值。
与Gameplay技能系统交互的Actor须拥有 技能系统组件。此组件将激活技能、存储属性、更新效果,和处理Actor间的交互。启用Gameplay技能系统并创建包含技能系统组件的Actor后,便可创建技能并决定Actor的响应方式。
由于Gameplay技能系统是一个插件,因此你需要先启用它才能使用。只需两步即可启用它:
在 编辑(Edit) -> 插件(Plugins) 窗口中启用Gameplay技能系统插件。 要使用此系统的全部功能,添加"GameplayAbilities"、"GameplayTags"和"GameplayTasks"到项目的"(ProjectName).Build.cs"文件中的 PublicDependencyModuleNames 中。这很容易做到,只需找到公用模块列表:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay" });
要使用Gameplay技能系统,将上述三个模块名称添加到花括号列表中的任意位置,如下所示:
PublicDependencyModuleNames.AddRange(new string[] { "GameplayAbilities", "GameplayTags", "GameplayTasks", "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay" });
技能系统组件是游戏中的角色访问Gameplay技能系统的主要接口。此组件管理Gameplay属性,运行Gameplay事件,存储Gameplay技能,甚至处理玩家输入到Gameplay技能激活、确认及取消命令的绑定。任何要与Gameplay技能系统交互的Actor都应具有技能系统组件。
这是一种可以添加到 Actor 中的组件,它可以处理许多重要的事情, 例如授予技能、激活这些技能、处理通知,当某些技能被激活或效果被应用时,以及许多其他我们会做的事情。 技能系统组件是我们必须添加到角色中的东西。
与任何给定对象或角色相关联的许多属性,这些属性的范围包括生命和法力,几乎是你能想到的GAS的任何其他属性。 我们将这些属性存储在属性集上,属性集具有许多功能,使我们能够将这些属性与GAS系统的其他各个部分相关联。
游戏技能系统的核心是技能。 游戏技能是我们用来封装某种事物功能的类,是角色或物体在游戏中可以做的事情。 像攻击、施法之类的东西一般都是技能,而游戏技能让我们将所有代码和功能保留在特定的游戏技能类别中。
游戏技能运行异步任务,我们称之为技能任务。 这些允许我们执行异步代码,这意味着一旦我们启动任务,它就可以执行它的工作并立即完成,或者它的工作可能跨越一段时间并且可能去做其他事情。 技能任务就像是为游戏技能本身执行工作的工人。
用来更改属性值的内容,它们具有多种与属性相关的不同的功能。 我们使用游戏效果来直接改变属性,随着时间的推移改变它们,周期性地增加或减少它们,并将这些属性更改与采用其他属性参数的各种计算相关联。
负责处理粒子系统和声音等外观效果,并且可以处理网络复制。
技能系统的另一个核心部分是游戏玩法标签。 游戏标签实际上并不是GAS系统所独有的。 它们存在于GAS之外,可以用于非游戏技能系统项目。 它们的层次性质使它们比变量(例如枚举、布尔值或字符串)简单。
添加技能系统组件的方式可以有所不同。
1-可以直接在pawn类上添加技能系统组件。 可以对属性集执行相同的操作。
2-采用与您的 pawn 相关的其他类,例如玩家状态。 并将技能系统组件和属性集添加到该类中。
1-假设我们已将技能、系统组件和属性集添加到 pawn Class。
假设这是一个多人游戏,此时我们的Actor死了,我们决定摧毁它的pawn 以便我们可以重生另一个。 因为那个Actor上存在技能系统组件和属性集。 当Actor被摧毁时,技能、系统组件和属性集也会被摧毁。 如果您没有采取措施保存这些类的任何相关数据,例如在保存游戏对象中或者在某个数据库中,该数据就消失了。 当你重生一个新的 pawn 时,技能系统组件和属性集都会被重新创建, 从默认值和默认功能开始。
将技能系统组件和属性集保留在Pawn上的原因可能是Pawn可能很简单。 您的游戏可能有简单的敌人,它们确实具有技能系统组件和属性集,但是这些敌人不需要玩家状态,因为它们很简单。 他们只需要拥有属性和技能,并基于人工智能执行逻辑即可。 因此直接在 pawn 上设置的技能系统组件和属性可能适合该情况。
2-假设在玩家状态上设置技能系统组件和属性。这与我们的Pawn相关。 如果我们的 pawn 死亡并且我们摧毁了它,则技能系统组件和属性集不会被摧毁,因为它们不存在于该类中。 它们存在于玩家状态中,当您销毁 pawn 时,该状态仍然存在,因此我们可以生成一个新的 pawn 并将其与我们的玩家状态相关联。然后我们的属性集中仍然有相同的值,我们的技能系统中仍然有相同的技能组件,因为它们尚未被破坏。
另一个原因是您可能不希望您的技能系统组件和属性集使角色变得混乱。 你可能希望将你的角色换成另一个角色,但你可能不想要你的技能系统组件和属性设置于任何给定角色关联。 您可能希望将其与玩家本身保持关联。
敌人角色将直接拥有他们的技能系统组件和属性集,与 Character 关联。 玩家控制的角色,我们将把我们的技能系统组件和属性与 玩家状态 关联。
https://docs.unrealengine.com/5.3/en-US/API/Runtime/Engine/GameFramework/APlayerState/
再 C++ Player文件夹内
基于 Player State 创建 AuraPlayerState 类
E:/Unreal Projects 532/Aura/Source/Aura/Public/Player/AuraPlayerState.h
NetUpdateFrequency=100.f; 网络更新频率 这是服务器尝试更新客户端的频率。 当服务器上发生玩家状态的更改时,服务器将发送更新给所有客户端,以便它们可以与服务器版本同步。 任何应该复制的变量都会更新 通常玩家状态的净更新频率相当低,大约半秒。 如果我们要在玩家状态上设置我们的技能系统组件和属性,应该设置这个以更新得更快。 事实上,在 Lyra 项目中,在像《堡垒之夜》这样的游戏中,技能系统组件和属性集位于玩家状态上,它们将净更新频率设置为更高的值,通常是100左右。
Source/Aura/Public/Player/AuraPlayerState.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "AuraPlayerState.generated.h"
UCLASS()
class AURA_API AAuraPlayerState : public APlayerState
{
GENERATED_BODY()
public:
AAuraPlayerState();
};
Source/Aura/Private/Player/AuraPlayerState.cpp
#include "Player/AuraPlayerState.h"
AAuraPlayerState::AAuraPlayerState()
{
// 网络更新频率
// 这是服务器尝试更新客户端的频率。
// 当服务器上发生玩家状态的更改时,服务器将发送更新给所有客户端,以便它们可以与服务器版本同步。
// 任何应该复制的变量都会更新
// 通常玩家状态的净更新频率相当低,大约半秒。
// 如果我们要在玩家状态上设置我们的技能系统组件和属性,应该设置这个以更新得更快。
NetUpdateFrequency=100.f;
}
E:/Unreal Projects 532/Aura/Content/Blueprints/Player/BP_AuraPlayerState.uasset
打开 BP_AuraGameMode 细节-高级-类-玩家状态类-BP_AuraPlayerState 此时Aura玩家加使用该状态。 技能系统组件和属性集将适用于我们的玩家控制角色的 Aura。
基于 AbilitySystemComponent 类 新建 C++ 类 AuraAbilitySystemComponent
作为其他技能系统组件的基类
E:/Unreal Projects 532/Aura/Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "AuraAbilitySystemComponent.generated.h"
UCLASS()
class AURA_API UAuraAbilitySystemComponent : public UAbilitySystemComponent
{
GENERATED_BODY()
};
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
#include "AbilitySystem/AuraAbilitySystemComponent.h"
基于 AttributeSet 类 新建 C++ 类 AuraAttributeSet
E:/Unreal Projects 532/Aura/Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AuraAttributeSet.generated.h"
UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
GENERATED_BODY()
};
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
#include "AbilitySystem/AuraAttributeSet.h"
在多人游戏中存在一台服务器,并且在大多数情况下只有一台服务器。 该服务器是游戏的一个实例,独立于其他机器运行。 他们也在运行自己的游戏版本。 我们称这些客户为客户端Clent。 服务器与客户端不同。
专用服务器没有人类玩家,也不会渲染到屏幕上。 只是一台运行游戏模拟的计算机,因为没有人类玩家,也没有实际的渲染到屏幕。 不要让这种情况欺骗您,让您认为服务器上没有发生任何事情, 例如玩家在射击游戏中跑来跑去互相射击,在魔法游戏中施展咒语,升级并获得经验之类的东西。 这些都是在游戏的服务器版本上可以并且确实发生的事情。 他们只是没有通过将图像渲染到屏幕来显示.。
与专用服务器不同,因为监听服务器是人类玩家,或者至少它是一台运行游戏版本的计算机,人类玩家在其中控制Actor或角色。 我们说那个人类玩家正在主持游戏。 现在主机在监听服务器模型上有一个优势,这个优势就是主机没有延迟。 延迟是数据从客户端到服务器通过网络传输数据所需时间的结果。 在侦听服务器中,主机是服务器,服务器不必通过网络发送数据到其自身,因此主机玩家不会出现延迟。
在虚幻引擎中,我们将服务器视为游戏的权威版本。 每台机器上都在发生事情。 玩家四处奔跑,改变位置,这种情况在游戏的每个版本中都会发生。 但由于延迟,您计算机上的角色位置版本将会与你的角色在其他玩家的机器和服务器上的位置有所不同。 所以我们必须确定哪台机器是正确的版本。 我们认为服务器是游戏的正确版本,因此在服务器上我们做了重要的事情。 服务器始终被认为是游戏的权威版本。
如果您尝试访问其中一台客户端上的游戏模式,您将得到一个空指针。 这是因为游戏模式负责诸如游戏规则、生成玩家和重新启动游戏之类的事情。 这些事情只能在服务器发生。
所以服务器有每个玩家控制器的权威服务器版本,每个客户端都有自己的本地版本。 但请注意,客户端零在其机器上只有玩家控制器零。 没有玩家控制器一或玩家控制器二。 所有其他玩家的玩家控制器都不存在于客户端零上。 只有服务器拥有游戏中的所有玩家控制器并可以访问它们。
客户端零具有玩家状态零的版本,以及玩家状态一和玩家状态二。 这就是玩家状态与玩家控制器类的区别。
服务器拥有游戏中的所有三个pawn ,但每个客户端也拥有游戏中的所有三个pawn 。 如果你正在玩这个游戏,你必须能够看到你的pawn 到处跑,但你也能查看客户端一控制的 pawn 和客户端二控制的 pawn。 只有您的机器拥有游戏中所有三个pawn 的副本才有意义。 所以游戏中的所有pawn 都存在于所有机器上。 玩家状态也是如此。
如果我们谈论的是专用服务器,则服务器上没有 HUD。 如果它是侦听服务器,则唯一存在的 HUD 是播放该游戏的本地玩家的 HUD。 每个客户端只有自己的 HUD 以及显示在该玩家屏幕上的任何关联小部件。 零号客户端无权访问一号客户端的 HUD。
将玩家控制器类设置为需要复制 玩家状态设置为净更新频率为 100。 网络更新频率只是意味着随着服务器上的更改,每次网络更新,服务器将数据发送到客户端,以便他们可以获知该更改。
净更新频率为 100 意味着这些更改将在客户端上每秒更新 100 次, 只要服务器能够管理数据从服务器传输到客户端时的情况。 这就是 复制。
假设您的 pawn 类中有一个变量。 现在它可以是浮点数、整数或任何其他类型。 现在它存在于 pawn 类中,这意味着 pawn 的每个版本都有该变量,即客户端上 pawn 的版本,服务器上 pawn 的版本。 他们都有变量。 如果该变量被指定为复制变量,那么如果该变量的值在服务器上发生变化。 该更改的下一个网络更新将发送到所有客户端,以便客户端的该变量的版本可以更新为新值。 这种将数据发送到客户端的行为称为复制。
如果我们有一个复制变量并且它在其中一个客户端上发生更改,服务器不会更改。 复制不是双向的。 复制仅以从服务器到客户端这一种方式工作,因此如果变量被标记为已复制,则它不应在客户端上更改,因为服务器不会知道该更改,任何其他客户也不会知道。 只有更改该变量的客户端才会知道它,并且该变量现在将不与该值的服务器版本同步。 服务器是正确的版本,因此客户端上的变量不正确。
我们用键盘和鼠标或控制器进行输入。 该输入必须以某种方式到达服务器。 它以复制函数的形式进行,我们称之为 RPC 或远程过程调用。 这种复制的高级概述就是您需要了解的有关多人游戏的全部内容。
将技能系统组件和属性集实际添加到相应的类。
1-AuraCharacterBase 角色基类存储技能系统组件和属性集的指针,但不构造。 可以被其他敌人角色类继承,在敌人角色类中构造。
2-但对于玩家控制的角色,技能系统组件和属性集将由玩家状态存储和构造。
Source/Aura/Public/Character/AuraCharacterBase.h
AuraCharacterBase 需要继承 IAbilitySystemInterface
#include "AbilitySystemInterface.h"
class UAbilitySystemComponent;
class UAttributeSet;
class AURA_API AAuraCharacterBase : public ACharacter, public IAbilitySystemInterface
public:
// 获取技能系统组件
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
// 获取属性集
UAttributeSet* GetAttributeSet() const { return AttributeSet; }
protected:
// 技能系统组件
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
// 属性集
UPROPERTY()
TObjectPtr<UAttributeSet> AttributeSet;
Source/Aura/Private/Character/AuraCharacterBase.cpp
UAbilitySystemComponent* AAuraCharacterBase::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
Source/Aura/Private/Character/AuraEnemy.cpp
#include "AbilitySystem/AuraAbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
AAuraEnemy::AAuraEnemy()
{
// 设置敌人基类的网格体组件的碰撞预设为 custom,检测响应-Visibility-阻挡,
// 使光标跟踪生效,因为光标跟踪Visibility通道。
// GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
// 构造敌人类的技能系统组件
AbilitySystemComponent = CreateDefaultSubobject<UAuraAbilitySystemComponent>("AbilitySystemComponent");
// 设置为网络复制
AbilitySystemComponent->SetIsReplicated(true);
// 构造敌人类的属性集
AttributeSet = CreateDefaultSubobject<UAuraAttributeSet>("AttributeSet");
}
AuraPlayerState 需要继承 IAbilitySystemInterface
用以检查和获取技能系统组件
Source/Aura/Public/Player/AuraPlayerState.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "AbilitySystemInterface.h"
#include "AuraPlayerState.generated.h"
class UAbilitySystemComponent;
class UAttributeSet;
UCLASS()
class AURA_API AAuraPlayerState : public APlayerState, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AAuraPlayerState();
// 获取技能系统组件
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
// 获取属性集
UAttributeSet* GetAttributeSet() const { return AttributeSet; }
protected:
// 技能系统组件
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
// 属性集
UPROPERTY()
TObjectPtr<UAttributeSet> AttributeSet;
};
Source/Aura/Private/Player/AuraPlayerState.cpp
#include "Player/AuraPlayerState.h"
#include "AbilitySystem/AuraAbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
AAuraPlayerState::AAuraPlayerState()
{
// 构造玩家状态类的技能系统组件
AbilitySystemComponent = CreateDefaultSubobject<UAuraAbilitySystemComponent>("AbilitySystemComponent");
// 设置为网络复制
AbilitySystemComponent->SetIsReplicated(true);
// 构造玩家状态类的属性集
AttributeSet = CreateDefaultSubobject<UAuraAttributeSet>("AttributeSet");
// 网络更新频率
// 这是服务器尝试更新客户端的频率。
// 当服务器上发生玩家状态的更改时,服务器将发送更新给所有客户端,以便它们可以与服务器版本同步。
// 任何应该复制的变量都会更新
// 通常玩家状态的净更新频率相当低,大约半秒。
// 如果我们要在玩家状态上设置我们的能力系统组件和属性,应该设置这个以更新得更快。
NetUpdateFrequency=100.f;
}
UAbilitySystemComponent* AAuraPlayerState::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
https://docs.unrealengine.com/5.3/zh-CN/networking-and-multiplayer-in-unreal-engine/
Replication Mode | Use Case | Description |
---|---|---|
Full | Single Player | Gameplay Effects arereplicated to all clients |
Mixed | Multiplayer,Player-Controlled | Gameplay Effects arereplicated to the owningclient only. Gameplay Cuesand Gameplay Tagsreplicated to all clients. |
Minimal | Multiplayer,Al-Controlled | Gameplay Effects are noteplicated.Gameplay Cuesand Gameplay Tagsreplicated to all clients. |
复制模式 | 使用案例 | 描述 |
---|---|---|
完整 | 单人 | 游戏效果复制到所有客户端 |
混合 | 多人游戏,玩家控制 | 游戏效果仅复制到自己的客户端。游戏提示和游戏标签复制到所有客户端。 |
最小 | 多人游戏,Al控制 | 游戏效果不重复。游戏提示和游戏标签复制到所有客户端。 |
How gameplay effects will be replicated to clients
Minimal: 最小的 只复制最小的游戏效果信息。注意:这不适用于Owned AbilitySystem Components(请改用Mixed)
游戏效果不会被重复复制。 这对于人工智能控制的角色来说是有好处的,因为游戏效果可能会在服务器上发生,但是这并不一定意味着应该将游戏效果复制到客户端。 游戏提示和游戏标签仍将被复制到所有客户端
Mixed:混合的 仅将最小的游戏效果信息复制到模拟代理,但将完整信息复制到所有者和自主代理
对于像 Aura 这样的多人游戏玩家控制的角色,我们需要这些游戏效果复制到拥有的客户端。 如果我们是客户端,那么如果服务器上对我们应用了效果,我们希望收到通知。 我们希望它被复制,以便我们可以更新我们的 HUD 并在我们的客户端计算机上做出相应的响应。
Full:完整的 将完整的游戏信息复制到所有人
将游戏效果复制到所有客户端并不一定是您想要的事情,你只会在单人游戏中这样做。
您可以为多人游戏编写一个项目,并且它仍然可以在单人游戏中运行。 你不能编写一个单人游戏,然后尝试在多人游戏中运行。
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
Source/Aura/Private/Player/AuraPlayerState.cpp
AAuraPlayerState::AAuraPlayerState()
{
// 构造玩家状态类的技能系统组件
AbilitySystemComponent = CreateDefaultSubobject<UAuraAbilitySystemComponent>("AbilitySystemComponent");
// 设置为网络复制
AbilitySystemComponent->SetIsReplicated(true);
// 设置复制模式 游戏效果将如何复制到客户端 游戏效果仅复制到自己的客户端。游戏提示和游戏标签复制到所有客户端。
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
// 构造玩家状态类的属性集
AttributeSet = CreateDefaultSubobject<UAuraAttributeSet>("AttributeSet");
// 网络更新频率
// 这是服务器尝试更新客户端的频率。
// 当服务器上发生玩家状态的更改时,服务器将发送更新给所有客户端,以便它们可以与服务器版本同步。
// 任何应该复制的变量都会更新
// 通常玩家状态的净更新频率相当低,大约半秒。
// 如果我们要在玩家状态上设置我们的能力系统组件和属性,应该设置这个以更新得更快。
NetUpdateFrequency=100.f;
}
Source/Aura/Private/Character/AuraEnemy.cpp
AAuraEnemy::AAuraEnemy()
{
// 设置敌人基类的网格体组件的碰撞预设为 custom,检测响应-Visibility-阻挡,
// 使光标跟踪生效,因为光标跟踪Visibility通道。
// GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
// 构造敌人类的技能系统组件
AbilitySystemComponent = CreateDefaultSubobject<UAuraAbilitySystemComponent>("AbilitySystemComponent");
// 设置为网络复制
AbilitySystemComponent->SetIsReplicated(true);
// 设置复制模式 游戏效果不重复。游戏提示和游戏标签复制到所有客户端。
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
// 构造敌人类的属性集
AttributeSet = CreateDefaultSubobject<UAuraAttributeSet>("AttributeSet");
}
技能系统组件的所有者: 敌人类:明确知道是敌人类自身; 玩家状态类:玩家状态类只是构建了技能系统组件。
技能系统组件有了技能参与者信息的概念。 这样,技能系统组件就可以始终知道参与者信息,例如谁拥有该技能系统组件。 技能系统组件也理解它可能由某个参与者拥有的概念, 由某个Pawn拥有,或者它可以由另一种类型的实体(例如玩家状态)拥有。
所有者 Owner Actor 是实际拥有技能系统组件的任何类。 化身 Avatar Actor是与该技能系统组件相关的世界中的代表。
对于敌人角色来说,这两个是相同的,因为敌人角色类构建了技能系统组件。 所以Owner Actor是敌人角色,化身 Avatar Actor也是敌人角色。这就是视觉表现。
对于玩家控制的角色来说,这有点不同,因为技能系统组件由玩家状态构建,这就是我们希望将其视为技能系统组件所有者 Owner 的类。 所有者 Owner Actor 是玩家状态。 化身 Avatar Actor 是 所使用的 Actor,即在这个世界上看到的这个角色。
在这种情况下,Owner 、Actor和化身 Avatar Actor是两个不同的Actor。 技能系统组件区分这两者。
https://docs.unrealengine.com/5.3/en-US/API/Plugins/GameplayAbilities/UAbilitySystemComponent/InitAbilityActorInfo/ 初始化了Ababilities的ActorInfo——该结构包含有关我们对谁采取行动以及谁控制我们的信息。
virtual void InitAbilityActorInfo
(
[AActor](https://docs.unrealengine.com/5.3/en-US/API/Runtime/Engine/GameFramework/AActor) * InOwnerActor,
[AActor](https://docs.unrealengine.com/5.3/en-US/API/Runtime/Engine/GameFramework/AActor) * InAvatarActor
)
调用该函数来设置所有者 Owner 和化身 Avatar Actor。 它是技能系统组件上的一个函数,称为构造技能参与者信息InitAbilityActorInfo。
服务器: PossessedBy 必须在必须为 pawn 设置控制器之后进行。 对于一个玩家控制的角色,其中技能系统组件存在于 pawn 本身上,
客户端:AcknowledgePossession 在这个函数中我们知道占有已经发生。 我们的 pawn 确实有一个有效的控制器。 所以在这种情况下,如果我们将我们的技能系统组件放在Actor上,或者在我们的例子中是角色类上。
在这种情况下,Owner 和化身 Avatar是相同的。
对于玩家控制的角色,其中技能系统组件存在于玩家状态上,
服务器: PossessedBy
客户端:OnRep_PlayerState 因为技能系统组件依赖于玩家状态。 所以不仅需要确保控制器已经设置,还需要确保玩家状态在此时也是有效的。 此时将有一个有效的控制器和一个有效的玩家状态。 玩家状态将在服务器上设置,并且玩家状态是复制的实体,这意味着一旦为pawn 设置了该玩家状态,该状态就会被复制触发此代表通知,该通知将被调用以响应复制的发生。此时玩家状态已在服务器上设置并在客户端上复制下来并且是一个有效的指针。 因此,我们将在此处初始化技能系统组件的技能参与者信息。 把所有者 Owner actor 设置为玩家状态,将化身 Avatar actor 设置为 pawn,即玩家角色。
敌人角色类构建了技能系统组件。
服务器:BeginPlay 客户端:BeginPlay
Source/Aura/Public/Character/AuraEnemy.h
protected:
virtual void BeginPlay() override;
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::BeginPlay()
{
Super::BeginPlay();
// 初始技能参与者信息 服务器和客户端都在此设置
// 两者均为敌人类自身角色
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
Source/Aura/Public/Character/AuraCharacter.h
public:
virtual void PossessedBy(AController* NewController) override;
virtual void OnRep_PlayerState() override;
private:
void InitAbilityActorInfo();
Source/Aura/Private/Character/AuraCharacter.cpp
#include "AbilitySystemComponent.h"
void AAuraCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// 服务端 初始技能参与者信息
// Init ability actor info for the Server
InitAbilityActorInfo();
}
void AAuraCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
// 客户端 初始技能参与者信息
// Init ability actor info for the Client
InitAbilityActorInfo();
}
void AAuraCharacter::InitAbilityActorInfo()
{
// 获取玩家状态
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
if (AuraPlayerState == nullptr)return; // 不同模式下的玩家状态可访问时机不同/独立游戏/监听主机 多人服务端/多人客户端
// check(AuraPlayerState); //原课程代码
// 从玩家状态获取技能系统组件
// 然后初始技能参与者信息
// owner 为 玩家状态类,avatar 为当前类即玩家角色
AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState, this);
// 将 玩家状态上的 技能系统组件 和 属性集 拷贝到 角色类上,因为角色基类也有同样的变量需要构造
// 技能系统组件
AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
// 属性集
AttributeSet = AuraPlayerState->GetAttributeSet();
}
技能系统组件的 Owner Actor,所有者 Owner 必须是控制者Controller。 [玩家状态]玩家状态的所有者 Owner 会自动设置为控制器,因此不需要执行任何操作。 同样,如果技能系统组件的所有者 Owner Actor 是 pawn, Owner 拥有的 pawn将被设置为控制器。
但如果你的拥有者角色不是玩家状态,并且你正在使用混合复制模式,你必须在拥有者角色上调用SetOwner()
并将其拥有者设置为控制器。
当前项目不需要担心,因为我们的拥有者角色是玩家状态,玩家状态的拥有者已经被设置为控制器。
其他项目需要考虑你的拥有者角色可能是某个不会自动将其拥有者设置为控制器的类。 你可能在某个没有控制器的角色上有一个技能系统组件。 对于混合复制模式,技能系统组件的owner actor的Owner必须是控制器。【自身】
当我们在owner actor 的技能系统组件旁边构建属性集时, 属性集会自动注册到技能系统组件中。 可以有多个属性集。 技能系统组件可以访问注册的任何属性集。 可以根据类别将属性分布在多个不同的属性集中。 其中每一个都必须是独立的类。 同一类不能有多个属性集。否则,尝试从技能系统组件检索它时会出现歧义。 将所有属性包含在同一属性上是完全可以接受的, 这可以使事情变得更简单,特别是如果属性集进行计算时需要了解的其他属性。 属性占用的内存可以忽略不计,因此您甚至可以在所有类之间共享单个属性集。
属性是与游戏中给定实体(例如角色)相关的数值。 所有属性都是浮点数。 它们存在于称为游戏属性数据的结构中。 属性存储在属性集上,属性集对其进行密切监督。 我们可以知道属性何时发生变化,并使用我们喜欢的任何功能来响应它。
可以直接在代码中设置属性值,但更改它们的首选方法是应用游戏效果。 除了游戏效果的内置功能之外,使用它们的另一个原因是游戏玩法效果使我们能够预测属性的变化。 预测意味着客户端不需要等待服务器的许可来更改值。 该值可以在客户端立即更改,并且服务器会收到更改通知。 然后,服务器可以回滚任何它认为无效的更改。 这是一个巨大的好处,因为预测可能需要大量额外的工作来自己编程。 预测可以让多人游戏体验更加流畅。
向服务器发送一个请求,告诉它属性值需要更改,以便服务器接收。 该请求根据开发人员设置的任意数量的标准来决定该请求是否有效, 如果该更改被认为有效,服务器会将确认发送回客户端现在可以更改属性值。 由于数据在网络上传输需要时间,因此这会导致时间上的明显延迟。 客户端需要将该值更改为从服务器收到的实际许可的更改。 通过GAS预测,游戏效果会修改客户端的属性,并且可以立即在客户端上感知到该变化。 无滞后时间。 然后,该更改将发送到服务器,服务器仍然负责验证该更改。 如果服务器认为这是有效的更改。 它可以将更改通知其他客户端。 如果服务器确定更改无效,假设客户端破解了游戏并尝试造成不合常理的损害, 那么服务器可以拒绝该更改并回滚更改,设置客户端到正确的那个的值。 所以服务器仍然是权威的,但是我们的客户端不必有延迟。 预测很复杂,将其作为整个GAS的内置功能是一个巨大的好处。 让我们专注于创建游戏机制,而不用担心实施滞后补偿。
因此,属性是游戏属性数据类型的对象,并且存储在属性集中。
属性实际上由两个值组成:基值和当前值。 基值是属性的永久值。 当前值是基础值加上游戏效果造成的任何临时修改。 从 buff 和 debuff 的角度考虑一下,您可能在一段时间内会产生增加或减少值的效果,一旦该时间结束,该修改将被撤消,并且该属性返回到其基本值。 属性的最大值与属性本身是分开的。 如果最大值可以更改(这很常见),则最大值应该是其自己的单独属性。 这样我们就可以将游戏效果分别应用于属性本身或应用于最大属性。 例如,你的健康栏的百分比可以简单地是健康除以最大生命值的分数。
属性应为一个复制变量。 大多数属性都将被复制。 属性被复制到所有客户端。 如果服务器上的值发生变化,那么客户端将获得更新后的值。 如果通过游戏效果改变它,这些是可预测的属性。 在客户端本地更改,服务器将收到通知,以便服务器可以更改它。 一旦服务器上发生更改,所有其他客户端都需要知道该更改。
为了使变量被复制,用UPROPERTY(Replicated)说明符将其标记为已复制。
但对于属性,需要使用响应通知标记为已复制。ReplicatedUsing = OnRep_Health
。
当变量复制时,rep 通知会自动调用,因此当服务器复制时将变量发送给客户端,客户端将触发该变量的响应通知 OnRep_Health。
因此,需要一个响应健康的通知。
响应通知OnRep_Health可以接受 0-1个参数。参数必须是复制变量的类型。即游戏属性数据。
被调用以响应正在复制的运行状况,然后它将获取作为输入传入的旧值。
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Vital Attributes")
FGameplayAttributeData Health;
UFUNCTION()
void OnRep_Health(const FGameplayAttributeData& OldHealth) const;
为属性集中的复制属性设置响应通知时,必须通知那个变化的技能系统。 技能系统可以完成保留技能所需的所有幕后协同工作。 现在游戏技能系统将知道生命值刚刚被复制。 这负责通知技能系统我们正在复制一个值,它的值已经刚刚从服务器复制下来并进行了更改,现在技能系统可以注册该更改并保留跟踪其旧值,以防万一需要回滚任何内容。 在预测的情况下,如果服务器认为发生变化,则可以回滚更改并撤消它们。
void UAuraAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Health, OldHealth);
}
要将变量标记为已复制,需要类必须具有一个特定的函数才能注册变量以进行复制, 注册要复制的健康值 这是想要复制的任何内容所必需的。 COND_None 条件,表示 不为这个变量的复制设置任何条件,我们总是想复制它,无条件地复制 REPNOTIFY_Always 始终响应通知意味着如果在服务器上设置了该值,则复制它。在客户端上该值将被更新和设置。
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
}
还可以设置其他条件。 例如,如果您只想复制给所有者。
REPNOTIFY_OnChanged: 如果您在服务器上设置了运行状况值并且该值没有更改,则不会进行复制。
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AuraAttributeSet.generated.h"
UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UAuraAttributeSet();
// 要将变量标记为已复制,需要类必须具有一个特定的函数才能注册变量以进行复制,
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// 健康
// 为了使变量被复制,用UPROPERTY(Replicated)说明符将其标记为已复制。
// 当变量复制时 客户端将触发该变量的响应通知 OnRep_Health。
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Vital Attributes")
FGameplayAttributeData Health;
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxHealth, Category = "Vital Attributes")
FGameplayAttributeData MaxHealth;
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Mana, Category = "Vital Attributes")
FGameplayAttributeData Mana;
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxMana, Category = "Vital Attributes")
FGameplayAttributeData MaxMana;
// 响应通知OnRep_Health可以接受 0-1个参数。参数必须是复制变量的类型。即游戏属性数据。
UFUNCTION()
void OnRep_Health(const FGameplayAttributeData& OldHealth) const;
UFUNCTION()
void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth) const;
UFUNCTION()
void OnRep_Mana(const FGameplayAttributeData& OldMana) const;
UFUNCTION()
void OnRep_MaxMana(const FGameplayAttributeData& OldMaxMana) const;
};
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
#include "AbilitySystem/AuraAttributeSet.h"
#include "AbilitySystemComponent.h"
#include "Net/UnrealNetwork.h"
UAuraAttributeSet::UAuraAttributeSet()
{
}
void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 注册要复制的健康值 这是想要复制的任何内容所必需的。
// COND_None 条件,表示 不为这个变量的复制设置任何条件,我们总是想复制它,无条件地复制
// REPNOTIFY_Always 始终响应通知意味着如果在服务器上设置了该值,则复制它。在客户端上该值将被更新和设置。
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Mana, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxMana, COND_None, REPNOTIFY_Always);
}
void UAuraAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth) const
{
// 为属性集中的复制属性设置响应通知时,必须通知那个变化的技能系统。
// 技能系统可以完成保留技能所需的所有幕后协同工作。
// 现在游戏技能系统将知道生命值刚刚被复制。
// 这负责通知技能系统我们正在复制一个值,它的值已经刚刚从服务器复制下来并进行了更改,现在技能系统可以注册该更改并保留跟踪其旧值,以防万一需要回滚任何内容。
// 在预测的情况下,如果服务器认为发生变化,则可以回滚更改并撤消它们。
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Health, OldHealth);
}
void UAuraAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, MaxHealth, OldMaxHealth);
}
void UAuraAttributeSet::OnRep_Mana(const FGameplayAttributeData& OldMana) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Mana, OldMana);
}
void UAuraAttributeSet::OnRep_MaxMana(const FGameplayAttributeData& OldMaxMana) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, MaxMana, OldMaxMana);
}
属性访问器 依赖 AbilitySystemComponent.h 所以在头文件引入该依赖,在cpp删除该依赖。
属性访问器时Init,get,set的快捷宏。
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "AuraAttributeSet.generated.h"
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UAuraAttributeSet();
// 要将变量标记为已复制,需要类必须具有一个特定的函数才能注册变量以进行复制,
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// 健康
// 为了使变量被复制,用UPROPERTY(Replicated)说明符将其标记为已复制。
// 当变量复制时 客户端将触发该变量的响应通知 OnRep_Health。
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Vital Attributes")
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Health);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxHealth, Category = "Vital Attributes")
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, MaxHealth);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Mana, Category = "Vital Attributes")
FGameplayAttributeData Mana;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Mana);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxMana, Category = "Vital Attributes")
FGameplayAttributeData MaxMana;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, MaxMana);
// 响应通知OnRep_Health可以接受 0-1个参数。参数必须是复制变量的类型。即游戏属性数据。
UFUNCTION()
void OnRep_Health(const FGameplayAttributeData& OldHealth) const;
UFUNCTION()
void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth) const;
UFUNCTION()
void OnRep_Mana(const FGameplayAttributeData& OldMana) const;
UFUNCTION()
void OnRep_MaxMana(const FGameplayAttributeData& OldMaxMana) const;
};
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
UAuraAttributeSet::UAuraAttributeSet()
{
InitHealth(100.f);
InitMaxHealth(100.f);
InitMana(50.f);
InitMaxMana(50.f);
}
运行游戏,按~
波浪键唤起控制台,输入命令showdebug abilitysystem
这将打开技能系统的调试视图,其中包含大量信息。
可使用上下翻页键循环显示其他调试信息。
使用某种可以拾取的物体来影响属性。 更改属性的首选方法是通过游戏效果。 但是我们还没有学会如何创建和使用游戏效果。 因此,我们将创建一个直接更改属性的Actor以了解其局限性。
基于 Actor 类创建C++ 类 AuraEffectActor
Source/Aura/Public/Actor/AuraEffectActor.h
#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<USphereComponent> Sphere;
UPROPERTY(VisibleAnywhere)
TObjectPtr<UStaticMeshComponent> Mesh;
};
Source/Aura/Private/Actor/AuraEffectActor.cpp
#include "Actor/AuraEffectActor.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystemInterface.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "Components/SphereComponent.h"
AAuraEffectActor::AAuraEffectActor()
{
PrimaryActorTick.bCanEverTick = false;
Mesh = CreateDefaultSubobject<UStaticMeshComponent>("Mesh");
SetRootComponent(Mesh);
Sphere = CreateDefaultSubobject<USphereComponent>("Sphere");
Sphere->SetupAttachment(GetRootComponent());
}
void AAuraEffectActor::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// 更改此项以应用游戏效果。现在,使用const_cast作为破解!
//TODO: Change this to apply a Gameplay Effect. For now, using const_cast as a hack!
if (IAbilitySystemInterface* ASCInterface = Cast<IAbilitySystemInterface>(OtherActor))
{
// ASCInterface->GetAbilitySystemComponent() 获取技能系统组件
// UAuraAttributeSet::StaticClass() 属性集的静态类 ,TSubclassOf<UAttributeSet>
// ASCInterface->GetAbilitySystemComponent()->GetAttributeSet(UAuraAttributeSet::StaticClass()) 返回属性集类型的对象
// 然后转换为 UAuraAttributeSet 类型的属性集
const UAuraAttributeSet* AuraAttributeSet = Cast<UAuraAttributeSet>(ASCInterface->GetAbilitySystemComponent()->GetAttributeSet(UAuraAttributeSet::StaticClass()));
// 强制转换const类型为普通类型用以修改属性集
UAuraAttributeSet* MutableAuraAttributeSet = const_cast<UAuraAttributeSet*>(AuraAttributeSet);
// 不应该像这样直接在属性集上设置生命值。属性集应该设置自己的属性值,或者对游戏效果产生响应。
// 仅用于学习目的
MutableAuraAttributeSet->SetHealth(AuraAttributeSet->GetHealth() + 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);
}
在 内容-Blueprints 新建 目录 Actor, 基于C++类 AuraEffectActor 创建蓝图 BP_HealthPotion 打开 BP_HealthPotion mesh组件-细节-静态网格体-静态网格体-SM_PotionBottle mesh组件-细节-变换-缩放-0.2
Sphere组件-细节-形状-球体半径-225
将 BP_HealthPotion 拖入关卡测试重叠效果,
运行游戏,按~
波浪键唤起控制台,输入命令showdebug abilitysystem
人物移动到红药水处,健康值增大了25点:125
然后红药水消失。
控件组件 https://docs.unrealengine.com/5.3/zh-CN/widget-components-in-unreal-engine/
屏幕上显示的对象称为小控件Widgets。 在虚幻中,我们创建小控件蓝图来提供游戏数据的可视化表示。 继承自UUserWidget。 小控件对象可以通过多种方式设法深入游戏代码,检索角色控制器、玩家状态、技能系统组件的指针和引用,属性集并直接访问所需的所有数据。 但这不是最好的方法。
在一个结构良好的程序中,应该分离关注点。 用户界面有三个不同的域。
1-View 第一个领域是数据视觉效果的展示,例如生命条和能力图标。 基本上所有的小控件,我们将这个域称为视图,因为它包含玩家玩游戏时的视图的所有内容。
2-Model 数据本身 玩家的生命值,玩家的等级,法力经验,技能已被解锁,它们的级别是什么以及这些技能被分配到哪些按钮。 所有这些数据都存在于游戏项目本身的代码库中,我们将这些数据称为模型,因为它对游戏的基本规则和结果进行建模。 这些数据最终将驱动在视图域中看到的小控件。
3-Controller 从模型获取数据到视图是我们需要考虑的任务。 Controller 处理从模型检索数据并将其广播到视图。 该类不仅可以负责数据的检索,还可以负责任何计算或算法。处理该数据所涉及的功能。 控制器将负责从模型中检索数据并将其传递给视图,以便它可以直观地描述数据。 现在我们不是在讨论引擎中的控制器或玩家控制器类。 我们讨论的是一个控制器类,用于将数据驱动到视图。 称为小控件控制器。 这意味着视图可以简单地关注数据应该如何接收来自任何由控制器制成广播的数据。
视图可能包含玩家可以与之交互的小控件,例如按钮。 当玩家单击按钮时,该操作可能会导致模型发生一些变化,例如增加属性或赋予玩家新的技能。 因此,控制器决定小控件交互,所产生的玩家操作导致模型发生变化。 控制器是视图和模型之间的中间人。
这种软件架构模式的某些实现与其他模式不同。 有时小控件控制器只负责从模型中检索数据并呈现它到视图。
但对于我们的项目来说,小控件控制器将承担更多的责任。 它将处理按钮按下和玩家将提供的信息并实际发送信号到模型。 因此模型可以通过模型视图控制器架构进行更改。 每个域都与其他域隔离。这使得系统高度模块化。 它可以防止我们对依赖项进行硬编码,从而使系统变得僵化。 我们的模型不应该需要关心使用哪些控制器或小控件来表示他们的数据。 控制器本身依赖于模型中的类。 控制器永远不需要知道哪些小控件正在接收向它们广播的数据。 这是依赖于控制器的小控件。 如果我们维护这些单向依赖关系,那么模型就可以更改其小控件控制器用于其他小控件控制器,无需更改模型中类中的任何代码。
控制器不需要知道系统有哪些类型的小控件,因为小控件依赖于于控制器。 控制器也可以切换其小控件,而无需更改控制器中的任何代码。 控制器可以是一个简单的对象,UAuraWidgetController 我们将赋予它从系统收集数据的技能并将其广播到小控件。 我们将创建与我们的小控件控制器类兼容的用户小控件的子类。UAuraUserWidget
基于 UserWidget 创建 C++ AuraUserWidget 类
UserWidget :一个通过WidgetBlueprint实现UI可扩展性的控件。
Source/Aura/Public/UI/Widget/AuraUserWidget.h
基于 Object 创建 C++ AuraWidgetController
Source/Aura/Public/UI/Controller/AuraWidgetController.h
当控件控制器广播数据时,我们的控件将接收该数据并对其做出响应。 控件依赖控制器变量。 控件控制器不会知道它与哪些控件关联,但控件本身知道他们的控制器是什么。 控件控制器将需要从蓝图访问,因为所有的用户控件类基于该C++类创建。 我们将使其蓝图只读,以便我们只能访问但不能直接设置它。 用户控件负责的大部分内容是控件的视觉效果。 每当我们为给定的用户控件设置控件控制器时,我们都会想要初始化视觉效果。
Source/Aura/Public/UI/Widget/AuraUserWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "AuraUserWidget.generated.h"
UCLASS()
class AURA_API UAuraUserWidget : public UUserWidget
{
GENERATED_BODY()
public:
// 一个可调用蓝图的新函数,以便我们可以从蓝图设置控件控制器
UFUNCTION(BlueprintCallable)
void SetWidgetController(UObject* InWidgetController);
// 控件控制器不会知道它与哪些控件关联,但控件本身知道他们的控制器是什么。
// 使其蓝图只读,以便我们只能访问但不能直接设置它。
UPROPERTY(BlueprintReadOnly)
TObjectPtr<UObject> WidgetController;
protected:
// 一个蓝图可实现事件
// 每当我们为给定的控件设置控件控制器时,我们也会调用此函数
UFUNCTION(BlueprintImplementableEvent)
void WidgetControllerSet();
};
Source/Aura/Private/UI/Widget/AuraUserWidget.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "UI/Widget/AuraUserWidget.h"
void UAuraUserWidget::SetWidgetController(UObject* InWidgetController)
{
WidgetController = InWidgetController;
WidgetControllerSet();
}
Source/Aura/Public/UI/Controller/AuraWidgetController.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "AuraWidgetController.generated.h"
class UAttributeSet;
class UAbilitySystemComponent;
UCLASS()
class AURA_API UAuraWidgetController : public UObject
{
GENERATED_BODY()
protected:
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<APlayerController> PlayerController;
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<APlayerState> PlayerState;
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<UAttributeSet> AttributeSet;
};
Source/Aura/Private/UI/Controller/AuraWidgetController.cpp
#include "UI/Controller/AuraWidgetController.h"
Blueprints 下新建UI目录/ProgressBar目录 右键-用户界面-控件蓝图-AuraUserWidget :WBP_GlobeProgressBar
一个控件,允许使用者指定其报告拥有和所需的大小。并非所有控件报告的所需大小均为用户实际所需的大小。将其包装在一个SizeBox中后,大小框会将其强制设为一个特定大小。 *单一子项,固定大小
打开 BP_GlobeProgressBar 控件蓝图 添加 面板-尺寸框 SIze Box ,可填满屏幕
右上角选择-所需 在细节面板调整尺寸框宽高
如果我们希望能够在子类中覆盖它的宽高,那就为宽度和高度创建变量
左侧重命名 尺寸框为 SIzeBox_Root
点击右上方-图表 进入事件图表
仅客户端 由游戏和编辑器调用。允许使用者对其控件进行初始设置,以更好地在设计器中预览设置;因为运行时通常需要相同的设置代码,其也将会被调用。
警告: 这纯粹只用于使用本地拥有数据的装饰更新,使用者无法安全访问游戏相关的状态;如果调用不应在编辑器时间中运行的内容,编辑器可能崩溃 将蓝图代码保存到资产,如果计算时出现崩溃,可在编辑器偏好设置”的“控件设计器”设置中关闭PreConstruct计算。 目标是用户控件 Cosmetic. This event is only for cosmetic, non-gameplay actions.
图表界面-左侧-变量 添加浮点类型变量 BoxWidth :尺寸框宽度 BoxHeight :尺寸框高度
编译,然后为变量设置默认值 细节-高级-默认值-BoxWidth-250 细节-高级-默认值-BoxHeight-250
可以在 Event pre construct 事件中设置变量调整尺寸框宽高
设计器界面-选择控件层级、 右上方-细节-SIzeBox_Root-是变量-启用 此时 尺寸框 SIzeBox_Root 控件将可当作变量使用和设置
图表 界面-拖入 尺寸框 SIzeBox_Root 控件,从SIzeBox_Root输出 布局-尺寸框-Set Width Override 函数节点用以设置宽度。
添加 BoxWidth 变量,输出至 Set Width Override 的 In Width Override 节点,以设置宽度的值。
拖入 尺寸框 SIzeBox_Root 控件,从SIzeBox_Root输出 布局-尺寸框-Set Height Override 函数节点用以设置宽度。
选择所有节点-右键-折叠到函数-生成新的折叠函数节点-重命名为 Update Box Size 节点将作为函数变量
Event pre construct 事件更改宽高将实时影响 图表中控件的宽高
控件可互相重叠在彼此之上,使用每个图层内容的简单流布局。
设计器面板-添加 面板-覆层 放置到 SIzeBox_Root 控件的子级
重命名为:Overlay_Root
之后可以在 Overlay_Root 之上放置控件。
允许显示Slate笔刷、或UI中纹理/材质的图像控件 *无子项
添加 通用-图像 控件 到 Overlay_Root 子级 名称:Image_Background
调整 Image_Background 填充满 Overlay_Root 作为背景图片。
细节-插槽(覆层插槽)-填充-水平对齐-水平填充 细节-插槽(覆层插槽)-填充-垂直对齐-垂直填充
选择 Image_Background,标记为 变量
图表-为 Image_Background 添加变量 BackgroundBrush 类型:Slate Brush / Slate 笔刷 用来设置图片纹理资源
图表界面-BoxWidth-细节-类别-GlobeProperties [球体属性]
添加 节点: Image_Background BackgroundBrush Set Bursh
BackgroundBrush 节点 -细节-默认值-Background Brush-图像-GlobeRing
添加 通用-Progress Bar 进度条 控件 到Overlay_Root 子级 名称:ProgressBar_Globe
设置以填充整个 Overlay_Root
Progress Bar -细节-样式-样式-填充图-图像-MI_HealthGlobe 细节-样式-样式-填充图-绘制为-图像 【否则铺满界面为方形】 细节-样式-样式-进度-百分比-1 外观-填充颜色和不透明度-1,1,1,1 白色
去掉灰色背景: 细节-样式-样式-背景图-着色-不透明度为0 即 :1,1,1,0
进度条方向-由下向上: 细节-进度-条填充类型-底到顶
设置 Progress Bar 为变量
添加节点: ProgressBar_Globe 变量-样式-Set Style 从 Set Style 的 样式节点拖出 Make ProgressBarStyle
从 Make ProgressBarStyle 的 Background Image 节点拖出 Make SlateBrush 节点 设置背景 从 Make SlateBrush 的 Tint 节点 拖出 Make SlateColor 设置背景颜色 Make SlateColor 设置颜色为1,1,1,0 不透明度为0。即背景透明。
添加变量 ProgressBarFillImage 类型: Slate Brush / Slate 笔刷 类别:GlobeProperties ProgressBarFillImage 输出至 Make ProgressBarStyle 的 Fill Image 节点,用来设置进度条图片材质 编译蓝图 选择 ProgressBarFillImage 节点 -细节-默认值-Progress Bar Fill Image -图像-MI_HealthGlobe
当前节点部分折叠到函数 UpdateGlobeImage
图表: 拖入变量 ProgressBar_Globe 从 节点 ProgressBar_Globe 拖出 Slot as overlay slot 从 Slot as overlay slot 节点 拖出 Set Padding 函数节点 从 Set Padding 的 In Padding 节点 拖出 Make Margin 从 Make Margin 的Left 节点拖出 promote to variable 提升为变量 Left 变量 Left 重命名为 GlobePadding 类别:GlobeProperties 默认值:10
GlobePadding 节点输出至Make Margin的Left,Top,Right,Bottom
折叠到函数 UpdateGlobePadding
设计器: 细节-插槽--填充-10 使金属圈边缘显示
以上只是配置控件外观,非运行时设置。
设计器: 添加Image图片控件到Overlay_Root 子级 名称:Image_Glass 填充整个Overlay_Root区域 设为变量
图表: 添加 Slate 笔刷/Slate Brush 类型变量:GlassBrush 类别:GlobeProperties GlassBrush -细节-默认值-Glass Brush -图像-MI_EmptyGlobe
添加节点: Image_Glass 从 Image_Glass 拖出 外观-Set Brush 函数 Glass Brush 变量节点
折叠到函数 UpdateGlassBrush
设计器: Image_Glass 控件的 外观-笔刷-图像属性已设置为MI_EmptyGlobe
细节-插槽-填充-10
图表: 添加节点: Image_Glass Image_Glass 节点拖出 Slot as overlay slot Slot as overlay slot 节点拖出 Set Padding 函数节点 Set Padding 的 In Padding 节点拖出 Make Margin 添加节点 Globe Padding 输出至 Make Margin 上下左右节点。
折叠到函数 UpdateGlassPadding
右键-用户界面-控件蓝图 选择 WBP_GlobeProgressBar
打开 WBP_HealthGlobe 图表-显示继承的变量
变量 ProgressBarFillImage-细节-默认值-Progress Bar Fill Image-图像-MI_HealthGlobe
UI 目录下新建Overlay 目录 基于 AuraUserWidget 新建控件蓝图 WBP_Overlay WBP_Overlay 是屏幕上的整体控件蓝图,包含多个控件。
打开 WBP_Overlay 添加控件 canvas 面板-画布画板。
尺寸框与覆层效率更高。 画布画板比较消耗性能。 所以只会为整个应用程序提供一个整体覆盖画布画板控件。
添加控件 用户创建-WBP_HealthGlobe WBP_HealthGlobe-细节-插槽-锚点-中间底部 使WBP_HealthGlobe的锚点 对齐到画布画板的中间底部 WBP_HealthGlobe 相对 锚点缩放。
打开关卡蓝图 事件图表-添加节点 用户界面-create widget class 选择 WBP_Overlay
从 create widget 的 输出节点拖出 add to viewport 这会把 WBP_Overlay 控件添加到游戏视图上
运行游戏,可看到健康进度球。与视图大小一起缩放。
打开 WBP_HealthGlobe 图表-显示继承的变量 变量 ProgressBarFillImage-细节-默认值-Progress Bar Fill Image-图像-MI_ManaGlobe
打开 WBP_Overlay 添加控件 WBP_ManaGlobe 到 画布画板直接子级
WBP_ManaGlobe-细节-插槽-锚点-中间底部 WBP_ManaGlobe-细节-插槽-尺寸X,尺寸Y-250
将 WBP_ManaGlobe ,WBP_HealthGlobe 的位置Y设置一样的值
不使用关卡蓝图添加 控件到视图。 删除 关卡蓝图事件图表的控件节点。 通过GameMode中设置HUD来添加控件到视图。
蓝图中设置 OverlayWidgetClass,然后C++将其添加到视口。
Source/Aura/Public/UI/HUD/AuraHUD.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "AuraHUD.generated.h"
class UAuraUserWidget;
UCLASS()
class AURA_API AAuraHUD : public AHUD
{
GENERATED_BODY()
public:
UPROPERTY()
TObjectPtr<UAuraUserWidget> OverlayWidget;
protected:
virtual void BeginPlay() override;
private:
UPROPERTY(EditAnywhere)
TSubclassOf<UAuraUserWidget> OverlayWidgetClass;
};
Source/Aura/Private/UI/HUD/AuraHUD.cpp
#include "UI/HUD/AuraHUD.h"
#include "UI/Widget/AuraUserWidget.h"
void AAuraHUD::BeginPlay()
{
Super::BeginPlay();
UUserWidget* Widget = CreateWidget<UUserWidget>(GetWorld(), OverlayWidgetClass);
Widget->AddToViewport();
}
在 Blueprints/UI 目录下新建目录 HUD
打开 BP_AuraHUD 细节-Aura HUD-Overlay Widget Class-WBP_Overlay
打开 BP_AuraGameMode 细节-类-HUD类-BP_AuraHUD
运行游戏
控件依赖控件控制器 为了方便设置玩家控制器,玩家状态,技能系统组件,属性集,创建包含4个属性的结构体 FWidgetControllerParams
Source/Aura/Public/UI/Controller/AuraWidgetController.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "UObject/NoExportTypes.h"
#include "AuraWidgetController.generated.h"
class UAttributeSet;
class UAbilitySystemComponent;
// 为了方便设置玩家控制器,玩家状态,技能系统组件,属性集,创建包含4个属性的结构体
USTRUCT(BlueprintType)
struct FWidgetControllerParams
{
GENERATED_BODY()
FWidgetControllerParams() {}
FWidgetControllerParams(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
: PlayerController(PC), PlayerState(PS), AbilitySystemComponent(ASC), AttributeSet(AS) {}
// 结构体属性必须初始化 这里初始化为 nullptr
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<APlayerController> PlayerController = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<APlayerState> PlayerState = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<UAttributeSet> AttributeSet = nullptr;
};
UCLASS()
class AURA_API UAuraWidgetController : public UObject
{
GENERATED_BODY()
public:
// 将在蓝图中设置属性
UFUNCTION(BlueprintCallable)
void SetWidgetControllerParams(const FWidgetControllerParams& WCParams);
protected:
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<APlayerController> PlayerController;
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<APlayerState> PlayerState;
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<UAttributeSet> AttributeSet;
};
Source/Aura/Private/UI/Controller/AuraWidgetController.cpp
#include "UI/Controller/AuraWidgetController.h"
void UAuraWidgetController::SetWidgetControllerParams(const FWidgetControllerParams& WCParams)
{
PlayerController = WCParams.PlayerController;
PlayerState = WCParams.PlayerState;
AbilitySystemComponent = WCParams.AbilitySystemComponent;
AttributeSet = WCParams.AttributeSet;
}
Source/Aura/Public/UI/Controller/OverlayWidgetController.h
#pragma once
#include "CoreMinimal.h"
#include "UI/WidgetController/AuraWidgetController.h"
#include "OverlayWidgetController.generated.h"
UCLASS()
class AURA_API UOverlayWidgetController : public UAuraWidgetController
{
GENERATED_BODY()
};
Source/Aura/Private/UI/Controller/OverlayWidgetController.cpp
#include "UI/WidgetController/OverlayWidgetController.h"
游戏中只有一个 OverlayWidgetController /单例。 将之前的额UI下的 Controller 目录重命名为 WidgetController 控件控制器 需要 参数 玩家控制器,玩家状态,技能系统组件,属性集。
游戏开始时,begin paly 时,还不能访问 玩家控制器,玩家状态,技能系统组件,属性集。 必须在可以访问的地方使用这些创建控件控制器。 所以在自定义方法 InitOverlay 中设置。
Source/Aura/Public/UI/HUD/AuraHUD.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "AuraHUD.generated.h"
class UAttributeSet;
class UAbilitySystemComponent;
class UOverlayWidgetController;
class UAuraUserWidget;
struct FWidgetControllerParams;
UCLASS()
class AURA_API AAuraHUD : public AHUD
{
GENERATED_BODY()
public:
UPROPERTY()
TObjectPtr<UAuraUserWidget> OverlayWidget;
// 游戏中只有一个 OverlayWidgetController /单例
UOverlayWidgetController* GetOverlayWidgetController(const FWidgetControllerParams& WCParams);
void InitOverlay(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS);
protected:
private:
// 在蓝图子类中指定
UPROPERTY(EditAnywhere)
TSubclassOf<UAuraUserWidget> OverlayWidgetClass;
UPROPERTY()
TObjectPtr<UOverlayWidgetController> OverlayWidgetController;
// 在蓝图子类中指定
UPROPERTY(EditAnywhere)
TSubclassOf<UOverlayWidgetController> OverlayWidgetControllerClass;
};
Source/Aura/Private/UI/HUD/AuraHUD.cpp
#include "UI/HUD/AuraHUD.h"
#include "UI/Widget/AuraUserWidget.h"
#include "UI/WidgetController/OverlayWidgetController.h"
UOverlayWidgetController* AAuraHUD::GetOverlayWidgetController(const FWidgetControllerParams& WCParams)
{
// 游戏中只有一个 OverlayWidgetController /单例
if (OverlayWidgetController == nullptr)
{
OverlayWidgetController = NewObject<UOverlayWidgetController>(this, OverlayWidgetControllerClass);
OverlayWidgetController->SetWidgetControllerParams(WCParams);
return OverlayWidgetController;
}
return OverlayWidgetController;
}
void AAuraHUD::InitOverlay(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
{
// checkf 检查并打印数据到日志文件
// 蓝图子类中需要覆盖设置 控件类和控件控制器类
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);
OverlayWidget = Cast<UAuraUserWidget>(Widget);
// 游戏开始时,begin paly 时,还不能访问 玩家控制器,玩家状态,技能系统组件,属性集。
// 所以在自定义方法 InitOverlay 中设置。
const FWidgetControllerParams WidgetControllerParams(PC, PS, ASC, AS);
UOverlayWidgetController* WidgetController = GetOverlayWidgetController(WidgetControllerParams);
// 为覆盖控件设置控件控制器
OverlayWidget->SetWidgetController(WidgetController);
// 将覆盖控件添加到视图
Widget->AddToViewport();
}
InitAbilityActorInfo 初始化技能组件Actor信息的时候 已经可以访问 玩家控制器,玩家状态,技能系统组件,属性集。
在多人游戏,只有服务端的玩家控制器有效, 服务器拥有所有玩家的玩家控制器,但每个玩家只有自己的玩家控制器。 在控制该特定角色的客户端机器上,该玩家控制器是有效的。 但是该客户端计算机上非本地控制的其他角色没有有效的玩家控制器。 例如,在三人游戏中,如果您是客户端,则您的玩家控制器有效, 但在你的机器上,另外两个角色,这两个副本没有有效的玩家控制器和初始化能力演员信息。 在这种情况下,将调用此函数InitAbilityActorInfo,并且/或玩家控制器将是空指针。 在这种情况下,对于此功能或玩家控制器,在多人游戏中可以为空, 我们只想在它不为空时继续执行。 所以这种情况使用if检查【为空是合理的,只要不继续执行】。不使程序崩溃。 否则使用check断言,程序崩溃。【游戏前置条件不能继续执行】
Source/Aura/Private/Character/AuraCharacter.cpp
#include "Player/AuraPlayerController.h"
#include "UI/HUD/AuraHUD.h"
#include "Character/AuraCharacter.h"
#include "AbilitySystemComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Player/AuraPlayerController.h"
#include "Player/AuraPlayerState.h"
#include "UI/HUD/AuraHUD.h"
AAuraCharacter::AAuraCharacter()
{
// 获取角色运动组件
// 启用:方向旋转到运动
GetCharacterMovement()->bOrientRotationToMovement = true;
// 可以通过获取角色移动旋转速率来控制旋转速率。
// 角色就会以这个速度400,在偏航旋转方向上运动,角色运动可以迫使我们将运动限制在一个平面上。
// yaw():航向,将物体绕Y轴旋转
GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f);
// 角色被捕捉到平面
GetCharacterMovement()->bConstrainToPlane = true;
// 在开始时捕捉到平面
GetCharacterMovement()->bSnapToPlaneAtStart = true;
// 角色本身不应该使用控制器的旋转
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
bUseControllerRotationYaw = false;
}
void AAuraCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// 服务端 初始技能参与者信息
// Init ability actor info for the Server
InitAbilityActorInfo();
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(2, 1.F, FColor::Cyan, FString("AAuraCharacter::PossessedBy-AuraPlayerState"));
}
}
void AAuraCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
// 客户端 初始技能参与者信息
// Init ability actor info for the Client
InitAbilityActorInfo();
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(1, 1.F, FColor::Cyan,
FString("AAuraCharacter::OnRep_PlayerState-AuraPlayerState"));
}
}
void AAuraCharacter::InitAbilityActorInfo()
{
// 获取玩家状态
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
if (AuraPlayerState == nullptr)return;
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(1, 1.F, FColor::Cyan, FString("AuraPlayerState"));
}
// check(AuraPlayerState);
// 从玩家状态获取技能系统组件
// 然后初始技能参与者信息
// owner 为 玩家状态类,avatar 为当前类即玩家角色
AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState, this);
// 将 玩家状态上的 技能系统组件 和 属性集 拷贝到 角色类上,因为角色基类也有同样的变量需要构造
// 技能系统组件
AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
// 属性集
AttributeSet = AuraPlayerState->GetAttributeSet();
// 初始化并添加覆盖控件,覆盖控件控制器
// 在多人游戏,只有服务端的玩家控制器有效,
// 服务器拥有所有玩家的玩家控制器,但每个玩家只有自己的玩家控制器。
// 在控制该特定角色的客户端机器上,该玩家控制器是有效的。
// 但是该客户端计算机上非本地控制的其他角色没有有效的玩家控制器。
// 例如,在三人游戏中,如果您是客户端,则您的玩家控制器有效,
// 但在你的机器上,另外两个角色,这两个副本没有有效的玩家控制器和初始化能力演员信息。
// 在这种情况下,将调用此函数InitAbilityActorInfo,并且/或玩家控制器将是空指针。
// 在这种情况下,对于此功能或玩家控制器,在多人游戏中可以为空,
// 我们只想在它不为空时继续执行。
// 所以这种情况使用if检查【为空是合理的,只要不继续执行】。不使程序崩溃。
// 否则使用check断言,程序崩溃。【游戏前置条件不能继续执行】
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
{
if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(AuraPlayerController->GetHUD()))
{
AuraHUD->InitOverlay(AuraPlayerController, AuraPlayerState, AbilitySystemComponent, AttributeSet);
}
}
}
Source/Aura/Private/Player/AuraPlayerController.cpp
中
AAuraPlayerController::BeginPlay()
只有本地玩家能获得有效的子系统 Subsystem, 应该使用if 而非 check.
所以修改 其中部分代码为
// check(Subsystem);
if (Subsystem)
{
//添加输入映射情景,可以同时有多个输入映射情景
//当前只添加一个输入映射情景,优先级为0.
Subsystem->AddMappingContext(AuraContext, 0);
}
打开 BP_AuraHUD 蓝图 细节-Aura HUD-Overlay Widget Controller Class-OverlayWidgetController OverlayWidgetController 为C++类,临时设置,之后将设置为蓝图类。
运行游戏 ,覆盖控件可以显示
打开 控件 WBP_Overlay 图表: 添加事件:Event Widget Controller Set 添加节点: Widget Controller 从 Widget Controller 拖出节点 Get Object Name [访问控件控制器实例的名称] Get Object Name 输出到 Print String 的 In String 节点 运行游戏将显示控件控制器实例名称,这在控件控制器被设置时触发。
删除该测试节点。
现在,控件控制器可以访问 玩家控制器,玩家状态,技能系统组件,属性集。
Source/Aura/Public/Player/AuraPlayerController.h
// 帧更新之前光标跟踪的Actor
TObjectPtr<IEnemyInterface> LastActor;
// 光标跟踪的当前Actor
TObjectPtr<IEnemyInterface> ThisActor;
https://docs.unrealengine.com/5.3/zh-CN/delegates-and-lamba-functions-in-unreal-engine/
委托 是一种泛型但类型安全的方式,可在C++对象上调用成员函数。可使用委托动态绑定到任意对象的成员函数,之后在该对象上调用函数,即使调用程序不知对象类型也可进行操作。复制委托对象很安全。你也可以利用值传递委托,但这样操作需要在堆上分配内存,因此通常并不推荐。请尽量通过引用传递委托。虚幻引擎共支持三种类型的委托:
单点委托
组播委托/多播委托/Multi-cast Delegates
动态委托/Dynamic Delegates(UObject, serializable)
声明委托: 如需声明委托,请使用下文所述的宏。请根据与委托相绑定的函数(或多个函数)的函数签名来选择宏。每个宏都为新的委托类型名称、函数返回类型(如果不是 void 函数)及其参数提供了参数。当前,支持以下使用任意组合的委托签名:
返回一个值的函数。 声明为 常 函数。 最多4个"载荷"变量。 最多8个函数参数。
使用此表格查找要用于声明委托的生命宏:
函数签名 | 声明宏 |
---|---|
void Function() | DECLARE_DELEGATE(DelegateName) |
void Function(Param1) | DECLARE_DELEGATE_OneParam(DelegateName, Param1Type) |
void Function(Param1, Param2) | DECLARE_DELEGATE_TwoParams(DelegateName, Param1Type, Param2Type) |
void Function(Param1, Param2, ...) | DECLAREDELEGATE |
DECLARE_DELEGATE_RetVal(RetValType, DelegateName) | |
DECLARE_DELEGATE_RetVal_OneParam(RetValType, DelegateName, Param1Type) | |
DECLARE_DELEGATE_RetVal_TwoParams(RetValType, DelegateName, Param1Type, Param2Type) | |
DECLARE_DELEGATERetVal |
委托函数支持与UFunctions相同的说明符,但使用 UDELEGATE 宏而不是 UFUNCTION。例如,以下代码将 BlueprintAuthorityOnly 说明符添加到 FInstigatedAnyDamageSignature 委托中
UDELEGATE(BlueprintAuthorityOnly)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FInstigatedAnyDamageSignature, float, Damage, const UDamageType*, DamageType, AActor*, DamagedActor, AActor*, DamageCauser);
关于组播委托、动态委托和封装委托,上述宏的变体如下:
DECLARE_MULTICAST_DELEGATE...
DECLARE_DYNAMIC_DELEGATE...
DECLARE_DYNAMIC_MULTICAST_DELEGATE...
DECLARE_DYNAMIC_DELEGATE...
DECLARE_DYNAMIC_MULTICAST_DELEGATE...
委托签名声明可存在于全局范围内、命名空间内、甚至类声明内。此类声明可能不在于函数体内。
https://docs.unrealengine.com/5.3/zh-CN/multicast-delegates-in-unreal-engine/ 可以绑定到多个函数并一次性同时执行它们的委托。
多播委托拥有大部分与单播委托相同的功能。它们只拥有对对象的弱引用,可以与结构体一起使用,可以四处轻松复制等等。 就像常规委托一样,多播委托可以远程加载/保存和触发;但多播委托函数不能使用返回值。它们最适合用来 四处轻松传递一组委托。
https://docs.unrealengine.com/5.3/zh-CN/dynamic-delegates-in-unreal-engine/ 可序列化且支持反射的委托
动态委托可序列化,其函数可按命名查找,但其执行速度比常规委托慢。
控件控制器不知道控件,但可以向控件广播数据。例如广播初始健康值,魔力值。
Source/Aura/Public/UI/WidgetController/AuraWidgetController.h
public:
virtual void BroadcastInitialValues();
Source/Aura/Private/UI/WidgetController/AuraWidgetController.cpp
void UAuraWidgetController::BroadcastInitialValues()
{
}
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
#pragma once
#include "CoreMinimal.h"
#include "UI/WidgetController/AuraWidgetController.h"
#include "OverlayWidgetController.generated.h"
// 参数1:委托类型 FOnHealtChangedSignature
// 参数2:发送的数据类型
// 参数3:通过动态多播委托 发送一个值:健康值
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealtChangedSignature, float, NewHealth);
// 通过动态多播委托 发送一个值:最大健康值
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMaxHealtChangedSignature, float, NewMaxHealth);
/**
*
*/
UCLASS(BlueprintType, Blueprintable)
class AURA_API UOverlayWidgetController : public UAuraWidgetController
{
GENERATED_BODY()
public:
virtual void BroadcastInitialValues() override;
// 蓝图子类通过访问控件控制器,分配事件来接受健康值
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnHealtChangedSignature OnHealthChanged;
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnMaxHealtChangedSignature OnMaxHealthChanged;
};
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
#include "UI/WidgetController/OverlayWidgetController.h"
#include "AbilitySystem/AuraAttributeSet.h"
void UOverlayWidgetController::BroadcastInitialValues()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
OnHealthChanged.Broadcast(AuraAttributeSet->GetHealth());
OnMaxHealthChanged.Broadcast(AuraAttributeSet->GetMaxHealth());
}
Source/Aura/Private/UI/HUD/AuraHUD.cpp
void AAuraHUD::InitOverlay(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
{
// checkf 检查并打印数据到日志文件
// 蓝图子类中需要覆盖设置 控件类和控件控制器类
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);
OverlayWidget = Cast<UAuraUserWidget>(Widget);
// 游戏开始时,begin paly 时,还不能访问 玩家控制器,玩家状态,技能系统组件,属性集。
// 所以在自定义方法 InitOverlay 中设置。
const FWidgetControllerParams WidgetControllerParams(PC, PS, ASC, AS);
UOverlayWidgetController* WidgetController = GetOverlayWidgetController(WidgetControllerParams);
// 为覆盖控件设置控件控制器
OverlayWidget->SetWidgetController(WidgetController);
// 蓝图子类就可以接受并响应该广播的值
// 在控件控制器设置好后,广播初始值
WidgetController->BroadcastInitialValues();
// 将覆盖控件添加到视图
Widget->AddToViewport();
}
设计器: 将控件 WBP_HealthGlobe ,WBP_ManaGlobe 设置为变量。
图表: 添加事件:Event Widget Controller Set 添加节点: WBP_HealthGlobe 从 WBP_HealthGlobe 拖出 Set Widget Controller
WBP_ManaGlobe 从 WBP_ManaGlobe 拖出 Set Widget Controller
添加 Widget Controller 输出到 WBP_HealthGlobe-Set Widget Controller 的 In Widget Controller 节点 添加 Widget Controller 输出到 WBP_ManaGlobe -Set Widget Controller 的 In Widget Controller 节点
蓝图的Set Widget Controller 将被 父类 AruaHUD - InitOverlay()-OverlayWidget->SetWidgetController(WidgetController); 触发以设置好控件控制器。
C++ OverlayWidget->SetWidgetController 对应 蓝图事件 Event Widget Controller Set。
基于C++ OverlayWidgetController ,在 Blueprints/UI/WIdgetController 目录下新建 蓝图类 BP_OverlayWidgetController
打开 BP_OverlayWidgetController
打开 BP_AuraHUD 细节-Aura HUD-Overlay Widget Controller Class-BP_OverlayWidgetController
图表: 添加事件:Event Widget Controller Set 添加节点: Widget Controller //由于覆层已设置控件控制器,所以这里空间控制器已经可用。覆层先执行。 Widget Controller 拖出 cast to BP_OverlayWidgetController
此时 WBP_HealthGlobe 获取了控件控制器,可以通过事件接受控件控制器广播的健康值。
从 cast to BP_OverlayWidgetController 的输出拖出 assign on health changed
再从 cast to BP_OverlayWidgetController 的输出拖出 assign on max health changed
根据广播的值,设置健康百分比。 将从 on health changed 接受的 New Health 输出提升为变量:Health 将从 on max health changed接受的 New Max Health 输出提升为变量:MaxHealth
设计器的 控件由父类 WBP_GlobeProgressBar 控制,在 WBP_HealthGlobe 上无法更改百分比进度。
图表: 左侧添加函数:SetProgressBarPercent 选择 函数节点 SetProgressBarPercent 细节-输入-添加一个浮点输入 Percent 拖入变量 ProgressBar_Globe 从 Progress Bar Globe 拖出 Set Percent 将 SetProgressBarPercent 的 Percent 节点输入至 Set Percent 的 In Percent 节点
使用广播接受的值设置百分比
添加节点: Set Progress Bar Percent 函数节点 Health 变量 MaxHealth 变量 Safe Divide [相除的两个数如果有0则返回0] 从事件拖出 流程控制-Sequence
将 cast to BP_OverlayWidgetController 的输出提升为变量 BPOverlayWidgetController
每当设置 Health,MaxHealth时,都会重新设置百分比。
运行游戏,健康值百分比为100.
GetGameplayAttributeValueChangeDelegate()
返回游戏属性更改这个多播委托,非动态。
所以不能使用 AddDynamic
只能使用 AddUObject 将回调绑定到此多播委托
当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(
AuraAttributeSet->GetHealthAttribute()).AddUObject(this, &UOverlayWidgetController::HealthChanged);
Source/Aura/Public/UI/WidgetController/AuraWidgetController.h
public:
virtual void BindCallbacksToDependencies();
Source/Aura/Private/UI/WidgetController/AuraWidgetController.cpp
void UAuraWidgetController::BindCallbacksToDependencies()
{
}
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
public:
virtual void BindCallbacksToDependencies() override;
protected:
void HealthChanged(const FOnAttributeChangeData& Data) const;
void MaxHealthChanged(const FOnAttributeChangeData& Data) const;
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
#include "UI/WidgetController/OverlayWidgetController.h"
#include "AbilitySystem/AuraAttributeSet.h"
void UOverlayWidgetController::BroadcastInitialValues()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
OnHealthChanged.Broadcast(AuraAttributeSet->GetHealth());
OnMaxHealthChanged.Broadcast(AuraAttributeSet->GetMaxHealth());
}
void UOverlayWidgetController::BindCallbacksToDependencies()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性更改多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(
AuraAttributeSet->GetHealthAttribute()).AddUObject(this, &UOverlayWidgetController::HealthChanged);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(
AuraAttributeSet->GetMaxHealthAttribute()).AddUObject(this, &UOverlayWidgetController::MaxHealthChanged);
}
void UOverlayWidgetController::HealthChanged(const FOnAttributeChangeData& Data) const
{
OnHealthChanged.Broadcast(Data.NewValue);
}
void UOverlayWidgetController::MaxHealthChanged(const FOnAttributeChangeData& Data) const
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
Source/Aura/Private/UI/HUD/AuraHUD.cpp
UOverlayWidgetController* AAuraHUD::GetOverlayWidgetController(const FWidgetControllerParams& WCParams)
{
// 游戏中只有一个 OverlayWidgetController /单例
if (OverlayWidgetController == nullptr)
{
OverlayWidgetController = NewObject<UOverlayWidgetController>(this, OverlayWidgetControllerClass);
OverlayWidgetController->SetWidgetControllerParams(WCParams);
// 此时已设置完成控件控制器的各项参数,包括属性集,可以将属性集的变更回调绑定。多播委托。
OverlayWidgetController->BindCallbacksToDependencies();
return OverlayWidgetController;
}
return OverlayWidgetController;
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
UAuraAttributeSet::UAuraAttributeSet()
{
InitHealth(50.f);
InitMaxHealth(100.f);
InitMana(50.f);
InitMaxMana(50.f);
}
技能系统组件不依赖控件控制器。 控件控制器不依赖控件。
Source/Aura/Private/Actor/AuraEffectActor.cpp
void AAuraEffectActor::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// 更改此项以应用游戏效果。现在,使用const_cast作为破解!
//TODO: Change this to apply a Gameplay Effect. For now, using const_cast as a hack!
if (IAbilitySystemInterface* ASCInterface = Cast<IAbilitySystemInterface>(OtherActor))
{
// ASCInterface->GetAbilitySystemComponent() 获取技能系统组件
// UAuraAttributeSet::StaticClass() 属性集的静态类 ,TSubclassOf<UAttributeSet>
// ASCInterface->GetAbilitySystemComponent()->GetAttributeSet(UAuraAttributeSet::StaticClass()) 返回属性集类型的对象
// 然后转换为 UAuraAttributeSet 类型的属性集
const UAuraAttributeSet* AuraAttributeSet = Cast<UAuraAttributeSet>(ASCInterface->GetAbilitySystemComponent()->GetAttributeSet(UAuraAttributeSet::StaticClass()));
// 强制转换const类型为普通类型用以修改属性集
UAuraAttributeSet* MutableAuraAttributeSet = const_cast<UAuraAttributeSet*>(AuraAttributeSet);
// 不应该像这样直接在属性集上设置生命值。属性集应该设置自己的属性值,或者对游戏效果产生响应。
// 仅用于学习目的
MutableAuraAttributeSet->SetHealth(AuraAttributeSet->GetHealth() + 25.f);
MutableAuraAttributeSet->SetMana(AuraAttributeSet->GetMana() - 25.f);
Destroy();
}
}
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnManaChangedSignature, float, NewMana);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMaxManaChangedSignature, float, NewMaxMana);
public:
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnManaChangedSignature OnManaChanged;
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnMaxManaChangedSignature OnMaxManaChanged;
protected:
void ManaChanged(const FOnAttributeChangeData& Data) const;
void MaxManaChanged(const FOnAttributeChangeData& Data) const;
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
#include "UI/WidgetController/OverlayWidgetController.h"
#include "AbilitySystem/AuraAttributeSet.h"
void UOverlayWidgetController::BroadcastInitialValues()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
OnHealthChanged.Broadcast(AuraAttributeSet->GetHealth());
OnMaxHealthChanged.Broadcast(AuraAttributeSet->GetMaxHealth());
OnManaChanged.Broadcast(AuraAttributeSet->GetMana());
OnMaxManaChanged.Broadcast(AuraAttributeSet->GetMaxMana());
}
void UOverlayWidgetController::BindCallbacksToDependencies()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性更改多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
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);
}
void UOverlayWidgetController::HealthChanged(const FOnAttributeChangeData& Data) const
{
OnHealthChanged.Broadcast(Data.NewValue);
}
void UOverlayWidgetController::MaxHealthChanged(const FOnAttributeChangeData& Data) const
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
void UOverlayWidgetController::ManaChanged(const FOnAttributeChangeData& Data) const
{
OnManaChanged.Broadcast(Data.NewValue);
}
void UOverlayWidgetController::MaxManaChanged(const FOnAttributeChangeData& Data) const
{
OnMaxManaChanged.Broadcast(Data.NewValue);
}
打开 WBP_ManaGlobe 与WBP_HealthGlobe节点相似。
添加事件:Event Widget Controller Set 从事件拖出 流程控制-Sequence
添加节点: Widget Controller //由于覆层已设置控件控制器,所以这里空间控制器已经可用。覆层先执行。 Widget Controller 拖出 cast to BP_OverlayWidgetController 将 cast to BP_OverlayWidgetController 的输出提升为变量 BPOverlayWidgetController
此时 WBP_ManaGlobe 获取了控件控制器,可以通过事件接受控件控制器广播的健康值。
添加变量 BPOverlayWidgetController 从 BPOverlayWidgetController 拖出 assign on mana changed
添加变量 BPOverlayWidgetController 从 BPOverlayWidgetController 拖出 assign on max mana changed
根据广播的值,设置健康百分比。 将从 on mana changed 接受的 New mana 输出提升为变量:Mana 将从 on max mana changed接受的 New mana 输出提升为变量:MaxMana
添加节点: Set Progress Bar Percent 函数节点 Mana 变量 MaxMana 变量 Safe Divide [相除的两个数如果有0则返回0]
拾取红药水时,健康增加25点,魔力减少25点。
Gameplay技能系统会利用 Gameplay效果 更改Gameplay技能所针对Actor的属性。Gameplay效果包含你可以应用到Actor属性的函数库。这些效果可以是即时效果,比如施加伤害,也可以是持续效果,比如毒杀,在一定的时间内对角色造成伤害。
你可以使用Gameplay技能效果进行增益和减益。
根据你的游戏设计,让你的角色变得更强或更弱。
Gameplay效果属于资产,因此在运行时不可变。
也有例外情况,例如,当在运行时创建Gameplay效果,但未在创建和配置时修改数据的情况。
Gameplay效果规格 是Gameplay效果的运行时版本。
它们是Gameplay效果的实例数据封装器(Gameplay效果是一种资产)。 蓝图功能本身与Gameplay效果规格相关,而不是与Gameplay效果相关,这体现在技能系统蓝图库中。
Gameplay效果规格会添加到Actor的技能系统组件。
这适用于持续效果,并且可以设置为在失效前具有有限的生命周期,然后删除效果,撤销对目标Actor Gameplay属性 的更改。
Gameplay效果的 时长(Duration) 可设置为 即时(Instant) 、 无限(Infinite) 或 有持续时间(Has Duration) 。 具有持续时间的Gameplay效果将添加到 激活Gameplay效果容器(Active Gameplay Effects Container) 。激活Gameplay效果容器是技能系统组件的一部分。
即时的Gameplay效果声明为"已执行(Executed)"。 它永远不会进入激活Gameplay效果容器。
对于即时和持续时间两者均适用的情况,使用的术语是"已施加(Applied)"。 例如,方法 CanApplyGameplayEffect 不会考虑是即时还是有持续时间。
周期性效果在每个周期执行;因此,同时为"已添加(Added)"和"已执行(Executed)"。
有一种例外情况:预测客户端的Gameplay效果(当客户端先于服务器时)。 在这种情况下,系统会预测持续效果并等待服务器确认。
下表列出了你可以调整的Gameplay效果的属性。
属性 | 说明 |
---|---|
持续时间(Duration) | Gameplay效果可以立即应用(比如受到攻击时生命值减少),在有限的持续时间内应用(比如持续几秒的移速提升),或无限应用(比如角色随着时间的推移自然再生魔法值)。具有非即时持续时间的效果本身能以不同的时间间隔应用。对于Gameplay和影音效果的时机而言,这种间隔可能会改变效果的运行方式。 |
组件(Components) | 定义Gameplay效果如何呈现的Gameplay效果组件。如需可用组件的完整列表,请参阅[#Gameplay效果组件] |
修饰符(Modifiers) | 决定Gameplay效果如何与属性交互的修饰符。其中包括与属性本身的数学交互,例如将护甲等级属性按其基础值的5%提升,并包括执行该效果的Gameplay标签要求。 |
执行(Executions) | 使用UGameplayEffectExecutionCalculation定义Gameplay效果执行时的自定义行为。对于定义修饰符未充分覆盖的复杂方程来说,执行尤其有用。 |
Gameplay提示(Gameplay Cues) | Gameplay提示是一种管理装饰效果(如粒子或声音)的网络高效方式,你可以使用Gameplay技能系统进行控制。Gameplay技能和Gameplay效果可以触发Gameplay提示。Gameplay提示通过四个主要函数起作用,这些函数可以在原生代码或蓝图代码中重载:On ActiveWhile ActiveRemovedExecuted(仅通过Gameplay效果使用)。所有Gameplay提示必须与以 GameplayCue 开头的Gameplay标签关联,如 GameplayCue.ElectricalSparks 或 GameplayCue.WaterSplash.Big 。 |
堆叠(Stacking) | 堆叠是指将增益或减益(或Gameplay效果)应用于已携带效果的目标的策略。堆叠还涵盖处理溢出,其中新Gameplay效果应用于原始Gameplay效果已经完全饱和的目标(例如,不断累积的毒药计,仅在溢出后产生一定时间内的毒药伤害)。该系统支持各种堆叠行为,如:累积效果直到突破阈值。维持"堆叠计数",该计数随着每个新应用程序增加,直至达到最大限值。重置或附加限时效果的时间。使用单独的计时器独立应用效果的多个实例。 |
Gameplay效果包含用于确定Gameplay效果如何呈现的 Gameplay效果组件 (GEComponents)。 Gameplay效果可以:
更改对其应用Gameplay效果的Actor的Gameplay标签,或根据条件删除其他激活的Gameplay效果。
你可以创建自己的游戏才有的Gameplay效果组件,组件有助于扩展Gameplay效果的可用性。 Gameplay效果组件的实现者必须仔细阅读Gameplay效果流程,并注册全部所需的回调以实现所需效果,而非提供适用于所有所需功能的更大型API。这样会将Gameplay效果组件的实现限制到原生代码。
GEComponents存在于Gameplay效果中,Gameplay效果是一种仅限数据的蓝图资产。因此,和Gameplay效果一样,所有应用实例仅存在一个GEComponent。
下表包含完整的可用Gameplay效果组件列表:
Gameplay效果组件 | 说明 |
---|---|
UChanceToApplyGameplayEffectComponent | 应用Gameplay效果的概率。 |
UBlockAbilityTagsGameplayEffectComponent | 根据所有者Gameplay效果目标Actor的Gameplay标签,进行Gameplay技能激活阻止处理。 |
UAssetTagsGameplayEffectComponent | Gameplay效果资产拥有的标签。这些标签 不会 转移到Actor。 |
UAdditionalEffectsGameplayEffectComponent | 添加尝试在特定条件下激活(或任何条件下都不激活)的其他Gameplay效果。 |
UTargetTagsGameplayEffectComponent | 将标签授予Gameplay效果的目标(有时指所有者)。 |
UTargetTagRequirementsGameplayEffectComponent | 指定如果此GE须应用或继续执行,目标(Gameplay效果的拥有者)必须具备的标签要求。 |
URemoveOtherGameplayEffectComponent | 基于某些条件移除其他Gameplay效果。 |
UCustomCanApplyGameplayEffectComponent | 处理CustomApplicationRequirement函数的配置,以查看是否应该应用此Gameplay效果。 |
UImmunityGameplayEffectComponent | 免疫会阻止其他GameplayEffectSpecs的应用。 |
Gameplay属性(Gameplay Attribute) 包含Actor当前状态的测量值,当前状态可通过单浮点值描述,如:
生命值
物理力量
移速
魔抗
等等。属性在属性集中声明为FGameplayAttribute类型的UProperties,属性集包含各种属性并监督所有对属性进行修改的尝试。
属性和属性集必须以原生代码创建,无法在蓝图中创建。
遵照以下步骤创建属性集
从UAttributeSet继承类,然后添加标记为UPROPERTY的Gameplay属性数据成员。例如,属性集仅包含类似如下的"生命值"属性:
创建属性集后,你必须通过技能系统组件注册属性集。你可以将属性集添加为技能系统组件拥有Actor的子对象,或将其传递给技能系统组件的GetOrCreateAttributeSubobject函数。
可以重载属性集的函数有多个,这些函数可用于处理Gameplay效果尝试修改属性时属性的响应方式。例如,示例USimpleAttributeSet中的"生命值"属性可以存储浮点值,该值可以通过Gameplay技能系统访问或更改。目前,当生命值降至零后,实际上什么也不会发生,也没有什么会阻止其下降至零以下。
要使"生命值"属性以你想要的方式呈现,属性集本身可通过重载多个虚拟函数来介入,虚拟函数负责处理对其属性的修改尝试。
以下函数通常由属性集重载:
函数名称 | 用途 |
---|---|
PreAttributeChange / PreAttributeBaseChange | 这些函数在即将修改属性之前调用。函数旨在实施关于属性值的规则,例如,"生命值必须介于0和最大生命值"之间,并且不得对属性更改触发游戏内响应。 |
PreGameplayEffectExecute | 在即将修改属性值之前,此函数可以拒绝或更改拟定修改。 |
PostGameplayEffectExecute | 在修改属性值后,此函数可立即对更改做出响应。这通常包括限制属性的最终值或触发对新值的游戏内响应,例如当"生命值"属性降至零时死亡。 |
游戏效果是 UGameplayEffect 类型 的对象。 我们使用游戏效果来改变属性和游戏标签。 游戏效果仅是数据。我们不给它们添加逻辑。
游戏效果通过修改器和执行来改变属性。
效果 Actor 的外观和重叠事件 ,例如红药水将在蓝图中配置,而不在C++中。 这使其更加通用。
Source/Aura/Public/Actor/AuraEffectActor.h
#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();
protected:
virtual void BeginPlay() override;
};
Source/Aura/Private/Actor/AuraEffectActor.cpp
#include "Actor/AuraEffectActor.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
AAuraEffectActor::AAuraEffectActor()
{
PrimaryActorTick.bCanEverTick = false;
SetRootComponent(CreateDefaultSubobject<USceneComponent>("SceneRoot"));
}
void AAuraEffectActor::BeginPlay()
{
Super::BeginPlay();
}
打开 BP_HealthPotion
PotionMesh-细节-静态网格体-静态网格体-SM_PotionBottle PotionMesh-细节-变换-缩放-0.2 不缩放根组件。
PotionMesh-细节-碰撞-碰撞预设-NoCollision 防止网格体与其他物体碰撞。
Sphere-细节-碰撞-碰撞预设-使用默认 OverlapAllDynamic
选择组件 Sphere 添加事件:On Component Begin Overlap (Sphere)
只要AuraEffectActor的子类指定某种游戏效果,就可以将游戏效果应用于具由技能系统组件的目标【例如玩家角色】 需要在蓝图子类中通过重叠事件,使用 ApplyEffectToTarget 对玩家应用游戏效果。
Source/Aura/Public/Actor/AuraEffectActor.h
#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();
protected:
virtual void BeginPlay() override;
// 应用效果到actor
UFUNCTION(BlueprintCallable)
void ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass);
// 即时游戏效果类 在蓝图中指定
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
TSubclassOf<UGameplayEffect> InstantGameplayEffectClass;
};
Source/Aura/Private/Actor/AuraEffectActor.cpp
#include "Actor/AuraEffectActor.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
AAuraEffectActor::AAuraEffectActor()
{
PrimaryActorTick.bCanEverTick = false;
SetRootComponent(CreateDefaultSubobject<USceneComponent>("SceneRoot"));
}
void AAuraEffectActor::BeginPlay()
{
Super::BeginPlay();
}
void AAuraEffectActor::ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass)
{
// 获取Target【例如玩家角色】的技能系统组件
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
// 如果目标【例如玩家角色】没有技能系统组件 则什么都不做
// 例如红药水与玩家重叠
if (TargetASC == nullptr) return;
// 游戏效果类必须有效,无论目标是否具由技能系统组件。否则崩溃
check(GameplayEffectClass);
// 制作游戏效果情景句柄【轻量级指针】,与游戏效果相关的东西,包含 背景,效果目标,谁造成的效果,效果是什么
// 句柄是一个轻量级包装器,它将实际效果上下文存储为指针。
// 它有能力清除该指针。有办法获取影响上下文的任何游戏标签它有很多实用程序
FGameplayEffectContextHandle EffectContextHandle = TargetASC->MakeEffectContext();
// 添加导致此游戏效果的来源【例如红药水】
EffectContextHandle.AddSourceObject(this);
// 制作游戏效果规范句柄
// 参数1:效果类
// 参数2:效果等级
// 参数3: 效果情景
const FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(
GameplayEffectClass, 1.f, EffectContextHandle);
// 应用游戏效果规格句柄的数据【游戏效果】到Target自身【例如玩家角色自身】
// 参数2:预测,补偿
// Get 返回原始指针
// * 星号取消这个原始指针 获取游戏效果
TargetASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
UAuraAttributeSet::UAuraAttributeSet()
{
InitHealth(50.f);
InitMaxHealth(100.f);
InitMana(10.f);
InitMaxMana(50.f);
}
将 BP_HealthPotion 移动到目录 Blueprints/Actor/Potion
导航到 目录 Blueprints/Actor/Potion
打开 游戏效果 GE_PotionHeal 细节-持续时间-Duration Policy-instant 即时
细节-Gameplay Effect-Modifiers-添加一组修改属性的方式 效果可以影响多个属性
细节-Gameplay Effect-Modifiers-索引0-Attribute-AuraAttributeSet.Health 属性集的健康属性 细节-Gameplay Effect-Modifiers-索引0-Modifier Op-Add 表示增加健康值 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type -scalable float 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-scalable float Magnitude-25
当我们应用此效果时,我们将获取生命值,为其添加 25 的值,效果将立即应用。
打开 BP_HealthPotion
事件图表: 添加 ApplyEffectToTarget 函数节点 添加节点: Instant Gameplay Effect Class 【继承自AuraEffectActor】 Instant Gameplay Effect Class 输出至 ApplyEffectToTarget 的 Gameplay Effect Class 节点
On Component Begin Overlap (Sphere) 的 other actor 输出至 ApplyEffectToTarget 的 Target Actor 节点 Destroy Actor
选择 Instant Gameplay Effect Class 节点 细节-默认值-Instant Gameplay Effect Class-GE_PotionHeal 游戏效果
与红药水重叠的玩家被应用游戏效果后销毁红药水自身。 玩家健康值增加25点。
打开 GE_PotionMana
打开 BP_ManaPotion 细节-Instant Gameplay Effect Class-GE_PotionMana 游戏效果
SM_ManaPotionBottle 使用材质实例 MI_BlueLiquid
PotionMesh-细节-静态网格体-静态网格体-SM_ManaPotionBottle PotionMesh-细节-变换-缩放-0.2 不缩放根组件。
PotionMesh-细节-碰撞-碰撞预设-NoCollision 【可以省略这个设置】 防止网格体与其他物体碰撞。
Box-细节-碰撞-碰撞预设-使用默认 OverlapAllDynamic
选择组件 Box 添加事件:On Component Begin Overlap (Box)
添加 ApplyEffectToTarget 函数节点 添加节点: Instant Gameplay Effect Class 【继承自AuraEffectActor】 Instant Gameplay Effect Class 输出至 ApplyEffectToTarget 的 Gameplay Effect Class 节点
On Component Begin Overlap (Box) 的 other actor 输出至 ApplyEffectToTarget 的 Target Actor 节点 Destroy Actor
与魔力药水重叠的玩家被应用游戏效果后销毁红魔力药水自身。 玩家魔力值增加30点。
持续时间结束后效果自动移除或撤销。
Source/Aura/Public/Actor/AuraEffectActor.h
protected:
// 持续时间游戏效果 在蓝图中指定
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
TSubclassOf<UGameplayEffect> DurationGameplayEffectClass;
打开 GE_CrystalHeal
细节-持续时间-Duration Policy-Has Duration 有持续时间 细节-持续时间-Duration Magnitude-Magnitude calculation Type-scalable float 细节-持续时间-Duration Magnitude-Scalable Float Magnitude-2 【该效果持续2秒,然后自行消失】
细节-Gameplay Effect-Modifiers-添加一组修改属性的方式 效果可以影响多个属性
细节-Gameplay Effect-Modifiers-索引0-Attribute-AuraAttributeSet.MaxHealth 属性集的最大健康属性 细节-Gameplay Effect-Modifiers-索引0-Modifier Op-Add 表示增加健康值 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type -Scalable Float 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-100 [最大健康值将增加100,最终为200]
打开 BP_HealthCrystal 细节-Duration Gameplay Effect Class-GE_CrystalHeal 游戏效果
CrystalMesh-细节-静态网格体-静态网格体-SM_HealthCrystal CrystalMesh-细节-变换-缩放-0.2 不缩放根组件。
CrystalMesh-细节-碰撞-碰撞预设-NoCollision 【可以省略这个设置】 防止网格体与其他物体碰撞。
Capsule-细节-形状-胶囊体半高-24 Capsule-细节-形状-半径-14 Capsule-细节-碰撞-碰撞预设-使用默认 OverlapAllDynamic
进入顶视图移动 静态网格体
选择组件 Capsule 添加事件:On Component Begin Overlap (Capsule)
添加 ApplyEffectToTarget 函数节点 添加节点: Duration Gameplay Effect Class 【继承自AuraEffectActor】 Duration Gameplay Effect Class 输出至 ApplyEffectToTarget 的 Gameplay Effect Class 节点
On Component Begin Overlap (Capsule) 的 other actor 输出至 ApplyEffectToTarget 的 Target Actor 节点 Destroy Actor
与健康水晶重叠的玩家被应用游戏效果2秒后销毁健康水晶自身。 玩家最大健康值增加100至200,持续2秒。 2秒后玩家最大健康值恢复为原来的100. 仅为演示持续时间效果。
周期性效果在每个周期执行。 定期执行某种更改。
游戏属性具由 BaseValue 基础值 和 Current Value 当前值。
即时效果似乎是永久性的,这是因为即时效果会影响具有永久变化的基值。 当我们应用即时效果时,我们不会将该更改应用于当前值,因为我们以后不要打算撤消它。
这与持续时间效果和无限效果的工作方式不同。 持续时间效果和无限效果会修改Current Value 当前值。如果效果被移除,则可以撤消该值。
基于持续时间的效果会自行消失。 设置持续时间两秒,两秒后效果就会被删除,这意味着2秒后撤消对属性的更改。 无限效果的行为类似。唯一的区别是它不会自行移除。如果您想删除它们,则必须手动执行此操作。
持续时间和无限效果可以转换为周期性效果,这意味着它们执行定期对属性进行某种更改,只要该效果当前有效。 周期性的游戏效果是一种持续时间效果和无限效果的特殊类型, 但是周期性的变化执行时被视为即时游戏效果,并且永久更改基本值。不可以撤消。
如果设置为周期性的持续时间效果或无限游戏效果被删除,所有改变都会生效。 每个周期发生的改变都是永久性的。当效果被移除时,它们不会被撤销。
可以将任何持续时间效果或无限游戏效果转换为周期性游戏效果, 只需将其周期更改为非零值即可。
打开 GE_CrystalHeal
细节-持续时间-Period-Period 周期 0 表示这不是周期性效果。 1 表示每一秒应用一次效果。 设置为正数后,将出现更多选项:
细节-持续时间-Period-Execute Periodic Effect on Application 对应用程序执行周期性效果- 如果为true,则在应用程序上立即执行效果,修改器会立即,然后在每个周期间隔执行效果。 如果为false,则在第一个周期过去之前不会应用效果,必须等待一个周期才能应用效果。//EditCondition in FGameplayEfectDetails
细节-持续时间-Period-Periodic Inhibition Policy 周期抑制策略 当一个周期性的游戏效果不再被抑制时,我们应该如何应对//在FGameplayEffectDetails中的EditCondition
细节-持续时间-Duration Policy-Has Duration 有持续时间 细节-持续时间-Duration Magnitude-Magnitude calculation Type-scalable float 细节-持续时间-Duration Magnitude-Scalable Float Magnitude-2 【该周期性效果持续2秒,然后自行消失】
细节-Gameplay Effect-Modifiers-索引0-Attribute-AuraAttributeSet.Health 属性集的健康属性 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-10 [健康值将增加10]
每1秒应用一次效果,即健康值增加10点。周期效果持续2秒。即在2秒持续时间内会定期增加健康值。每一秒增加一次直到2秒后。
初始健康值50; 运行游戏后,人物与健康水晶重叠,健康立即增加一次到60. 然后经过1秒增加10,为70. 再经过1秒增加10,为80. 2秒的持续时间结束,周期效果完成。不再增加健康值。 健康值也不会撤销,会固定在80。 health的变化:50-60-70-80.
如果 细节-持续时间-Duration Policy-Instant 即时,将不可以在 细节-持续时间-Period-Period 设置周期。 只有持续效果或无线效果才可以设置为周期性效果。
打开 GE_CrystalMana 细节-持续时间-Duration Policy-Has Duration 有持续时间 细节-持续时间-Duration Magnitude-Magnitude calculation Type-scalable float 细节-持续时间-Duration Magnitude-Scalable Float Magnitude-3 【该效果持续3秒,然后自行消失】
细节-持续时间-Period-Period 周期-0.1 细节-持续时间-Period-Execute Periodic Effect on Application 对应用程序执行周期性效果-启用
细节-Gameplay Effect-Modifiers-添加一组修改属性的方式 效果可以影响多个属性
细节-Gameplay Effect-Modifiers-索引0-Attribute-AuraAttributeSet.Mana 属性集的魔力属性 细节-Gameplay Effect-Modifiers-索引0-Modifier Op-Add 表示增加魔力值 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type -Scalable Float 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 [魔力值将增加1]
在 3秒的持续时间内,每隔0.1秒魔力值增加1,并在开始时立即增加1。3秒后不再增加。
打开 BP_ManaCrystal 细节-Duration Gameplay Effect Class-GE_CrystalMana 游戏效果
CrystalMesh-细节-静态网格体-静态网格体-SM_Shard_debris CrystalMesh-细节-变换-缩放-0.2 不缩放根组件。
CrystalMesh-细节-碰撞-碰撞预设-NoCollision 【可以省略这个设置】 防止网格体与其他物体碰撞。
Sphere-细节-碰撞-碰撞预设-使用默认 OverlapAllDynamic
进入顶视图移动 静态网格体
选择组件 Sphere 添加事件:On Component Begin Overlap (Sphere)
添加 ApplyEffectToTarget 函数节点 添加节点: Duration Gameplay Effect Class 【继承自AuraEffectActor】 Duration Gameplay Effect Class 输出至 ApplyEffectToTarget 的 Gameplay Effect Class 节点
On Component Begin Overlap (Sphere) 的 other actor 输出至 ApplyEffectToTarget 的 Target Actor 节点 Destroy Actor
魔力值在3秒内 将从 10 增加到41后停止。魔力水晶消失。
打开 GE_CrystalMana 魔力水晶 游戏效果 细节-Stacking-Stacking Type-None: 将 3个 BP_ManaCrystal 魔力水晶放入关卡并互相靠近。 与角色重叠时,角色魔力快速增加。但这不是真的堆叠。 这只是应用了3次游戏效果。效果相互独立。 3个是1个的3被效果。
假设我们想拾取一颗水晶,而该水晶的持续时间为两秒。 如果在那两秒钟内我们拿起另一个水晶怎么办? 我们不希望我们的法力以两倍的速度增长。 我们仍然希望以相同的速度增加法力,但我们希望刷新该持续时间。
假设我们拿起第一个水晶,一秒钟后,我们拿起第二个水晶。 也许我们只是想刷新持续时间,这样我们的法力就能再增加两秒。
将3个魔力水晶堆叠,使玩家同时接触3个。
细节-持续时间-Period-Period 周期-0.1 细节-持续时间-Duration Magnitude-Scalable Float Magnitude-1 【持续时间改为1秒。】 细节-持续时间-Period-Execute Periodic Effect on Application 对应用程序执行周期性效果-禁用
这意味着在一秒钟内我们将执行修饰符十次, 每次法力值增加了 1 点,即每0.1秒增加1,法力值在一秒的时间内总共增加了 10 点法力值.
来源实际上指的是导致这种情况的技能系统组件。 玩家的技能系统组件存在与玩家状态,敌人的存在于敌人。各自只有一个。 在我们的例子中,无论我们选择源还是目标,我们都会得到堆栈限制2。魔力只能增加20. 每个来源限制2个同时生效,当前只有一个来源【玩家状态】 如果玩家同时接触3个。则只有2个生效,一共增加20点魔力。
Stacking-Stacking Type 堆叠类型-Aggregate by Source 按来源汇总 Stack Limit Count 堆栈限制计数-2 Stack Duration Refresh Policy 堆栈持续时间刷新策路-Refresh on Successful Application 应用程序成功时剧新 Stack Period Reset Policy 堆核周期重置策略-Reset on Successful Application 应用程序成功后重置 Stack Expiration Policy 堆栈到期策略-Clear Entire Stack 清除整个堆栈
每个游戏效果都有自己的堆叠。
堆栈限制计数是按源强制执行的。 如果我们有一个游戏效果源并且它将游戏效果应用于目标,我们将其算作一个堆栈游戏效果并在源上进行聚合。 因此,源现在具有应用于目标的此游戏效果的一个堆栈计数。 现在,相同的来源再次应用相同的游戏效果,将其视为第二个堆栈,并且由源聚合。 现在,有两层游戏效果,这就是极限。因为此效果的堆栈限制计数设置为 2。 如果相同的源应用相同的效果,这将再次是第三个堆栈,但这超出了极限。 所以这个效果不能应用。 现在我们按源聚合,这意味着每个源都会保留该特定堆栈的计数源造成的影响。
Stacking-Stacking Type 堆叠类型-Aggregate by Source 按目标汇总 Stack Limit Count 堆栈限制计数-2 Stack Duration Refresh Policy 堆栈持续时间刷新策路-Refresh on Successful Application 应用程序成功时剧新 Stack Period Reset Policy 堆核周期重置策略-Reset on Successful Application 应用程序成功后重置 Stack Expiration Policy 堆栈到期策略-Clear Entire Stack 清除整个堆栈
每个目标都有自己的堆叠
每个目标限制2个同时生效,当前只有一个目标【玩家】 如果玩家同时接触3个。则只有2个生效,一共增加20点魔力。
Stack Duration Refresh Policy 堆栈持续时间刷新策路-Refresh on Successful Application 应用程序成功时剧新 开始接触1个水晶,0.2秒后,同时接触2个水晶,刷新持续时间,重新开始计时。 1秒后,第一个水晶持续时间结束。计数=1,第三个水晶开始生效,但自身持续时间仅剩0.2秒。所以10+10+2=22. 共增加22.
Stack Duration Refresh Policy 堆栈持续时间刷新策路-Necer Refresh 永不刷新。
开始接触1个水晶,计数=1. 0.2秒后,同时接触2个水晶。计数=2.持续时间已过0.2秒。 1秒后,第一个水晶持续时间结束。第二个水晶只生效了0.8秒,第三个不会生效。 共增加18.
Stack Period Reset Policy 堆核周期重置策略-Reset on Successful Application 应用程序成功后重置
Never Reset 从不重置
Stack Expiration Policy 堆栈到期策略-Clear Entire Stack 清除整个堆栈 一旦该效果到期,所有堆栈都会被清除。堆栈计数清零。
Remove Single Stack and Refresh Duration 删除单个堆栈并刷新持续时间 随着持续时间结束后,我们删除一个堆栈,然后持续时间重新开始。 因此,您可以叠加其中两个效果,并且当持续时间到期时,叠加计数会消失减至一,然后重新开始计时。
Refresh Duration 刷新持续时间 这本质上使效果的持续时间无限,因为它不断刷新。
Stacking-Stacking Type 堆叠类型-Aggregate by Source 按目标汇总 Stack Limit Count 堆栈限制计数-2 Stack Duration Refresh Policy 堆栈持续时间刷新策路-Refresh on Successful Application 应用程序成功时剧新 Stack Period Reset Policy 堆核周期重置策略-Reset on Successful Application 应用程序成功后重置 Stack Expiration Policy 堆栈到期策略-Remove Single Stack and Refresh Duration 删除单个堆栈并刷新持续时间 当我们拾取越来越多的晶体时,就会延长持续时间,获得更多法力。 在1秒内可以捡起4个并且都生效。增加46点。因为之前的水晶效果被之后的延长了。
Stacking-Stacking Type 堆叠类型-Aggregate by Target 按目标汇总 Stack Limit Count 堆栈限制计数-1 Stack Duration Refresh Policy 堆栈持续时间刷新策路-Refresh on Successful Application 应用程序成功时剧新 Stack Period Reset Policy 堆核周期重置策略-Reset on Successful Application 应用程序成功后重置 Stack Expiration Policy 堆栈到期策略-Clear Entire Stack 清除整个堆栈
Source/Aura/Public/Actor/AuraEffectActor.h
protected:
// 无限时间游戏效果 在蓝图中指定
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
TSubclassOf<UGameplayEffect> InfiniteGameplayEffectClass;
在Blueprints/Actor/Area目录下: 基于 AuraEffectActor 新建蓝图 BP_FireArea
打开 BP_FireArea 添加 Box Collision 盒子组件 Box 到 Root组件下。
添加 Niagara 粒子系统组件到 Root组件下。 Niagara 粒子系统组件-细节-Niagara-Niagara系统资产-NS_Fire
当角色与火焰区域重叠时,应用无限游戏效果。
打开 GE_FireArea 细节-持续时间-Duration Policy-Infinite 无限
细节-持续时间-Period-Period 周期-1
细节-Gameplay Effect-Modifiers-添加一组修改属性的方式 细节-Gameplay Effect-Modifiers-索引0-Attribute-AuraAttributeSet.Health 细节-Gameplay Effect-Modifiers-索引0-Modifier Op-Add 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type -Scalable Float 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude- -5 减去5点健康
BP_FireArea-细节-Infinite Gameplay Effect Class-GE_FireArea
选择组件 Box 添加事件:On Component Begin Overlap (Box)
添加 ApplyEffectToTarget 函数节点 添加节点: Infinite Gameplay Effect Class 【继承自AuraEffectActor】 Infinite Gameplay Effect Class 输出至 ApplyEffectToTarget 的 Gameplay Effect Class 节点
On Component Begin Overlap (Box) 的 other actor 输出至 ApplyEffectToTarget 的 Target Actor 节点
当玩家与火焰区域重叠时,健康值会无限减少。 但玩家离开火焰区域依然在减少健康值。 必须手动删该无限效果。
一旦您应用了游戏效果,该游戏效果就会变为活动状态,并且这些应用功能返回该效果的句柄。 所以我们以后总是可以使用该句柄,例如如果它是无限时间游戏效果,则将其效果删除。
需要小心我们这样做的方式。 可能有多个参与者重叠。 其中一些可能有技能系统组件,有些可能没有。 我们不能仅仅假设目标 actor 和重叠是任何给定效果句柄的正确 actor。 如果多个Actor重叠并且我们设置一个效果句柄变量,那么我们将覆盖它并丢失该变量之前存储的效果句柄。
当我们应用游戏效果时,如果该游戏效果是无限持续时间游戏效果,我们可以存储手柄,但我们也可以存储我们正在应用这个效果的Actor。实际上可以存储技能系统组件,代替拥有ASC的Actor。
Source/Aura/Public/Actor/AuraEffectActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GameplayEffectTypes.h"
#include "AuraEffectActor.generated.h"
class UAbilitySystemComponent;
class UGameplayEffect;
// 效果应用策略
UENUM(BlueprintType)
enum class EEffectApplicationPolicy
{
ApplyOnOverlap,
ApplyOnEndOverlap,
DoNotApply
};
// 效果移除策略
UENUM(BlueprintType)
enum class EEffectRemovalPolicy
{
RemoveOnEndOverlap,
DoNotRemove
};
UCLASS()
class AURA_API AAuraEffectActor : public AActor
{
GENERATED_BODY()
public:
AAuraEffectActor();
protected:
virtual void BeginPlay() override;
// 应用效果到actor
UFUNCTION(BlueprintCallable)
void ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass);
UFUNCTION(BlueprintCallable)
void OnOverlap(AActor* TargetActor);
UFUNCTION(BlueprintCallable)
void OnEndOverlap(AActor* TargetActor);
// 效果移除后是否销毁Actor
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
bool bDestroyOnEffectRemoval = false;
// 即时游戏效果类 在蓝图中指定
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
TSubclassOf<UGameplayEffect> InstantGameplayEffectClass;
// 即时游戏效果 应用策略
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
EEffectApplicationPolicy InstantEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
// 持续时间游戏效果 在蓝图中指定
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
TSubclassOf<UGameplayEffect> DurationGameplayEffectClass;
// 持续时间游戏效果 应用策略
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
EEffectApplicationPolicy DurationEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
// 无限时间游戏效果 在蓝图中指定
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
TSubclassOf<UGameplayEffect> InfiniteGameplayEffectClass;
// 无限时间游戏效果 应用策略
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
EEffectApplicationPolicy InfiniteEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
// 无限时间游戏效果 移除策略
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
EEffectRemovalPolicy InfiniteEffectRemovalPolicy = EEffectRemovalPolicy::RemoveOnEndOverlap;
// FActiveGameplayEffectHandle 被用作 TMap 的键,这通常需要对该类型有完整定义,因为 TMap 可能需要知道其大小和其他属性。
// 所以不能前向声明,需要引入头文件
TMap<FActiveGameplayEffectHandle, UAbilitySystemComponent*> ActiveEffectHandles;
};
Source/Aura/Private/Actor/AuraEffectActor.cpp
#include "Actor/AuraEffectActor.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
AAuraEffectActor::AAuraEffectActor()
{
PrimaryActorTick.bCanEverTick = false;
SetRootComponent(CreateDefaultSubobject<USceneComponent>("SceneRoot"));
}
void AAuraEffectActor::BeginPlay()
{
Super::BeginPlay();
}
void AAuraEffectActor::ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass)
{
// 获取Target【例如玩家角色】的技能系统组件
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
// 如果目标【例如玩家角色】没有技能系统组件 则什么都不做
// 例如红药水与玩家重叠
if (TargetASC == nullptr) return;
// 游戏效果类必须有效,无论目标是否具由技能系统组件。否则崩溃
check(GameplayEffectClass);
// 制作游戏效果情景句柄【轻量级指针】,与游戏效果相关的东西,包含 背景,效果目标,谁造成的效果,效果是什么
// 句柄是一个轻量级包装器,它将实际效果上下文存储为指针。
// 它有能力清除该指针。有办法获取影响上下文的任何游戏标签它有很多实用程序
FGameplayEffectContextHandle EffectContextHandle = TargetASC->MakeEffectContext();
// 添加导致此游戏效果的来源【例如红药水】
EffectContextHandle.AddSourceObject(this);
// 制作游戏效果规范句柄
// 参数1:效果类
// 参数2:效果等级
// 参数3: 效果情景
const FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(
GameplayEffectClass, 1.f, EffectContextHandle);
// 应用游戏效果规格句柄的数据【游戏效果】到Target自身【例如玩家角色自身】
// 参数2:预测,补偿
// Get 返回原始指针
// * 星号取消这个原始指针 获取游戏效果
// 一旦您应用了游戏效果,该游戏效果就会变为活动状态,并且这些应用功能返回该效果的句柄 ActiveEffectHandle。
// 所以我们以后总是可以使用该句柄ActiveEffectHandle ,例如如果它是无限时间游戏效果,则将其效果删除。
const FActiveGameplayEffectHandle ActiveEffectHandle = TargetASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
// 效果的持续时间类型
const bool bIsInfinite = EffectSpecHandle.Data.Get()->Def.Get()->DurationPolicy == EGameplayEffectDurationType::Infinite;
if (bIsInfinite && InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
{
// 存储无限时间游戏效果的 游戏效果规格句柄+技能系统组件 键值对 用以删除无限时间游戏效果
// 其他类型效果不需要存储,因为他们自动删除自己
ActiveEffectHandles.Add(ActiveEffectHandle, TargetASC);
}
}
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);
}
// 如果需要在重叠结束时删除无限效果,那么开始删除对应的无限效果数组的每一项
if (InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
{
// 获取目标的技能系统组件
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
if (!IsValid(TargetASC)) return;
TArray<FActiveGameplayEffectHandle> HandlesToRemove;
for (TTuple<FActiveGameplayEffectHandle, UAbilitySystemComponent*> HandlePair : ActiveEffectHandles)
{
// 如果找到了当前技能组件系统对应的键值对
if (TargetASC == HandlePair.Value)
{
// 移除该技能组件系统上的活跃效果
TargetASC->RemoveActiveGameplayEffect(HandlePair.Key);
HandlesToRemove.Add(HandlePair.Key);
}
}
for (FActiveGameplayEffectHandle& Handle : HandlesToRemove)
{
ActiveEffectHandles.FindAndRemoveChecked(Handle);
}
}
}
打开 BP_FireArea 事件图表: 修改节点 删除事件外的全部节点。 添加 调用函数-On Overlap 函数节点。【在父类中处理ApplyEffectToTarget等等】
选择Box组件,添加事件 On Component End Overlap(Box) 添加 调用函数-On End Overlap 函数节点。
配置BP_FireArea类
此时离开火焰区域将停止减少健康值。
Stacking-Stacking Type 堆叠类型-Aggregate by Target 按目标汇总 Stack Limit Count 堆栈限制计数-3 Stack Duration Refresh Policy 堆栈持续时间刷新策路-Refresh on Successful Application 应用程序成功时剧新 Stack Period Reset Policy 堆核周期重置策略-Reset on Successful Application 应用程序成功后重置 Stack Expiration Policy 堆栈到期策略-Remove Single Stack and Refresh Duration 删除单个堆栈并刷新持续时间
一次最多可以堆叠3个。 结束时堆叠清零。
关卡放置3个火焰在一起。 重叠1个火焰区域,每秒减5点。 重叠2个火焰区域,每秒减52点。 重叠3个火焰区域,每秒减53点。
如果突然离开其中一个火焰区域,但没离开另一个火焰区域。人物将不再减少健康值。 这是因为 移除游戏效果时,会将全部游戏效果移除。
TargetASC->RemoveActiveGameplayEffect(HandlePair.Key, 1);
移除该技能组件系统上的活跃效果
参数1:活跃效果句柄
参数2:要移除的堆栈 默认为 -1,表示全部移除。同类型效果全部失效。1表示只移除一个堆栈
Source/Aura/Private/Actor/AuraEffectActor.cpp
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);
}
// 如果需要在重叠结束时删除无限效果,那么开始删除对应的无限效果数组的每一项
if (InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
{
// 获取目标的技能系统组件
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
if (!IsValid(TargetASC)) return;
TArray<FActiveGameplayEffectHandle> HandlesToRemove;
for (TTuple<FActiveGameplayEffectHandle, UAbilitySystemComponent*> HandlePair : ActiveEffectHandles)
{
// 如果找到了当前技能组件系统对应的键值对
if (TargetASC == HandlePair.Value)
{
// 移除该技能组件系统上的活跃效果
// 参数1:活跃效果句柄
// 参数2:要移除的堆栈 默认为 -1,表示全部移除。同类型效果全部失效。1表示只移除一个堆栈
TargetASC->RemoveActiveGameplayEffect(HandlePair.Key, 1);
HandlesToRemove.Add(HandlePair.Key);
}
}
for (FActiveGameplayEffectHandle& Handle : HandlesToRemove)
{
ActiveEffectHandles.FindAndRemoveChecked(Handle);
}
}
}
将 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude- -1 减去1点健康 方便测试
对属性修改添加限制。 预属性更改是我们可以修改属性当前值的更改的地方。 并且该函数在更改实际发生之前被调用。 预属性更改是由属性更改触发的,无论是通过属性访问器还是 游戏效果创建的设置器。 从字面上看,任何改变属性的东西都可以触发这个函数。
这不会永久更改给定属性的修饰符。 它只是查询属性修饰符返回的值。
我们可以在变化发生之前钳制变化,但是后面有一些操作发生在预属性更改之后, 这些将重新计算所有涉及的修饰符的当前值。
因此,如果由于任何原因,这之后的修改器导致健康值发生变化,将重新计算健康,这意味着我们的夹紧将不起作用,我们将有稍后再次夹紧。
所以预属性改变并不是实现最终夹紧改变最有效的选择。
最有效的限制是 PostGameplayEffectExecute 这是在游戏效果更改属性之后发生的事件。
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
public:
// 属性发生变化时的响应函数 无论来自游戏效果修改,还是直接修改
// Epic建议我们只使用这个函数来进行钳位,不可以处理游戏逻辑
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);
// 如果变化的属性是 Health ,无论来自游戏效果修改,还是直接修改
if (Attribute == GetHealthAttribute())
{
// 将新的属性值限制在0和最大健康值之间,直接修改。
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
}
// 如果变化的属性是 Mana ,无论来自游戏效果修改,还是直接修改
if (Attribute == GetManaAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxMana());
}
}
游戏效果后执行,在游戏属性改变后执行。 可以访问到大量属性信息。
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
// 游戏效果后执行 获取的数据
USTRUCT()
struct FEffectProperties
{
GENERATED_BODY()
FEffectProperties(){}
FGameplayEffectContextHandle EffectContextHandle;
UPROPERTY()
UAbilitySystemComponent* SourceASC = nullptr;
UPROPERTY()
AActor* SourceAvatarActor = nullptr;
UPROPERTY()
AController* SourceController = nullptr;
UPROPERTY()
ACharacter* SourceCharacter = nullptr;
UPROPERTY()
UAbilitySystemComponent* TargetASC = nullptr;
UPROPERTY()
AActor* TargetAvatarActor = nullptr;
UPROPERTY()
AController* TargetController = nullptr;
UPROPERTY()
ACharacter* TargetCharacter = nullptr;
};
public:
// 游戏效果后执行,在游戏属性改变后执行
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
private:
// 游戏效果后执行时获取,设置,填充各项数据到Props供后续使用
void SetEffectProperties(const FGameplayEffectModCallbackData& Data, FEffectProperties& Props) const;
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::SetEffectProperties(const FGameplayEffectModCallbackData& Data, FEffectProperties& Props) const
{
// Source = causer of the effect, Target = target of the effect (owner of this AS)
Props.EffectContextHandle = Data.EffectSpec.GetContext();
Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
if (IsValid(Props.SourceASC) && Props.SourceASC->AbilityActorInfo.IsValid() && Props.SourceASC->AbilityActorInfo->AvatarActor.IsValid())
{
Props.SourceAvatarActor = Props.SourceASC->AbilityActorInfo->AvatarActor.Get();
Props.SourceController = Props.SourceASC->AbilityActorInfo->PlayerController.Get();
if (Props.SourceController == nullptr && Props.SourceAvatarActor != nullptr)
{
if (const APawn* Pawn = Cast<APawn>(Props.SourceAvatarActor))
{
Props.SourceController = Pawn->GetController();
}
}
if (Props.SourceController)
{
Props.SourceCharacter = Cast<ACharacter>(Props.SourceController->GetPawn());
}
}
if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
{
Props.TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
Props.TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
Props.TargetCharacter = Cast<ACharacter>(Props.TargetAvatarActor);
Props.TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Props.TargetAvatarActor);
}
}
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
}
打开 GE_PotionHeal 游戏效果 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude- 25,使用 Curve Tables 曲线表
在 内容-Blueprints-Actor-Potion 目录下新建 曲线表格 右键-其他-曲线表格-创建曲线表-CT_PotionHeal
打开 CT_PotionHeal 曲线表格
添加新列:
表头表示等级 1-10,下面表示该等级对应的治疗量 5.0 - 100.0 不同等级的健康药剂对玩家增加不同的健康值。
切换到曲线视图: 线性 重命名曲线名:HealingCurve
打开 GE_PotionHeal 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude- 25, 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-使用 Curve Tables 曲线表-CT_PotionHeal 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-选择曲线 HealingCurve
治疗效果增加的值为 Scalable Float Magnitude- 25 乘以 曲线上不同等级的的值 例如: 1级健康药剂的HealingCurve曲线值为 5:治疗量=255=125 2级健康药剂的HealingCurve曲线值为 7.5:治疗量=257.5=187.5 预览已显示最终值。
为了方便计算,将 治疗量基础值改为1 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude- 1,
代码更新为使用等级变量 ActorLevel 替换原来的固定等级1:
const FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(GameplayEffectClass, ActorLevel, EffectContextHandle);
Source/Aura/Public/Actor/AuraEffectActor.h
protected:
// 游戏效果 Actor的等级
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
float ActorLevel = 1.f;
Source/Aura/Private/Actor/AuraEffectActor.cpp
void AAuraEffectActor::ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass)
{
// 获取Target【例如玩家角色】的技能系统组件
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
// 如果目标【例如玩家角色】没有技能系统组件 则什么都不做
// 例如红药水与玩家重叠
if (TargetASC == nullptr) return;
// 游戏效果类必须有效,无论目标是否具由技能系统组件。否则崩溃
check(GameplayEffectClass);
// 制作游戏效果情景句柄【轻量级指针】,与游戏效果相关的东西,包含 背景,效果目标,谁造成的效果,效果是什么
// 句柄是一个轻量级包装器,它将实际效果上下文存储为指针。
// 它有能力清除该指针。有办法获取影响上下文的任何游戏标签它有很多实用程序
FGameplayEffectContextHandle EffectContextHandle = TargetASC->MakeEffectContext();
// 添加导致此游戏效果的来源【例如红药水】
EffectContextHandle.AddSourceObject(this);
// 制作游戏效果规范句柄
// 参数1:效果类
// 参数2:效果等级
// 参数3: 效果情景
const FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(
GameplayEffectClass, ActorLevel, EffectContextHandle);
// 应用游戏效果规格句柄的数据【游戏效果】到Target自身【例如玩家角色自身】
// 参数2:预测,补偿
// Get 返回原始指针
// * 星号取消这个原始指针 获取游戏效果
// 一旦您应用了游戏效果,该游戏效果就会变为活动状态,并且这些应用功能返回该效果的句柄 ActiveEffectHandle。
// 所以我们以后总是可以使用该句柄ActiveEffectHandle ,例如如果它是无限时间游戏效果,则将其效果删除。
const FActiveGameplayEffectHandle ActiveEffectHandle = TargetASC->ApplyGameplayEffectSpecToSelf(
*EffectSpecHandle.Data.Get());
// 效果的持续时间类型
const bool bIsInfinite = EffectSpecHandle.Data.Get()->Def.Get()->DurationPolicy ==
EGameplayEffectDurationType::Infinite;
if (bIsInfinite && InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
{
// 存储无限时间游戏效果的 游戏效果规格句柄+技能系统组件 键值对 用以删除无限时间游戏效果
// 其他类型效果不需要存储,因为他们自动删除自己
ActiveEffectHandles.Add(ActiveEffectHandle, TargetASC);
}
}
打开 BP_HealthPotion 蓝图-细节-Applied Effects-Actor Level-1 可修改等级
在关卡中拖入多个 BP_HealthPotion 实例,可与i单独修改每个实例的 Actor Level
将 CT_PotionHeal 重命名为 CT_Potion CT_Potion 添加曲线 ManaCurve 曲线视图中,可以拖动点修改数值。
打开 GE_PotionMana 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude- 1, 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-使用 Curve Tables 曲线表-CT_Potion 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-选择曲线 ManaCurve
https://docs.unrealengine.com/5.3/zh-CN/using-gameplay-tags-in-unreal-engine/
Gameplay标签(Gameplay Tags) 是用户定义的字符串,充当概念性的分层标签。你可以将Gameplay标签应用于项目中的对象,并对其求值以驱动你的Gameplay实现,类似于检查布尔值或标记。
你可以使用它们传达许多不同的概念,包括以下概念:
对象的属性,例如 Character.Enemy.Zombie
对象在执行或能够执行的事情,例如 Movement.Mode.Swimming
游戏事件和触发器,例如 GameplayEvent.RequestReset
Gameplay标签可以有任意数量的分层级别,以 . 字符分隔表示。例如,标签 Event.Movement.Dash 有三个级别,其中 Event 是层级中最宽泛的标识符,而 Dash 是最具体的。
你必须将Gameplay标签添加到标签字典,以便虚幻引擎识别它们。你可以使用以下某种方法添加(或删除)标签:
直接在 项目设置(Project Settings) 中添加或删除
从 数据表(Data Table) 资产导入
使用C++定义
以上所有方法都在 项目设置(Project Settings) 的 Gameplay标签(GameplayTags) 分段中的 项目(Project) 标题下设置。
定义新Gameplay标签的最简单方式是,直接在 项目设置(Project Settings) 中添加。
要在 项目设置(Project Settings) 中添加标签,请执行以下操作:
启用 从配置导入标签(Import Tags From Config) 。这会导入 .ini 文件中的所有Gameplay标签,包括 Config/DefaultGameplayTags.ini 以及 Config/Tags 中的所有标签。
(可选)点击 添加新Gameplay标签源(Add new Gameplay Tag source) 按钮,在 Config/Tags 中创建新的源 .ini 文件来存储Gameplay标签。为项目的各个方面创建单独的源文件,可能对于大型项目的组织和协作很有用。
点击 Gameplay标签列表(Gameplay Tag List) 条目旁边的 管理Gameplay标签(Manage Gameplay Tags) 按钮。这会打开 Gameplay标签管理器(Gameplay Tag Manager) 窗口。
在 Gameplay标签管理器(Gameplay Tag Manager) 窗口中,点击左上角的 添加(Add (+)) 按钮。
输入所需的 名称(Name) 、 注释(Comment) 和 源(Source) 。注释显示在标签的提示文本上,源是存储标签的 .ini 文件。
点击 添加新标签(Add New Tag) 按钮。
你可以重命名、删除、复制标签或向其添加新的子标签,方法是在列表中右键点击它并从快捷菜单中选择相应选项。若标签的来源不是 .ini 文件,则不能在 Gameplay标签管理器(Gameplay Tag Manager) 窗口中重命名或删除。
你可以使用文本编辑器编辑标签 .ini 源文件,但你必须重启编辑器才能加载更改。
你可以使用行类型 GameplayTagTableRow 从数据表资产导入Gameplay标签。使用此方法,你可以:
在 数据表编辑器(Data Table Editor) 中管理标签。
在编辑器运行期间更改数据表。
通过将 .csv 或 .json 文件作为数据表导入来添加标签。
要从数据表导入标签,请在 项目设置(Project Settings) 中执行以下操作:
点击 Gameplay标签表列表(Gameplay Tag Table List) 旁边的 添加元素(Add Element (+)) 按钮。
点击新索引的下拉菜单并选择你的数据表。
你可以使用 NativeGameplayTags.h 中定义的以下宏,通过C++来定义Gameplay标签:
UE_DECLARE_GAMEPLAY_TAG_EXTERN :在 .h 文件中用于声明 .cpp 文件中定义的标签。
UE_DEFINE_GAMEPLAY_TAG :在 .cpp 文件中用于定义 .h 文件中声明的标签,不带提示文本注释。
UE_DEFINE_GAMEPLAY_TAG_COMMENT :在 .cpp 文件中用于定义 .h 文件中声明的标签,带有提示文本注释。
UE_DEFINE_GAMEPLAY_TAG_STATIC :在 .cpp 文件中用于定义仅对定义文件可用的标签。不同于其他 DEFINE 宏,这不应该与 DECLARE 宏调用配对。
你必须将 GameplayTags 模块添加到你的项目的 Build.cs 文件,才能在C++中访问Gameplay标签功能。
示例实现
// 在.h文件中
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Movement_Mode_Walking);
// 在.cpp文件中
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Movement_Mode_Walking, "Movement.Mode.Walking", "Default Character movement tag");
如需更详细的示例实现,请参阅Lyra示例游戏项目中的 LyraGameplayTags.h 和 LyraGameplayTags.cpp 。
经过定义之后,你可以将标签应用于对象并对标签求值,以在项目中驱动Gameplay。
要将标签应用于对象,请执行以下操作:
将 Gameplay标签容器(Gameplay Tag Container) (FGameplayTagContainer)类型变量添加到对象。此变量存储多个Gameplay标签。
使用"添加Gameplay标签"(AddTag)函数将指定标签添加到容器。
你还可以使用"删除Gameplay标签"(RemoveTag)函数从容器删除标签,并使用"附加Gameplay标签容器"(AppendTags)函数将Gameplay标签容器附加到一起。
你可以直接使用Gameplay标签(FGameplayTag)类型变量,但对象往往有多个标签,因此经常需要Gameplay标签容器。
你可以基于对象的标签来驱动你的Gameplay实现。要对存储在对象的Gameplay标签容器中的标签求值,你可以使用各种条件函数,例如:
有标签(HasTag)
有任何标签(HasAny)
有所有标签(HasAll)
请参阅FGameplayTagContainerC++ API参考和GameplayTags蓝图API参考,了解更多信息。
除了 HasAll 之类的 All 函数之外,使用空的Gameplay标签容器作为输入参数调用条件函数会返回false。这是因为,容器中的所有标签在源集内都没有缺失。
Gameplay标签查询(Gameplay Tag Query) (FGameplayTagQuery)类型变量组合了条件函数,以更直白精简的方式建立复杂逻辑。
Gameplay标签查询支持以下表达式:
任何标签匹配(Any Tags Match) :测试是否能在容器中发现查询中的至少一个标签。
所有标签匹配(All Tags Match) :测试查询中的所有标签是否都在容器中。如果查询为空,这会返回true。
无标签匹配(No Tags Match) :测试查询中的所有标签是否都不在容器中。如果查询为空,这会返回true。
此外,Gameplay标签查询支持基于子表达式求值的以下根表达式:
任何表达式匹配(Any Expressions Match) :测试是否有任何子表达式返回true。
所有表达式匹配(All Expressions Match) :测试是否所有子表达式都返回true。如果没有子表达式,这会返回true。
无表达式匹配(No Expressions Match) :测试是否没有子表达式返回true。如果没有子表达式,这会返回true。
你可以限制用户对Gameplay标签进行编辑(在任意层级级别)。
要限制编辑,请在 项目设置(Project Settings) 的 高级Gameplay标签(Advanced Gameplay Tags)> 高级(Advanced) 下进行以下设置:
受限制的配置文件(Restricted Config Files) :用于存储受限制标签的 .ini 文件列表,这些标签与具有编辑权限的 所有者(Owners) 列表配对。
受限制的标签列表(Restricted Tag List) :显示 Gameplay标签管理器(Gameplay Tag Manager) 窗口,你可以在该窗口中修改受限制标签。
如果有用户(非列表中的所有者)尝试编辑受限制的标签,将弹出警告消息,要求用户确认自己已获得所有者的编辑授权。如果用户无法确认,则不会做出编辑。
受限制的标签在创建之后,不能在编辑器中删除。要删除受限制的标签,必须直接编辑 .ini 文件。
你可以使用IGameplayTagAssetInterface改进你的Gameplay标签实现。该接口提供了以下优势:
你不用显式将对象转型就可以获取对象的标签。
你可以为每种可能的类型编写自定义代码。
实现该接口并重载GetOwnedGameplayTags函数,就能创建一种能够被蓝图访问的方法,来为Gameplay标签容器填充与该对象关联的标签。在大部分情况下,这意味着将基类中的标签复制到新容器中,但你的实现可以从多个容器收集标签,或调用蓝图函数以访问蓝图声明的标签或你的对象需要的任意内容。
关于该实现的示例,请参阅Lyra示例游戏项目中的 ALyraTaggedActor 类。
游戏标签本质上是分层的。 层次结构的每个级别都用点分隔。 类型:FGameplayTag
使用游戏标签容器存储游戏标签。 游戏标签容器具有标签映射计数的概念, 标签映射计数告诉我们容器中存在多少给定标签。
游戏标签是整个项目范围内的。 是在游戏标签管理器中注册的。
项目设置-项目-GameplayTags
Gameplay标签-Gameplay标签列表-管理Gameplay标签-
有默认的标签
点击加号添加新标签
添加标签 Attributes.Vital.Health
注释:属性.重要.健康 玩家在死亡前可以承受的伤害量
源:DefaultGameplayTages.ini
添加标签 Attributes.Vital.Mana
添加标签 Attributes.Vital.MaxMana
添加标签 Attributes.Vital.MaxHealth
标签存在于项目文件默认配置中 E:\Unreal Projects 532\Aura\Config\DefaultGameplayTags.ini
可以直接编辑。
[/Script/GameplayTags.GameplayTagsSettings]
ImportTagsFromConfig=True
WarnOnInvalidTags=True
ClearInvalidTags=False
AllowEditorTagUnloading=True
AllowGameTagUnloading=False
FastReplication=False
InvalidTagCharacters="\"\',"
NumBitsForContainerSize=6
NetIndexFirstBitSegment=16
+GameplayTagList=(Tag="Attributes.Vital.Health",DevComment="属性.重要.健康 玩家在死亡前可以承受的伤害量")
+GameplayTagList=(Tag="Attributes.Vital.Mana",DevComment="")
+GameplayTagList=(Tag="Attributes.Vital.MaxHealth",DevComment="")
+GameplayTagList=(Tag="Attributes.Vital.MaxMana",DevComment="")
在目录 内容-Blueprints-AbilitySystem-GameplayTags 下 右键-其他-数据表格 选择行结构: GameplayTagTableRow 名称:DT_PrimaryAttributes
打开 游戏标签表格行 数据表 DT_PrimaryAttributes
添加新行 玩法标签-标签-Attributes.Primary.Strength 玩法标签-开发评论-力量 提高物理伤害
添加新行 玩法标签-标签-Attributes.Primary.Intelligence 玩法标签-开发评论-智力 提高魔法伤害
添加新行 玩法标签-标签-Attributes.Primary.Resilience 玩法标签-开发评论-恢复力/韧性 提高护甲和护甲穿透力
添加新行 玩法标签-标签-Attributes.Primary.Vigor 玩法标签-开发评论-活力,提高健康值
项目设置-项目-GameplayTags-Gameplay标签-Gameplay标签列表 数组- 新建一组标签 下拉选择 游戏标签表格行 数据表 DT_PrimaryAttributes
Gameplay标签-Gameplay标签列表-管理Gameplay标签-可以看到从数据表导入的新标签
如何将游戏标签添加到任何给定的技能系统组件
打开 GE_CrystalHeal 游戏效果 GE_CrystalHeal-细节-Gameplay Effect-Components-添加一个元素 新的元素-类型选择 Asset Tags Gameplay Effect Component 资产标签游戏效果组件【这些是游戏效果资产本身“拥有”的标签。这些不转让给任何演员】
细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Asset Tags Gameplay Effect Component
细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Add Tags-Combined Tags-空 细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Add Tags-Added-Attributes.Vital.Health 细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Add Tags-Removed-空
此时 该游戏效果 GE_CrystalHeal 上将具由游戏标签 Attributes.Vital.Health
在玩家和敌人的BeginPlay时,开始绑定技能系统组件委托事件 在 玩家和敌人的BeginPlay 中 InitAbilityActorInfo。 在 InitAbilityActorInfo 执行执行技能系统组件的 AbilityActorInfoSet 在 技能系统组件的 AbilityActorInfoSet 中为游戏效果应用委托绑定函数 EffectApplied EffectApplied 函数中可以访问到技能系统组件,效果规格,激活的效果句柄,
无论游戏效果是什么类型,即时,持续,无限时间。 当玩家靠近 健康水晶等具由游戏效果的物体时,最终触发 EffectApplied 函数。等等信息。
OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAuraAbilitySystemComponent::EffectApplied);
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
void AbilityActorInfoSet();
protected:
void EffectApplied(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayEffectSpec& EffectSpec,
FActiveGameplayEffectHandle ActiveEffectHandle);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::AbilityActorInfoSet()
{
// 为游戏效果应用委托绑定函数 EffectApplied
OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAuraAbilitySystemComponent::EffectApplied);
}
void UAuraAbilitySystemComponent::EffectApplied(UAbilitySystemComponent* AbilitySystemComponent,
const FGameplayEffectSpec& EffectSpec,
FActiveGameplayEffectHandle ActiveEffectHandle)
{
GEngine->AddOnScreenDebugMessage(1, 8.f, FColor::Blue, FString("Effect Applied!"));
}
Source/Aura/Public/Character/AuraCharacterBase.h
protected:
virtual void InitAbilityActorInfo();
Source/Aura/Private/Character/AuraCharacterBase.cpp
void AAuraCharacterBase::InitAbilityActorInfo()
{
}
Source/Aura/Public/Character/AuraCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "Character/AuraCharacterBase.h"
#include "AuraCharacter.generated.h"
UCLASS()
class AURA_API AAuraCharacter : public AAuraCharacterBase
{
GENERATED_BODY()
public:
AAuraCharacter();
virtual void PossessedBy(AController* NewController) override;
virtual void OnRep_PlayerState() override;
private:
virtual void InitAbilityActorInfo() override;
};
Source/Aura/Public/Character/AuraCharacter.h
#include "AbilitySystem/AuraAbilitySystemComponent.h"
void AAuraCharacter::InitAbilityActorInfo()
{
// 获取玩家状态
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
if (AuraPlayerState == nullptr)return;
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(1, 1.F, FColor::Cyan, FString("AuraPlayerState"));
}
// check(AuraPlayerState);
// 从玩家状态获取技能系统组件
// 然后初始技能参与者信息
// owner 为 玩家状态类,avatar 为当前类即玩家角色
AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState, this);
// 为技能系统组件设置技能Actor相关信息
Cast<UAuraAbilitySystemComponent>(AuraPlayerState->GetAbilitySystemComponent())->AbilityActorInfoSet();
// 将 玩家状态上的 技能系统组件 和 属性集 拷贝到 角色类上,因为角色基类也有同样的变量需要构造
// 技能系统组件
AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
// 属性集
AttributeSet = AuraPlayerState->GetAttributeSet();
// 初始化并添加覆盖控件,覆盖控件控制器
// 在多人游戏,只有服务端的玩家控制器有效,
// 服务器拥有所有玩家的玩家控制器,但每个玩家只有自己的玩家控制器。
// 在控制该特定角色的客户端机器上,该玩家控制器是有效的。
// 但是该客户端计算机上非本地控制的其他角色没有有效的玩家控制器。
// 例如,在三人游戏中,如果您是客户端,则您的玩家控制器有效,
// 但在你的机器上,另外两个角色,这两个副本没有有效的玩家控制器和初始化能力演员信息。
// 在这种情况下,将调用此函数InitAbilityActorInfo,并且/或玩家控制器将是空指针。
// 在这种情况下,对于此功能或玩家控制器,在多人游戏中可以为空,
// 我们只想在它不为空时继续执行。
// 所以这种情况使用if检查【为空是合理的,只要不继续执行】。不使程序崩溃。
// 否则使用check断言,程序崩溃。【游戏前置条件不能继续执行】
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
{
if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(AuraPlayerController->GetHUD()))
{
AuraHUD->InitOverlay(AuraPlayerController, AuraPlayerState, AbilitySystemComponent, AttributeSet);
}
}
}
Source/Aura/Public/Character/AuraEnemy.h
protected:
virtual void InitAbilityActorInfo() override;
Source/Aura/Public/Character/AuraEnemy.h
void AAuraEnemy::BeginPlay()
{
Super::BeginPlay();
InitAbilityActorInfo();
}
void AAuraEnemy::InitAbilityActorInfo()
{
// 初始技能参与者信息 服务器和客户端都在此设置
// 两者均为敌人类自身角色
AbilitySystemComponent->InitAbilityActorInfo(this, this);
Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent)->AbilityActorInfoSet();
}
游戏效果带有游戏标签。可在技能系统组件中访问到效果规格, 再从效果规格中访问到游戏标签。 获取所有资产标签,存储到标签容器中,标签容器比数组优化更多。 控件控制器依赖技能系统组件。可接受技能系统组件Delegate广播的值:游戏效果带有的游戏标签。 控件控制器接受到游戏标签数据后再Delegate广播到控件。绘制到HUD。
最终,游戏效果中的游戏标签被层层广播到HUD。
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::EffectApplied(UAbilitySystemComponent* AbilitySystemComponent,
const FGameplayEffectSpec& EffectSpec,
FActiveGameplayEffectHandle ActiveEffectHandle)
{
// 游戏标签容器
FGameplayTagContainer TagContainer;
// 获取所有资产标签,存储到标签容器中,标签容器比数组优化更多
EffectSpec.GetAllAssetTags(TagContainer);
for (const FGameplayTag& Tag : TagContainer)
{
// 准备向控件控制器广播游戏标签
//TODO: Broadcast the tag to the Widget Controller
const FString Msg = FString::Printf(TEXT("GE Tag: %s"), *Tag.ToString());
// key -1,旧消息不会替换新消息
GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Blue, Msg);
}
}
此时,玩家解除健康水晶后,屏幕打印处 效果的标签:Attributes.Vital.Health
在 技能系统组件中定义委托 EffectAssetTags 。
效果应用的时候广播资产标签容器数据。
在 OverlayWidgetController 覆层控件控制器中获取技能系统组件,为其EffectAssetTags绑定该委托的响应/回调函数 :EffectAssetTags.AddLambda
该回调函数将接受委托传入的资产标签容器,包含了激活的游戏效果的所有资产标签
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
// 声明一个多播委托
// 控件控制器将绑定到该委托
// 有一个参数,该参数将是资产标签,或者是包含所有资产标签的游戏标签容器
DECLARE_MULTICAST_DELEGATE_OneParam(FEffectAssetTags, const FGameplayTagContainer& /*AssetTags*/);
public:
// 委托
FEffectAssetTags EffectAssetTags;
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::EffectApplied(UAbilitySystemComponent* AbilitySystemComponent,
const FGameplayEffectSpec& EffectSpec,
FActiveGameplayEffectHandle ActiveEffectHandle)
{
// 游戏标签容器
FGameplayTagContainer TagContainer;
// 获取所有资产标签,存储到标签容器中,标签容器比数组优化更多
EffectSpec.GetAllAssetTags(TagContainer);
// 广播资产标签容器
EffectAssetTags.Broadcast(TagContainer);
}
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
#include "AbilitySystem/AuraAbilitySystemComponent.h"
void UOverlayWidgetController::BindCallbacksToDependencies()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性更改多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
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);
// 资产标签响应函数
Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent)->EffectAssetTags.AddLambda(
// 响应技能系统组件的委托时获取 传入的资产标签容器参数 AssetTags
[](const FGameplayTagContainer& AssetTags)
{
for (const FGameplayTag& Tag : AssetTags)
{
const FString Msg = FString::Printf(TEXT("GE Tag: %s"), *Tag.ToString());
GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Blue, Msg);
}
}
);
}
运行游戏,玩家解除健康水晶时,屏幕打印健康水晶的标签:Attributes.Vital.Health 接触其他水晶,则打印其效果自带的对应标签。 例如 为 GE_CrystalMana 效果添加标签 Attributes.Vital.Mana
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
class UAuraUserWidget;
// 用于在UI显示提示消息的消息属性数据表的行结构
USTRUCT(BlueprintType)
struct FUIWidgetRow : public FTableRowBase
{
GENERATED_BODY()
// 游戏标签
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FGameplayTag MessageTag = FGameplayTag();
// 消息文本
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FText Message = FText();
// UI控件
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSubclassOf<UAuraUserWidget> MessageWidget;
// 消息的图像 例如药剂瓶 可选
UPROPERTY(EditAnywhere, BlueprintReadOnly)
UTexture2D* Image = nullptr;
};
在 内容-Blueprints-UI-Data 目录下新建
右键-其他-数据图表-选择 UIWidgetRow
这是在 Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
中定义的结构体 struct FUIWidgetRow
名称 DT_MessageWidgetData
打开 DT_MessageWidgetData 包含了C++ struct FUIWidgetRow 结构体定义的4个属性
添加新行,可设置各个属性
项目设置-项目-GameplayTags Gameplay标签-Gameplay标签列表-管理Gameplay标签-
添加标签:Message.HealthPotion 使用默认源。 继续添加标签: Message.HealthCrystal Message.ManaCrystal Message.ManaPotion
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
protected:
// 在子类蓝图中指定消息控件数据表资产
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Widget Data")
TObjectPtr<UDataTable> MessageWidgetDataTable;
打开 BP_OverlayWidgetController 细节-Widget Data-Message Widget Data Table-DT_MessageWidgetData 数据表 之后可以在C++和蓝图中访问该数据表
建议使用数据资产替代数据表。此处仅作学习数据表使用。
打开 数据表 DT_MessageWidgetData
第1行: Row Name 行命名 : Message.HealthCrystal Messgae Tag : Message.HealthCrystal Message :捡起健康水晶 Message Widget : None Image : T_HealthCrystal 纹理
第2行: Row Name 行命名 : Message.HealthPotion Messgae Tag : Message.HealthPotion Message :捡起健康药剂 Message Widget : None Image : T_Potion_red 纹理
第3行: Row Name 行命名 : Message.ManaCrystal Messgae Tag : Message.ManaCrystal Message :捡起魔力水晶 Message Widget : None Image : T_ManaCrystal 纹理
第4行: Row Name 行命名 : Message.ManaPotion Messgae Tag : Message.ManaPotion Message :捡起魔力药剂 Message Widget : None Image : T_Potion_Blue 纹理
之后将通过行名称/标签名称查找数据。 在 覆层控件控制器的 EffectAssetTags.AddLambda 函数中执行查找。
打开 GE_PotionHeal 细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Asset Tags Gameplay Effect Component
细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Add Tags-Added-Message.HealthPotion
打开 GE_PotionHeal 细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Asset Tags Gameplay Effect Component
细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Add Tags-Added-Message.ManaPotion
打开 GE_PotionHeal 清除原标签。 细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Asset Tags Gameplay Effect Component
细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Add Tags-Added-Message.HealthCrystal
打开 GE_PotionHeal 清除原标签。 细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Asset Tags Gameplay Effect Component
细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Add Tags-Added-Message.ManaCrystal
现在拾起对应物体会显示效果自带的游戏标签。
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
protected:
// 通用
// 返回任意类型的数据表行
// 传入数据表,通过标签查找对应行的数据
template<typename T>
T* GetDataTableRowByTag(UDataTable* DataTable, const FGameplayTag& Tag);
// 之后应放在静态函数库中 作为通用工具
template <typename T>
T* UOverlayWidgetController::GetDataTableRowByTag(UDataTable* DataTable, const FGameplayTag& Tag)
{
// 参数1:行名称,数据表中与标签同名
// 参数2:上下文字符串
// 返回对应行数据
return DataTable->FindRow<T>(Tag.GetTagName(), TEXT(""));
}
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
void UOverlayWidgetController::BindCallbacksToDependencies()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性更改多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
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);
// 资产标签响应函数
Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent)->EffectAssetTags.AddLambda(
// 响应技能系统组件的委托时获取 传入的资产标签容器参数 AssetTags
// 传入this参数,可使用当前类中的属性和方法
[this](const FGameplayTagContainer& AssetTags)
{
for (const FGameplayTag& Tag : AssetTags)
{
const FString Msg = FString::Printf(TEXT("GE Tag: %s"), *Tag.ToString());
GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Blue, Msg);
// 通过行名称/标签名称查找对应的文本消息等数据。
FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
}
}
);
}
EffectAssetTags.AddLambda 中后续将消息行广播到控件
FGameplayTag::RequestGameplayTag
函数根据提供的字符串(在这里是 "Message")请求一个对应的游戏标签。
如果该标签已经存在于游戏的标签系统中,则返回它;如果不存在,则创建一个新的标签。
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
// 用于在UI显示提示消息的消息属性数据表的行结构
USTRUCT(BlueprintType)
struct FUIWidgetRow : public FTableRowBase
{
GENERATED_BODY()
// 游戏标签
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FGameplayTag MessageTag = FGameplayTag();
// 消息文本
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FText Message = FText();
// UI控件
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSubclassOf<class UAuraUserWidget> MessageWidget;
// 消息的图像 例如药剂瓶 可选
UPROPERTY(EditAnywhere, BlueprintReadOnly)
UTexture2D* Image = nullptr;
};
class UAuraUserWidget;
// 参数1:委托类型 FOnHealtChangedSignature
// 参数2:发送的数据类型
// 参数3:通过动态多播委托 发送一个值
// 消息控件动态多播委托 将数据表的一行数据广播
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMessageWidgetRowSignature, FUIWidgetRow, Row);
public:
// 消息控件动态多播委托成员变量 可在蓝图中指定
UPROPERTY(BlueprintAssignable, Category="GAS|Messages")
FMessageWidgetRowSignature MessageWidgetRowDelegate;
protected:
// 在子类蓝图中指定消息控件数据表资产
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Widget Data")
TObjectPtr<UDataTable> MessageWidgetDataTable;
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
void UOverlayWidgetController::BindCallbacksToDependencies()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性更改多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
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);
// 资产标签响应函数
Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent)->EffectAssetTags.AddLambda(
// 响应技能系统组件的委托时获取 传入的资产标签容器参数 AssetTags
// 传入this参数,可使用当前类中的属性和方法
[this](const FGameplayTagContainer& AssetTags)
{
for (const FGameplayTag& Tag : AssetTags)
{
const FString Msg = FString::Printf(TEXT("GE Tag: %s"), *Tag.ToString());
GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Blue, Msg);
// 通过行名称/标签名称查找对应的文本消息等数据。
FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
// 查找标签是否包含 Message 字符,是表示消息游戏标签
// For example, say that Tag = Message.HealthPotion
// "Message.HealthPotion".MatchesTag("Message") will return True, "Message".MatchesTag("Message.HealthPotion") will return False
FGameplayTag MessageTag = FGameplayTag::RequestGameplayTag(FName("Message"));
if (Tag.MatchesTag(MessageTag))
{
const FUIWidgetRow* MessageRow = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
// 将数据表的一行数据广播
MessageWidgetRowDelegate.Broadcast(*MessageRow);
//之后在控件蓝图时间中绑定覆盖该事件以接受该行数据
}
}
}
);
}
打开 WBP_Overlay 事件图表: 添加序列 Sequence 先将 控制器转换为 Cast To BP_OverlayWidgetController . 当子类是覆层的时候这将成功。 保存为变量 BPOverlayWidgetController
BPOverlayWidgetController 绑定消息委托 assign Message Widget Row Delegate
Message Widget Row Delegate 输出一行消息数据。拆分该数据,打印其中的Message文本
捡起对应药剂将打印对应消息。
用于显示控件接受到的消息数据 在目录 内容-Blueprints-UI-Overlay-Subwidget 下右键-用户界面-控件蓝图-创建 名称:WBP_EffectMessage
打开 WBP_EffectMessage 设计器: 填充屏幕改为自定义 随意调整 控件根的大小
添加 Horizontal Box 水平框:HorizontalBox_Root
添加 Image 图片控件: Image_Icon 到 水平框 Image_Icon-外观-笔刷-图像-T_Potion_Red Image_Icon-外观-笔刷-图像大小-x,y -75 设置为变量
添加 Text 文本 控件: Text_Message 到 水平框 Text_Message-内容-文本:捡起了一个健康药剂 预览效果 Text_Message-插槽-垂直对齐-垂直居中对齐 Text_Message-外观-背景颜色-A-0 设置为变量
添加 Space 间隔区 到 水平框 放置在 图片与文本之间 外观-尺寸-20,0
打开 WBP_Overlay 添加 WBP_EffectMessage 到画布面板
打开 WBP_EffectMessage 图表: 左侧添加方法:SetImageAndText
SetImageAndText-细节-输入: 添加输入: Image - Texture2D 纹理2D Text - 文本
拖入 Image_Icon Image_Icon 拖出 Set Brush Set Brush 的笔刷节点拖出 Make SlateBrush 添加 变量 ImageSize - Vector 2D 向量2D 用以设置图像大小 输出至 Make SlateBrush 的 Image Size Image Size-细节-默认值-Image Size-X 75 ,-Y 75 SetImageAndText 的 Image 输出至 Make SlateBrush 的 Image
拖入 Text_Message Text_Message 拖出 Set Text
SetImageAndText 的 Text 输出至 Set Text 的 文本
打开 WBP_EffectMessage 为每一行的 Message Widget 指定:WBP_EffectMessage
打开 WBP_Overlay 图表: 删除 Print string 添加 用户界面-Create Widget Break UIWidgetRow 的 Message Widget 输出至 Create Widget 的 Class
添加 get player controller 输出至 Create Widget 的 Owning Player Create Widget 自动变更为 Create Aura User WIdget Widget Create Aura User WIdget Widget 拖出 cast to WBP_EffectMessage cast to WBP_EffectMessage 拖出 Set Image and Text Break UIWidgetRow 的 Image 输出至 Set Image and Text 的 Image Break UIWidgetRow 的 Message 输出至 Set Image and Text 的 Text cast to WBP_EffectMessage 另外拖出 Add To Viewport 以输出至视图
删除设计器上的测试 WBP_EffectMessage 控件 WBP_EffectMessage 控件的高度缩放至屏幕高度,发生变形。
打开 WBP_EffectMessage 控件 HorizontalBox_Root-右键-包裹-覆层 这在HorizontalBox_Root的父级添加了覆层控件 这时 WBP_EffectMessage 不在变形
图表: Create Aura User WIdget Widget 的输出 拖出 Set position in viewport
添加 Get Viewport Size Get Viewport Size 拖出 Multiply Multiply 的数字 上右键-至浮点双精度 数字改为0.5 Multiply 输出至 Set position in viewport 的 position 这将 消息控件的左上角定位在屏幕中心。
打开 WBP_EffectMessage 控件
图表: 图像输入为可选 Set Image and Text 的 Image 拖出 工具-Is Valid Is Valid 的 Is Valid 连接 Set Brush 的执行节点
打开 WBP_EffectMessage 控件 设计器-动画
添加动画-MessageAnimation
为 Text_Message 添加过渡动画 轨道-所有已命名控件-Text_Message
Text_Message 右侧加号-变换
移动时间轴到0.25 选择 Text_Message - 变换-平移-y- -160 使文本上移 自动创建关键帧。
移动时间轴到0.45 选择 Text_Message - 变换-平移-x- 150 使文本右移
点击曲线编辑器 调节曲线 曲线上-右键-可以添加关键帧 调节细节
添加 变量-动画-message animation message animation 拖出 用户界面-动画-play animation
没有图片也需要播放动画。
因为不能使用定时事件去销毁控件。
添加 custom event 自定义事件
名称 DestroyDelay DestroyDelay 拖出 Delay Delay : Duration =0.5 Delay 拖出 remove from parent 0.5秒后执行销毁。
添加 DestroyDelay play animation 的执行连接 DestroyDelay
现在消息控件动画可以执行,然后销毁。不会造成内存泄露。
设计器-打开动画 MessageAnimation-Text_Message 右侧加号-渲染不透明度
时间轴0-不透明度改为1 - 表示全显示
时间轴拖到最后-不透明度改为0 - 表示不显示
选择 MessageAnimation 轨道-所有已命名控件-Image_Icon
Image_Icon -右侧加号-变换
移动时间轴到0.25 选择 Image_Icon - 变换-平移-y- -160 使图片上移 自动创建关键帧。
移动时间轴到0.45 选择 Image_Icon - 变换-平移-x- 150 使图片右移
MessageAnimation-Text_Message 右侧加号-颜色和不透明度
时间轴0-颜色和不透明度-A-改为1 - 表示全显示
时间轴拖到最后--颜色和不透明度-A-改为0 - 表示不显示
对所有属性使用相同的委托
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
#pragma once
#include "CoreMinimal.h"
#include "UI/WidgetController/AuraWidgetController.h"
#include "OverlayWidgetController.generated.h"
// 用于在UI显示提示消息的消息属性数据表的行结构
USTRUCT(BlueprintType)
struct FUIWidgetRow : public FTableRowBase
{
GENERATED_BODY()
// 游戏标签
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FGameplayTag MessageTag = FGameplayTag();
// 消息文本
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FText Message = FText();
// UI控件
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSubclassOf<class UAuraUserWidget> MessageWidget;
// 消息的图像 例如药剂瓶 可选
UPROPERTY(EditAnywhere, BlueprintReadOnly)
UTexture2D* Image = nullptr;
};
class UAuraUserWidget;
// 对所有属性使用相同的委托
// 参数1:委托类型 FOnAttributeChangedSignature
// 参数2:发送的数据类型
// 参数3:通过动态多播委托 发送一个值
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAttributeChangedSignature, float, NewValue);
// 消息控件动态多播委托 将数据表的一行数据广播
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMessageWidgetRowSignature, FUIWidgetRow, Row);
UCLASS(BlueprintType, Blueprintable)
class AURA_API UOverlayWidgetController : public UAuraWidgetController
{
GENERATED_BODY()
public:
virtual void BroadcastInitialValues() override;
virtual void BindCallbacksToDependencies() override;
// 蓝图子类通过访问控件控制器,分配事件来接受健康值
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnAttributeChangedSignature OnHealthChanged;
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnAttributeChangedSignature OnMaxHealthChanged;
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnAttributeChangedSignature OnManaChanged;
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnAttributeChangedSignature OnMaxManaChanged;
// 消息控件动态多播委托成员变量 可在蓝图中指定
UPROPERTY(BlueprintAssignable, Category="GAS|Messages")
FMessageWidgetRowSignature MessageWidgetRowDelegate;
protected:
// 在子类蓝图中指定消息控件数据表资产
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Widget Data")
TObjectPtr<UDataTable> MessageWidgetDataTable;
// 通用
// 返回任意类型的数据表行
// 传入数据表,通过标签查找对应行的数据
template<typename T>
T* GetDataTableRowByTag(UDataTable* DataTable, const FGameplayTag& Tag);
};
// 之后应放在静态函数库中 作为通用工具
template <typename T>
T* UOverlayWidgetController::GetDataTableRowByTag(UDataTable* DataTable, const FGameplayTag& Tag)
{
// 参数1:行名称,数据表中与标签同名
// 参数2:上下文字符串
// 返回对应行数据
return DataTable->FindRow<T>(Tag.GetTagName(), TEXT(""));
}
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
#include "UI/WidgetController/OverlayWidgetController.h"
#include "AbilitySystem/AuraAbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
void UOverlayWidgetController::BroadcastInitialValues()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
OnHealthChanged.Broadcast(AuraAttributeSet->GetHealth());
OnMaxHealthChanged.Broadcast(AuraAttributeSet->GetMaxHealth());
OnManaChanged.Broadcast(AuraAttributeSet->GetMana());
OnMaxManaChanged.Broadcast(AuraAttributeSet->GetMaxMana());
}
void UOverlayWidgetController::BindCallbacksToDependencies()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性更改多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnManaChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxManaChanged.Broadcast(Data.NewValue);
}
);
// 资产标签响应函数
Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent)->EffectAssetTags.AddLambda(
// 响应技能系统组件的委托时获取 传入的资产标签容器参数 AssetTags
// 传入this参数,可使用当前类中的属性和方法
[this](const FGameplayTagContainer& AssetTags)
{
for (const FGameplayTag& Tag : AssetTags)
{
const FString Msg = FString::Printf(TEXT("GE Tag: %s"), *Tag.ToString());
GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Blue, Msg);
// 通过行名称/标签名称查找对应的文本消息等数据。
FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
// 查找标签是否包含 Message 字符,是表示消息游戏标签
// For example, say that Tag = Message.HealthPotion
// "Message.HealthPotion".MatchesTag("Message") will return True, "Message".MatchesTag("Message.HealthPotion") will return False
FGameplayTag MessageTag = FGameplayTag::RequestGameplayTag(FName("Message"));
if (Tag.MatchesTag(MessageTag))
{
const FUIWidgetRow* MessageRow = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
// 将数据表的一行数据广播
MessageWidgetRowDelegate.Broadcast(*MessageRow);
//之后在控件蓝图时间中绑定覆盖该事件以接受该行数据
}
}
}
);
}
打开 WBP_HealthGlobe 图表: OnHealthChanged 事件-右键-刷新节点 OnHealthChanged 事件 New Value 输出至 Set Health
OnMaxHealthChanged 事件-右键-刷新节点 OnMaxHealthChanged 事件 New Value 输出至 Set Max Health
图表: OnManaChanged 事件-右键-刷新节点 OnManaChanged 事件 New Value 输出至 Set Mana OnMaxManaChanged 事件-右键-刷新节点 OnMaxManaChanged 事件 New Value 输出至 Set Max Mana
优化 WBP_GlobeProgressBar 健康与魔力进度球的动画过渡,加入插值更平滑。 打开 WBP_GlobeProgressBar
加入一张背景,延时插值到最终的位置。
M_FlowingGlobe 材质能用于界面显示,是因为其 材质-材质域 -用户界面
PreAttributeChange 预属性更改正在从我们的游戏效果修改器中获得新的值。 即应用效果后的值。 然后我们在属性改变之前限制它。 但这里的这个限制不会永久改变这个修饰符。 它只是更改查询该修饰符返回的值。只是更改读取到的值,而非原值。 如果我们有其他一些游戏效果需要查询自己的修改器,那么它就会从当前值重新计算该值。 例如另一个效果读取的依然是未限制的原值。 使之前的限制失效。
所以这有点像这个值从未被真正限制过。 我们在属性更改之前限制了它,但该修改器会重新计算。
PreAttributeChange 的 NewValue 只是原值应用效果后的读取的值,非原值。 原值50,如果效果1【加50】应用在50上后增加50到150.虽然之后限制为100.但原值依然变更为为150. 但效果2【减20】应用在150上后减少20到130 ,NewValue=130 ,然后再次被限制为100 用户看到的效果即是 用户捡起药剂到达满血的状态【实际150,由于被限制显示100】, 然后被攻击后掉血20,但依然显示为满血【实际130,由于被限制显示满血100】。
需要在 PostGameplayEffectExecute 中再次限制 PostGameplayEffectExecute发生在游戏效果应用之后 SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
// NewValue 只是原值应用效果后的读取的值,非原值。
// 原值50,如果效果1【加50】应用在50上后增加50到150.虽然之后限制为100.但原值依然变更为为150.
// 但效果2【减20】应用在150上后减少20到130 ,NewValue=130 ,然后再次被限制为100
// 用户看到的效果即是 用户捡起药剂到达满血的状态【实际150,由于被限制显示100】,
// 然后被攻击后掉血20,但依然显示为满血【实际130,由于被限制显示满血100】。
Super::PreAttributeChange(Attribute, NewValue);
// 如果变化的属性是 Health ,无论来自游戏效果修改,还是直接修改
if (Attribute == GetHealthAttribute())
{
// 将新的属性值限制在0和最大健康值之间,直接修改。
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
}
// 如果变化的属性是 Mana ,无论来自游戏效果修改,还是直接修改
if (Attribute == GetManaAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxMana());
}
}
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
}
在 构造函数中直接初始化函数值不是首选方案。 使用数据表初始化属性也不是,此处使用数据表仅作学习。 【使用游戏效果初始化属性是推荐方案】
初始化主属性。
Source/Aura/Public/Player/AuraPlayerState.h
protected:
// 技能系统组件
// 公开给蓝图,以为其设置数据表属性
UPROPERTY(VisibleAnywhere)
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
public:
/*
* Primary Attributes
*/
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Strength, Category = "Primary Attributes")
FGameplayAttributeData Strength;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Strength);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Intelligence, Category = "Primary Attributes")
FGameplayAttributeData Intelligence;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Intelligence);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Resilience, Category = "Primary Attributes")
FGameplayAttributeData Resilience;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Resilience);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Vigor, Category = "Primary Attributes")
FGameplayAttributeData Vigor;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Vigor);
UFUNCTION()
void OnRep_Strength(const FGameplayAttributeData& OldStrength) const;
UFUNCTION()
void OnRep_Intelligence(const FGameplayAttributeData& OldIntelligence) const;
UFUNCTION()
void OnRep_Resilience(const FGameplayAttributeData& OldResilience) const;
UFUNCTION()
void OnRep_Vigor(const FGameplayAttributeData& OldVigor) const;
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 注册要复制的健康值 这是想要复制的任何内容所必需的。
// COND_None 条件,表示 不为这个变量的复制设置任何条件,我们总是想复制它,无条件地复制
// REPNOTIFY_Always 始终响应通知意味着如果在服务器上设置了该值,则复制它。在客户端上该值将被更新和设置。
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Strength, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Intelligence, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Resilience, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Vigor, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Mana, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxMana, COND_None, REPNOTIFY_Always);
}
void UAuraAttributeSet::OnRep_Strength(const FGameplayAttributeData& OldStrength) const
{
// 为属性集中的复制属性设置响应通知时,必须通知那个变化的技能系统。
// 技能系统可以完成保留技能所需的所有幕后协同工作。
// 现在游戏技能系统将知道生命值刚刚被复制。
// 这负责通知技能系统我们正在复制一个值,它的值已经刚刚从服务器复制下来并进行了更改,现在技能系统可以注册该更改并保留跟踪其旧值,以防万一需要回滚任何内容。
// 在预测的情况下,如果服务器认为发生变化,则可以回滚更改并撤消它们。
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Strength, OldStrength);
}
void UAuraAttributeSet::OnRep_Intelligence(const FGameplayAttributeData& OldIntelligence) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Intelligence, OldIntelligence);
}
void UAuraAttributeSet::OnRep_Resilience(const FGameplayAttributeData& OldResilience) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Resilience, OldResilience);
}
void UAuraAttributeSet::OnRep_Vigor(const FGameplayAttributeData& OldVigor) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Vigor, OldVigor);
}
打开 BP_AuraPlayerState 技能系统组件已公开可见
为玩家状态初始化属性 技能系统组件-细节-attribute test-添加 这需要具由正确行结构的数据表。
在目录 Blueprints-AbilitySystem-Data 下 右键-其他-数据表格-AttributeMetaData 名称 DT_InitialPrimaryValues 打开 DT_InitialPrimaryValues
添加一行,必须在名称中指定属性 行命名:AuraAttributeSet.Strength Base Value:10
打开 BP_AuraPlayerState 技能系统组件-细节-attribute test-索引0-Attributes-AuraAttributeSet 技能系统组件-细节-attribute test-索引0-Default Starting Table-DT_InitialPrimaryValues
运行-输入命令- showdebug abilitysystem
力量 Strength 属性 已被初始化为 10
AuraAttributeSet.Intelligence AuraAttributeSet.Resilience AuraAttributeSet.Vigor
建议方法。 在游戏开始时设置即可。
玩家状态 BP_AuraPlayerState 蓝图删除属性数据表 DT_InitialPrimaryValues 技能系统组件-细节-attribute test-删除索引0
Source/Aura/Public/Character/AuraCharacterBase.h
class UGameplayEffect;
protected:
// 主属性默认值
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Attributes")
TSubclassOf<UGameplayEffect> DefaultPrimaryAttributes;
// 初始化主属性
void InitializePrimaryAttributes() const;
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "AbilitySystemComponent.h"
void AAuraCharacterBase::InitializePrimaryAttributes() const
{
check(IsValid(GetAbilitySystemComponent()));
check(DefaultPrimaryAttributes);
const FGameplayEffectContextHandle ContextHandle = GetAbilitySystemComponent()->MakeEffectContext();
const FGameplayEffectSpecHandle SpecHandle = GetAbilitySystemComponent()->MakeOutgoingSpec(DefaultPrimaryAttributes, 1.f, ContextHandle);
// SpecHandle.Data.Get() Get() 获取效果句柄指针 *星号取出指针地址的值
// 将游戏效果规格应用于目标的技能系统组件
GetAbilitySystemComponent()->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data.Get(), GetAbilitySystemComponent());
}
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::InitAbilityActorInfo()
{
// 获取玩家状态
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
if (AuraPlayerState == nullptr)return;
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(1, 1.F, FColor::Cyan, FString("AuraPlayerState"));
}
// check(AuraPlayerState);
// 从玩家状态获取技能系统组件
// 然后初始技能参与者信息
// owner 为 玩家状态类,avatar 为当前类即玩家角色
AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState, this);
// 为技能系统组件设置技能Actor相关信息
Cast<UAuraAbilitySystemComponent>(AuraPlayerState->GetAbilitySystemComponent())->AbilityActorInfoSet();
// 将 玩家状态上的 技能系统组件 和 属性集 拷贝到 角色类上,因为角色基类也有同样的变量需要构造
// 技能系统组件
AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
// 属性集
AttributeSet = AuraPlayerState->GetAttributeSet();
// 初始化并添加覆盖控件,覆盖控件控制器
// 在多人游戏,只有服务端的玩家控制器有效,
// 服务器拥有所有玩家的玩家控制器,但每个玩家只有自己的玩家控制器。
// 在控制该特定角色的客户端机器上,该玩家控制器是有效的。
// 但是该客户端计算机上非本地控制的其他角色没有有效的玩家控制器。
// 例如,在三人游戏中,如果您是客户端,则您的玩家控制器有效,
// 但在你的机器上,另外两个角色,这两个副本没有有效的玩家控制器和初始化能力演员信息。
// 在这种情况下,将调用此函数InitAbilityActorInfo,并且/或玩家控制器将是空指针。
// 在这种情况下,对于此功能或玩家控制器,在多人游戏中可以为空,
// 我们只想在它不为空时继续执行。
// 所以这种情况使用if检查【为空是合理的,只要不继续执行】。不使程序崩溃。
// 否则使用check断言,程序崩溃。【游戏前置条件不能继续执行】
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
{
if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(AuraPlayerController->GetHUD()))
{
AuraHUD->InitOverlay(AuraPlayerController, AuraPlayerState, AbilitySystemComponent, AttributeSet);
}
}
// 此时技能系统组件已经初始化
// 初始化主属性
// 一般只在服务端初始化属性,因为属性设置了网络复制
// 此处会在服务端与客户端初始化属性,也可以,这无需等待从服务端复制
InitializePrimaryAttributes();
}
不同的玩家角色具由不同的初始化属性值
当前只为Aura角色。
在目录 Blueprints-AbilitySystem-GameplayEffects-PrimaryAttributes 下 基于 GameplayEffect 新建 游戏效果蓝图 GE_AuraPrimaryAttributes
打开 GE_AuraPrimaryAttributes
细节-Gameplay Effect-Modifiers- 添加4组元素对应4个主属性
细节-Gameplay Effect-Modifiers-索引0-Attribute-AuraAttributeSet.Strength 细节-Gameplay Effect-Modifiers-索引0-Modifier Op-Override 覆盖,表示初始化 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type -Scalable Float 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-10
分别设置 AuraAttributeSet.Intelligence AuraAttributeSet.Resilience AuraAttributeSet.Vigor
打开 BP_AuraCharacter
细节-属性-Default Primary Attribute-GE_AuraPrimaryAttributes
这对应 AuraCharacterBase 的 TSubclassOf<UGameplayEffect> DefaultPrimaryAttributes
运行游戏;
showdebug abilitysystem
Modifier Magnitude Magnitude可以理解为修改量,比如角色拥有20点Damage,可能经过一系列计算,最终造成的伤害是50,这个最终值就是Magnitude,计算最终伤害的方法就是Magnitude Calculation Type。
计算Magnitude的方法有四种: Scalable Float Attribute Based Custom Calculation Class Set By Caller
到目前为止,我们所有的游戏效果都使用 可缩放浮动幅度 Scalable Float Magnitude作为其修饰符大小。
打开 GE_PotionHeal
Attribute Based Modifiers 基于属性的修改器 能够根据其他属性更改属性。 这使得拥有从其他属性派生属性变得非常容易。
基于 Gameplay Effect 新建 GE_TestAttributeBased 蓝图类
在 目录 Blueprints-Actor-TestActor 下 基于 AuraEffectActor 新建蓝图类 BP_TestActor 打开BP_TestActor 添加 Box Collision 组件到根
事件图表: 选择 Box 添加事件 On Component Begin Overlap (Box) 添加 Apply Effect to target 从左侧选择拖入变量 Instant Gameplay Effect Class 将 BP_TestActor 拖入关卡
细节-Applied Effects-InstantGameplayEffectClass-GE_TestAttributeBased
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.Health 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Add 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Vigor 由于 Modifier Op-Add ,将为玩家的健康值增加一个值,该值为玩家的活力Vigor值。 由于 Attribute Source 属性源-Target ,那么 活力Vigor值 是来自效果的应用目标,即玩家。
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 选择 Target,因为当 Aura 重叠时会将这个游戏效果应用到 Aura 上。 要从Aura玩家角色中获取属性。
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用 它与何时捕获属性有关,我们何时应该使用该属性值? 应该在创建导致此游戏效果的游戏效果规格时使用它还是在它被应用时。 快照决定了我们捕获该属性的确切时间。
当前玩家 健康值50,活力值9,当玩家与BP_TestActor重叠时玩家应用效果 GE_TestAttributeBased,健康值增加9,成为59.
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.Health 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Add 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Strength Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
这将继续为健康增加值,值来源于目标即玩家的力量值。 由于两个修改器都是Add类型,修改器按顺序计算。 所以重叠时玩家健康增加 活力+力量 9+10 最终为 50+9+10=69
将健康初始值改为 10 方便计算
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
UAuraAttributeSet::UAuraAttributeSet()
{
InitHealth(10.f);
InitMaxHealth(100.f);
InitMana(10.f);
InitMaxMana(50.f);
}
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.Health 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Add 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Resilience Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
细节-Gameplay Effect-Modifiers-索引-Modifier Op- Multiply 结果为( (Health + Vigor) * Strength)+Resilience =202 按顺序基于上一个修改器的结果进行操作。
为方便测试,暂时取消
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
中的限制大小
PreAttributeChange
PostGameplayEffectExecute
细节-Gameplay Effect-Modifiers-索引-Modifier Op- Divide 结果为( (Health + Vigor) * Strength)/ Resilience =15.83
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.Health 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Add 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.MaxHealth Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
结果为(( (Health + Vigor) * Strength)/ Resilience ) + MaxHealth=115.83
Modifier Magnitude-Attribute Based Magnitude-Coefficient 系数 系数到属性计算
Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value 预乘叠加值 属性计算的附加值,在应用系数之前添加
Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value 乘法后叠加值 添加值到属性计算中,在系数应用后添加
工作原理: 计算方式:(系数 * (捕获的游戏属性 + 预乘叠加值))+ 乘法后叠加值
多个修改器: 个修改器分别计算出效果值,再依次为前一个效果器叠加。
部分属性完全依赖其他属性
主要属性将是不依赖于任何其他属性的独立属性。 可以根据不同的情况增加或减少它们。
所有其他属性都以某种方式依赖于这些属性。
四个主要属性:力量 Strength,智力 Intelligence,韧性 Resilience,活力 Vigor
力量 Strength:增加物理伤害
智力 Intelligence:增加魔法伤害
韧性 Resilience:增加护甲和护甲穿透力
活力 Vigor:增加健康
次要属性将更密切地影响游戏机制 主要是战斗,这些属性将部分或全部源自其他属性。
Armor 护甲:护甲依赖于韧性,随着韧性的增加,护甲将会增加一定数量。 减少受到的伤害并提高格挡几率。
Armor Penetration 护甲穿透:护甲穿透力将取决于韧性,忽略敌人护甲的百分比,增加爆击几率。 当我们攻击敌人时,如果我们有很高的护甲穿透力,该值将使我们忽略我们正在攻击的敌人的一些护甲,使我们能够造成更多伤害并增加获得暴击的机会。
Block Chance 格挡几率:依赖于Armor 护甲。这是一个依赖于另一个次要属性的次要属性。 格挡将有机会将损失减少一半。如果我们成功格挡,我们就会将传入的伤害减少一半。
Critical Hit Chance 暴击率:依赖于 Armor Penetration 护甲穿透。随着护甲穿透力的增加,暴击率也会增加。 这将有机会使伤害加倍,并加上暴击加成。 如果我们成功获得暴击,我们的伤害将会翻倍。
Critical Hit Damage 暴击伤害:依赖于Armor Penetration 护甲穿透。获得暴击时所增加的额外伤害。
Critical Hit Resilience 暴击抗性:依赖于Armor 护甲。减少敌人攻击的暴击几率。
Health Regeneration 健康恢复力: 依赖于 活力 Vigor。生命恢复量是每秒恢复的生命量。
Mana Regeneration 魔力恢复力:依赖于 智力 Intelligence。每秒恢复的法力值。
Max Health 最大生命值:依赖于 活力 Vigor。可获得的最大生命值。
Max Mana 最大魔力值:依赖于 智力 Intelligence。可获得的最大法力值
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "AuraAttributeSet.generated.h"
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
// 游戏效果后执行 获取的数据
USTRUCT()
struct FEffectProperties
{
GENERATED_BODY()
FEffectProperties(){}
FGameplayEffectContextHandle EffectContextHandle;
UPROPERTY()
UAbilitySystemComponent* SourceASC = nullptr;
UPROPERTY()
AActor* SourceAvatarActor = nullptr;
UPROPERTY()
AController* SourceController = nullptr;
UPROPERTY()
ACharacter* SourceCharacter = nullptr;
UPROPERTY()
UAbilitySystemComponent* TargetASC = nullptr;
UPROPERTY()
AActor* TargetAvatarActor = nullptr;
UPROPERTY()
AController* TargetController = nullptr;
UPROPERTY()
ACharacter* TargetCharacter = nullptr;
};
UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UAuraAttributeSet();
// 要将变量标记为已复制,需要类必须具有一个特定的函数才能注册变量以进行复制,
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// 属性发生变化时的响应函数 无论来自游戏效果修改,还是直接修改
// Epic建议我们只使用这个函数来进行钳位,不可以处理游戏逻辑
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
// 游戏效果后执行,在游戏属性改变后执行
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
/*
* Primary Attributes 主要属性
*/
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Strength, Category = "Primary Attributes")
FGameplayAttributeData Strength;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Strength);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Intelligence, Category = "Primary Attributes")
FGameplayAttributeData Intelligence;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Intelligence);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Resilience, Category = "Primary Attributes")
FGameplayAttributeData Resilience;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Resilience);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Vigor, Category = "Primary Attributes")
FGameplayAttributeData Vigor;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Vigor);
/*
* Secondary Attributes 次要/辅助属性
*/
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Armor, Category = "Secondary Attributes")
FGameplayAttributeData Armor;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Armor);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_ArmorPenetration, Category = "Secondary Attributes")
FGameplayAttributeData ArmorPenetration;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, ArmorPenetration);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_BlockChance, Category = "Secondary Attributes")
FGameplayAttributeData BlockChance;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, BlockChance);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_CriticalHitChance, Category = "Secondary Attributes")
FGameplayAttributeData CriticalHitChance;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, CriticalHitChance);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_CriticalHitDamage, Category = "Secondary Attributes")
FGameplayAttributeData CriticalHitDamage;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, CriticalHitDamage);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_CriticalHitResistance, Category = "Secondary Attributes")
FGameplayAttributeData CriticalHitResistance;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, CriticalHitResistance);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_HealthRegeneration, Category = "Secondary Attributes")
FGameplayAttributeData HealthRegeneration;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, HealthRegeneration);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_ManaRegeneration, Category = "Secondary Attributes")
FGameplayAttributeData ManaRegeneration;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, ManaRegeneration);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxHealth, Category = "Vital Attributes")
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, MaxHealth);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxMana, Category = "Vital Attributes")
FGameplayAttributeData MaxMana;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, MaxMana);
/*
* Vital Attributes 重要属性
*/
// 健康
// 为了使变量被复制,用UPROPERTY(Replicated)说明符将其标记为已复制。
// 当变量复制时 客户端将触发该变量的响应通知 OnRep_Health。
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Vital Attributes")
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Health);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Mana, Category = "Vital Attributes")
FGameplayAttributeData Mana;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Mana);
// 响应通知OnRep_Health可以接受 0-1个参数。参数必须是复制变量的类型。即游戏属性数据。
UFUNCTION()
void OnRep_Health(const FGameplayAttributeData& OldHealth) const;
UFUNCTION()
void OnRep_Mana(const FGameplayAttributeData& OldMana) const;
UFUNCTION()
void OnRep_Strength(const FGameplayAttributeData& OldStrength) const;
UFUNCTION()
void OnRep_Intelligence(const FGameplayAttributeData& OldIntelligence) const;
UFUNCTION()
void OnRep_Resilience(const FGameplayAttributeData& OldResilience) const;
UFUNCTION()
void OnRep_Vigor(const FGameplayAttributeData& OldVigor) const;
UFUNCTION()
void OnRep_Armor(const FGameplayAttributeData& OldArmor) const;
UFUNCTION()
void OnRep_ArmorPenetration(const FGameplayAttributeData& OldArmorPenetration) const;
UFUNCTION()
void OnRep_BlockChance(const FGameplayAttributeData& OldBlockChance) const;
UFUNCTION()
void OnRep_CriticalHitChance(const FGameplayAttributeData& OldCriticalHitChance) const;
UFUNCTION()
void OnRep_CriticalHitDamage(const FGameplayAttributeData& OldCriticalHitDamage) const;
UFUNCTION()
void OnRep_CriticalHitResistance(const FGameplayAttributeData& OldCriticalHitResistance) const;
UFUNCTION()
void OnRep_HealthRegeneration(const FGameplayAttributeData& OldHealthRegeneration) const;
UFUNCTION()
void OnRep_ManaRegeneration(const FGameplayAttributeData& OldManaRegeneration) const;
UFUNCTION()
void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth) const;
UFUNCTION()
void OnRep_MaxMana(const FGameplayAttributeData& OldMaxMana) const;
private:
// 游戏效果后执行时获取,设置,填充各项数据到Props供后续使用
void SetEffectProperties(const FGameplayEffectModCallbackData& Data, FEffectProperties& Props) const;
};
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
#include "AbilitySystem/AuraAttributeSet.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "GameFramework/Character.h"
#include "GameplayEffectExtension.h"
#include "Net/UnrealNetwork.h"
UAuraAttributeSet::UAuraAttributeSet()
{
InitHealth(10.f);
InitMaxHealth(100.f);
InitMana(10.f);
InitMaxMana(50.f);
}
void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 注册要复制的健康值 这是想要复制的任何内容所必需的。
// COND_None 条件,表示 不为这个变量的复制设置任何条件,我们总是想复制它,无条件地复制
// REPNOTIFY_Always 始终响应通知意味着如果在服务器上设置了该值,则复制它。在客户端上该值将被更新和设置。
// Primary Attributes 主要属性
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Strength, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Intelligence, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Resilience, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Vigor, COND_None, REPNOTIFY_Always);
// Secondary Attributes 次要/辅助属性
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Armor, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, ArmorPenetration, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, BlockChance, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, CriticalHitChance, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, CriticalHitDamage, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, CriticalHitResistance, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, HealthRegeneration, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, ManaRegeneration, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxMana, COND_None, REPNOTIFY_Always);
// Vital Attributes 重要属性
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Mana, COND_None, REPNOTIFY_Always);
}
void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
// NewValue 只是原值应用效果后的读取的值,非原值。
// 原值50,如果效果1【加50】应用在50上后增加50到150.虽然之后限制为100.但原值依然变更为为150.
// 但效果2【减20】应用在150上后减少20到130 ,NewValue=130 ,然后再次被限制为100
// 用户看到的效果即是 用户捡起药剂到达满血的状态【实际150,由于被限制显示100】,
// 然后被攻击后掉血20,但依然显示为满血【实际130,由于被限制显示满血100】。
Super::PreAttributeChange(Attribute, NewValue);
// 如果变化的属性是 Health ,无论来自游戏效果修改,还是直接修改
if (Attribute == GetHealthAttribute())
{
// 将新的属性值限制在0和最大健康值之间,直接修改。
//NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
}
// 如果变化的属性是 Mana ,无论来自游戏效果修改,还是直接修改
if (Attribute == GetManaAttribute())
{
//NewValue = FMath::Clamp(NewValue, 0.f, GetMaxMana());
}
}
void UAuraAttributeSet::SetEffectProperties(const FGameplayEffectModCallbackData& Data, FEffectProperties& Props) const
{
// Source = causer of the effect, Target = target of the effect (owner of this AS)
Props.EffectContextHandle = Data.EffectSpec.GetContext();
Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
if (IsValid(Props.SourceASC) && Props.SourceASC->AbilityActorInfo.IsValid() && Props.SourceASC->AbilityActorInfo->AvatarActor.IsValid())
{
Props.SourceAvatarActor = Props.SourceASC->AbilityActorInfo->AvatarActor.Get();
Props.SourceController = Props.SourceASC->AbilityActorInfo->PlayerController.Get();
if (Props.SourceController == nullptr && Props.SourceAvatarActor != nullptr)
{
if (const APawn* Pawn = Cast<APawn>(Props.SourceAvatarActor))
{
Props.SourceController = Pawn->GetController();
}
}
if (Props.SourceController)
{
Props.SourceCharacter = Cast<ACharacter>(Props.SourceController->GetPawn());
}
}
if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
{
Props.TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
Props.TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
Props.TargetCharacter = Cast<ACharacter>(Props.TargetAvatarActor);
Props.TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Props.TargetAvatarActor);
}
}
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
//SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
//SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
}
void UAuraAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth) const
{
// 为属性集中的复制属性设置响应通知时,必须通知那个变化的技能系统。
// 技能系统可以完成保留技能所需的所有幕后协同工作。
// 现在游戏技能系统将知道生命值刚刚被复制。
// 这负责通知技能系统我们正在复制一个值,它的值已经刚刚从服务器复制下来并进行了更改,现在技能系统可以注册该更改并保留跟踪其旧值,以防万一需要回滚任何内容。
// 在预测的情况下,如果服务器认为发生变化,则可以回滚更改并撤消它们。
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Health, OldHealth);
}
void UAuraAttributeSet::OnRep_Mana(const FGameplayAttributeData& OldMana) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Mana, OldMana);
}
void UAuraAttributeSet::OnRep_Strength(const FGameplayAttributeData& OldStrength) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Strength, OldStrength);
}
void UAuraAttributeSet::OnRep_Intelligence(const FGameplayAttributeData& OldIntelligence) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Intelligence, OldIntelligence);
}
void UAuraAttributeSet::OnRep_Resilience(const FGameplayAttributeData& OldResilience) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Resilience, OldResilience);
}
void UAuraAttributeSet::OnRep_Vigor(const FGameplayAttributeData& OldVigor) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Vigor, OldVigor);
}
void UAuraAttributeSet::OnRep_Armor(const FGameplayAttributeData& OldArmor) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Armor, OldArmor);
}
void UAuraAttributeSet::OnRep_ArmorPenetration(const FGameplayAttributeData& OldArmorPenetration) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, ArmorPenetration, OldArmorPenetration);
}
void UAuraAttributeSet::OnRep_BlockChance(const FGameplayAttributeData& OldBlockChance) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, BlockChance, OldBlockChance);
}
void UAuraAttributeSet::OnRep_CriticalHitChance(const FGameplayAttributeData& OldCriticalHitChance) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, CriticalHitChance, OldCriticalHitChance);
}
void UAuraAttributeSet::OnRep_CriticalHitDamage(const FGameplayAttributeData& OldCriticalHitDamage) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, CriticalHitDamage, OldCriticalHitDamage);
}
void UAuraAttributeSet::OnRep_CriticalHitResistance(const FGameplayAttributeData& OldCriticalHitResistance) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, CriticalHitResistance, OldCriticalHitResistance);
}
void UAuraAttributeSet::OnRep_HealthRegeneration(const FGameplayAttributeData& OldHealthRegeneration) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, HealthRegeneration, OldHealthRegeneration);
}
void UAuraAttributeSet::OnRep_ManaRegeneration(const FGameplayAttributeData& OldManaRegeneration) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, ManaRegeneration, OldManaRegeneration);
}
void UAuraAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, MaxHealth, OldMaxHealth);
}
void UAuraAttributeSet::OnRep_MaxMana(const FGameplayAttributeData& OldMaxMana) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, MaxMana, OldMaxMana);
}
Source/Aura/Public/Character/AuraCharacterBase.h
protected:
// 次属性默认值
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Attributes")
TSubclassOf<UGameplayEffect> DefaultSecondaryAttributes;
// 应用游戏效果
void ApplyEffectToSelf(TSubclassOf<UGameplayEffect> GameplayEffectClass, float Level) const;
void InitializeDefaultAttributes() const;
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "Character/AuraCharacterBase.h"
#include "AbilitySystemComponent.h"
AAuraCharacterBase::AAuraCharacterBase()
{
PrimaryActorTick.bCanEverTick = false;
//初始化武器
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>("weapon");
//将武器附加到骨架插槽
Weapon->SetupAttachment(GetMesh(),FName("WeaponHandSocket"));
//武器不应有任何碰撞
Weapon->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
UAbilitySystemComponent* AAuraCharacterBase::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
void AAuraCharacterBase::BeginPlay()
{
Super::BeginPlay();
}
void AAuraCharacterBase::InitAbilityActorInfo()
{
}
//
void AAuraCharacterBase::ApplyEffectToSelf(TSubclassOf<UGameplayEffect> GameplayEffectClass, float Level) const
{
check(IsValid(GetAbilitySystemComponent()));
check(GameplayEffectClass);
const FGameplayEffectContextHandle ContextHandle = GetAbilitySystemComponent()->MakeEffectContext();
const FGameplayEffectSpecHandle SpecHandle = GetAbilitySystemComponent()->MakeOutgoingSpec(GameplayEffectClass, Level, ContextHandle);
// SpecHandle.Data.Get() Get() 获取效果句柄指针 *星号取出指针地址的值
// 将游戏效果规格应用于目标的技能系统组件
GetAbilitySystemComponent()->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data.Get(), GetAbilitySystemComponent());
}
void AAuraCharacterBase::InitializeDefaultAttributes() const
{
ApplyEffectToSelf(DefaultPrimaryAttributes, 1.f);
ApplyEffectToSelf(DefaultSecondaryAttributes, 1.f);
}
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::InitAbilityActorInfo()
{
// 获取玩家状态
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
if (AuraPlayerState == nullptr)return;
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(1, 1.F, FColor::Cyan, FString("AuraPlayerState"));
}
// check(AuraPlayerState);
// 从玩家状态获取技能系统组件
// 然后初始技能参与者信息
// owner 为 玩家状态类,avatar 为当前类即玩家角色
AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState, this);
// 为技能系统组件设置技能Actor相关信息
Cast<UAuraAbilitySystemComponent>(AuraPlayerState->GetAbilitySystemComponent())->AbilityActorInfoSet();
// 将 玩家状态上的 技能系统组件 和 属性集 拷贝到 角色类上,因为角色基类也有同样的变量需要构造
// 技能系统组件
AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
// 属性集
AttributeSet = AuraPlayerState->GetAttributeSet();
// 初始化并添加覆盖控件,覆盖控件控制器
// 在多人游戏,只有服务端的玩家控制器有效,
// 服务器拥有所有玩家的玩家控制器,但每个玩家只有自己的玩家控制器。
// 在控制该特定角色的客户端机器上,该玩家控制器是有效的。
// 但是该客户端计算机上非本地控制的其他角色没有有效的玩家控制器。
// 例如,在三人游戏中,如果您是客户端,则您的玩家控制器有效,
// 但在你的机器上,另外两个角色,这两个副本没有有效的玩家控制器和初始化能力演员信息。
// 在这种情况下,将调用此函数InitAbilityActorInfo,并且/或玩家控制器将是空指针。
// 在这种情况下,对于此功能或玩家控制器,在多人游戏中可以为空,
// 我们只想在它不为空时继续执行。
// 所以这种情况使用if检查【为空是合理的,只要不继续执行】。不使程序崩溃。
// 否则使用check断言,程序崩溃。【游戏前置条件不能继续执行】
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
{
if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(AuraPlayerController->GetHUD()))
{
AuraHUD->InitOverlay(AuraPlayerController, AuraPlayerState, AbilitySystemComponent, AttributeSet);
}
}
// 此时技能系统组件已经初始化
// 初始化主属性
// 一般只在服务端初始化属性,因为属性设置了网络复制
// 此处会在服务端与客户端初始化属性,也可以,这无需等待从服务端复制
InitializeDefaultAttributes();
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
UAuraAttributeSet::UAuraAttributeSet()
{
InitHealth(10.f);
InitMana(10.f);
}
void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
// NewValue 只是原值应用效果后的读取的值,非原值。
// 原值50,如果效果1【加50】应用在50上后增加50到150.虽然之后限制为100.但原值依然变更为为150.
// 但效果2【减20】应用在150上后减少20到130 ,NewValue=130 ,然后再次被限制为100
// 用户看到的效果即是 用户捡起药剂到达满血的状态【实际150,由于被限制显示100】,
// 然后被攻击后掉血20,但依然显示为满血【实际130,由于被限制显示满血100】。
Super::PreAttributeChange(Attribute, NewValue);
// 如果变化的属性是 Health ,无论来自游戏效果修改,还是直接修改
if (Attribute == GetHealthAttribute())
{
// 将新的属性值限制在0和最大健康值之间,直接修改。
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
}
// 如果变化的属性是 Mana ,无论来自游戏效果修改,还是直接修改
if (Attribute == GetManaAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxMana());
}
}
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
}
次要游戏属性始终跟随主属性变化。
重命名目录 PrimaryAttributes 为 DefaultAttributes
在 目录 Blueprints-AbilitySystem-GameplayEffects-DefaultAttributes 下 基于 Gameplay Effect 新建 GE_SecondaryAttribute 蓝图类
BP_AuraCharacter-细节-属性-Default Secondary Attributes-GE_SecondaryAttribute
细节-持续时间-Duration Policy-Infinite
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.armor 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Resilience Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 此时目标和源是相同的 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.25 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -2 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-6
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.ArmorPenetration 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Resilience Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 此时目标和源是相同的 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.15 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -1 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-3
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.ArmorPenetration 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Armor Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 此时目标和源是相同的 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.25 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-4
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.CriticalHitChance 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.ArmorPenetration Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 此时目标和源是相同的 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.25 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-2
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.CriticalHitResistance 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Armor Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 此时目标和源是相同的 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.25 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-10
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.CriticalHitDamage 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.ArmorPenetration Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 此时目标和源是相同的 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-1.5 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-5
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.HealthRegeneration 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Vigor Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 此时目标和源是相同的 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.1 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-1
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.ManaRegeneration 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Intelligence Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 此时目标和源是相同的 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.1 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-1
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.MaxHealth 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Vigor Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 此时目标和源是相同的 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-2.5 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-80
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.MaxMana 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Intelligence Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target 此时目标和源是相同的 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-2 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-50
Source/Aura/Public/Player/AuraPlayerState.h
public:
// 注册Level变量以进行复制
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
//
FORCEINLINE int32 GetPlayerLevel() const { return Level; }
private:
// 需要网络复制
UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_Level)
int32 Level = 1;
//
UFUNCTION()
void OnRep_Level(int32 OldLevel);
Source/Aura/Private/Player/AuraPlayerState.cpp
#include "Net/UnrealNetwork.h"
// 注册Level变量以进行复制
void AAuraPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AAuraPlayerState, Level);
}
Source/Aura/Public/Character/AuraEnemy.h
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character Class Defaults")
int32 Level = 1;
在敌人与玩家控制器之间共享
在 interaction 目录下新建C++ CombatInterface,基于 Unreal 接口
Source/Aura/Public/Interaction/CombatInterface.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "CombatInterface.generated.h"
UINTERFACE(MinimalAPI)
class UCombatInterface : public UInterface
{
GENERATED_BODY()
};
class AURA_API ICombatInterface
{
GENERATED_BODY()
public:
virtual int32 GetPlayerLevel();
};
Source/Aura/Private/Interaction/CombatInterface.cpp
#include "Interaction/CombatInterface.h"
int32 ICombatInterface::GetPlayerLevel()
{
return 0;
}
Source/Aura/Public/Character/AuraCharacterBase.h
#include "Interaction/CombatInterface.h"
class AURA_API AAuraCharacterBase : public ACharacter, public IAbilitySystemInterface, public ICombatInterface
Source/Aura/Public/Character/AuraEnemy.h
public:
/** Combat Interface */
virtual int32 GetPlayerLevel() override;
/** end Combat Interface */
Source/Aura/Private/Character/AuraEnemy.cpp
int32 AAuraEnemy::GetPlayerLevel()
{
return Level;
}
玩家等级从玩家状态获取
Source/Aura/Public/Character/AuraCharacter.h
public:
/** Combat Interface */
virtual int32 GetPlayerLevel() override;
/** end Combat Interface */
Source/Aura/Private/Character/AuraCharacter.cpp
int32 AAuraCharacter::GetPlayerLevel()
{
const AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
return AuraPlayerState->GetPlayerLevel();
}
Source/Aura/Public/Player/AuraPlayerController.h
private:
// 帧更新之前光标跟踪的Actor
IEnemyInterface* LastActor;
// 光标跟踪的当前Actor
IEnemyInterface* ThisActor;
制作一个自定义计算类,以便我们可以获得最大生命值和最大生命值
在 C++ AbilitySystem-ModMagCalc 目录新建 基于 GameplayModMagnitudeCalculation
用于通过蓝图或本机代码执行自定义游戏效果修饰符计算的类
Source/Aura/Public/AbilitySystem/ModMagCalc/MMC_MaxHealth.h
#pragma once
#include "CoreMinimal.h"
#include "GameplayModMagnitudeCalculation.h"
#include "MMC_MaxHealth.generated.h"
UCLASS()
class AURA_API UMMC_MaxHealth : public UGameplayModMagnitudeCalculation
{
GENERATED_BODY()
public:
UMMC_MaxHealth();
// 修改器幅值计算
virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
private:
// 修改器要捕获的属性
FGameplayEffectAttributeCaptureDefinition VigorDef;
};
Source/Aura/Private/AbilitySystem/ModMagCalc/MMC_MaxHealth.cpp
#include "AbilitySystem/ModMagCalc/MMC_MaxHealth.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "Interaction/CombatInterface.h"
UMMC_MaxHealth::UMMC_MaxHealth()
{
// 从效果应用的目标获取属性 Vigor
VigorDef.AttributeToCapture = UAuraAttributeSet::GetVigorAttribute();
VigorDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
VigorDef.bSnapshot = false;//创建有效果后立即应用,
// 要捕获的属性组
RelevantAttributesToCapture.Add(VigorDef);
}
float UMMC_MaxHealth::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
// 从源和目标收集标签
// Gather tags from source and target
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
float Vigor = 0.f;
// 捕获属性 VigorDef 的值,通过 Vigor 传出
GetCapturedAttributeMagnitude(VigorDef, Spec, EvaluationParameters, Vigor);
// 限制属性为非负
Vigor = FMath::Max<float>(Vigor, 0.f);
// Spec.GetContext().GetSourceObject() 是玩家角色或敌人角色
// 从战斗接口获取玩家等级 玩家状态和敌人角色都实现了战斗接口
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Spec.GetContext().GetSourceObject());
const int32 PlayerLevel = CombatInterface->GetPlayerLevel();
return 80.f + 2.5f * Vigor + 10.f * PlayerLevel;
}
为效果设置源
Source/Aura/Private/Character/AuraCharacterBase.cpp
void AAuraCharacterBase::ApplyEffectToSelf(TSubclassOf<UGameplayEffect> GameplayEffectClass, float Level) const
{
check(IsValid(GetAbilitySystemComponent()));
check(GameplayEffectClass);
FGameplayEffectContextHandle ContextHandle = GetAbilitySystemComponent()->MakeEffectContext();
// 为效果设置源 即 玩家或敌人 之后效果计算时需要源对象
ContextHandle.AddSourceObject(this);
const FGameplayEffectSpecHandle SpecHandle = GetAbilitySystemComponent()->MakeOutgoingSpec(GameplayEffectClass, Level, ContextHandle);
// SpecHandle.Data.Get() Get() 获取效果句柄指针 *星号取出指针地址的值
// 将游戏效果规格应用于目标的技能系统组件
GetAbilitySystemComponent()->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data.Get(), GetAbilitySystemComponent());
}
打开 GE_SecondaryAttribute MaxHealth 的 修改器部分 Magnitude calculation Type 计算类型改为 Custom Calculation Class 执行自定义计算,能够捕获和处理多个属性,无论是BP还是原生。 Calculation Class-MMC_MaxHealth
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.MaxHealth 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Custom Calculation Class 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Custom Magnitude-Calculation Class-MMC_MaxHealth
Custom Magnitude-Coefficient-1 Custom Magnitude-Pre Multiply Additive Value -0 Custom Magnitude-Post Multiply Additive Value-0
最大健康值 112.5
在 C++ AbilitySystem-ModMagCalc 目录新建 基于 GameplayModMagnitudeCalculation
用于通过蓝图或本机代码执行自定义游戏效果修饰符计算的类
Source/Aura/Public/AbilitySystem/ModMagCalc/MMC_MaxMana.h
#pragma once
#include "CoreMinimal.h"
#include "GameplayModMagnitudeCalculation.h"
#include "MMC_MaxMana.generated.h"
UCLASS()
class AURA_API UMMC_MaxMana : public UGameplayModMagnitudeCalculation
{
GENERATED_BODY()
public:
UMMC_MaxMana();
virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
private:
FGameplayEffectAttributeCaptureDefinition IntDef;
};
Source/Aura/Private/AbilitySystem/ModMagCalc/MMC_MaxMana.cpp
#include "AbilitySystem/ModMagCalc/MMC_MaxMana.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "Interaction/CombatInterface.h"
UMMC_MaxMana::UMMC_MaxMana()
{
IntDef.AttributeToCapture = UAuraAttributeSet::GetIntelligenceAttribute();
IntDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
IntDef.bSnapshot = false;
RelevantAttributesToCapture.Add(IntDef);
}
float UMMC_MaxMana::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
// Gather tags from source and target
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
float Int = 0.f;
GetCapturedAttributeMagnitude(IntDef, Spec, EvaluationParameters, Int);
Int = FMath::Max<float>(Int, 0.f);
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Spec.GetContext().GetSourceObject());
const int32 PlayerLevel = CombatInterface->GetPlayerLevel();
return 50.f + 2.5f * Int + 15.f * PlayerLevel;
}
打开 GE_SecondaryAttribute MaxMana 的 修改器部分 Magnitude calculation Type 计算类型改为 Custom Calculation Class 执行自定义计算,能够捕获和处理多个属性,无论是BP还是原生。 Calculation Class-MMC_MaxMana
细节-Gameplay Effect-Modifiers-索引-Attribute-AuraAttributeSet.MaxMana 细节-Gameplay Effect-Modifiers-索引-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Magnitude calculation Type -Custom Calculation Class 细节-Gameplay Effect-Modifiers-索引-Modifier Magnitude-Custom Magnitude-Calculation Class-MMC_MaxMana
Custom Magnitude-Coefficient-1 Custom Magnitude-Pre Multiply Additive Value -0 Custom Magnitude-Post Multiply Additive Value-0
健康值和魔力值是即时效果。 在最大健康值和最大法力已经初始化或至少第一次设置后执行此操作。 将健康值设置为最大健康值,并将你的法力值设置为最大法力值。
Source/Aura/Public/Character/AuraCharacterBase.h
// 重要属性默认值
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Attributes")
TSubclassOf<UGameplayEffect> DefaultVitalAttributes;
Source/Aura/Private/Character/AuraCharacterBase.cpp
void AAuraCharacterBase::InitializeDefaultAttributes() const
{
ApplyEffectToSelf(DefaultPrimaryAttributes, 1.f);
ApplyEffectToSelf(DefaultSecondaryAttributes, 1.f);
ApplyEffectToSelf(DefaultVitalAttributes, 1.f);
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
UAuraAttributeSet::UAuraAttributeSet()
{
}
在 目录 Blueprints-AbilitySystem-GameplayEffects-DefaultAttributes 下 基于 Gameplay Effect 新建 GE_AuraVitalAttributes 蓝图类
打开 GE_AuraVitalAttributes
健康:
细节-Gameplay Effect-Modifiers-索引0-Attribute-AuraAttributeSet.Health 细节-Gameplay Effect-Modifiers-索引0-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.MaxHealth Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
魔力:
细节-Gameplay Effect-Modifiers-索引0-Attribute-AuraAttributeSet.Mana 细节-Gameplay Effect-Modifiers-索引0-Modifier Op-Override 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.MaxMana Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
打开 BP_AuraCharacter Default Vital Attributes-GE_AuraVitalAttributes
运行游戏,健康和魔力进度球都为100%。
在 目录 Blueprint-UI-AttributeMenu 基于 AuraUserWidget-右键-用户界面-控件蓝图 创建 控件蓝图 WBP_FramedValue
打开 WBP_FramedValue 设计器:
添加 尺寸框:SizeBox_Root SizeBox_Root 设置为变量。 模式:desired 所需 宽:80,高:45
图表:参数化 新建变量: BoxWidth 浮点 分组-Frame Properties 默认值-80 BoxHeight 浮点 分组-Frame Properties 默认值-45
拖入节点 SizeBox_Root Set Width Override BoxWidth
拖入节点 SizeBox_Root Set Height Override BoxHeight
折叠到函数 UpdateFrameSize
设计器: 添加 覆层 :Overlay_root 到 SizeBox_Root 添加 image : Image_Background到 覆层 -水平填充,垂直填充 。设置为变量 image-外观-笔刷-图像-MI_FlowingUIBG
图表:参数化 拖入节点: Image_Background Set Brush Set Brush 的 In Brush 拖出提升为变量 BackgroundBrush 分组-Frame Properties BackgroundBrush 分组-Frame Properties 默认值-图像-MI_FlowingUIBG
折叠到函数 UpdateBackgroundBrush
设计器: 准备添加边框 拖入节点: Image :Image_Border到 Overlay_root ,水平填充,垂直填充 , image-外观-笔刷-图像-Border_1_png image-外观-笔刷-绘制为-边界 image-外观-笔刷-边缘-0.5,0.5,0.5,0.5
拖入 Text 文本:TextBlock_Value 到 Overlay_root 内容-文本-99 水品居中对齐,垂直居中对齐 对齐-将文本中对齐 设置为变量
在 目录 Blueprint-UI-AttributeMenu 基于 AuraUserWidget-右键-用户界面-控件蓝图 创建 控件蓝图 WBP_TextValueRow
打开 WBP_TextValueRow
设计器: 添加 尺寸框 :SizeBox_Root 模式-所需 宽度重载 启用 高度重载 启用 设为变量
图表:参数化 新建变量: BoxWidth 浮点 分组-Row Properties 默认值-720 BoxHeight 浮点 分组-Row Properties 默认值-60 点击变量右侧眼睛图表,公开变量
拖入节点 SizeBox_Root Set Width Override BoxWidth
拖入节点 SizeBox_Root Set Height Override BoxHeight
拖入节点 水平框:HorizontalBox 到 SizeBox_Root
拖入节点 文本 到 HorizontalBox 水平向左对齐,垂直居中对齐 内容-文本-Attribute
拖入节点 WBP_FramedValue 到 HorizontalBox 插槽-尺寸-填充 水平向右对齐,垂直居中对齐
拖入节点 间隔区: Space 外观-尺寸-40,1
拖入节点 命名的插槽 Named Slot 到 HorizontalBox 当其他控件继承该控件时,可在插槽处添加自定义控件,例如按钮。
基于 WBP_TextValueRow 创建 控件 WBP_TextValueButtonRow
打开 WBP_TextValueButtonRow 默认已存在部分控件
设计器: 添加控件: 覆层 :Overlay到 插槽
图像:Image_Border : 到 Overlay 外观-笔刷-图像-Button_Border 图像大小-xy-40,40 水平居中对齐,垂直居中对齐
按钮:Button 到 Overlay 水平居中对齐,垂直居中对齐 外观-样式-普通-绘制为-图像 【消除默认的圆角】 图像大小-xy-40,40 外观-样式-普通-图像-Button
外观-样式-已悬停-图像-Button_Highlighted 外观-样式-已悬停-绘制为-图像
外观-样式-已按压-图像-Button_Pressed 外观-样式-已按压-绘制为-图像
外观-样式-已禁用-图像-Button_Grayed_Out 外观-样式-已禁用-绘制为-图像
文本:Text 到 Overlay 插槽-文本- +
水平居中对齐,垂直居中对齐 对齐-将文本中对齐 【将文本右对齐 可能才会真正居中】 轮廓大小-1
在 目录 Blueprint-UI-AttributeMenu 基于 AuraUserWidget-右键-用户界面-控件蓝图 创建 控件蓝图 WBP_AttributeMenu
打开 WBP_AttributeMenu
设计器: 添加控件: 尺寸框:SizeBox_Root 模式=所需 重载宽 805 重载高 960
覆层:Overlay_Root 到 SizeBox_Root
图像:Image_Background 到 Overlay_Root 水平填充,垂直填充 外观-笔刷-图像-MI_FlowingUIBG
图像:Image_Border 到 Overlay_Root 水平居中对齐,垂直居中对齐 外观-笔刷-图像-Border_Large 外观-笔刷-图像-绘制为-Border 边界 外观-笔刷-图像-边缘-0.5
包裹框:Wrap Box 到 Overlay_Root 内部会自动换行 水平居中对齐,垂直居中对齐 插槽-填充-40
文本 :Text 到 Wrap Box
文本-Attributes 外观-文本风格-尺寸-36 外观-文本风格-字间距-400 轮廓大小-1 插槽-填充空白空间-启用 水平居中对齐,垂直填充 插槽-填充-25
文本 内容-主要属性 插槽-小于该值时填充跨度-1000 【使文本换行】 水平居中对齐,垂直填充
添加 间隔区 到 两个文本之间 外观-尺寸-750,20 用以换行
拷贝间隔区 到 Wrap Box
WBP_TextValueRow 到 Wrap Box Row Properties-Box Width-750 Row Properties-Box Height-60
WBP_TextValueButtonRow 到 Wrap Box
尺寸框:SizeBox_Scroll用以限制滚动框大小 水平居中对齐,垂直居中对齐 插槽-子布局-宽度重载-600 插槽-子布局-高度重载-240 插槽-填充空白空间-启用
滚动框 Scroll_Box 到 SizeBox_Scroll 内部控件大小自适应宽度。
WBP_TextValueRow 到 Scroll_Box 10个
关闭 属性框的按钮
添加 尺寸框 SizeBox_Close 到 Overlay_Root 水平向右对齐,垂直向下对齐 插槽-子布局-宽度重载-40 插槽-子布局-高度重载-40
插槽-填充-右边-25 插槽-填充-底部-25
覆层 Overlay_Close 到 SizeBox_Close
图像:Image_CloseButtonBorder 到 Overlay_Close 水平填充,垂直填充 外观-笔刷-图像-Button_Border 绘制为-图像
Button_Close 到 Overlay_Close 水平填充,垂直填充 外观-样式-普通-绘制为-图像 【消除默认的圆角】 图像大小-xy-40,40 外观-样式-普通-图像-Button
外观-样式-已悬停-图像-Button_Highlighted 外观-样式-已悬停-绘制为-图像
外观-样式-已按压-图像-Button_Pressed 外观-样式-已按压-绘制为-图像
外观-样式-已禁用-图像-Button_Grayed_Out 外观-样式-已禁用-绘制为-图像
文本 Text_Close 到 Button_Close 文本必须添加到按钮的子级,否则会出现无法准确点击按钮的问题。
内容-文本-X 将文本中对齐
公开全部属性变量
在 目录 Blueprint-UI-Button 下 基于 AuraUserWidget-右键-用户界面-控件蓝图 创建 控件蓝图 WBP_Button
打开 WBP_Button
设计器:
打开 WBP_Overlay
尺寸框 SizeBox_Root 重载宽高:40
图表:参数化 新建变量: BoxWidth 浮点 分类-Button Propertites 默认值-40 BoxHeight 浮点 分类-Button Propertites 默认值-40 点击变量右侧眼睛图表,公开变量
拖入节点 SizeBox_Root Set Width Override BoxWidth
拖入节点 SizeBox_Root Set Height Override BoxHeight
折叠到函数 UpdateFrameSize
设计器: 模式-所需
覆层 Overlay_Root
Image_Border 到 Overlay_Root 设置为变量 外观-笔刷-Button_Border 水平填充,垂直填充
Button 到 Overlay_Root 水平填充,垂直填充 设置为变量
Text 到 Overlay_Root 设置为变量
图表: Image_Border Set Brush BorderBrush :图像-Button_Border 分类-Button Propertites
折叠到函数 UpdateBorderBrush
Button Set Style Make ButtonStyle Make ButtonStyle 拖出提升为变量: ButtonNormalBrush 分类-Button Propertites 默认图像-Button ButtonHoveredBrush 分类-Button Propertites 默认图像-Button_Highlighted ButtonPressedBrush 分类-Button Propertites 默认图像-Button_Pressed ButtonDisabledBrush 分类-Button Propertites 默认图像-Button_Grayed_Out
折叠导函数 UpdateButtonBrushes
Text Set Text Box Text 分类-Button Propertites 默认X 默认值 X
Text Set Font Make SlateFontInfo
Make FontOutlineSetting
Font Family- 默认 roboto Outline Size- FontSize Letter Spacing
折叠到函数 UpdateText
删除 SizeBox_Close 下的 全部子节点 添加 WBP_Button 到 SizeBox_Close
打开 WBP_TextValueButtonRow 删除插槽全部节点,替换为 WBP_Button
设置 WBP_TextValueButtonRow中的 WBP_Button-Button Propertites-Box Text-+
基于 WBP_Button 创建控件蓝图 WBP_WideButton 打开 WBP_WideButton
图表: BoxWidth 默认值 200
BoxHeight 默认值 65
BorderBrush 默认值 清除留空,着色-透明
ButtonNormalBrush 默认图像-WideButton ButtonHoveredBrush 默认图像-WideButton_Highlighted ButtonPressedBrush 默认图像-WideButton_Pressed_2 ButtonDisabledBrush 默认图像-WideButton_GrayedOut
ButtonText 默认文字 按钮
打开 WBP_Overlay 添加 WBP_WideButton 名称 AttributeMenuButton
WBP_Overlay - AttributeMenuButton -Button Propertites- Button Text- 属性
打开 WBP_Overlay
AttributeMenuButton 设置为变量
图表: Event Construct 事件
拖出 AttributeMenuButton AttributeMenuButton 拖出 get button 获取其中的按钮部分 get button 的 button 输出 assign on clicked
添加自定义事件 add custom event - AttributeMenuButtonClicked OnClicked_Event 调用 AttributeMenuButtonClicked 函数 拖出 AttributeMenuButton AttributeMenuButton 拖出 get button 获取其中的按钮部分 get button 的 button 输出 控件-set is enabled
AttributeMenuButtonClicked 事件内禁用按钮,即 界面的属性按钮点击时,属性按钮自身被禁用。
set is enabled 调用 create widget create widget -class 选择 WBP_AttributeMenu 自动变为 create WBP Attribute Menu widget
get player controller 输出至 create WBP Attribute Menu widget - owning player create WBP Attribute Menu widget 输出 add to viewport 这将在点击 属性 按钮时 ,显示 属性控件 WBP_AttributeMenu
打开 WBP_AttributeMenu
Overlay_Root 重命名为 Overlay_Box
SizeBox_Root -右键-包裹为 覆层:Overlay_Root
此时属性控件不在缩放至全屏了 覆层可以保证子级控件正确的尺寸。
Image_Background -填充-5
图表: create WBP Attribute Menu widget 输出 set position viewport set position viewport 的 position:25,25
WBP Button 改为 CloseButton 设为变量
图表: Event Construct 事件: 拖出 CloseButton CloseButton 输出 get button
get button 的 button 输出 assign on clicked
OnClicked_事件 调用 remove from parent
属性菜单不应依赖覆盖层。 因为覆盖层依赖属性菜单。 否则会循环依赖。
覆盖层可以创建事件委托。
打开 WBP_AttributeMenu 图表: 事件分发器-创建一个事件委托-AttributeMenuClosed
每当销毁属性菜单时,广播这个事件。
添加事件 event destruct [控件销毁事件]
event destruct 调用 call Attribute Menu Closed 委托
订阅 call Attribute Menu Closed 委托 的其他蓝图都会获得通知 例如 属性控件广播自身销毁关闭。 覆盖层订阅该关闭委托。启用属性按钮。
打开 WBP_Overlay
create WBP Attribute Menu widget 输出 assign Attribute Menu Closed create WBP Attribute Menu widget 输出了 属性控件。 覆盖层在属性控件上订阅属性控件关闭事件,随时接受通知后响应,将覆盖层的属性按钮启用。
拖出 Attribute Menu Button Attribute Menu Button 输出 get button
get button 的 button 输出 set is enabled 启用
动态创建,销毁控件性能消耗不高。 也可以隐藏控件,但是这会在后台保留点击事件,不推荐。
可以在空间控制器中广播属性变更,例如健康值变更。 然后对应 的控件接受广播,更新UI。 但是属性过多,每次添加新属性时,都需要修改控件控制器和控件。 所以不推荐此方法。
推荐方式: 控件控制器广播一个包含属性,描述等等多种信息的通用结构类型的游戏标签。 控件订阅该委托,检索游戏标签是否包含自身所需的标签。 控件已绑定特有标签。 1.创建次要属性Gameplay标签 (在C++中更好地处理标签) 2创建属性数据资产 3.创建FAuraAttributelnfo结构 4.填写每个属性的数据资产 5.创建UAttributeMenuWidgetControlle
集中存取标签。
基于 C++ None/无 创建 C++ 游戏标签单例 AuraGameplayTags
该单例标签为结构体
Source/Aura/Public/AuraGameplayTags.h
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
/**
* AuraGameplayTags
*
* Singleton containing native Gameplay Tags
*/
// 游戏标签结构体 单例
struct FAuraGameplayTags
{
public:
// 返回游戏标签的唯一实例
static const FAuraGameplayTags& Get() { return GameplayTags;}
static void InitializeNativeGameplayTags();
protected:
private:
static FAuraGameplayTags GameplayTags;
};
Source/Aura/Private/AuraGameplayTags.cpp
#include "AuraGameplayTags.h"
#include "GameplayTagsManager.h"
FAuraGameplayTags FAuraGameplayTags::GameplayTags;
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
// 获取游戏标签管理器,Get() 返回唯一的游戏标签管理器
UGameplayTagsManager::Get()
// 添加原生游戏标签 参数1-标签 参数2-注释
.AddNativeGameplayTag(FName("Attributes.Secondary.Armor"),
FString("Reduces damage taken, improves Block Chance 减少受到的伤害,提高格挡几率"));
}
https://docs.unrealengine.com/5.3/zh-CN/asset-management-in-unreal-engine/
资产加载与卸载
基于 Asset Manager 创建 C++ AuraAssetManager
Source/Aura/Public/AuraAssetManager.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/AssetManager.h"
#include "AuraAssetManager.generated.h"
UCLASS()
class AURA_API UAuraAssetManager : public UAssetManager
{
GENERATED_BODY()
public:
// 唯一的资产管理器引用
static UAuraAssetManager& Get();
protected:
// 开始加载游戏资产
virtual void StartInitialLoading() override;
};
Source/Aura/Private/AuraAssetManager.cpp
#include "AuraAssetManager.h"
#include "AuraGameplayTags.h"
// 返回资产管理器
UAuraAssetManager& UAuraAssetManager::Get()
{
check(GEngine);
UAuraAssetManager* AuraAssetManager = Cast<UAuraAssetManager>(GEngine->AssetManager);
return *AuraAssetManager;
}
void UAuraAssetManager::StartInitialLoading()
{
Super::StartInitialLoading();
// 开始加载游戏资产时初始化游戏标签
FAuraGameplayTags::InitializeNativeGameplayTags();
}
Aura\Config\DefaultEngine.ini
在 [/Script/Engine.Engine]
处设置资产管理器 AssetManagerClassName=/Script/Aura.AuraAssetManager
[/Script/Engine.Engine]
+ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/Aura")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/Aura")
AssetManagerClassName=/Script/Aura.AuraAssetManager
编辑-项目设置-项目-GameplayTags-管理Gameplay标签- attributes-Secondary-Armor 上,将提示:Native 这是在C++资产管理器中初始化的游戏标签。是由C++原生定义的标签。 其他标签没有 Native 提示,不是有C++原生定义。
这将使这些标签失效,之后通过标签管理器加载标签。 在C++ AbilityActorInfoSet 设置技能Actor时,加载主属性游戏标签。
Source/Aura/Public/AuraGameplayTags.h
public:
// 用下划线代替标签的点
FGameplayTag Attributes_Secondary_Armor;
将C++添加的原生标签 存储在标签变量中
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
// 获取游戏标签管理器,Get() 返回唯一的游戏标签管理器
GameplayTags.Attributes_Secondary_Armor = UGameplayTagsManager::Get()
// 添加原生游戏标签 参数1-标签 参数2-注释
.AddNativeGameplayTag(FName("Attributes.Secondary.Armor"),
FString("Reduces damage taken, improves Block Chance 减少受到的伤害,提高格挡几率"));
}
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
#include "AuraGameplayTags.h"
void UAuraAbilitySystemComponent::AbilityActorInfoSet()
{
// 为游戏效果应用委托绑定函数 EffectApplied
OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAuraAbilitySystemComponent::EffectApplied);
// 获取游戏标签管理器
// 从 标签管理器 获取游戏标签
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
GEngine->AddOnScreenDebugMessage(
-1,
10.f,
FColor::Orange,
FString::Printf(TEXT("Tag: %s"), *GameplayTags.Attributes_Secondary_Armor.ToString())
);
}
每个玩家或敌人实例均会获取到 Armor 标签,然后打印。
删除项目设置中的MaxHealth MaxMana 标签
C++ 中设置标签
Source/Aura/Public/AuraGameplayTags.h
public:
// 用下划线代替标签的点
FGameplayTag Attributes_Primary_Strength;
FGameplayTag Attributes_Primary_Intelligence;
FGameplayTag Attributes_Primary_Resilience;
FGameplayTag Attributes_Primary_Vigor;
FGameplayTag Attributes_Secondary_Armor;
FGameplayTag Attributes_Secondary_ArmorPenetration;
FGameplayTag Attributes_Secondary_BlockChance;
FGameplayTag Attributes_Secondary_CriticalHitChance;
FGameplayTag Attributes_Secondary_CriticalHitDamage;
FGameplayTag Attributes_Secondary_CriticalHitResistance;
FGameplayTag Attributes_Secondary_HealthRegeneration;
FGameplayTag Attributes_Secondary_ManaRegeneration;
FGameplayTag Attributes_Secondary_MaxHealth;
FGameplayTag Attributes_Secondary_MaxMana;
Source/Aura/Private/AuraGameplayTags.cpp
#include "AuraGameplayTags.h"
#include "GameplayTagsManager.h"
FAuraGameplayTags FAuraGameplayTags::GameplayTags;
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
// 添加原生标签
/*
* Primary Attributes
*/
// 获取游戏标签管理器,Get() 返回唯一的游戏标签管理器
// 添加原生游戏标签 参数1-标签 参数2-注释
GameplayTags.Attributes_Primary_Strength = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Primary.Strength"),
FString("Increases physical damage 力量-增加物理伤害")
);
GameplayTags.Attributes_Primary_Intelligence = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Primary.Intelligence"),
FString("Increases magical damage 智力-增加魔法伤害")
);
GameplayTags.Attributes_Primary_Resilience = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Primary.Resilience"),
FString("Increases Armor and Armor Penetration 韧性-增加护甲和护甲穿透")
);
GameplayTags.Attributes_Primary_Vigor = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Primary.Vigor"),
FString("Increases Health 活力-增加生命值")
);
/*
* Secondary Attributes
*/
GameplayTags.Attributes_Secondary_Armor = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.Armor"),
FString("Reduces damage taken, improves Block Chance 护甲-减少受到的伤害,提高格挡几率")
);
GameplayTags.Attributes_Secondary_ArmorPenetration = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.ArmorPenetration"),
FString("Ignores Percentage of enemy Armor, increases Critical Hit Chance 护甲穿透-忽略敌方护甲百分比,增加暴击几率")
);
GameplayTags.Attributes_Secondary_BlockChance = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.BlockChance"),
FString("Chance to cut incoming damage in half 格挡几率-将受到的伤害减半的几率")
);
GameplayTags.Attributes_Secondary_CriticalHitChance = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.CriticalHitChance"),
FString("Chance to double damage plus critical hit bonus 暴击几率-有机会获得双倍伤害加暴击伤害加成")
);
GameplayTags.Attributes_Secondary_CriticalHitDamage = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.CriticalHitDamage"),
FString("Bonus damage added when a critical hit is scored 暴击伤害-获得暴击时增加的额外伤害")
);
GameplayTags.Attributes_Secondary_CriticalHitResistance = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.CriticalHitResistance"),
FString("Reduces Critical Hit Chance of attacking enemies 暴击抗性-降低敌人攻击的暴击几率")
);
GameplayTags.Attributes_Secondary_HealthRegeneration = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.HealthRegeneration"),
FString("Amount of Health regenerated every 1 second 健康回复-每1秒再生的生命值")
);
GameplayTags.Attributes_Secondary_ManaRegeneration = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.ManaRegeneration"),
FString("Amount of Mana regenerated every 1 second 魔力回复-每秒再生的魔法量")
);
GameplayTags.Attributes_Secondary_MaxHealth = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.MaxHealth"),
FString("Maximum amount of Health obtainable 最大健康值-可获得的最大健康量")
);
GameplayTags.Attributes_Secondary_MaxMana = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.MaxMana"),
FString("Maximum amount of Mana obtainable 最大魔力值-可获得的最大魔法量")
);
}
此时,项目中可以看到这些标签,都有 Native 提示 蓝图中也可以使用
去除调试信息
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::AbilityActorInfoSet()
{
// 为游戏效果应用委托绑定函数 EffectApplied
OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAuraAbilitySystemComponent::EffectApplied);
}
基于 DataAsset 新建 C++ AttributeInfo
信息数据资产可以存储在蓝图中
存储属性信息的结构 用以在属性变化时,将此属性结构广播到控件蓝图中 控件蓝图以此更新自身信息
Source/Aura/Public/AbilitySystem/Data/AttributeInfo.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "AttributeInfo.generated.h"
// 存储属性信息的结构
// 用以在属性变化时,将此属性结构广播到控件蓝图中
// 控件蓝图以此更新自身信息
USTRUCT(BlueprintType)
struct FAuraAttributeInfo
{
GENERATED_BODY()
// 标签
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag AttributeTag = FGameplayTag();
// 标签在控件UI中的显示名称:健康等
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FText AttributeName = FText();
// 属性描述
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FText AttributeDescription = FText();
// 属性值
UPROPERTY(BlueprintReadOnly)
float AttributeValue = 0.f;
};
/**
*
*/
UCLASS()
class AURA_API UAttributeInfo : public UDataAsset
{
GENERATED_BODY()
public:
FAuraAttributeInfo FindAttributeInfoForTag(const FGameplayTag& AttributeTag, bool bLogNotFound = false) const;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TArray<FAuraAttributeInfo> AttributeInformation;
};
Source/Aura/Private/AbilitySystem/Data/AttributeInfo.cpp
#include "AbilitySystem/Data/AttributeInfo.h"
FAuraAttributeInfo UAttributeInfo::FindAttributeInfoForTag(const FGameplayTag& AttributeTag, bool bLogNotFound) const
{
for (const FAuraAttributeInfo& Info : AttributeInformation)
{
if (Info.AttributeTag.MatchesTagExact(AttributeTag))
{
return Info;
}
}
if (bLogNotFound)
{
UE_LOG(LogTemp, Error, TEXT("Can't find Info for AttributeTag [%s] on AttributeInfo [%s]."),
*AttributeTag.ToString(), *GetNameSafe(this));
}
return FAuraAttributeInfo();
}
创建一个简单的资产,在该类的实例中存储与特定系统相关的数据资产可以在内容浏览器中使用任何继承自它的原生类来创建。如果你希望拥有数据继承性或一个复杂的层级,则应该创建纯数据蓝图类
在目录 Blueprints-AbilitySystem-Data 右键-其他-数据资产-AttributeInfo 【C++中创建的类】 名称:DA_AttributeInfo
打开 DA_AttributeInfo AttributeValue属性值未公开在编辑器,只能在蓝图中可读。
添加属性 Intelligence
Attribute Tag-Attributes.Primary.Intelligence Attribute Name-智力 Attribute Description-增加魔法伤害
依次添加所有主要属性,辅助属性, Attributes.Primary.Strength; 力量 增加物理伤害 Attributes.Primary.Intelligence; 智力 增加魔法伤害 Attributes.Primary.Resilience; 韧性 增加护甲和护甲穿透 Attributes.Primary.Vigor; 活力 增加生命值
Attributes.Secondary.Armor; 护甲 减少受到的伤害,提高阻挡几率 Attributes.Secondary.ArmorPenetration; 护甲穿透 忽略敌方护甲百分比,增加暴击几率 Attributes.Secondary.BlockChance; 格挡几率 将受到的伤害减半的几率 Attributes.Secondary.CriticalHitChance; 暴击几率 有机会获得双倍伤害加暴击伤害加成 Attributes.Secondary.CriticalHitDamage; 暴击伤害 获得暴击时增加的额外伤害 Attributes.Secondary.CriticalHitResistance; 暴击抗性 降低敌人攻击的暴击几率 Attributes.Secondary.HealthRegeneration; 健康回复 每1秒再生的生命值 Attributes.Secondary.ManaRegeneration; 魔力回复 每秒再生的魔法量 Attributes.Secondary.MaxHealth; 最大健康值 可获得的最大健康量 Attributes.Secondary.MaxMana; 最大魔力值 可获得的最大魔法量
基于 C++ AuraWidgetController 类创建 属性菜单控件控制器 C++ AttributeMenuWidgetController
Source/Aura/Public/UI/WidgetController/AttributeMenuWidgetController.h
#pragma once
#include "CoreMinimal.h"
#include "UI/WidgetController/AuraWidgetController.h"
#include "AttributeMenuWidgetController.generated.h"
UCLASS(BlueprintType, Blueprintable)
class AURA_API UAttributeMenuWidgetController : public UAuraWidgetController
{
GENERATED_BODY()
public:
virtual void BindCallbacksToDependencies() override;
virtual void BroadcastInitialValues() override;
};
Source/Aura/Private/UI/WidgetController/AttributeMenuWidgetController.cpp
#include "UI/WidgetController/AttributeMenuWidgetController.h"
void UAttributeMenuWidgetController::BindCallbacksToDependencies()
{
}
void UAttributeMenuWidgetController::BroadcastInitialValues()
{
}
Aura技能系统蓝图库 作为一个单例 用以访问控件控制器
基于 C++ BlueprintFunctionLibrary 新建 C++ AuraAbilitySystemLibrary
静态函数可以直接调用. 因为静态函数所属的类本身可能不存在于世界上。 静态函数无法访问世界上存在的任何对象。 所以需要一个世界上下文对象。
BlueprintPure: 纯函数蓝图。 它不需要执行引脚或类似的东西。它只是执行某种操作并返回结果。
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "AuraAbilitySystemLibrary.generated.h"
class UOverlayWidgetController;
UCLASS()
class AURA_API UAuraAbilitySystemLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
// 返回覆盖控件控制器
// 静态函数可以直接调用.
// 因为静态函数所属的类本身可能不存在于世界上。
// 静态函数无法访问世界上存在的任何对象。
// 所以需要一个世界上下文对象。
// BlueprintPure:纯函数蓝图。它不需要执行引脚或类似的东西。它只是执行某种操作并返回结果。
UFUNCTION(BlueprintPure, Category="AuraAbilitySystemLibrary|WidgetController")
static UOverlayWidgetController* GetOverlayWidgetController(const UObject* WorldContextObject);
};
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "UI/WidgetController/AuraWidgetController.h"
#include "Player/AuraPlayerState.h"
#include "UI/HUD/AuraHUD.h"
UOverlayWidgetController* UAuraAbilitySystemLibrary::GetOverlayWidgetController(const UObject* WorldContextObject)
{
if (APlayerController* PC = UGameplayStatics::GetPlayerController(WorldContextObject, 0))
{
if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(PC->GetHUD()))
{
AAuraPlayerState* PS = PC->GetPlayerState<AAuraPlayerState>();
UAbilitySystemComponent* ASC = PS->GetAbilitySystemComponent();
UAttributeSet* AS = PS->GetAttributeSet();
const FWidgetControllerParams WidgetControllerParams(PC, PS, ASC, AS);
// 返回唯一的控件控制器,如果不存在则构造新的
return AuraHUD->GetOverlayWidgetController(WidgetControllerParams);
}
}
return nullptr;
}
打开 WBP_Overlay 控件
图表: 右键-AuraAbilitySystemLibrary
添加 this 节点-get a refference to self 表示世界变量
仅做测试,删除。
必须的参数:
属性菜单 控件变量 属性菜单 控件class 属性菜单 控件控制器变量 属性菜单 控件控制器class
Source/Aura/Public/UI/HUD/AuraHUD.h
class UAttributeMenuWidgetController;
public:
// 构造 属性菜单 控件控制器
UAttributeMenuWidgetController* GetAttributeMenuWidgetController(const FWidgetControllerParams& WCParams);
private:
// 改为私有变量
UPROPERTY()
TObjectPtr<UAuraUserWidget> OverlayWidget;
// 属性菜单 控件控制器变量
UPROPERTY()
TObjectPtr<UAttributeMenuWidgetController> AttributeMenuWidgetController;
// 属性菜单 控件控制器class
UPROPERTY(EditAnywhere)
TSubclassOf<UAttributeMenuWidgetController> AttributeMenuWidgetControllerClass;
Source/Aura/Private/UI/HUD/AuraHUD.cpp
#include "UI/WidgetController/AttributeMenuWidgetController.h"
UAttributeMenuWidgetController* AAuraHUD::GetAttributeMenuWidgetController(const FWidgetControllerParams& WCParams)
{
if (AttributeMenuWidgetController == nullptr)
{
AttributeMenuWidgetController = NewObject<UAttributeMenuWidgetController>(this, AttributeMenuWidgetControllerClass);
AttributeMenuWidgetController->SetWidgetControllerParams(WCParams);
AttributeMenuWidgetController->BindCallbacksToDependencies();
}
return AttributeMenuWidgetController;
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
class UAttributeMenuWidgetController;
public:
// 返回属性菜单控件控制器
UFUNCTION(BlueprintPure, Category="AuraAbilitySystemLibrary|WidgetController")
static UAttributeMenuWidgetController* GetAttributeMenuWidgetController(const UObject* WorldContextObject);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
UAttributeMenuWidgetController* UAuraAbilitySystemLibrary::GetAttributeMenuWidgetController(
const UObject* WorldContextObject)
{
if (APlayerController* PC = UGameplayStatics::GetPlayerController(WorldContextObject, 0))
{
if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(PC->GetHUD()))
{
AAuraPlayerState* PS = PC->GetPlayerState<AAuraPlayerState>();
UAbilitySystemComponent* ASC = PS->GetAbilitySystemComponent();
UAttributeSet* AS = PS->GetAttributeSet();
const FWidgetControllerParams WidgetControllerParams(PC, PS, ASC, AS);
return AuraHUD->GetAttributeMenuWidgetController(WidgetControllerParams);
}
}
return nullptr;
}
在目录 Blueprint-UI-WidgetController
基于 AttributeMenuWidgetController 类创建 属性控件控制器蓝图类
如果查找不到 AttributeMenuWidgetController 类,需要为类AttributeMenuWidgetController 声明UCLASS(BlueprintType, Blueprintable)
可蓝图化
打开 BP_AuraHUD 细节-AuraHUD-AttributeMenuWidgetControllerClass-BP_AttributeMenuWidgetController
打开 WBP_AttributeMenu
图表: 右键-AttributeMenuWidgetController 添加 this 节点-get a refference to self 表示世界变量
set widget controller 用来存储属性菜单控件控制器
get widget controller 就可以获取属性菜单控件控制器
AuraHUD 中包含 变量 AttributeMenuWidgetController,蓝图函数库通过获取 AuraHUD,在其中在获取 AttributeMenuWidgetController。 控件控制器将使用 BindCallbacksToDependencies绑定技能系统组件,玩家状态等变量。
控件控制器准备信息并广播属性信息资产AttributeInfo中的值 。 属性控件可接受该值。
Source/Aura/Public/UI/WidgetController/AttributeMenuWidgetController.h
class UAttributeInfo;
struct FAuraAttributeInfo;
// 属性信息动态多播委托
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAttributeInfoSignature, const FAuraAttributeInfo&, Info);
public:
// 属性委托
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FAttributeInfoSignature AttributeInfoDelegate;
protected:
// 属性信息资产
UPROPERTY(EditDefaultsOnly)
TObjectPtr<UAttributeInfo> AttributeInfo;
Source/Aura/Private/UI/WidgetController/AttributeMenuWidgetController.cpp
#include "UI/WidgetController/AttributeMenuWidgetController.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "AbilitySystem/Data/AttributeInfo.h"
#include "AuraGameplayTags.h"
void UAttributeMenuWidgetController::BindCallbacksToDependencies()
{
}
void UAttributeMenuWidgetController::BroadcastInitialValues()
{
// 属性集
UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
// 检查属性信息资产在蓝图中是否设置
check(AttributeInfo);
// 从 属性信息资产 中查找力量标签变量 后 获取力量标签
FAuraAttributeInfo Info = AttributeInfo->FindAttributeInfoForTag(FAuraGameplayTags::Get().Attributes_Primary_Strength);
// 将技能系统组件的力量的值赋予属性信息的力量 蓝图中未设置力量值
Info.AttributeValue = AS->GetStrength();
AttributeInfoDelegate.Broadcast(Info);
}
使广播函数可被蓝图调用
Source/Aura/Public/UI/WidgetController/AuraWidgetController.h
public:
UFUNCTION(BlueprintCallable)
virtual void BroadcastInitialValues();
打开 BP_AttributeMenuWidgetController AttributeInfo-DA_AttributeInfo
之后需要为属性菜单空间的每一个属性绑定标签
设计器: 重命名标签文本节点为 TextBlock_label TextBlock_label 设置为变量
图表: 左侧新建函数 SetLabelText
SetLabelText 函数 细节-输入-添加输入 LabelText 文本类型
拖出 TextBlock_label 拖出 SetText(Text)
SetLabelText 的 LabelText 输出至 SetText(Text) 的 In Text
WBP_TextValueButtonRow 继承自 WBP_TextValueRow ,也有设置标签名称函数 SetLabelText
打开 WBP_TextValueButtonRow 图表: 右键-AttributeMenuWidgetController 添加 this 节点-get a refference to self 表示世界变量
从 get Attribute Menu Widget Controller 的输出 拖出 assign Attribute Info Delegate [订阅控件控制器中定义的委托]
Attribute Info Delegate_事件 的 Info 输出 拖出 break AuraAttribute
收到广播时,设置WBP_TextValueButtonRow标签控件名称。 名称来自广播的属性信息的 Attribute Name 字段
添加 SetLabelText
打开 WBP_AttributeMenu 图表: 在 get Attribute Menu Widget Controller 拖出 Broadcast Initial Values 函数 当前该函数只广播了力量信息作为示例。
现在打开菜单时,将显示力量标签名
打开 WBP_TextValueRow
可设置小数或整数,2个版本。
打开 WBP_TextValueButtonRow 图表: 左侧新建函数 SetNumericalValueInt SetNumericalValueInt -细节-输入 添加输入参数 Value 整数类型
拖出 WBP_FramedValue WBP_FramedValue 拖出 TextBlock_Value TextBlock_Value 拖出 控件-setText(text)
SetLabelText 函数 细节-输入-添加输入 LabelText 文本类型
拖出 TextBlock_label
SetNumericalValueInt 的 value 拖出 to text (interger)
打开 WBP_TextValueButtonRow 图表: Set Numerical Value Int 函数
力量值已正确设置
打开 WBP_TextValueRow 图表: 左侧添加变量-AttributeTag 类型:gameplay标签 公开此变量 使其可被外部编辑
打开 WBP_AttributeMenu
设计器:
将主要属性区域的每个 WBP_TextValueButtonRow 控件重明名为对应的属性名称: Row_Strength Row_Intelligence Row_Resilience Row_Vigor
将辅助属性区域的每个 WBP_TextValueRow 控件重明名为对应的属性名称: Row_Armor Row_ArmorPenetration Row_BlockChance Row_CriticalHitChance Row_CriticalHitDamage Row_CriticalHitResistance Row_HealthRegeneration Row_ManaRegeneration Row_MaxHealth Row_MaxMana
设置为变量。
图表: 流程控制-sequence
左侧添加函数 SetAttributeTags
拖出 SetAttributeTags
SetAttributeTags 函数图表: 拖出主要属性和辅助属性的控件变量 Row_Strength Row_Intelligence Row_Resilience Row_Vigor
Row_Armor Row_ArmorPenetration Row_BlockChance Row_CriticalHitChance Row_CriticalHitDamage Row_CriticalHitResistance Row_HealthRegeneration Row_ManaRegeneration Row_MaxHealth Row_MaxMana
流程控制-sequence
Row_Strength 拖出 变量-默认-set attribute tag set attribute tag 的 attribute tag 选择 Attribute.Primary.Strength
为其他控件执行同样操作
图表
break aureAttributeInfo 的 Attribute Tag 拖出 Matches Tag
变量-默认-attribute tag
branch
如果当前WBP_TextValueButtonRow 的标签属性等于 从广播接受的标签,那么使用接受的标签设置标签名称和值。
当前只广播了力量属性信息。所以属性菜单只有力量栏显示正确。
WBP_TextValueButtonRow
打开 WBP_TextValueRow 图表: event construct 事件: 右键-AttributeMenuWidgetController 添加 this 节点-get a refference to self 表示世界变量
从 get Attribute Menu Widget Controller 的输出 拖出 assign Attribute Info Delegate [订阅控件控制器中定义的委托]
Attribute Info Delegate_事件 的 Info 输出 拖出 break AuraAttribute
当前只广播了力量属性信息。所以属性菜单只有力量栏显示正确。
从属性信息资产获取所有标签,广播到控件。
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
// 模板别名
// 一个模板,所以本质上是一个能够存储函数地址的函数指针
// typedef特定于FGameplayAttribute()签名,但TStaticFunPtr对所选的任何签名都是通用的
// typedef is specific to the FGameplayAttribute() signature, but TStaticFunPtr is generic to any signature chosen
//
// typedef TBaseStaticDelegateInstance<FGameplayAttribute(), FDefaultDelegateUserPolicy>::FFuncPtr FAttributeFuncPtr;
template<class T>
using TStaticFuncPtr = typename TBaseStaticDelegateInstance<T, FDefaultDelegateUserPolicy>::FFuncPtr;
public:
// 将标签映射到属性
TMap<FGameplayTag, TStaticFuncPtr<FGameplayAttribute()>> TagsToAttributes;
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
#include "AuraGameplayTags.h"
UAuraAttributeSet::UAuraAttributeSet()
{
//
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Strength, GetStrengthAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Intelligence, GetIntelligenceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Resilience, GetResilienceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Vigor, GetVigorAttribute);
}
Source/Aura/Private/UI/WidgetController/AttributeMenuWidgetController.cpp
void UAttributeMenuWidgetController::BroadcastInitialValues()
{
// 属性集
UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
// 检查属性信息资产在蓝图中是否设置
check(AttributeInfo);
for (auto& Pair : AS->TagsToAttributes)
{
// 从 属性信息资产 中查找某属性标签变量 后 获取某属性标签
FAuraAttributeInfo Info = AttributeInfo->FindAttributeInfoForTag(Pair.Key);
// 将技能系统组件的某属性的值赋予属性信息的某属性 蓝图中未设置某属性值
Info.AttributeValue = Pair.Value().GetNumericValue(AS);
AttributeInfoDelegate.Broadcast(Info);
}
}
Source/Aura/Public/UI/WidgetController/AttributeMenuWidgetController.h
private:
//
void BroadcastAttributeInfo(const FGameplayTag& AttributeTag, const FGameplayAttribute& Attribute) const;
Source/Aura/Private/UI/WidgetController/AttributeMenuWidgetController.cpp
#include "UI/WidgetController/AttributeMenuWidgetController.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "AbilitySystem/Data/AttributeInfo.h"
void UAttributeMenuWidgetController::BindCallbacksToDependencies()
{
UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
check(AttributeInfo);
for (auto& Pair : AS->TagsToAttributes)
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Pair.Value()).AddLambda(
[this, Pair](const FOnAttributeChangeData& Data)
{
BroadcastAttributeInfo(Pair.Key, Pair.Value());
}
);
}
}
void UAttributeMenuWidgetController::BroadcastInitialValues()
{
// 属性集
UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
// 检查属性信息资产在蓝图中是否设置
check(AttributeInfo);
for (auto& Pair : AS->TagsToAttributes)
{
BroadcastAttributeInfo(Pair.Key, Pair.Value());
}
}
void UAttributeMenuWidgetController::BroadcastAttributeInfo(const FGameplayTag& AttributeTag, const FGameplayAttribute& Attribute) const
{
// 从 属性信息资产 中查找某属性标签变量 后 获取某属性标签
FAuraAttributeInfo Info = AttributeInfo->FindAttributeInfoForTag(AttributeTag);
// 将技能系统组件的某属性的值赋予属性信息的某属性 蓝图中未设置某属性值
Info.AttributeValue = Attribute.GetNumericValue(AttributeSet);
AttributeInfoDelegate.Broadcast(Info);
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
UAuraAttributeSet::UAuraAttributeSet()
{
//
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Strength, GetStrengthAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Intelligence, GetIntelligenceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Resilience, GetResilienceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Vigor, GetVigorAttribute);
/* Secondary Attributes */
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_Armor, GetArmorAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_ArmorPenetration, GetArmorPenetrationAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_BlockChance, GetBlockChanceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_CriticalHitChance, GetCriticalHitChanceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_CriticalHitResistance, GetCriticalHitResistanceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_CriticalHitDamage, GetCriticalHitDamageAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_HealthRegeneration, GetHealthRegenerationAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_ManaRegeneration, GetManaRegenerationAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_MaxHealth, GetMaxHealthAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_MaxMana, GetMaxManaAttribute);
}
现在可显示所有属性,并且实时更新属性值。
游戏技能: 定义技能或技能的类
必须被授予 -在服务器上授予 -Spec复制到拥有客户端
必须激活才能使用
成本和冷却时间
技能异步运行 -一次多个活动
技能任务 -异步执行操作xin
目录 Public/AbilitySystem/Abilities
Source/Aura/Public/AbilitySystem/Abilities/AuraGameplayAbility.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "AuraGameplayAbility.generated.h"
UCLASS()
class AURA_API UAuraGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
};
Source/Aura/Private/AbilitySystem/Abilities/AuraGameplayAbility.cpp
#include "AbilitySystem/Abilities/AuraGameplayAbility.h"
从游戏一开始就应该赋予特定技能
Source/Aura/Public/Character/AuraCharacterBase.h
class UGameplayAbility;
public:
// 添加技能
void AddCharacterAbilities();
private:
// 这些将是从游戏一开始就应该赋予的技能列表
UPROPERTY(EditAnywhere, Category = "Abilities")
TArray<TSubclassOf<UGameplayAbility>> StartupAbilities;
只能由服务器端添加节能
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "AbilitySystem/AuraAbilitySystemComponent.h"
void AAuraCharacterBase::AddCharacterAbilities()
{
UAuraAbilitySystemComponent* AuraASC = CastChecked<UAuraAbilitySystemComponent>(AbilitySystemComponent);
// 只能由服务器端添加节能
if (!HasAuthority()) return;
AuraASC->AddCharacterAbilities(StartupAbilities);
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
// 添加启动就拥有的技能
void AddCharacterAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupAbilities);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
// 添加技能
void UAuraAbilitySystemComponent::AddCharacterAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupAbilities)
{
for (TSubclassOf<UGameplayAbility> AbilityClass : StartupAbilities)
{
// 为每个技能类创建一个技能规格 暂时使用技能等级1
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
//GiveAbility(AbilitySpec);
// 赋予该技能并立即激活它。
GiveAbilityAndActivateOnce(AbilitySpec);
}
}
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// 服务端 初始技能参与者信息
// Init ability actor info for the Server
InitAbilityActorInfo();
// 服务端 此时添加初始技能
AddCharacterAbilities();
}
在目录 Blueprints-AbilitySystem-GameplayAbilities
基于 AuraGameplayAbility 新建蓝图类 GA_TestGameplayAbility
打开 GA_TestGameplayAbility
图表: 技能激活事件
打开 BP_AuraCharacter Abilities-StartupAbilities-添加一组技能 GA_TestGameplayAbility
游戏运行时将激活技能
打开 GA_TestGameplayAbility 图表 添加 delay 添加 end ability 5秒后结束技能
打开 技能 GA_TestGameplayAbility
细节-标签
游戏标签 | 描述 |
---|---|
Ability Tags 技能标签 | 这个技能有这些标签 |
Cancel Abilities with Tag 使用标签取消技能 | 执行此技能时,具有这些标签的技能会取消 |
Block Abilities with Tag 带标签的阻挡技能 | 具有这些标签的技能在此技能处于作用中时被封锁 |
Activation Owned Tags 激活拥有的标签 | 当此技能处于活动状态时,要套用至启动拥有者的标签。如果启用了ReplicateActivationOwnedTag,则复制这些abilitySystemGlobals |
Activation Required Tags 激活所需标签 | 只有当激活的参与者/组件具有所有这些标签时才能激活此技能 |
Activation Blocked Tags 激活阻止标签 | 如果激活的参与者/组件具有以下标签中的任何一个,此技能将被阻止 |
Source Required Tags 源所需标签 | 只有当源参与者/组件具有所有这些标签时才能激活此技能 |
Source Blocked Tags 阻止源标签 | 如果源参与者/组件具有以下任何一个标签,则阻止此技能 |
Target Required Tags 所需的目标标签 | 只有当目标参与者/组件具有所有这些标签时才能激活此技能 |
Target Blocked Tags 目标阻止标签 | 如果目标参与者/组件具有以下标签中的任何一个,则阻止此技能 |
技能在执行时是如何实例化的。这限制了一个技能在其实现中所能做的事情。
选项: Non Instanced 不实例化: 此技能从未实例化。执行该技能的任何东西都在CDO上运行。
Instanced Per Actor 每个参与者都实例化 每个 Actor都有自己的这种技能的实例。状态可以保存,复制是可能的
Instanced Per Execution 每次执行都实例化 每次执行这个技能时,我们都会实例化它。可以复制,但不建议复制。
实例化策略 | 描述 | 详细资料 |
---|---|---|
每个参与者实例化 | 将为该技能创建单个实例。每次激活都会重复使用。 | 可以存储持久数据。每次都必须手动重置变量。 |
每次执行实例化 | 每次激活时创建的新实例 | 在激活之间不存储持久性数据。比每个参与者实例化性能更低 |
不实例化 | 只使用类默认对象不创建实例。 | 无法存储状态,无法绑定到技能任务委托上。三个选项中性能最佳。 |
Net Execution Policy | 描述 |
---|---|
Local Only 仅限于本地 | 只在本地客户端运行。服务器不运行的技能。 |
Local Predicted 局部预测 | 在本地客户端上激活,然后在服务器上激活。利用预测。服务器可以回滚无效的更改。 |
Server Only 仅服务器 | 服务器上运行。 |
Server Initiated 服务器启动 | 先在服务器上运行,然后在所属的本地客户端上运行 |
默认即可,游戏会自动复制。
通过输入激活技能
1-旧版输入:【弃用】
将输入直接绑定到技能系统组件。 按下一个按钮,一个技能会自动接收该输入,然后激活或执行 做什么取决于你如何编码该技能。 这是通过创建一个具有技能输入常量的枚举来完成的。 将这些枚举常量硬编码为技能输入枚举,并且这些输入将被链接到那些特定的技能。
2-增强输入:
输入操作通过输入映射上下文绑定到输入。
可以决定如何激活技能来响应这些输入,可以用任何方式做到这一点。
Lyra,Epic Games 提供的多人射击游戏示例项目,提供了一个极佳示例, 将增强输入与该项目的游戏技能系统技能联系起来。
3-当前项目:【增强输入】 采用数据驱动的方法。简化的Lyra。 能够在运行时更改技能映射的输入。 例如能够在运行时进行更改,按下鼠标左键来激活特定的技能。
需要创建一个包含输入操作的数据资产,将输入操作与游戏标签联系起来。 为每个输入添加一个游戏标签。例如,一键、鼠标左键、鼠标右键等。 在运行时,我们应该能够为我们的游戏技能分配各种标签。 然后当我们激活技能时,每个技能都会与其输入标签相关联,这可以进行检查和更改。
第一步就是把这个数据资产化。
基于 DataAsset 创建 C++ AuraInputConfig
Public/Input
创建一个结构体,将一组输入操作链接到游戏标签。 它将包含一个输入操作和一个游戏标签。
Source/Aura/Public/Input/AuraInputConfig.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "AuraInputConfig.generated.h"
// 创建一个结构体,将一组输入操作链接到游戏标签。
USTRUCT(BlueprintType)
struct FAuraInputAction
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly)
const class UInputAction* InputAction = nullptr;
UPROPERTY(EditDefaultsOnly)
FGameplayTag InputTag = FGameplayTag();
};
UCLASS()
class AURA_API UAuraInputConfig : public UDataAsset
{
GENERATED_BODY()
public:
// 通过标签找到输入操作
const UInputAction* FindAbilityInputActionForTag(const FGameplayTag& InputTag, bool bLogNotFound = false) const;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TArray<FAuraInputAction> AbilityInputActions;
};
Source/Aura/Private/Input/AuraInputConfig.cpp
#include "Input/AuraInputConfig.h"
const UInputAction* UAuraInputConfig::FindAbilityInputActionForTag(const FGameplayTag& InputTag, bool bLogNotFound) const
{
for (const FAuraInputAction& Action: AbilityInputActions)
{
if (Action.InputAction && Action.InputTag == InputTag)
{
return Action.InputAction;
}
}
if (bLogNotFound)
{
UE_LOG(LogTemp, Error, TEXT("Can't find AbilityInputAction for InputTag [%s], on InputConfig [%s]"), *InputTag.ToString(), *GetNameSafe(this));
}
return nullptr;
}
Source/Aura/Public/AuraGameplayTags.h
public:
// 输入操作标签
FGameplayTag InputTag_LMB;
FGameplayTag InputTag_RMB;
FGameplayTag InputTag_1;
FGameplayTag InputTag_2;
FGameplayTag InputTag_3;
FGameplayTag InputTag_4;
Source/Aura/Private/AuraGameplayTags.cpp
函数内新增 输入操作标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
///...
///以下为新增部分 省略原有的前部
/*
* Input Tags 输入操作标签
*/
GameplayTags.InputTag_LMB = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.LMB"),
FString("Input Tag for Left Mouse Button 鼠标左键")
);
GameplayTags.InputTag_RMB = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.RMB"),
FString("Input Tag for Right Mouse Button 鼠标右键")
);
GameplayTags.InputTag_1 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.1"),
FString("Input Tag for 1 key")
);
GameplayTags.InputTag_2 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.2"),
FString("Input Tag for 2 key")
);
GameplayTags.InputTag_3 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.3"),
FString("Input Tag for 3 key")
);
GameplayTags.InputTag_4 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.4"),
FString("Input Tag for 4 key")
);
}
在目录 Blueprints-Input-
右键-其他-数据资产-选择 AuraInputConfig 名称 DA_AuraInputConfig
技能输入是一维的。
在 Blueprints-Input-InputActions 目录 右键-输入-输入操作 名称: IA_LMB
打开 IA_LMB 值类型-Axis 1D(浮点)
鼠标右键 输入操作 IA_RMB 1键 IA_1 2键 IA_2 3键 IA_3 4键 IA_4
值类型-Axis 1D(浮点)
打开 IMC_AuraContext
添加 映射: 选择 数据资产 IA_1 绑定键盘 1键
添加 映射: 选择 数据资产 IA_2 绑定键盘 2键
添加 映射: 选择 数据资产 IA_3 绑定键盘 3键
添加 映射: 选择 数据资产 IA_RMB 绑定鼠标右键
添加 映射: 选择 数据资产 IA_LMB 绑定鼠标左键
运行时可以动态修改输入数据资产。
打开 DA_AuraInputConfig
依次配置
input action -IA_1 input tag - InputTag.1
input action -IA_2 input tag - InputTag.2
input action -IA_3 input tag - InputTag.3
input action -IA_4 input tag - InputTag.4
input action -IA_LMB input tag - InputTag.LMB
input action -IA_RMB input tag - InputTag.RMB
将回调函数绑定到输入
可以将回调绑定到游戏标签,然后找到对应的输入。
在 Public/Input 基于 EnhancedInputComponent 新建 C++
Source/Aura/Public/Input/AuraInputComponent.h
#pragma once
#include "CoreMinimal.h"
#include "EnhancedInputComponent.h"
#include "AuraInputConfig.h"
#include "AuraInputComponent.generated.h"
UCLASS()
class AURA_API UAuraInputComponent : public UEnhancedInputComponent
{
GENERATED_BODY()
public:
// 绑定技能输入模板函数
// InputConfig 输入配置数据资产
// Object 用户对象
template<class UserClass, typename PressedFuncType, typename ReleasedFuncType, typename HeldFuncType>
void BindAbilityActions(const UAuraInputConfig* InputConfig, UserClass* Object, PressedFuncType PressedFunc, ReleasedFuncType ReleasedFunc, HeldFuncType HeldFunc);
};
template <class UserClass, typename PressedFuncType, typename ReleasedFuncType, typename HeldFuncType>
void UAuraInputComponent::BindAbilityActions(const UAuraInputConfig* InputConfig, UserClass* Object, PressedFuncType PressedFunc, ReleasedFuncType ReleasedFunc, HeldFuncType HeldFunc)
{
check(InputConfig);
for (const FAuraInputAction& Action : InputConfig->AbilityInputActions)
{
if (Action.InputAction && Action.InputTag.IsValid())
{
if (PressedFunc)
{
// 只会调用一次
// PressedFunc 之后的参数例如标签,将被当作可变参数传入 PressedFunc
BindAction(Action.InputAction, ETriggerEvent::Started, Object, PressedFunc, Action.InputTag);
}
if (ReleasedFunc)
{
// 只会调用一次
BindAction(Action.InputAction, ETriggerEvent::Completed, Object, ReleasedFunc, Action.InputTag);
}
if (HeldFunc)
{
// 每一帧都调用
BindAction(Action.InputAction, ETriggerEvent::Triggered, Object, HeldFunc, Action.InputTag);
}
}
}
}
Source/Aura/Private/Input/AuraInputComponent.cpp
#include "Input/AuraInputComponent.h"
玩家控制器中调用输入回调 按下时触发 根据标签识别按键
Source/Aura/Public/Player/AuraPlayerController.h
#include "GameplayTagContainer.h"
class UAuraInputConfig;
private:
// 输入回调
void AbilityInputTagPressed(FGameplayTag InputTag);
void AbilityInputTagReleased(FGameplayTag InputTag);
void AbilityInputTagHeld(FGameplayTag InputTag);
// 输入配置
UPROPERTY(EditDefaultsOnly, Category="Input")
TObjectPtr<UAuraInputConfig> InputConfig;
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "Player/AuraPlayerController.h"
#include "EnhancedInputSubsystems.h"
//https://docs.unrealengine.com/5.3/en-US/API/Plugins/EnhancedInput/UEnhancedInputLocalPlayerSubsyst-/
#include "Input/AuraInputComponent.h"
#include "Interaction/EnemyInterface.h"
// 按下时触发 根据标签识别按键
void AAuraPlayerController::AbilityInputTagPressed(FGameplayTag InputTag)
{
GEngine->AddOnScreenDebugMessage(1, 3.f, FColor::Red, *InputTag.ToString());
}
void AAuraPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)
{
GEngine->AddOnScreenDebugMessage(2, 3.f, FColor::Blue, *InputTag.ToString());
}
void AAuraPlayerController::AbilityInputTagHeld(FGameplayTag InputTag)
{
GEngine->AddOnScreenDebugMessage(3, 3.f, FColor::Green, *InputTag.ToString());
}
void AAuraPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
UAuraInputComponent* AuraInputComponent = CastChecked<UAuraInputComponent>(InputComponent);
// 将移动输入操作绑定到输入组件的Move回调,用以移动角色。
// 参数1:输入操作
// 参数2:否希望在输入操作开始时调用 move
// 参数3: 用户对象 控制器
// 参数4:回调函数
AuraInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AAuraPlayerController::Move);
// 将技能输入操作InputConfig绑定到输入组件的操作回调
// &ThisClass 当前类
AuraInputComponent->BindAbilityActions(InputConfig, this, &ThisClass::AbilityInputTagPressed, &ThisClass::AbilityInputTagReleased, &ThisClass::AbilityInputTagHeld);
}
项目设置-引擎-输入-默认类-AuraInputComponent
打开 BP_AuraPlayerController 输入- Input Config-DA_AuraInputConfig Input Config 是 C++中定义的变量。
运行游戏,按下,长按,松开 1,2,3,4,鼠标左键,鼠标右键 可触发对应回调函数。
Source/Aura/Public/AbilitySystem/Abilities/AuraGameplayAbility.h
public:
// 游戏启动时的技能输入标签
// 不在运行时更新
UPROPERTY(EditDefaultsOnly, Category="Input")
FGameplayTag StartupInputTag;
如果要 要改变技能的标签,就不能在游戏技能类别上使用技能标签变量。 游戏技能规格有一个游戏标签容器,专门用于添加标签或在整个游戏过程中动态删除标签。 这对于我们的输入标签来说是完美的。
技能系统组件在游戏开始时添加了初始技能。 作为启动技能,可以检查启动技能的输入标签,并将它们添加到该技能的技能规格。
如果技能已激活,则不再之后每一帧继续执行激活
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
// 输入操作中激活技能标签
void AbilityInputTagHeld(const FGameplayTag& InputTag);
void AbilityInputTagReleased(const FGameplayTag& InputTag);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
#include "AbilitySystem/Abilities/AuraGameplayAbility.h"
// 添加技能
void UAuraAbilitySystemComponent::AddCharacterAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupAbilities)
{
for (const TSubclassOf<UGameplayAbility> AbilityClass : StartupAbilities)
{
// 为每个技能类创建一个技能规格 暂时使用技能等级1
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
if (const UAuraGameplayAbility* AuraAbility = Cast<UAuraGameplayAbility>(AbilitySpec.Ability))
{
// 将初始技能输入标签动态加入技能规格的动态技能标签中
// 动态技能标签可在运行时修改
AbilitySpec.DynamicAbilityTags.AddTag(AuraAbility->StartupInputTag);
// 赋予技能
GiveAbility(AbilitySpec);
}
}
}
void UAuraAbilitySystemComponent::AbilityInputTagHeld(const FGameplayTag& InputTag)
{
if (!InputTag.IsValid()) return;
// 根据技能标签激活技能
// 如果技能已激活,则不再之后每一帧继续执行激活
// 根据输入标签检查是否有可激活的技能
// GetActivatableAbilities() 获取可激活的技能,会返回一系列游戏技能规格
// 可激活的技能意味着我们拥有可以激活的技能。
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
// 检查输入标签,激活任何具有输入标签的技能
// 动态技能标签是一个游戏标签容器
if (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag))
{
//通知技能规格,已经按下输入
AbilitySpecInputPressed(AbilitySpec);
if (!AbilitySpec.IsActive())
{
// 尝试激活技能
TryActivateAbility(AbilitySpec.Handle);
}
}
}
}
void UAuraAbilitySystemComponent::AbilityInputTagReleased(const FGameplayTag& InputTag)
{
if (!InputTag.IsValid()) return;
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
if (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag))
{
//通知技能规格,已经释放输入按键
AbilitySpecInputReleased(AbilitySpec);
}
}
}
Source/Aura/Public/Player/AuraPlayerController.h
class UAuraAbilitySystemComponent;
UPROPERTY()
TObjectPtr<UAuraAbilitySystemComponent> AuraAbilitySystemComponent;
UAuraAbilitySystemComponent* GetASC();
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystem/AuraAbilitySystemComponent.h"
// 按下时触发 根据标签识别按键
void AAuraPlayerController::AbilityInputTagPressed(FGameplayTag InputTag)
{
}
void AAuraPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)
{
if (GetASC() == nullptr) return;
GetASC()->AbilityInputTagReleased(InputTag);
}
void AAuraPlayerController::AbilityInputTagHeld(FGameplayTag InputTag)
{
if (GetASC() == nullptr) return;
GetASC()->AbilityInputTagHeld(InputTag);
}
UAuraAbilitySystemComponent* AAuraPlayerController::GetASC()
{
if (AuraAbilitySystemComponent == nullptr)
{
AuraAbilitySystemComponent = Cast<UAuraAbilitySystemComponent>(
UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetPawn<APawn>()));
}
return AuraAbilitySystemComponent;
}
打开 GA_TestGameplayAbility
细节-input-startup input tag-InputTag.LMB 这将设置初始技能标签为 InputTag.LMB
所以一旦我按下鼠标左键,我就会激活这个技能,然后它将处于激活状态,然后五秒钟后它会自行结束。 只能在它尚未激活的情况下激活它。
运行游戏,按下左键会激活技能。5秒后技能结束。
如果去掉延时。 将可以频繁触发技能激活。 因为技能在输入时就激活,然后自动取消激活,可以再次激活。
鼠标左键为特殊按键, 可以激活技能,也可以使角色自动奔跑。
Source/Aura/Public/Player/AuraPlayerController.h
class USplineComponent;
private:
// 缓存鼠标点击的目的地位置
FVector CachedDestination = FVector::ZeroVector;
// 鼠标按下的跟随时间,短按时初始化0
float FollowTime = 0.f;
// 短按阙值,鼠标按下的时间超过该值表示是长按
float ShortPressThreshold = 0.5f;
// 如果为真,当鼠标短按时,在起始和目标沿曲线开始自动奔跑,需要每一帧都调用运动输入函数以前进
bool bAutoRunning = false;
// 鼠标是否跟踪到敌人目标
bool bTargeting = false;
// 奔跑时,距离目标半径多少时开始停止奔跑
UPROPERTY(EditDefaultsOnly)
float AutoRunAcceptanceRadius = 50.f;
// 样条平滑曲线,自动奔跑的轨迹,放置拐弯时突然转向
UPROPERTY(VisibleAnywhere)
TObjectPtr<USplineComponent> Spline;
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "AuraGameplayTags.h"
#include "Components/SplineComponent.h"
AAuraPlayerController::AAuraPlayerController()
{
//启用玩家控制器的网络复制
//多人游戏中的复制本质上是当服务器上的实体发生变化时。服务器上发生的更改将复制或发送到连接到的所有客户端.
bReplicates = true;
// 构造样条平滑曲线组件
Spline = CreateDefaultSubobject<USplineComponent>("Spline");
}
// 按下时触发 根据标签识别按键
void AAuraPlayerController::AbilityInputTagPressed(FGameplayTag InputTag)
{
// 鼠标左键为特殊按键,
// 可以激活技能,ThisActor 表示敌人,如果鼠标跟踪到敌人,则激活技能
// 也可以使角色自动奔跑。如果鼠标没有跟踪到敌人,并且是短按
if (InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
// 鼠标是否跟踪到敌人
bTargeting = ThisActor ? true : false;
// 按下鼠标左键瞬间,还无法确定是否短按,不应自动奔跑。需要等待鼠标左键释放时确定短按长按
bAutoRunning = false;
}
}
void AAuraPlayerController::AbilityInputTagHeld(FGameplayTag InputTag)
{
// 如果按住的不是鼠标左键,而是其他键,则激活按住对应键的技能
// 此时一定不是自动奔跑或鼠标左键单击技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())
{
GetASC()->AbilityInputTagHeld(InputTag);
}
return;
}
// 如果是鼠标左键按住,并且鼠标跟踪到敌人目标,则激活按住左键技能
if (bTargeting)
{
if (GetASC())
{
GetASC()->AbilityInputTagHeld(InputTag);
}
}
// 如果是鼠标左键按住,并且鼠标没有跟踪到敌人目标,则执行移动
else
{
// 开始累计按住的鼠标跟随时间,在鼠标释放时用以判断短按或长按
FollowTime += GetWorld()->GetDeltaSeconds();
// 鼠标按住时获取移动目的地
FHitResult Hit;
// 跟踪通道
// false 不跟踪复杂碰撞
if (GetHitResultUnderCursor(ECC_Visibility, false, Hit))
{
CachedDestination = Hit.ImpactPoint;
}
// 如果有可控制pawn
if (APawn* ControlledPawn = GetPawn())
{
// 移动的方向 :从受控pawn 到 目的地 的方向向量,归一化
const FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
// 向该方向移动 每帧执行
ControlledPawn->AddMovementInput(WorldDirection);
}
}
}
此时,只要长按鼠标,玩家则向目标不断移动。
而且在多人模式下,服务端可客户端都可以正常运行。
释放鼠标左键时,鼠标跟随时间小于短按阙值时,是短按, 则生成曲线导航路径,自动奔跑至目标点。
导航系统模块
Source/Aura/Aura.Build.cs
PrivateDependencyModuleNames.AddRange(new string[] { "NavigationSystem" });
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "NavigationPath.h"
#include "NavigationSystem.h"
void AAuraPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)
{
// 如果释放的不是鼠标左键,而是其他键,则激活释放对应键的技能
// 此时一定不是自动奔跑或鼠标释放左键技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())
{
GetASC()->AbilityInputTagReleased(InputTag);
}
return;
}
// 如果释放的是鼠标左键,且跟踪到敌人,则执行释放鼠标左键的技能
if (bTargeting)
{
if (GetASC())
{
GetASC()->AbilityInputTagReleased(InputTag);
}
}
// 如果是鼠标左键释放,并且鼠标没有跟踪到敌人目标,则自动奔跑至目的地
else
{
APawn* ControlledPawn = GetPawn();
// 如果鼠标跟随时间小于短按阙值,表示是短按
if (FollowTime <= ShortPressThreshold && ControlledPawn)
{
// 通过受控pawn位置和目的地位置,同步查找位置路径,生成导航路径
if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControlledPawn->GetActorLocation(), CachedDestination))
{
//清除样条线的点
Spline->ClearSplinePoints();
// 循环导航路径的点
for (const FVector& PointLoc : NavPath->PathPoints)
{
//向样条曲线添加点
Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Green, false, 5.f);
}
// 正在自动奔跑为真
bAutoRunning = true;
}
}
//自动奔跑时重置跟随时间
FollowTime = 0.f;
// 自动奔跑时没有跟踪敌人
bTargeting = false;
}
}
按 P 键使 导航网格体体边界体积 可视化
放大到整个关卡
添加障碍物,释放左键时,可在玩家和目的地之间新城样条曲线路径点。 此时不能自动奔跑。
为客户端启用导航 项目设置-引擎-导航-运行客户端导航-启用 否则生成的导航路径无效
Source/Aura/Public/Player/AuraPlayerController.h
// 自动奔跑
void AutoRun();
Source/Aura/Private/Player/AuraPlayerController.cpp
void AAuraPlayerController::PlayerTick(float DeltaTime)
{
Super::PlayerTick(DeltaTime);
CursorTrace();
AutoRun();
}
void AAuraPlayerController::AutoRun()
{
if (!bAutoRunning) return;
if (APawn* ControlledPawn = GetPawn())
{
const FVector LocationOnSpline = Spline->FindLocationClosestToWorldLocation(ControlledPawn->GetActorLocation(), ESplineCoordinateSpace::World);
const FVector Direction = Spline->FindDirectionClosestToWorldLocation(LocationOnSpline, ESplineCoordinateSpace::World);
ControlledPawn->AddMovementInput(Direction);
const float DistanceToDestination = (LocationOnSpline - CachedDestination).Length();
if (DistanceToDestination <= AutoRunAcceptanceRadius)
{
bAutoRunning = false;
}
}
}
void AAuraPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)
{
// 如果释放的不是鼠标左键,而是其他键,则激活释放对应键的技能
// 此时一定不是自动奔跑或鼠标释放左键技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())
{
GetASC()->AbilityInputTagReleased(InputTag);
}
return;
}
// 如果释放的是鼠标左键,且跟踪到敌人,则执行释放鼠标左键的技能
if (bTargeting)
{
if (GetASC())
{
GetASC()->AbilityInputTagReleased(InputTag);
}
}
// 如果是鼠标左键释放,并且鼠标没有跟踪到敌人目标,则自动奔跑至目的地
else
{
APawn* ControlledPawn = GetPawn();
// 如果鼠标跟随时间小于短按阙值,表示是短按
if (FollowTime <= ShortPressThreshold && ControlledPawn)
{
// 通过受控pawn位置和目的地位置,同步查找位置路径,生成导航路径
if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControlledPawn->GetActorLocation(), CachedDestination))
{
//清除样条线的点
Spline->ClearSplinePoints();
// 循环导航路径的点
for (const FVector& PointLoc : NavPath->PathPoints)
{
//向样条曲线添加点
Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Green, false, 5.f);
}
// 减去目的地路径导航点的最后一个点,防止目标点为障碍物中心时,玩家永远无法到达而不停奔跑
CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
// 正在自动奔跑为真
bAutoRunning = true;
}
}
//自动奔跑时重置跟随时间
FollowTime = 0.f;
// 自动奔跑时没有跟踪敌人
bTargeting = false;
}
}
选中障碍物后方的地点。 默认鼠标跟踪将被障碍物挡住,无法选取被障碍物遮挡的地点作为导航目的地。 需要设置障碍物的碰撞: 碰撞预设-自定义 碰撞预设-碰撞响应-检测响应-Visibility 忽略。 这将使鼠标跟踪穿透障碍物到达被障碍物遮挡的地面。
此时,短按一次鼠标左键时,玩家将自动奔跑至目的地。
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
protected:
// client 使其成为RPC ,如果没有client则只在服务端运行。有client 使其在服务端执行,然后复制到拥有权限的客户端
// Reliable 即使丢包也能保证复制到达客户端
// RPC 的实现必须使用约定 ClientEffectApplied_Implementation
UFUNCTION(Client, Reliable)
void ClientEffectApplied(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayEffectSpec& EffectSpec, FActiveGameplayEffectHandle ActiveEffectHandle);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::AbilityActorInfoSet()
{
// 为游戏效果应用委托绑定函数 EffectApplied
// 在服务端运行,广播,然后复制到客户端即 ClientEffectApplied_Implementation
OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAuraAbilitySystemComponent::ClientEffectApplied);
}
// 由服务端复制而来
void UAuraAbilitySystemComponent::ClientEffectApplied_Implementation(UAbilitySystemComponent* AbilitySystemComponent,
const FGameplayEffectSpec& EffectSpec,
FActiveGameplayEffectHandle ActiveEffectHandle)
{
// 游戏标签容器
FGameplayTagContainer TagContainer;
// 获取所有资产标签,存储到标签容器中,标签容器比数组优化更多
EffectSpec.GetAllAssetTags(TagContainer);
// 广播资产标签容器
EffectAssetTags.Broadcast(TagContainer);
}
Source/Aura/Public/Player/AuraPlayerController.h
private:
// 存储光标命中处,后期可添加特效,例如选择目标点特效
FHitResult CursorHit;
Source/Aura/Private/Player/AuraPlayerController.cpp
void AAuraPlayerController::CursorTrace()
{
// 光标命中的结果 使用 ECC_Visibility 通道进行跟踪 ,简单碰撞跟踪
GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
// 检查跟踪结果
if (!CursorHit.bBlockingHit) return;
LastActor = ThisActor;
ThisActor = Cast<IEnemyInterface>(CursorHit.GetActor());
if (LastActor != ThisActor)
{
if (LastActor) LastActor->UnHighlightActor();
if (ThisActor) ThisActor->HighlightActor();
}
}
void AAuraPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)
{
// 如果释放的不是鼠标左键,而是其他键,则激活释放对应键的技能
// 此时一定不是自动奔跑或鼠标释放左键技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())GetASC()->AbilityInputTagReleased(InputTag);
return;
}
// 如果释放的是鼠标左键,且跟踪到敌人,则执行释放鼠标左键的技能
if (bTargeting)
{
if (GetASC())GetASC()->AbilityInputTagReleased(InputTag);
}
// 如果是鼠标左键释放,并且鼠标没有跟踪到敌人目标,则自动奔跑至目的地
else
{
const APawn* ControlledPawn = GetPawn();
// 如果鼠标跟随时间小于短按阙值,表示是短按
if (FollowTime <= ShortPressThreshold && ControlledPawn)
{
// 通过受控pawn位置和目的地位置,同步查找位置路径,生成导航路径
if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControlledPawn->GetActorLocation(), CachedDestination))
{
//清除样条线的点
Spline->ClearSplinePoints();
// 循环导航路径的点
for (const FVector& PointLoc : NavPath->PathPoints)
{
//向样条曲线添加点
Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
//DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Green, false, 5.f);
}
// 减去目的地路径导航点的最后一个点,防止目标点为障碍物中心时,玩家永远无法到达而不停奔跑
CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
// 正在自动奔跑为真
bAutoRunning = true;
}
}
//自动奔跑时重置跟随时间
FollowTime = 0.f;
// 自动奔跑时没有跟踪敌人
bTargeting = false;
}
}
void AAuraPlayerController::AbilityInputTagHeld(FGameplayTag InputTag)
{
// 如果按住的不是鼠标左键,而是其他键,则激活按住对应键的技能
// 此时一定不是自动奔跑或鼠标左键单击技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())GetASC()->AbilityInputTagHeld(InputTag);
return;
}
// 如果是鼠标左键按住,并且鼠标跟踪到敌人目标,则激活按住左键技能
if (bTargeting)
{
if (GetASC())GetASC()->AbilityInputTagHeld(InputTag);
}
// 如果是鼠标左键按住,并且鼠标没有跟踪到敌人目标,则执行移动
else
{
// 开始累计按住的鼠标跟随时间,在鼠标释放时用以判断短按或长按
FollowTime += GetWorld()->GetDeltaSeconds();
// 鼠标按住时获取移动目的地
// 跟踪通道
// false 不跟踪复杂碰撞
if (CursorHit.bBlockingHit) CachedDestination = CursorHit.ImpactPoint;
// 如果有可控制pawn
if (APawn* ControlledPawn = GetPawn())
{
// 移动的方向 :从受控pawn 到 目的地 的方向向量,归一化
const FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
// 向该方向移动 每帧执行
ControlledPawn->AddMovementInput(WorldDirection);
}
}
}
让玩家角色可以发射各类投射物,例如魔法球
在目录 Public/Actor 基于 Actor 新建 C++ AuraProjectile
Source/Aura/Public/Actor/AuraProjectile.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AuraProjectile.generated.h"
class USphereComponent;
class UProjectileMovementComponent;
UCLASS()
class AURA_API AAuraProjectile : public AActor
{
GENERATED_BODY()
public:
AAuraProjectile();
// 投射物运动组件
UPROPERTY(VisibleAnywhere)
TObjectPtr<UProjectileMovementComponent> ProjectileMovement;
protected:
virtual void BeginPlay() override;
// 投射物球体碰撞组件的重叠事件
UFUNCTION()
void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
private:
// 投射物球体碰撞组件
UPROPERTY(VisibleAnywhere)
TObjectPtr<USphereComponent> Sphere;
};
Source/Aura/Private/Actor/AuraProjectile.cpp
#include "Actor/AuraProjectile.h"
#include "Components/SphereComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
AAuraProjectile::AAuraProjectile()
{
PrimaryActorTick.bCanEverTick = false;
Sphere = CreateDefaultSubobject<USphereComponent>("Sphere");
SetRootComponent(Sphere);
Sphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Sphere->SetCollisionResponseToAllChannels(ECR_Ignore);
Sphere->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
Sphere->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Overlap);
Sphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>("ProjectileMovement");
ProjectileMovement->InitialSpeed = 550.f;
ProjectileMovement->MaxSpeed = 550.f;
// 没有重力
ProjectileMovement->ProjectileGravityScale = 0.f;
}
void AAuraProjectile::BeginPlay()
{
Super::BeginPlay();
Sphere->OnComponentBeginOverlap.AddDynamic(this, &AAuraProjectile::OnSphereOverlap);
}
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
}
在目录 Blueprints-AbilitySystem-GameplayAbilities-Fire-FireBolt 基于 AuraProjectile 新建蓝图 BP_FireBolt
打开 BP_FireBolt
sphere 过大会与玩家网格体重叠,导致攻击到玩家自身。【后期可以为玩家单独添加的通道种类】
Niagara 组件 -细节-Niagara-Niagara 系统资产-NS_Fire_3 火球资产
将 BP_FireBolt 火球放置到关卡,火球具由速度,将会朝面向的方向飞远。
准备创建可以产生 BP_FireBolt火球的技能。
在目录 Public/AbilitySystem/Abilities
Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraGameplayAbility.h"
#include "AuraProjectileSpell.generated.h"
UCLASS()
class AURA_API UAuraProjectileSpell : public UAuraGameplayAbility
{
GENERATED_BODY()
protected:
// 激活技能
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
};
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
#include "AbilitySystem/Abilities/AuraProjectileSpell.h"
#include "Kismet/KismetSystemLibrary.h"
void UAuraProjectileSpell::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
UKismetSystemLibrary::PrintString(this, FString("ActivateAbility (C++)"), true, true, FLinearColor::Yellow, 3);
}
在目录 Blueprints-AbilitySystem-GameplayAbilities-Fire-FireBolt 基于 AuraProjectileSpell 新建技能蓝图 GA_FireBolt
打开 GA_FireBolt
激活时打印文字
细节-输入-Startup input tag -InputTag.LMB 将左键单击输入标签绑定到初始输入标签属性。
打开 BP_AuraCharacter 细节-Abilities- startup Abilities-删除测试技能,选择 技能 GA_FireBolt火球术 玩家初始时将在单击敌人时激活,施放火球技能。 c++和蓝图版本技能都已激活。
Source/Aura/Private/Actor/AuraProjectile.cpp
AAuraProjectile::AAuraProjectile()
{
PrimaryActorTick.bCanEverTick = false;
// 投射物 AuraProjectile 基类需要可网络复制
bReplicates = true;
Sphere = CreateDefaultSubobject<USphereComponent>("Sphere");
SetRootComponent(Sphere);
Sphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Sphere->SetCollisionResponseToAllChannels(ECR_Ignore);
Sphere->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
Sphere->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Overlap);
Sphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>("ProjectileMovement");
ProjectileMovement->InitialSpeed = 550.f;
ProjectileMovement->MaxSpeed = 550.f;
// 没有重力
ProjectileMovement->ProjectileGravityScale = 0.f;
}
技能激活时生成投射物时需要位置等变换信息。 玩家实现带有变换信息的接口。 技能依赖有变换信息的接口。 该接口由玩家与敌人共享。
AuraCharacterBase 基类继承了接口 CombatInterface
Source/Aura/Public/Interaction/CombatInterface.h
public:
// 技能激活时生成投射物需要变换,位置信息
virtual FVector GetCombatSocketLocation();
Source/Aura/Private/Interaction/CombatInterface.cpp
FVector ICombatInterface::GetCombatSocketLocation()
{
return FVector();
}
角色基类需要提供
Source/Aura/Public/Character/AuraCharacterBase.h
protected:
// 为技能投射物提供生成位置信息的武器插槽名称
UPROPERTY(EditAnywhere, Category = "Combat")
FName WeaponTipSocketName;
// 获取用于技能投射物生成的插槽位置
virtual FVector GetCombatSocketLocation() override;
Source/Aura/Private/Character/AuraCharacterBase.cpp
// 通过武器插槽名称获取插槽位置
// 提供欸技能投射物生成位置用
FVector AAuraCharacterBase::GetCombatSocketLocation()
{
check(Weapon);
return Weapon->GetSocketLocation(WeaponTipSocketName);
}
Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h
class AAuraProjectile;
protected:
// 投射物类 例如火球 Projectile BP_FireBolt
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSubclassOf<AAuraProjectile> ProjectileClass;
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
#include "AbilitySystem/Abilities/AuraProjectileSpell.h"
#include "Actor/AuraProjectile.h"
#include "Interaction/CombatInterface.h"
// 技能激活时
void UAuraProjectileSpell::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = HasAuthority(&ActivationInfo);
if (!bIsServer) return;
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation 设置投射物旋转
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 给投射物一个造成伤害的游戏效果规格
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
}
从 BP_AuraCharacter 找到武器网格 SKM_Staff
打开 SKM_Staff 武器尖端带有一个插槽-TipSocket
打开 玩家 BP_AuraCharacter 细节-Combat-Weapon Tip Socket Name-TipSocket Weapon Tip Socket Name 是基类定义的变量。 将通过此插槽名获取武器上该插槽的世界位置,用以在此处生成技能投射物,例如火球。
ProjectileClass 在技能基类中定义
打开 GA_FireBolt 火球术技能 细节-Aura Projectile Spell-Projectile Class-BP_FireBolt
现在,点击敌人,激活技能GA_FireBolt 火球术,将会在玩家武器处生成一个火球,发射出去。 此时火球方向错误。尚未设置。 只能发射一次,因为技能激活后永远没有结束。
https://docs.unrealengine.com/5.3/zh-CN/gameplay-ability-tasks-in-unreal-engine/
技能任务(C++类"UAbilityTask")是更常规的技能任务类的特殊形式,旨在使用游戏性技能。使用游戏性技能系统的游戏通常包括各种自定义技能任务,这些任务实施其独特的游戏功能。它们在游戏性技能执行过程中执行异步工作,并且能够通过调用委托(Delegate) (在本地C++代码中)或移经一个或多个输出执行引脚(在蓝图中)来影响执行流。这使技能能够跨多个帧执行,并可在同一帧内执行多个不同的函数。大部分技能任务都有一个即时触发的执行引脚,使蓝图能够在任务开始后继续执行。此外,通常还有一些特定于任务的引脚,它们会在延迟后或在可能发生或不发生的某个事件之后触发。
技能任务可以通过调用"EndTask"函数自行终止,或者等待运行它的游戏性技能结束,此时它会自动终止。这可以防止幻影技能任务运行,有效地泄漏CPU周期和内存。例如,某个技能任务可能播放一个施法动画,而另一个任务则在玩家的瞄准点处放置一个靶向标线。如果玩家点击确认输入来施放该法术,或者等待动画结束而未确认该法术,游戏性技能就会结束。虽然它们可以在任何时候自动终止,但是技能任务保证最晚在主要技能结束时终止。
技能任务设计用于网络环境和非网络环境,但它们不会通过网络直接更新自己。它们通常是间接保持同步的,因为它们是由游戏性技能(会进行复制)创建,并且使用复制的信息(例如玩家输入或网络变量)来确定它们的执行流。
近战攻击游戏性技能,在蓝图中实施。中央的"播放蒙太奇并等待事件"技能任务是ActionRPG样本的一部分。
技能任务像用来完成游戏技能的工人,要么立即完成,要么跨越一段时间。 游戏技能系统中已经存在许多任务。 也可以自己创建游戏任务。
打开 GA_FireBolt 技能 图表:
它有一个输入执行引脚,也有一个常规输出执行引脚。 到达节点后,右上角的常规输出执行引脚将立即执行。 这是同步任务。
但也允许我们在到达目标后立即继续调用其他函数和事物节点。
但是,有许多情况可以触发这些其他输出执行线引脚。 例如 on completed 这只是一个蓝图潜在节点,也称为异步任务。 GAS具有这些潜在节点的更专门的形式,并且这些在技能系统中更加根深蒂固。
例如节点:ability-tasks-play montage and wait
它将播放蒙太奇,然后等待蒙太奇中的其中一件事情完成或混合或被中断或取消。
E:/Unreal Projects 532/Aura/Content/Assets/Characters/Aura/Animations/Abilities/Cast_FireBolt.uasset
Cast_FireBolt-右键-创建-创建动画蒙太奇 AM_Cast_FireBolt
AM_Cast_FireBolt 具由默认插槽
ABP_Aura 动画蓝图也在使用默认插槽,所以可以播放该动画。
ability-tasks-play montage and wait 节点 montage to play 选择资产 AM_Cast_FireBolt
激活此技能时将播放 AM_Cast_FireBolt 此时立即生成火球,时间过早。
打开 AM_Cast_FireBolt 资产详情-混合选项-混合-混合时间-0 不混合 资产详情-混合选项-混处-混合时间-0.1 这样蒙太奇播放的速度更快更灵敏。
打开 AM_Cast_FireBolt 图表: ability-tasks-wait gameplay event
具由该技能并且已激活的actor 将监听 动画蒙太奇的通知事件
项目设置-gameplayTags-管理gameplay标签- 添加标签 Event.Montage.FireBolt 默认源。
打开 AM_Cast_FireBolt 图表: ability-tasks-wait gameplay event event tag - Event.Montage.FireBolt
在 目录 Content/Blueprints/AnimNotifies/ 基于 AnimNotify 动画通知 新建 AN_MontageEvent 动画通知蓝图
用于在动画蒙太奇发送任意游戏事件
打开 AN_MontageEvent 左侧-我的蓝图-函数-重载-已接受通知 Received_Notify 可以覆盖收到的通知。 决定通知到达时发生什么。 当接受到达通知并且有与关联的网格组件时,将调用此函数执行此通知的动画蒙太奇,
mesh comp -始终可以获得该网格的所有者。
发送游戏事件:ability-send gameplay event to actor
当向演员发送游戏事件时,不仅发送识别标签,还可以通过有效负载发送附加数据。
GA_FireBolt 技能 的 wait gameplay event 可以接受该负载。
左侧新建变量 EventTag 类型:gameplay标签 公开变量 EventTag EventTag变量 输出至 send gameplay event to actor - Event Tag 节点
send gameplay event to actor - payload 拖出 Make GameplayEventData
打开 AM_Cast_FireBolt 在武器挥出时间点-添加通知-AN_MontageEvent
选择通知 AN_MontageEvent-细节-动画通知-event tag-Event.Montage.FireBolt event tag 变量是之前 AN_MontageEvent 中定义的变量。
当蒙太奇达到这一点时,将发送带有Event.Montage.FireBolt标签的事件通知, 然后 自定义动画通知蓝图 AN_MontageEvent 将收到有关此的通知。 并且 AN_MontageEvent 的 event tag 变量为接受到的标签 Event.Montage.FireBolt
AN_MontageEvent 中将发送 带游戏标签 Event.Montage.FireBolt 事件到actor 标签 Event.Montage.FireBolt 从 蒙太奇收到。
之后 actor 的 GA_FireBolt 技能的 wait gameplay event 的 event Received 节点 将接收到该事件 。 因为 wait gameplay event 已设置为 要过滤接受 带有 标签 Event.Montage.FireBolt 的事件。所以可以收到该事件。
所以应当在 GA_FireBolt 技能的 wait gameplay event 事件接受到通知时才生成火球,此时动作适合生成火球。 而非技能开始后立即生成火球。动画不协调。
不再在技能激活时自动生成
Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h
protected:
// 生成投射物 子类蓝图中调用
UFUNCTION(BlueprintCallable, Category = "Projectile")
void SpawnProjectile();
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
#include "AbilitySystem/Abilities/AuraProjectileSpell.h"
#include "Actor/AuraProjectile.h"
#include "Interaction/CombatInterface.h"
// 技能激活时
void UAuraProjectileSpell::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
}
void UAuraProjectileSpell::SpawnProjectile()
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 给投射物一个造成伤害的游戏效果规格
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
}
打开 GA_FireBolt 技能 图表: SpawnProjectile
End Ability 结束技能,使技能可再次释放
此时动画合适时才会生成火球。 并可以多次释放技能。
在目录 Public/AbilitySystem/AbilityTasks/ 基于 AbilityTask 新建 C++ TargetDataUnderMouse
获取光标跟踪命中结果
这是一个 异步蓝图技能任务节点 创建多个输出执行引脚的方式是通过广播委托。 任何输出执行引脚都需要作为此类的委托变量存在。
Source/Aura/Public/AbilitySystem/AbilityTasks/TargetDataUnderMouse.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "TargetDataUnderMouse.generated.h"
// 广播鼠标下跟踪的数据
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMouseTargetDataSignature, const FVector&, Data);
UCLASS()
class AURA_API UTargetDataUnderMouse : public UAbilityTask
{
GENERATED_BODY()
public:
// 创建光标跟踪数据
// DefaultToSelf = "OwningAbility", 默认传入技能指针 self 蓝图中 / this
UFUNCTION(BlueprintCallable, Category="Ability|Tasks",
meta = (DisplayName = "TargetDataUnderMouse", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility",
BlueprintInternalUseOnly = "true"))
static UTargetDataUnderMouse* CreateTargetDataUnderMouse(UGameplayAbility* OwningAbility);
// 创建多个输出执行引脚的方式是通过广播委托。
// 任何输出执行引脚都需要作为此类的委托变量存在。
// 这将成为该异步技能任务节点上的输出执行引脚。
UPROPERTY(BlueprintAssignable)
FMouseTargetDataSignature ValidData;
private:
virtual void Activate() override;
};
Source/Aura/Private/AbilitySystem/AbilityTasks/TargetDataUnderMouse.cpp
#include "AbilitySystem/AbilityTasks/TargetDataUnderMouse.h"
UTargetDataUnderMouse* UTargetDataUnderMouse::CreateTargetDataUnderMouse(UGameplayAbility* OwningAbility)
{
UTargetDataUnderMouse* MyObj = NewAbilityTask<UTargetDataUnderMouse>(OwningAbility);
return MyObj;
}
// 技能激活
// 获得了鼠标下方的鼠标点击位置并广播它
void UTargetDataUnderMouse::Activate()
{
// 技能任务拥有他们所属技能
APlayerController* PC = Ability->GetCurrentActorInfo()->PlayerController.Get();
FHitResult CursorHit;
// 光标只是追踪可见性通道
// 仅跟踪简单的碰撞并传递光标点击数据到 CursorHit
PC->GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
// 一旦激活此技能,就会广播该委托,这意味着有效的数据执行引脚将被执行。
// 但它不一定是立刻执行,这是一个异步任务。
// 根据某些技能任务的游戏机制,我们可能希望稍后广播这些委托
// 但对于当前情况,我们立即进行广播。
ValidData.Broadcast(CursorHit.Location);
}
打开 GA_FireBolt 图表: 可以添加C++ 中定义的蓝图节点 TargetDataUnderMouse
一旦执行到 TargetDataUnderMouse 节点, 就会调用 activate,将立即执行引脚 ValidData 广播有效数据 Data 。 Data 为光标跟踪的位置信息。 所以使用 ValidData 引脚,而非右上角引脚。
这在多人游戏中存在问题,客户端的光标位置在服务器端显示为世界中心点0,0,0。 因为客户端的光标位置不会发送到服务端。
技能在客户端激活后,也将在服务端激活,需要时间TimeActive。
技能在客户端激活后,将会执行到节点TargetDataUnderMouse,以获取光标跟踪位置,然后将此位置信息通过RPC同步到服务端,需要时间 TimeRPC。
1-如果 TimeActive 小于 TimeRPC,那么服务端将获取不到正确的 TargetDataUnderMouse 的数据,只能为0,0,0. 客户端的技能激活先发送到服务端。
2-如果 TimeRPC 小于 TimeActive, TargetDataUnderMouse 的数据RPC更早到达服务端,那么服务端将可获取正确的TargetDataUnderMouse 的数据。 客户端的目标数据RPC先发送到服务端。
但是 技能激活 TimeActive ,目标数据RPC TimeRPC 发送到服务端的时间无法确定。无法保证先后顺序。
技能系统通过目标数据系统来保证服务端获取正确数据:
gas 中内置了多种类型的目标数据,它们都基于一个结构体:游戏技能目标数据 FGameplayAbilityTargetData 这是一个基类,有多种子类。
方法1:AbilitySystemComponent->ServerSetReplicatedTargetData 服务器设置复制目标数据
它的作用是将数据发送到服务器,此时服务器接收到数据,
服务器通过获取它的目标集(一个委托 AbilitySystemComponent.Get()->AbilityTargetDataSetDelegate
)并广播该委托。
任何绑定到服务器上的此目标集委托的回调,可以在响应中接收该目标数据。
服务器上的一个 技能-目标数据 AbilityTargetDataMap ,保存 技能规格与目标数据的关联。map也包含预测键。
执行顺序: 客户端激活技能Active。 使用 AbilitySystemComponent->ServerSetReplicatedTargetData 将目标数据复制到服务器。
服务器激活技能Active,此时 目标数据复制到服务器 无法保证是否到达。 服务器通过为 目标集 AbilityTargetDataSetDelegate 委托绑定回调来监听该 ServerSetReplicatedTargetData 复制。
如果 为目标集 AbilityTargetDataSetDelegate 委托绑定回调 之后, 服务器才接收到 ServerSetReplicatedTargetData 复制来的目标数据。即 TimeActive 小于 TimeRPC,那么一切ok, 当 ServerSetReplicatedTargetData RPC 复制到服务器后,AbilityTargetDataSetDelegate 将广播委托。服务器就可以接受到客户端复制来的数据。ServerSetReplicatedTargetData 。
如果 服务器接收到 ServerSetReplicatedTargetData 复制来的目标数据之后,服务器才为目标集 AbilityTargetDataSetDelegate 委托绑定好回调, 然后服务器收到激活技能复制后,委托不会被触发以接受目标数据。
那么服务器必须通过 AbilitySystemComponent.Get()->CallReplicatedTargetDataDelegatesIfSet
强制 AbilityTargetDataSetDelegate 委托执行广播。
将触发委托的回调,然后服务器才可以接受到 复制来的目标数据。
如果强制触发委托依然未成功,说明目标数据尚未到达服务器。 需要等待
Source/Aura/Public/AbilitySystem/AbilityTasks/TargetDataUnderMouse.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "TargetDataUnderMouse.generated.h"
// 广播鼠标下跟踪的数据
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMouseTargetDataSignature, const FGameplayAbilityTargetDataHandle&,
DataHandle);
UCLASS()
class AURA_API UTargetDataUnderMouse : public UAbilityTask
{
GENERATED_BODY()
public:
// 创建光标跟踪数据
// DefaultToSelf = "OwningAbility", 默认传入技能指针 self 蓝图中 / this
UFUNCTION(BlueprintCallable, Category="Ability|Tasks",
meta = (DisplayName = "TargetDataUnderMouse", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility",
BlueprintInternalUseOnly = "true"))
static UTargetDataUnderMouse* CreateTargetDataUnderMouse(UGameplayAbility* OwningAbility);
// 创建多个输出执行引脚的方式是通过广播委托。
// 任何输出执行引脚都需要作为此类的委托变量存在。
// 这将成为该异步技能任务节点上的输出执行引脚。
UPROPERTY(BlueprintAssignable)
FMouseTargetDataSignature ValidData;
private:
virtual void Activate() override;
// 本地控制时调用该函数广播数据
// 如果在服务端本地控制,那么数据正常获取然后广播
// 果在客户端并且受到本地控制,会广播委托,而且还向服务器发送数据。
void SendMouseCursorData();
};
Source/Aura/Private/AbilitySystem/AbilityTasks/TargetDataUnderMouse.cpp
#include "AbilitySystem/AbilityTasks/TargetDataUnderMouse.h"
#include "AbilitySystemComponent.h"
UTargetDataUnderMouse* UTargetDataUnderMouse::CreateTargetDataUnderMouse(UGameplayAbility* OwningAbility)
{
UTargetDataUnderMouse* MyObj = NewAbilityTask<UTargetDataUnderMouse>(OwningAbility);
return MyObj;
}
// 技能激活
// 获得了鼠标下方的鼠标点击位置并广播它
void UTargetDataUnderMouse::Activate()
{
// 是否本地控制
const bool bIsLocallyControlled = Ability->GetCurrentActorInfo()->IsLocallyControlled();
if (bIsLocallyControlled)
{
SendMouseCursorData();
}
else
{
//TODO: We are on the server, so listen for target data.
// 非本地控制,服务端只是从客户端接受数据,然后控制
}
}
void UTargetDataUnderMouse::SendMouseCursorData()
{
// 预测窗口
FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent.Get());
// 技能任务拥有他们所属技能
APlayerController* PC = Ability->GetCurrentActorInfo()->PlayerController.Get();
FHitResult CursorHit;
// 光标只是追踪可见性通道
// 仅跟踪简单的碰撞并传递光标点击数据到 CursorHit
PC->GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
FGameplayAbilityTargetDataHandle DataHandle;
FGameplayAbilityTargetData_SingleTargetHit* Data = new FGameplayAbilityTargetData_SingleTargetHit();
// 目标数据对象
Data->HitResult = CursorHit;
DataHandle.Add(Data);
// 将光标跟踪位置 DataHandle 这个目标数据发送到服务器,
// 服务器接受的类型之一 FGameplayAbilityTargetDataHandle
// 参数1:技能规格句柄
// 参数2:预测键。 技能启动相关。 GetActivationPredictionKey:技能最初被激活的时间
// 参数3:目标数据句柄,打包了目标数据
// 参数4:游戏标签
// 参数5:当前预测键
AbilitySystemComponent->ServerSetReplicatedTargetData(
GetAbilitySpecHandle(),
GetActivationPredictionKey(),
DataHandle,
FGameplayTag(),
AbilitySystemComponent->ScopedPredictionKey);
// 发送到服务端后,如果可以,在本地也广播发送
if (ShouldBroadcastAbilityTaskDelegates())
{
// 一旦激活此技能,就会广播该委托,这意味着有效的数据执行引脚将被执行。
// 但它不一定是立刻执行,这是一个异步任务。
// 根据某些技能任务的游戏机制,我们可能希望稍后广播这些委托
// 但对于当前情况,我们立即进行广播。
ValidData.Broadcast(DataHandle);
}
}
Source/Aura/Public/AbilitySystem/AbilityTasks/TargetDataUnderMouse.h
private:
// 服务端目标数据委托的回调函数
void OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& DataHandle, FGameplayTag ActivationTag);
Source/Aura/Private/AbilitySystem/AbilityTasks/TargetDataUnderMouse.cpp
#include "AbilitySystem/AbilityTasks/TargetDataUnderMouse.h"
#include "AbilitySystemComponent.h"
UTargetDataUnderMouse* UTargetDataUnderMouse::CreateTargetDataUnderMouse(UGameplayAbility* OwningAbility)
{
UTargetDataUnderMouse* MyObj = NewAbilityTask<UTargetDataUnderMouse>(OwningAbility);
return MyObj;
}
// 技能激活
// 获得了鼠标下方的鼠标点击位置并广播它
void UTargetDataUnderMouse::Activate()
{
// 是否本地控制
const bool bIsLocallyControlled = Ability->GetCurrentActorInfo()->IsLocallyControlled();
if (bIsLocallyControlled)
{
SendMouseCursorData();
}
else
{
//TODO: We are on the server, so listen for target data.
// 非本地控制,服务端只是从客户端接受数据,然后控制
const FGameplayAbilitySpecHandle SpecHandle = GetAbilitySpecHandle();
const FPredictionKey ActivationPredictionKey = GetActivationPredictionKey();
// AbilityTargetDataSetDelegate 返回目标数据委托,需要与目标数据关联的预测键
// AddUObject 为委托绑定回调函数
// 一旦在服务端激活了技能 则会执行在此绑定的回调函数
AbilitySystemComponent.Get()->AbilityTargetDataSetDelegate(SpecHandle, ActivationPredictionKey).AddUObject(
this, &UTargetDataUnderMouse::OnTargetDataReplicatedCallback);
// 如果客户端发送目标数据先于服务端收到激活技能 ,则此委托永不会触发,服务器将不会从委托中接收到目标数据
// 如果从未触发过委托,那么强制广播委托,确保调用委托的回调,使服务器收到目标数据复制
const bool bCalledDelegate = AbilitySystemComponent.Get()->CallReplicatedTargetDataDelegatesIfSet(
SpecHandle, ActivationPredictionKey);
// 如果始终未触发委托,那么说明目标数据尚未到达服务器
// 需要等待
if (!bCalledDelegate)
{
SetWaitingOnRemotePlayerData();
}
}
}
void UTargetDataUnderMouse::SendMouseCursorData()
{
// 预测窗口
FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent.Get());
// 技能任务拥有他们所属技能
APlayerController* PC = Ability->GetCurrentActorInfo()->PlayerController.Get();
FHitResult CursorHit;
// 光标只是追踪可见性通道
// 仅跟踪简单的碰撞并传递光标点击数据到 CursorHit
PC->GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
FGameplayAbilityTargetDataHandle DataHandle;
FGameplayAbilityTargetData_SingleTargetHit* Data = new FGameplayAbilityTargetData_SingleTargetHit();
// 目标数据对象
Data->HitResult = CursorHit;
DataHandle.Add(Data);
// 将光标跟踪位置 DataHandle 这个目标数据发送到服务器,
// 服务器接受的类型之一 FGameplayAbilityTargetDataHandle
// 参数1:技能规格句柄
// 参数2:预测键。 技能启动相关。 GetActivationPredictionKey:技能最初被激活的时间
// 参数3:目标数据句柄,打包了目标数据
// 参数4:游戏标签
// 参数5:当前预测键
AbilitySystemComponent->ServerSetReplicatedTargetData(
GetAbilitySpecHandle(),
GetActivationPredictionKey(),
DataHandle,
FGameplayTag(),
AbilitySystemComponent->ScopedPredictionKey);
// 发送到服务端后,如果可以,在本地也广播发送
if (ShouldBroadcastAbilityTaskDelegates())
{
// 一旦激活此技能,就会广播该委托,这意味着有效的数据执行引脚将被执行。
// 但它不一定是立刻执行,这是一个异步任务。
// 根据某些技能任务的游戏机制,我们可能希望稍后广播这些委托
// 但对于当前情况,我们立即进行广播。
ValidData.Broadcast(DataHandle);
}
}
// 目标数据委托回调,接受目标数据
void UTargetDataUnderMouse::OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& DataHandle,
FGameplayTag ActivationTag)
{
// 告诉技能系统目标数据已经收到,不需要保留它的缓存。
// 将从 技能-目标数据 map 中清除该数据
AbilitySystemComponent->ConsumeClientReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey());
if (ShouldBroadcastAbilityTaskDelegates())
{
// 广播数据 执行引脚
ValidData.Broadcast(DataHandle);
}
}
初始化目标数据结构缓存 才可以使用目标数据复制相关的功能
UAbilitySystemGlobals::Get().InitGlobalData();
Source/Aura/Private/AuraAssetManager.cpp
#include "AbilitySystemGlobals.h"
void UAuraAssetManager::StartInitialLoading()
{
Super::StartInitialLoading();
// 开始加载游戏资产时初始化游戏标签
FAuraGameplayTags::InitializeNativeGameplayTags();
// This is required to use Target Data!
// 初始化目标数据结构缓存 才可以使用目标数据复制相关的功能
UAbilitySystemGlobals::Get().InitGlobalData();
}
打开 GA_FireBolt 图表: get hit result from target data break hit result
使用服务器+客户端模式启动游戏 这将启动一个服务端和一个客户端 此时客户端点击敌人显示调试球,服务端上的该客户端点击的敌人也会显示调试球。 目标数据由客户端发送到了服务端。
使用 服务器 + 2客户端模式
客户端的数据将会复制到服务端,但此时,服务端不会把该数据复制到所有客户端。 客户端1点击敌人,客户端1敌人出现调试球。 服务端敌人出现该调试球。 客户端2不会出现该调试球。
服务器此时可以向正确的方向发射火球。
多人游戏中,服务器应该有权对游戏状态进行任何重大更改。 如果客户可以改变重要因素,那么这就留下了一个可供作弊者利用的漏洞。 但是,如果客户端必须等待服务器确认其更改请求的有效, 那么客户端执行操作之间就会存在明显的延迟。
游戏利用预测解决该问题。
预测是指客户继续进行更改的时间。 移动角色、打开门、射击武器并将该变化通知服务器。 当服务器获取信息时,它会确保它是有效的更改。 如果更改无效,服务器将撤消这些更改。 快节奏的多人游戏必须利用预测。
开发人员不想要的是拥有充满分支的技能,就像如果权威做x,否则做 x 的预测版本。
相反,他们想要或多或少自动的预测。
并不是所有事情都需要预测。
例如,脚步声不需要预测,但诸如健康变化,法力造成伤害之类的重大事情需要预测。
Gas自动预测技能激,活触发事件游戏效果应用程序包括属性修改,但不执行计算。
得益于虚幻中的新角色运动系统,游戏还预测了蒙太奇和运动引擎。
但GAS不能预测游戏效果、移除效果或游戏周期性效果。 这些是一些尚未克服的技术挑战。
Gas 预测以预测密钥的概念为中心。
预测密钥只是一个唯一的 ID,并且这些被存储在客户端的中心位置。 当客户端执行预测操作时,它将向服务器发送预测密钥并与该键关联它的客户端预测操作和副作用。
服务器将接受或拒绝该密钥。 然后,它将把任何服务器端创建的副作用与该密钥关联起来,并响应客户端,通知密钥 效果已被接受或拒绝。 从客户端发送到服务器的 F 预测密钥将始终到达服务器。
复制仅发生从服务器到客户端,但是那指的是复制变量。 变量在其属性宏中标记有“replicated”或“replicated using”。 对于这些,复制仅从服务器到客户端,
但这里我们讨论的是发送服务器的预测密钥为此,为了方便起见,我们经常使用术语“复制”。 所以我们说这个密钥正在被复制到服务器,这常见于GAS. 当服务器将预测密钥发送回时,它只会返回给发送它的客户端.
Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h
protected:
// 生成投射物 子类蓝图中调用
UFUNCTION(BlueprintCallable, Category = "Projectile")
void SpawnProjectile(const FVector& ProjectileTargetLocation);
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
// 设置投射物旋转 方向
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
Rotation.Pitch = 0.f;
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 给投射物一个造成伤害的游戏效果规格
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
}
打开 GA_FireBolt 图表: 缓存获取的目标数据位置信息未 ProjectileTargetLocation ProjectileTargetLocation 输出给 SpawnProjectile 的 ProjectileTargetLocation
TargetDataUnderMouse 异步任务的 valid Data 引脚 连接后续节点。
因为 TargetDataUnderMouse 的 ValidData.Broadcast(DataHandle) 广播完成后才执行到该引脚。
wait gameplay event 的 event received 引脚连接后续节点 因为此为异步任务,需要等待接受到数据后再生成投射物
此时客户端无法发射火球,因为服务端引脚执行完后立刻终止了技能。
打开 BP_AuraCharacter 胶囊体组件-碰撞预设- 此处默认阻止了相机。应当设置为忽略相机。 网格体组件也阻止了相机。
在 C++中 设置为角色胶囊组件忽略相机。
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "Components/CapsuleComponent.h"
AAuraCharacterBase::AAuraCharacterBase()
{
PrimaryActorTick.bCanEverTick = false;
// 防止相机与角色碰撞导致相机视角放大
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
GetMesh()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
//初始化武器
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>("weapon");
//将武器附加到骨架插槽
Weapon->SetupAttachment(GetMesh(),FName("WeaponHandSocket"));
//武器不应有任何碰撞
Weapon->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
默认需要点击敌人才可以发射火球
Content/Blueprints/Input/InputActions/IA_SHIFT.uasset
在 目录 Content/Blueprints/Input/InputActions/
右键-输入-输入操作 新建 IA_SHIFT
打开 IA_SHIFT 值类型-Axis1D(浮点)
打开 IMC_AuraContext 输入操作上下文 添加一个映射 将 输入操作 IA_SHIFT 与 shift 键映射在一起 绑定输入操作 IA_SHIFT 绑定键-左shift和右shift
Source/Aura/Public/Player/AuraPlayerController.h
// shift + 鼠标悬浮 输入操作 用以发射技能
UPROPERTY(EditAnywhere, Category="Input")
TObjectPtr<UInputAction> ShiftAction;
// 悬浮+shift
void ShiftPressed() { bShiftKeyDown = true; };
void ShiftReleased() { bShiftKeyDown = false; };
bool bShiftKeyDown = false;
Source/Aura/Private/Player/AuraPlayerController.cpp
void AAuraPlayerController::AbilityInputTagHeld(FGameplayTag InputTag)
{
// 如果按住的不是鼠标左键,而是其他键,则激活按住对应键的技能
// 此时一定不是自动奔跑或鼠标左键单击技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())GetASC()->AbilityInputTagHeld(InputTag);
return;
}
if (GetASC()) GetASC()->AbilityInputTagReleased(InputTag);
// 如果是鼠标左键按住,并且鼠标跟踪到敌人目标,则激活按住左键技能
// 或者是鼠标悬浮+shift,并且鼠标跟踪到敌人目标,则激活按住左键技能
if (bTargeting || bShiftKeyDown)
{
if (GetASC())GetASC()->AbilityInputTagHeld(InputTag);
}
// 如果是鼠标左键按住,并且鼠标没有跟踪到敌人目标,则执行移动
else
{
// 开始累计按住的鼠标跟随时间,在鼠标释放时用以判断短按或长按
FollowTime += GetWorld()->GetDeltaSeconds();
// 鼠标按住时获取移动目的地
// 跟踪通道
// false 不跟踪复杂碰撞
if (CursorHit.bBlockingHit) CachedDestination = CursorHit.ImpactPoint;
// 如果有可控制pawn
if (APawn* ControlledPawn = GetPawn())
{
// 移动的方向 :从受控pawn 到 目的地 的方向向量,归一化
const FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
// 向该方向移动 每帧执行
ControlledPawn->AddMovementInput(WorldDirection);
}
}
}
void AAuraPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)
{
// 如果释放的不是鼠标左键,而是其他键,则激活释放对应键的技能
// 此时一定不是自动奔跑或鼠标释放左键技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())GetASC()->AbilityInputTagReleased(InputTag);
return;
}
// 如果释放的是鼠标左键,且跟踪到敌人,则执行释放鼠标左键的技能
if (GetASC()) GetASC()->AbilityInputTagReleased(InputTag);
// 如果是鼠标左键释放,并且鼠标没有跟踪到敌人目标,并且没有按下shift ,则自动奔跑至目的地
if (!bTargeting && !bShiftKeyDown)
{
const APawn* ControlledPawn = GetPawn();
// 如果鼠标跟随时间小于短按阙值,表示是短按
if (FollowTime <= ShortPressThreshold && ControlledPawn)
{
// 通过受控pawn位置和目的地位置,同步查找位置路径,生成导航路径
if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControlledPawn->GetActorLocation(), CachedDestination))
{
//清除样条线的点
Spline->ClearSplinePoints();
// 循环导航路径的点
for (const FVector& PointLoc : NavPath->PathPoints)
{
//向样条曲线添加点
Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
//DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Green, false, 5.f);
}
// 减去目的地路径导航点的最后一个点,防止目标点为障碍物中心时,玩家永远无法到达而不停奔跑
CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
// 正在自动奔跑为真
bAutoRunning = true;
}
}
//自动奔跑时重置跟随时间
FollowTime = 0.f;
// 自动奔跑时没有跟踪敌人
bTargeting = false;
}
}
void AAuraPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
UAuraInputComponent* AuraInputComponent = CastChecked<UAuraInputComponent>(InputComponent);
// 将移动输入操作绑定到输入组件的Move回调,用以移动角色。
// 参数1:输入操作
// 参数2:否希望在输入操作开始时调用 move
// 参数3: 用户对象 控制器
// 参数4:回调函数
AuraInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AAuraPlayerController::Move);
AuraInputComponent->BindAction(ShiftAction, ETriggerEvent::Started, this, &AAuraPlayerController::ShiftPressed);
AuraInputComponent->BindAction(ShiftAction, ETriggerEvent::Completed, this, &AAuraPlayerController::ShiftReleased);
// 将技能输入操作InputConfig绑定到输入组件的操作回调
// &ThisClass 当前类
AuraInputComponent->BindAbilityActions(InputConfig, this, &ThisClass::AbilityInputTagPressed,
&ThisClass::AbilityInputTagReleased, &ThisClass::AbilityInputTagHeld);
}
打开 BP_AuraPlayerController 细节-输入- Shift Action-IA_SHIFT
此时,服务器端 按住shift ,然后点击左键,可以向任意方向发射火球技能。 客户端存在问题
https://docs.unrealengine.com/5.0/zh-CN/motion-warping-in-unreal-engine/ 运动扭曲 是一种可以动态调整角色的根骨骼运动以对齐目标的功能
在角色蓝图中创建运动扭曲逻辑,在动画蒙太奇中分配运动扭曲窗口,并链接到指定位置。
让角色面向技能发射的方向
必须启用 运动扭曲(Motion Warping) 插件。如需详细了解插件及其安装方法,请参阅:使用插件。
运动扭曲利用了蓝图和动画蒙太奇工作流程。因此,你需要了解这些功能。
你的项目中有角色蓝图、输入功能按钮和动画,可用于创建Gameplay示例。
运动扭曲的整体功能可分为两大区域:
动画蒙太奇(Animation Montage) ,你可以在其中创建具备动画通知状态的运动扭曲窗口。
蓝图逻辑(Blueprint Logic) ,你可以在其中设置逻辑来分配扭曲目标并播放蒙太奇。
动态蒙太奇可供你指定运动扭曲区域,自定义其行为,并对其命名。
要新建一个运动扭曲区域,右键点击一个 通知(Notifies) 轨道,选择 添加通知状态...(Add Notify State...)> 运动扭曲(Motion Warping) 。
这些是带有开始和结束时间的可自定义区域,你可以将其对齐到动画中最适合应用扭曲的区域。
比如,在这个覆盖蒙太奇中,当角色把手放在障碍物上时,你可能需要确保起始扭曲区域覆盖整个区域。
动画通知 的 细节(Details) 面板包含运动扭曲正常运行所需的属性和设置。选择你的运动扭曲分段以显示这些细节。
细节名称 | 说明 |
---|---|
根骨骼运动修饰符(Root Motion Modifier) | 要指定的运动扭曲类型。缩放(Scale) :一种运动扭曲,可均匀地改变动画的比例。 倾斜扭曲(Skew Warp) :扭曲游戏对象的根骨骼运动,使其匹配关卡中扭曲窗口末尾的动画位置和旋转。 |
扭曲目标名称(Warp Target Name) | 用于查找此扭曲目标的名称。关联到 Add or Update Warp Target Point 蓝图节点。 |
扭曲点动画提供程序(Warp Point Anim Provider) | 为 扭曲点(Warp Point) 选择所需的提供程序。无(None) :此处没有声明扭曲点提供程序。 静态(Static) :用户定义的参数变换所定义的扭曲点,可以通过扭曲通知本身来声明。 骨骼(Bone) :扭曲点由骨骼定义。 |
扭曲点动画变换(Warp Point Anim Transform) | 变换动画扭曲点。仅当 扭曲点动画提供程序(Warp Point Anim Provider) 设置为 静态(Static) 时才相关。 |
扭曲点动画骨骼名称(Warp Point Anim Bone Name) | 声明要用作扭曲点目标的骨骼名称。仅当 扭曲点动画提供程序(Warp Point Anim Provider) 设置为 骨骼(Bone) 时才相关。 |
扭曲平移(Warp Translation) | 是否扭曲根骨骼运动的平移组件。 |
忽略Z轴(Ignore ZAxis) | 是否扭曲平移的Z组件。 |
扭曲旋转(Warp Rotation) | 是否扭曲根骨骼运动的旋转组件。 |
旋转类型(Rotation Type) | 是否应扭曲旋转以匹配扭曲目标的旋转或面向扭曲目标。默认(Default) :角色旋转以匹配扭曲目标的旋转。 面向(Facing) :角色旋转以面向扭曲目标。 |
扭曲旋转时间乘数(Warp Rotation Time Multiplier) | 修改旋转的扭曲速度。比如,如果运动扭曲(Motion Warping)窗口持续存在2秒,且此属性的值为0.5,则将在1秒后达到最终旋转。 |
通知颜色(Notify Color) | 设置运动扭曲通知关键帧的颜色。 |
蓝图用于添加你的运动扭曲组件,触发扭曲,并指定扭曲目标。
你必须将运动扭曲组件添加到蓝图才能启用运动扭曲行为。方法为点击 组件(Components) 面板中的 (+) 添加(*(+) Add) ,并在 移动(Movement) 类别下找到 运动扭曲(Motion Warping) 。点击即可添加。
现在将该组件从组件(Components)面板拖放到 事件图表(Event Graph) ,即可在你的蓝图图表中引用该组件。
从运动扭曲引用拖移链接后,你可以浏览与之相关的函数和事件。它们位于运动扭曲类别中。
你可以在蓝图中使用以下运动扭曲节点:
节点名称 | 节点图像 | 说明 |
---|---|---|
Add or Update WarpTarget | 此节点用于将蒙太奇资产中定义的扭曲目标名称链接到位置。右键点击 扭曲目标(Warp Target) 引脚并选择 分割结构体引脚(Split Struct Pin) ,可将该引脚分割成单独的 平移(Translation) 引脚和 旋转(Rotation) 引脚。反过来,可以使用 Remove Warp Target 节点来解除 扭曲目标名称(Warp Target Name) 的链接。 | |
Add Root Motion Modifier Skew Warp | 你可以使用此节点通过蓝图生成新运动扭曲窗口,而不是在蒙太奇资产中添加 倾斜扭曲动画通知(Skew Warp Anim Notifies) 。你也可在此处分配此运动扭曲窗口的设置,例如 开始时间(Start Time) 、 结束时间(End Time) 和 扭曲目标名称(Warp Target Name) 。此处还提供了 Add Root Motion Modifier for Scale 节点,以及用于禁用所有根骨骼运动修饰符的节点。 |
本小节介绍了如何设置角色扭曲到击打目标的简单运动扭曲示例。
开始之前,确保你打算使用的动画启用了根骨骼运动。具体做法是打开动画资产并启用 EnableRootMotion 。
第一步是创建并放置一个你要扭曲到的目标。此示例使用了圆柱体。
在 放置Actor(Place Actors) 面板中,点击 所有类(All Classes) 并找到 目标点(Target Point) 。将其拖放到关卡中以添加目标点。确保它对齐并旋转到你所需的扭曲点。
接下来,创建动画蒙太奇资产。要想从现有动画派生此类资产,有一个简单的方法,那就是是右键点击你的动画资产,并选择 创建(Create) > 创建动画蒙太奇(Create AnimMontage) 。创建蒙太奇之后,打开资产。 现在蒙太奇已打开,你可以在序列中推移以预览你的动画。下一步是在通知轨道下添加运动扭曲窗口。具体做法是,右键点击轨道区域并选择 添加通知状态...(Add Notify State...) > 运动扭曲(Motion Warping) 。 现在已创建运动扭曲窗口,你可以使用其中的控点设置开始和结束范围。
设置此运动扭曲窗口的范围,让它在动画开头附近开始,在角色攻击的那一刻结束。你还可以在移动 通知 关键帧时按住 Shift 键预览那个时刻的当前动画。
接下来,选择运动扭曲关键帧,并找到 细节(Details) 面板。你将在此处设置此关键帧的一些属性。
将 根骨骼运动修饰符配置(Root Motion Modifier Config) 设置为 倾斜(Skew)扭曲(Warp)** 。此操作用于指定扭曲类型。
为 扭曲目标名称(Warp Target Name) 设置名称。此操作用于用名称标识此扭曲。
现在打开你的角色蓝图资产。在事件图表中,创建映射到所需输入操作的 Input Action 节点。具体做法是,右键点击图表,并从 输入(Input)> 操作事件(Action Events) 中选择你的输入事件。
在本示例中,有一个用于打击的输入操作事件(Input Action Event for Punch)。
接下来,你需要获取你早先放在此示例中的目标点的位置。你有几种办法可以选择,但对于本示例,请创建 Get All Actors Of Class 节点。将 Actor类(Actor Class) 设置为 目标点(Target Point) 。最后,将Input Action节点中的 按下(Pressed) 输出引脚挂接到Get All Actors of Class函数的输入执行引脚。 最后,添加 Get(副本)节点以连接到输出Actor(Out Actors)数组数据引脚。你还要创建 Get Actor Location 函数,并将其输入 目标数据(Target data) 引脚连接到 Get 节点的输出数据引脚。
现在你要创建逻辑来获取目标点的位置。
首先,将运动扭曲组件添加到角色蓝图。具体做法是,点击组件(Components)面板中的 (+) 添加(*(+) Add) ,在移动(Movement)类别下找到运动扭曲(Motion Warping)。点击它以添加组件。 接下来,从组件(Components)面板将运动扭曲组件(Motion Warping Component)拖放到事件图表中。
从运动扭曲(Motion Warping)引用引脚拖移,以添加 Add or Update Warp Target from Transform 节点。创建后,将其输入事件引脚连接到 Get All Actors Of Class 节点。你还要确保将扭曲目标名称分配到 名称(Name) 引脚。此名称必须匹配你早先在蒙太奇中定义的扭曲目标名称。 你还需要将 Add or Update Warp Target from Transform 节点链接到目标位置。右键点击扭曲目标(Warp Target)引脚,选择 分割结构体引脚(Split Struct Pin) 将其转换为双位置/旋转引脚结构。然后将 Get Actor Location 的 返回值(Return Value) 引脚连接到 Get Actor Location 的返回值(Return Value)引脚。
连接Get Actor Location节点的 返回值(Return Value) (由黄色引脚指示的向量值)和Add or Update Warp Target from Transform节点的目标变换(Target Transform)引脚(由橙色引脚表示的变换值)时,将创建转换节点。如果存在不同值类型,但它们在转换后可兼容,则虚幻引擎会在连接引脚时自动创建转换节点。
现在,你可以在事件图表中引用 骨骼网格体(Skeletal Mesh) 组件,并在其上播放蒙太奇。将 骨骼网格体(Skeletal Mesh) 组件拖放到事件图表中。
右键点击图表并选择 动画(Animation)> 蒙太奇(Montage)> 播放蒙太奇(Play Montage) 以添加 Play Montage 节点。然后将你的蒙太奇资产分配到 要播放的蒙太奇(Montage to Play) 引脚。
现在你运行关卡时,应该能够看到角色在播放其打击动画时扭曲到相应的点。
你可以在下面看到本页用于将运动扭曲实现到简单扭曲目标位置的角色蓝图逻辑大图。
细节-根运动-启用根运动-启用 使其成为根运动动画,才可以使用 Motion Warping 运动扭曲 引擎功能
打开 BP_AuraCharacter 添加 Motion Warping 运动扭曲 组件
将通知轨道 1 重命名为 Events
再添加一个通知轨道 Motion Warp
调整时间 到 发射技能起始姿势处 在轨道 Motion Warp 添加通知状态-Motion Warping
调节通知状态 的末端覆盖技能动画的发射部分
选择运动扭曲关键帧-细节-
根骨骼运动修饰符配置(Root Motion Modifier Config) - 倾斜(Skew)扭曲(Warp)** 。 指定扭曲类型。
动画通知-root motion modifier-扭曲目标名称(Warp Target Name)- FacingTarget 用名称标识此扭曲。
动画通知-root motion modifier- wrap translation-不启用 这不会扭曲位置。
动画通知-root motion modifier- wrap translation-wrap rotation-启用 动画通知-root motion modifier- wrap translation-rotation type -Facing
这样,动画将旋转根骨骼以面向目标,我们将其称为面向目标。
打开 BP_AuraCharacter 事件图表
右键 custom event 添加自定义事件 SetFacingTarget SetFacingTarget 事件-细节-输入- 添加输入参数 FacingTarget 类型 vector 向量
拖入扭曲组件 扭曲组件 拖出 add or update warp target from location
SetFacingTarget - FacingTarget 输出至 add or update warp target from location - target location
add or update warp target from location - warp Target name 为 动画蒙太奇通知状态指定的 扭曲目标名称(Warp Target Name)- FacingTarget
当播放 AM_Cast_FireBolt 动画蒙太奇 时,只要处于这个运动扭曲窗口中【蒙太奇的扭曲通知状态时间轴范围内】,如果面向目标设定后,角色就会朝该方向旋转。
打开 GA_FireBolt 图表:
ability-get avatar actor from actor info 获取节能化身,将其转化为角色 cast to BP_AuraCharacter
cast to BP_AuraCharacter 输出至 set facing target 的 target
ProjectileTargetLocation 输出至 set facing target 的 facing target
现在释放技能时,角色将面向火球发射的方向。
现在 技能依赖角色蓝图。 但技能不应依赖角色,而应依赖角色实现的战斗接口。 为了分离这种依赖,在战斗接口中创建函数 这样,敌人也可以使用该接口实现扭曲。
Source/Aura/Public/Interaction/CombatInterface.h
UINTERFACE(MinimalAPI,BlueprintType)
public:
// 面向目标 在蓝图子类中实现
// native 事件不能标记为虚拟
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void UpdateFacingTarget(const FVector& Target);
打开 BP_AuraCharacter 事件图表 添加事件-UpdateFacingTarget 连接到 add or update warp target from location
删除事件 SetFacingTarget
打开 GA_FireBolt 删除 SetFacingTarget 删除 cast to BP_AuraCharacter
get avatar actor from actor info 转化为 cast to CombatInterface cast to CombatInterface 拖出 UpdateFacingTarget
断开最后的 end Ability 结束技能节点,使客户端也可以释放火球。 但此时有问题,只能释放一次,因为客户端技能始终没有结束,所以不能再次激活。
火球撞击敌人时产生效果
Source/Aura/Aura.Build.cs
PrivateDependencyModuleNames.AddRange(new string[] { "NavigationSystem","Niagara"});
Source/Aura/Public/Actor/AuraProjectile.h
class UNiagaraSystem;
protected:
virtual void Destroyed() override;
private:
// 投射物的生命周期 可存在时间 ,时间到期后销毁自身
UPROPERTY(EditDefaultsOnly)
float LifeSpan = 15.f;
bool bHit = false;
// 撞击 Niagara
UPROPERTY(EditAnywhere)
TObjectPtr<UNiagaraSystem> ImpactEffect;
// 撞击音效
UPROPERTY(EditAnywhere)
TObjectPtr<USoundBase> ImpactSound;
// 循环音效
UPROPERTY(EditAnywhere)
TObjectPtr<USoundBase> LoopingSound;
//UPROPERTY()
//TObjectPtr<UAudioComponent> LoopingSoundComponent;
Source/Aura/Private/Actor/AuraProjectile.cpp
#include "Actor/AuraProjectile.h"
#include "NiagaraFunctionLibrary.h"
#include "Components/AudioComponent.h"
#include "Components/SphereComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Kismet/GameplayStatics.h"
AAuraProjectile::AAuraProjectile()
{
PrimaryActorTick.bCanEverTick = false;
// 投射物 AuraProjectile 基类需要可网络复制
bReplicates = true;
Sphere = CreateDefaultSubobject<USphereComponent>("Sphere");
SetRootComponent(Sphere);
Sphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Sphere->SetCollisionResponseToAllChannels(ECR_Ignore);
Sphere->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
Sphere->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Overlap);
Sphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>("ProjectileMovement");
ProjectileMovement->InitialSpeed = 550.f;
ProjectileMovement->MaxSpeed = 550.f;
// 没有重力
ProjectileMovement->ProjectileGravityScale = 0.f;
}
void AAuraProjectile::BeginPlay()
{
Super::BeginPlay();
//SetLifeSpan(LifeSpan);
Sphere->OnComponentBeginOverlap.AddDynamic(this, &AAuraProjectile::OnSphereOverlap);
//LoopingSoundComponent = UGameplayStatics::SpawnSoundAttached(LoopingSound, GetRootComponent());
}
void AAuraProjectile::Destroyed()
{
if (!bHit && !HasAuthority())
{
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
//LoopingSoundComponent->Stop();
}
Super::Destroyed();
}
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
// 播放音效
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
// 撞击Niagara特效
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
//LoopingSoundComponent->Stop();
if (HasAuthority())
{
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
else
{
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
bHit = true;
}
}
细节-AuraProjectile-Impact Effect-NS_FireExplosion1 火球爆炸特效
细节-AuraProjectile-Impact Sound-sfx_FireBolt_Impact 火球爆炸音效
此时玩家可以向敌人发射火球
打开 AM_Cast_FireBolt
添加 Sound 轨道 添加通知-播放音效 选择 playSound-细节-音效-sfx_FireBolt
Source/Aura/Public/Actor/AuraProjectile.h
class UNiagaraSystem;
protected:
virtual void Destroyed() override;
private:
// 投射物的生命周期 可存在时间 ,时间到期后销毁自身
UPROPERTY(EditDefaultsOnly)
float LifeSpan = 15.f;
bool bHit = false;
// 撞击 Niagara
UPROPERTY(EditAnywhere)
TObjectPtr<UNiagaraSystem> ImpactEffect;
// 撞击音效
UPROPERTY(EditAnywhere)
TObjectPtr<USoundBase> ImpactSound;
// 循环音效
UPROPERTY(EditAnywhere)
TObjectPtr<USoundBase> LoopingSound;
UPROPERTY()
TObjectPtr<UAudioComponent> LoopingSoundComponent;
Source/Aura/Private/Actor/AuraProjectile.cpp
#include "Actor/AuraProjectile.h"
#include "NiagaraFunctionLibrary.h"
#include "Components/AudioComponent.h"
#include "Components/SphereComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Kismet/GameplayStatics.h"
AAuraProjectile::AAuraProjectile()
{
PrimaryActorTick.bCanEverTick = false;
// 投射物 AuraProjectile 基类需要可网络复制
bReplicates = true;
Sphere = CreateDefaultSubobject<USphereComponent>("Sphere");
SetRootComponent(Sphere);
Sphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Sphere->SetCollisionResponseToAllChannels(ECR_Ignore);
Sphere->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
Sphere->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Overlap);
Sphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>("ProjectileMovement");
ProjectileMovement->InitialSpeed = 550.f;
ProjectileMovement->MaxSpeed = 550.f;
// 没有重力
ProjectileMovement->ProjectileGravityScale = 0.f;
}
void AAuraProjectile::BeginPlay()
{
Super::BeginPlay();
SetLifeSpan(LifeSpan);
Sphere->OnComponentBeginOverlap.AddDynamic(this, &AAuraProjectile::OnSphereOverlap);
LoopingSoundComponent = UGameplayStatics::SpawnSoundAttached(LoopingSound, GetRootComponent());
}
void AAuraProjectile::Destroyed()
{
if (!bHit && !HasAuthority())
{
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
LoopingSoundComponent->Stop();
}
Super::Destroyed();
}
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
// 播放音效
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
// 撞击Niagara特效
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
LoopingSoundComponent->Stop();
if (HasAuthority())
{
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
else
{
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
bHit = true;
}
}
打开 BP_FireBolt 细节-AuraProjectile-Looping Sound-sfx_FireBoltHiss 火球循环音效
选择障碍物-搜索 generate 细节-物理-碰撞-生成重叠事件-启用
场景中大多数物体的 碰撞预设-对象类型 为 WorldStatic,WorldDynamic 所以需要一个 自定义对象类型
项目设置-引擎-碰撞-Object channels-新建object通道 命名-Projectile 默认响应-Ignore
设置为忽略响应后,可以手动他对其他对象的具体响应类型。
您最多可以拥有18个自定义通道(包括对象和检测通道)。这是您项目的对象类型列表。如果删除游戏正在使用的对象类型,那么此类型将变回WorldStatic,
Source/Aura/Aura.h
// 投射碰撞通道
// 第一次创建 所以占用通道1,以此类推
#define ECC_Projectile ECollisionChannel::ECC_GameTraceChannel1
蓝图中 Projectile 对象类型 对应 c++ 中 ECC_Projectile 通道
打开 BP_ManaPotion-box碰撞组件 细节-碰撞-碰撞预设-custom 细节-碰撞-碰撞预设-物体响应-Projectile-忽略 【与Projectile通道不会重叠或阻挡】
细节-碰撞-碰撞预设-物体响应-Projectile-重叠
打开 BP_FireBolt-Sphere球体碰撞组件 细节-碰撞-碰撞预设-custom 碰撞预设-对象类型 -Projectile
Source/Aura/Private/Actor/AuraProjectile.cpp
#include "Aura/Aura.h"
AAuraProjectile::AAuraProjectile()
{
PrimaryActorTick.bCanEverTick = false;
// 投射物 AuraProjectile 基类需要可网络复制
bReplicates = true;
Sphere = CreateDefaultSubobject<USphereComponent>("Sphere");
SetRootComponent(Sphere);
// 投射物的碰撞预设-对象类型 均为 ECC_Projectile
Sphere->SetCollisionObjectType(ECC_Projectile);
Sphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Sphere->SetCollisionResponseToAllChannels(ECR_Ignore);
Sphere->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
Sphere->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Overlap);
Sphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>("ProjectileMovement");
ProjectileMovement->InitialSpeed = 550.f;
ProjectileMovement->MaxSpeed = 550.f;
// 没有重力
ProjectileMovement->ProjectileGravityScale = 0.f;
}
所有基于 AuraProjectile 创建的投射物均为ECC_Projectile 类型,默认忽略一切。 现在所有的药剂都会忽略火球 但敌人也会被火球忽略。
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "Aura/Aura.h"
AAuraCharacterBase::AAuraCharacterBase()
{
PrimaryActorTick.bCanEverTick = false;
// 防止相机与角色碰撞导致相机视角放大
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
GetMesh()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
GetMesh()->SetCollisionResponseToChannel(ECC_Projectile, ECR_Overlap);
GetMesh()->SetGenerateOverlapEvents(true);
//初始化武器
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>("weapon");
//将武器附加到骨架插槽
Weapon->SetupAttachment(GetMesh(),FName("WeaponHandSocket"));
//武器不应有任何碰撞
Weapon->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
打开 BP_EnemyBase 网格体组件-碰撞-生成重叠事件-启用
此时 火球可以与敌人触发重叠事件。 火球可以攻击敌人。
仅作学习。
Content/Blueprints/AbilitySystem/GameplayEffects/GE_Damage.uasset
基于 GameplayEffect 新建 GE_Damage 打开 GE_Damage
细节-Gameplay Effect-Modifiers-索引0-Attribute-AuraAttributeSet.Health 细节-Gameplay Effect-Modifiers-索引0-Modifier Op-Add 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type -Scalable Float 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude- -10
Source/Aura/Public/Actor/AuraProjectile.h
#include "GameplayEffectTypes.h"
public:
// 使投射物携带游戏效果句柄 [轻型版本]
// 在生成时公开
UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = true))
FGameplayEffectSpecHandle DamageEffectSpecHandle;
在服务器上应用伤害效 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端果 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
Source/Aura/Private/Actor/AuraProjectile.cpp
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
// 播放音效
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
// 撞击Niagara特效
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
LoopingSoundComponent->Stop();
if (HasAuthority())
{
// 只在服务器上应用伤害效果即可
// 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端
// 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
TargetASC->ApplyGameplayEffectSpecToSelf(*DamageEffectSpecHandle.Data.Get());
}
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
else
{
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
bHit = true;
}
}
Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h
class UGameplayEffect;
protected:
// 伤害游戏效果类
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TSubclassOf<UGameplayEffect> DamageEffectClass;
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
// 设置投射物旋转 方向
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
Rotation.Pitch = 0.f;
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo());
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, GetAbilityLevel(), SourceASC->MakeEffectContext());
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
UE_LOG(LogTemp, Warning, TEXT("Changed Health on %s, Health: %f"), *Props.TargetAvatarActor->GetName(), GetHealth());
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
}
仅作学习
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::InitAbilityActorInfo()
{
// 初始技能参与者信息 服务器和客户端都在此设置
// 两者均为敌人类自身角色
AbilitySystemComponent->InitAbilityActorInfo(this, this);
Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent)->AbilityActorInfoSet();
// 为敌人基类临时添加初始化属性功能 仅作学习用 需要在蓝图中设置属性类
InitializeDefaultAttributes();
}
注意,GE_SecondaryAttributes 游戏属性重命名为 GE_AuraSecondaryAttributes
打开 BP_EnemyBase 暂时共享玩家的游戏属性。
Default Primary Attributes-GE_AuraPrimaryAttributes Default Secondary Attributes-GE_AuraSecondaryAttributes Default Vital Attributes-GE_AuraVitalAttributes
打开 GA_FireBolt Damage Effect Class-GE_Damage
此时 火球与敌人重叠时,会减少敌人的健康属性值
仅作测试多次技能伤害用 打开 GA_FireBolt
敌人的健康值在不断变少。 服务器端和客户端均正常。
LogTemp: Warning: Health: 102.500000
LogTemp: Warning: Health 的改变大小: -10.000000
LogTemp: Warning: Changed Health on BP_Goblin_Slingshot_C_2, Health: 102.500000
LogTemp: Warning: Health: 92.500000
LogTemp: Warning: Health 的改变大小: -10.000000
LogTemp: Warning: Changed Health on BP_Goblin_Slingshot_C_2, Health: 92.500000
LogTemp: Warning: Health: 82.500000
LogTemp: Warning: Health 的改变大小: -10.000000
LogTemp: Warning: Changed Health on BP_Goblin_Slingshot_C_2, Health: 82.500000
应该只有其中之一与火区域重叠。
打开 BP_AuraCharacter 选中 胶囊体组件-细节-碰撞-生成重叠事件 -不启用
此时,只有网格体组件会与火区域发生重叠。 角色网格体组件需要设置对 WorldDynamic 阻挡。 因为火区域为 WorldDynamic 对象类型。
Source/Aura/Private/Character/AuraCharacterBase.cpp
AAuraCharacterBase::AAuraCharacterBase()
{
PrimaryActorTick.bCanEverTick = false;
// 防止相机与角色碰撞导致相机视角放大
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
// 设置胶囊体不生成重叠事件,防止多次触发重叠事件,因为网格体组件已设置了重叠事件
GetCapsuleComponent()->SetGenerateOverlapEvents(false);
GetMesh()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
GetMesh()->SetCollisionResponseToChannel(ECC_Projectile, ECR_Overlap);
GetMesh()->SetGenerateOverlapEvents(true);
//初始化武器
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>("weapon");
//将武器附加到骨架插槽
Weapon->SetupAttachment(GetMesh(),FName("WeaponHandSocket"));
//武器不应有任何碰撞
Weapon->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
Source/Aura/Public/Character/AuraEnemy.h
#include "UI/WidgetController/OverlayWidgetController.h"
class UWidgetComponent;
protected:
// 健康条控件
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<UWidgetComponent> HealthBar;
Source/Aura/Private/Character/AuraEnemy.cpp
#include "Components/WidgetComponent.h"
#include "UI/Widget/AuraUserWidget.h"
AAuraEnemy::AAuraEnemy()
{
// 设置敌人基类的网格体组件的碰撞预设为 custom,检测响应-Visibility-阻挡,
// 使光标跟踪生效,因为光标跟踪Visibility通道。
// GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
// 构造敌人类的技能系统组件
AbilitySystemComponent = CreateDefaultSubobject<UAuraAbilitySystemComponent>("AbilitySystemComponent");
// 设置为网络复制
AbilitySystemComponent->SetIsReplicated(true);
// 设置复制模式 游戏效果不重复。游戏提示和游戏标签复制到所有客户端。
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
// 构造敌人类的属性集
AttributeSet = CreateDefaultSubobject<UAuraAttributeSet>("AttributeSet");
// 构造健康条控件
HealthBar = CreateDefaultSubobject<UWidgetComponent>("HealthBar");
// 将健康条控件附加到根组件
HealthBar->SetupAttachment(GetRootComponent());
}
Content/Blueprints/UI/ProgressBar/WBP_ProgressBar.uasset
右键-用户界面-控件蓝图-AuraUserWidget 名称 WBP_ProgressBar
打开 WBP_ProgressBar
设计器: 尺寸框-SizeBox_Root 设为变量 所需 宽度重载 60 高度重载 6
图表: 事件 event pre construct
SizeBox_Root 布局-尺寸框-set width override 函数
提升为变量 BoxWidth 分组 Progress Bar Properties 默认值 60
SizeBox_Root 布局-尺寸框-set height override 函数
提升为变量 BoxHeight 分组 Progress Bar Properties 默认值 6
折叠到函数 UpdateBoxSize
设计器: 覆层-Overlay_Root
进度条-ProgressBar_Front 设为变量 水平填充 垂直填充
ProgressBar-样式-样式-背景图-着色-A-0 ProgressBar-进度-百分比-30
图表: ProgressBar_Front 变量-样式-set style set style-样式 拖出 make ProgressBarStyle make ProgressBarStyle - background Image 拖出 make slateBrush make ProgressBarStyle - fill image 提升为变量 FrontBarFillBrush make slateBrush - tint 拖出 make slateColor make slateColor-specifie color-1,1,1,0
变量 FrontBarFillBrush 分组 Progress Bar Properties 默认值-着色-红色 折叠导函数 UpdateFrontFillBrush
打开 BP_EnemyBase 选择 Health Bar 组件-细节-用户界面-控件类-WBP_ProgressBar 组件-细节-用户界面-以所需大小绘制-启用
调整 Health Bar 组件 位置,防止被敌人网格体盖住 旋转敌人面对视图,因为控件只能对正面视角显示。
用以广播数据到健康控件
Source/Aura/Public/Character/AuraEnemy.h
#include "UI/WidgetController/OverlayWidgetController.h"
public:
// 健康值变更委托
// 包含 #include "UI/WidgetController/OverlayWidgetController.h" 以使用其中定义的委托
UPROPERTY(BlueprintAssignable)
FOnAttributeChangedSignature OnHealthChanged;
// 最大健康值变更委托
UPROPERTY(BlueprintAssignable)
FOnAttributeChangedSignature OnMaxHealthChanged;
Source/Aura/Private/Character/AuraEnemy.cpp
#include "UI/Widget/AuraUserWidget.h"
void AAuraEnemy::BeginPlay()
{
Super::BeginPlay();
InitAbilityActorInfo();
// 为健康条控件绑定控件控制器
// 敌人自身将成为健康条控件的控件控制器
if (UAuraUserWidget* AuraUserWidget = Cast<UAuraUserWidget>(HealthBar->GetUserWidgetObject()))
{
AuraUserWidget->SetWidgetController(this);
}
if (const UAuraAttributeSet* AuraAS = Cast<UAuraAttributeSet>(AttributeSet))
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
// 广播初始值
OnHealthChanged.Broadcast(AuraAS->GetHealth());
OnMaxHealthChanged.Broadcast(AuraAS->GetMaxHealth());
}
}
Content/Blueprints/UI/ProgressBar/WBP_EnemyHealthBar.uasset
右键-用户界面-控件蓝图-WBP_ProgressBar
名称 WBP_EnemyHealthBar
打开 WBP_EnemyHealthBar
打开 BP_EnemyBase 选择 Health Bar 组件-细节-用户界面-控件类-WBP_EnemyHealthBar
打开 WBP_EnemyHealthBar 图表: 添加事件-event widget controller set
Sequence
变量-get widget controller
widget controller 拖出 cast to BP_EnemyBase cast to BP_EnemyBase - as BP Enemy Base 提升为变量 BPEnemyBase
BPEnemyBase BPEnemyBase 拖出 assign on health changed
BPEnemyBase BPEnemyBase 拖出 assign on maxHealth changed
get progress bar front progress bar front 拖出 进度-set percent 函数
onHealthChaned_事件-new value 提升为变量 Health onMaxHealthChaned_事件-new value 提升为变量 MaxHealth
Health MaxHealth safe divide
现在对敌人施放火球技能,敌人的健康条会实时显示健康百分比
可能需要在敌人蓝图中调整 health bar 的缩放
幽灵进度条 留空待做。
基于 DataAsset 新建 C++ CharacterClassInfo
需要一种方法来存储每个角色的数据 ,还需要一个枚举对角色进行分类。
需要 一个包含每个职业的所有信息的结构
Source/Aura/Public/AbilitySystem/Data/CharacterClassInfo.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CharacterClassInfo.generated.h"
class UGameplayEffect;
// 角色进行分类 职业
UENUM(BlueprintType)
enum class ECharacterClass : uint8
{
//魔法师
Elementalist,
//战士
Warrior,
//游侠
Ranger
};
// 包含每个职业的所有信息的结构
USTRUCT(BlueprintType)
struct FCharacterClassDefaultInfo
{
GENERATED_BODY()
// 一个游戏效果来应用到主要属性。
// 一个能够存储新游戏效果的子类
UPROPERTY(EditDefaultsOnly, Category = "Class Defaults")
TSubclassOf<UGameplayEffect> PrimaryAttributes;
};
UCLASS()
class AURA_API UCharacterClassInfo : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, Category = "Character Class Defaults")
TMap<ECharacterClass, FCharacterClassDefaultInfo> CharacterClassInformation;
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
TSubclassOf<UGameplayEffect> SecondaryAttributes;
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
TSubclassOf<UGameplayEffect> VitalAttributes;
FCharacterClassDefaultInfo GetClassDefaultInfo(ECharacterClass CharacterClass);
};
Source/Aura/Private/AbilitySystem/Data/CharacterClassInfo.cpp
#include "AbilitySystem/Data/CharacterClassInfo.h"
FCharacterClassDefaultInfo UCharacterClassInfo::GetClassDefaultInfo(ECharacterClass CharacterClass)
{
return CharacterClassInformation.FindChecked(CharacterClass);
}
Content/Blueprints/AbilitySystem/Data/DA_CharacterClassInfo.uasset
右键-其他-数据资产-CharacterClassInfo 名称 DA_CharacterClassInfo
打开 DA_CharacterClassInfo 新建3个 职业信息 最后一个再使用默认职业,否则无法多次创建。
这将主要属性游戏效果子类化
SecondaryAttributes ,VitalAttributes 在所欲职业之间共享。
现在有了数据资产,可以用游戏效果填充它们。
Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/Aura/GE_AuraPrimaryAttributes.uasset
/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/Aura
目录存放玩家的游戏效果[用以属性]
/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/Enemy
目录存放敌人的游戏效果[用以属性]
基于 GameplayEffect 为各职业新建游戏效果 GE_PrimaryAttributes_Elementalist GE_PrimaryAttributes_Ranger GE_PrimaryAttributes_Warrior
Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/Enemy/GE_PrimaryAttributes_Elementalist.uasset
Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/Enemy/GE_PrimaryAttributes_Ranger.uasset
Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/Enemy/GE_PrimaryAttributes_Warrior.uasset
次要属性的游戏效果是所有职业共享的相同效果。作为派生属性,可以在主要属性的修改器中调整参数。
如果想为敌人使用不同与玩家的次要属性修改器,可以单独为敌人新建次要属性游戏效果。 例如 GE_SecondaryAttributes_Elementalist
此处不使用不同,玩家与敌人使用相同。
将玩家 的 次要属性 重要属性 移动至共享文件夹,与敌人共享
重命名。去掉Aura部分表示共享
Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/GE_SecondaryAttributes.uasset
Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/GE_VitalAttributes.uasset
打开 DA_CharacterClassInfo
各职业使用对应的主属性游戏效果
共享属性使用玩家共享的游戏效果
Warrior 职业主属性-GE_PrimaryAttributes_Warrior Ranger 职业主属性-GE_PrimaryAttributes_Ranger Elementalist 职业主属性-GE_PrimaryAttributes_Elementalist
所有职业通用次属性 SecondaryAttributes - GE_SecondaryAttributes 所有职业通用重要属性 VitalAttributes - GE_VitalAttributes
打开 GE_PrimaryAttributes_Elementalist
需要为4个主属性分别添加修改器,每个都使用曲线表,曲线表包含不同的等级对应的值
右键-其他-曲线表格-Cubic
三次曲线 不会自动插值
Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/Enemy/CurveTables/CT_PrimaryAttributes_Elementalist.uasset
打开 CT_PrimaryAttributes_Elementalist
与游戏标签保持一致
曲线上-右键-添加关键帧
选择关键帧:修改值 x-1,y-5 表示力量 1级是,5点力量。 虽然x显示为时间单位秒,但可以当作x值。
单击 缩放匹配,当前关键帧居中显示。
在曲线上继续添加关键帧,x-5,y-7
选择2个关键帧,在第一个关键帧点上-右键-自动 【三次插值,自动切线】 可以使关键帧之间的线段平滑
在曲线上继续添加关键帧,x-10,y-9.5
选择曲线视图模式-规格化视图模式 规格化视图,显示与规格化为[-1,1]的Y轴重叠的所有曲线 可显示所有的关键帧
在曲线上继续添加关键帧,x-15,y-12.5 在曲线上继续添加关键帧,x-20,y-14 在曲线上继续添加关键帧,x-40,y-25
全选关键帧-自动 使其平滑
添加曲线 Attributes.Primary.Intelligence
添加关键帧: 1,15 5,19 10,21 20,25 40,40
全选-自动
添加曲线 Attributes.Primary.Resilience
添加关键帧: 1,11 40,20 全选-自动
添加曲线 Attributes.Primary.Vigor
添加关键帧: 1,7 40,14
全选-自动
打开 GE_PrimaryAttributes_Elementalist
细节-Gameplay Effect-Modifiers 添加4组 对应4个主属性
使用曲线表格 CT_PrimaryAttributes_Elementalist 的 Attributes.Primary.Strength 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Strength Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Elementalist,Attributes.Primary.Strength
使用曲线表格 CT_PrimaryAttributes_Elementalist 的Attributes.Primary.Intelligence 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Intelligence Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Elementalist,Attributes.Primary.Intelligence
使用曲线表格 CT_PrimaryAttributes_Elementalist 的Attributes.Primary.Resilience 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Resilience Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Elementalist,Attributes.Primary.Strength
使用曲线表格 CT_PrimaryAttributes_Elementalist 的 Attributes.Primary.Vigor 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Vigor Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Elementalist,Attributes.Primary.Strength
CT_PrimaryAttributes_Elementalist-右键-导出为CSV CT_PrimaryAttributes_Elementalist.csv
Data/CT_PrimaryAttributes_Elementalist.csv
---,1.000000,5.000000,10.000000,15.000000,20.000000,40.000000
Attributes.Primary.Strength,5.000000,7.000000,9.500000,12.500000,14.000000,25.000000
Attributes.Primary.Intelligence,15.000000,19.000000,21.000000,25.000000,40.000000
Attributes.Primary.Resilience,11.000000,20.000000
Attributes.Primary.Vigor,7.000000,14.000000
第一行表示x,等级 其他行表示y
为其他属性的等级补全值
同名曲线将被覆盖
打开 CT_PrimaryAttributes_Elementalist 从源文件中重导入曲线表。所有变更将丢失。此操作不可撤消 4个曲线都被修改。 导入的曲线无法使用 自动 选项。
重命名为 CT_PrimaryAttributes_Ranger.csv
修改值
---,1,5,10,14.9,20.4,40
Attributes.Primary.Strength,6,9,14,18,26,34
Attributes.Primary.Intelligence,12,15,17,24,27,32
Attributes.Primary.Resilience,13,15,17,23,27,33
Attributes.Primary.Vigor,11,15,17,25,31,35
Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/Enemy/CurveTables/CT_PrimaryAttributes_Ranger.uasset
从文件 CT_PrimaryAttributes_Ranger.csv 导入曲线
CT_PrimaryAttributes_Elementalist-右键-导出为json CT_PrimaryAttributes_Elementalist.json
[
{
"Name": "Attributes.Primary.Strength",
"1": 5,
"5": 7,
"10": 9.5,
"15": 12.5,
"20": 14,
"40": 25
},
{
"Name": "Attributes.Primary.Intelligence",
"1": 15,
"5": 19,
"10": 21,
"15": 25,
"20": 35,
"40": 45
},
{
"Name": "Attributes.Primary.Resilience",
"1": 11,
"5": 15,
"10": 17,
"15": 24,
"20": 32,
"40": 40
},
{
"Name": "Attributes.Primary.Vigor",
"1": 7,
"5": 12.25,
"10": 13,
"15": 16,
"20": 20,
"40": 24
}
]
重命名为 Data/CT_PrimaryAttributes_Warrior.json 修改值
[
{
"Name": "Attributes.Primary.Strength",
"1": 15,
"5": 21,
"10": 26,
"14.9": 33,
"20.4": 37,
"40": 42
},
{
"Name": "Attributes.Primary.Intelligence",
"1": 5,
"5": 6.5,
"10": 7.9,
"14.9": 9,
"20.4": 11,
"40": 13
},
{
"Name": "Attributes.Primary.Resilience",
"1": 15,
"5": 17,
"10": 21,
"14.9": 25,
"20.4": 27,
"40": 31
},
{
"Name": "Attributes.Primary.Vigor",
"1": 11,
"5": 15,
"10": 16,
"14.9": 21,
"20.4": 25,
"40": 29
}
]
点击内容栏-导入 选择 Data/CT_PrimaryAttributes_Warrior.json 文件
导入为-CurveTable 选择曲线插值类型-Cubic 应用
自动创建曲线表格 CT_PrimaryAttributes_Warrior 与json文件同名
打开 CT_PrimaryAttributes_Warrior
使用json格式可以修改曲线。 而csv无法修改曲线。
打开 GE_PrimaryAttributes_Ranger
细节-Gameplay Effect-Modifiers 添加4组 对应4个主属性
使用曲线表格 CT_PrimaryAttributes_Elementalist 的 Attributes.Primary.Strength 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Strength Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Ranger,Attributes.Primary.Strength
使用曲线表格 CT_PrimaryAttributes_Elementalist 的Attributes.Primary.Intelligence 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Intelligence Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Ranger,Attributes.Primary.Intelligence
使用曲线表格 CT_PrimaryAttributes_Elementalist 的Attributes.Primary.Resilience 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Resilience Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Ranger,Attributes.Primary.Strength
使用曲线表格 CT_PrimaryAttributes_Elementalist 的 Attributes.Primary.Vigor 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Vigor Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Ranger,Attributes.Primary.Strength
打开 GE_PrimaryAttributes_Warrior
细节-Gameplay Effect-Modifiers 添加4组 对应4个主属性
使用曲线表格 CT_PrimaryAttributes_Elementalist 的 Attributes.Primary.Strength 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Strength Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Warrior,Attributes.Primary.Strength
使用曲线表格 CT_PrimaryAttributes_Elementalist 的Attributes.Primary.Intelligence 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Intelligence Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Warrior,Attributes.Primary.Intelligence
使用曲线表格 CT_PrimaryAttributes_Elementalist 的Attributes.Primary.Resilience 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Resilience Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Warrior,Attributes.Primary.Strength
使用曲线表格 CT_PrimaryAttributes_Elementalist 的 Attributes.Primary.Vigor 曲线
Modifiers-索引0-Attribute-Attributes.Primary.Vigor Modifiers-索引0-Modifier Op-Override Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-1 ,CT_PrimaryAttributes_Warrior,Attributes.Primary.Strength
打开 DA_CharacterClassInfo
玩家的主要属性可以在运行时变化, 玩家的辅助属性基于重要属性,应用了无限时间游戏效果随主属性变化。
但敌人的主要属性不需要在运行时变化, 敌人的主要属性根据其等级初始化,然后不再变化。 所以不需要为敌人辅助属性应用无限时间游戏效果。
拷贝 GE_SecondaryAttributes 辅助属性效果 重命名为 GE_SecondaryAttributes_Enemy 作为敌人的辅助属性游戏效果
Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/GE_SecondaryAttributes_Enemy.uasset
打开 GE_SecondaryAttributes_Enemy 持续时间-duration policy-instant 即时
打开 DA_CharacterClassInfo SecondaryAttributes-GE_SecondaryAttributes_Enemy
将 GE_SecondaryAttributes 移动至 Aura文件夹
每个角色都需要数据资产,将其存储在一个中心位置. 游戏模式是决定游戏规则的地方。 从某种意义上说,这个数据资产包含了很多规则,比如敌人根据他们的等级应该取什么属性值。 因此,可以在游戏模式上存储敌人职业数据资产,这样它就只存在于单个类中。 仅占用其中一项数据资产的内存。 将数据资产存储在游戏模式上,并能够从蓝图中进行设置。
Source/Aura/Public/Game/AuraGameModeBase.h
class UCharacterClassInfo;
public:
// 职业数据资产
UPROPERTY(EditDefaultsOnly, Category = "Character Class Defaults")
TObjectPtr<UCharacterClassInfo>
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
#include "Data/CharacterClassInfo.h"
class UAbilitySystemComponent;
// 获取职业数据资产并初始化职业属性
UFUNCTION(BlueprintCallable, Category="AuraAbilitySystemLibrary|CharacterClassDefaults")
static void InitializeDefaultAttributes(const UObject* WorldContextObject, ECharacterClass CharacterClass,
float Level, UAbilitySystemComponent* ASC);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
#include "Game/AuraGameModeBase.h"
void UAuraAbilitySystemLibrary::InitializeDefaultAttributes(const UObject* WorldContextObject,
ECharacterClass CharacterClass, float Level,
UAbilitySystemComponent* ASC)
{
AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if (AuraGameMode == nullptr) return;
AActor* AvatarActor = ASC->GetAvatarActor();
UCharacterClassInfo* CharacterClassInfo = AuraGameMode->CharacterClassInfo;
FCharacterClassDefaultInfo ClassDefaultInfo = CharacterClassInfo->GetClassDefaultInfo(CharacterClass);
FGameplayEffectContextHandle PrimaryAttributesContextHandle = ASC->MakeEffectContext();
PrimaryAttributesContextHandle.AddSourceObject(AvatarActor);
const FGameplayEffectSpecHandle PrimaryAttributesSpecHandle = ASC->MakeOutgoingSpec(
ClassDefaultInfo.PrimaryAttributes, Level, PrimaryAttributesContextHandle);
ASC->ApplyGameplayEffectSpecToSelf(*PrimaryAttributesSpecHandle.Data.Get());
FGameplayEffectContextHandle SecondaryAttributesContextHandle = ASC->MakeEffectContext();
SecondaryAttributesContextHandle.AddSourceObject(AvatarActor);
const FGameplayEffectSpecHandle SecondaryAttributesSpecHandle = ASC->MakeOutgoingSpec(
CharacterClassInfo->SecondaryAttributes, Level, SecondaryAttributesContextHandle);
ASC->ApplyGameplayEffectSpecToSelf(*SecondaryAttributesSpecHandle.Data.Get());
FGameplayEffectContextHandle VitalAttributesContextHandle = ASC->MakeEffectContext();
VitalAttributesContextHandle.AddSourceObject(AvatarActor);
const FGameplayEffectSpecHandle VitalAttributesSpecHandle = ASC->MakeOutgoingSpec(
CharacterClassInfo->VitalAttributes, Level, VitalAttributesContextHandle);
ASC->ApplyGameplayEffectSpecToSelf(*VitalAttributesSpecHandle.Data.Get());
}
Source/Aura/Public/Character/AuraCharacterBase.h
virtual void InitializeDefaultAttributes() const;
Source/Aura/Public/Character/AuraEnemy.h
#include "AbilitySystem/Data/CharacterClassInfo.h"
protected:
// 初始化职业默认属性
virtual void InitializeDefaultAttributes() const override;
// 职业类
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character Class Defaults")
ECharacterClass CharacterClass = ECharacterClass::Warrior;
Source/Aura/Private/Character/AuraEnemy.cpp
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
void AAuraEnemy::InitializeDefaultAttributes() const
{
// 使用蓝图库初始化职业默认属性
UAuraAbilitySystemLibrary::InitializeDefaultAttributes(this, CharacterClass, Level, AbilitySystemComponent);
}
打开 BP_AuraGameMode Character Class Info-DA_CharacterClassInfo
运行游戏,各类职业的敌人将拥有默认属性。
到目前为止,影响健康的游戏效果正在影响属性集并改变健康的值。 在更复杂的角色扮演游戏中,通常不会这样做。
因为,例如,每当我们对敌人造成伤害时,都会进行许多计算. 并且这些计算与攻击者和受害者的属性有关.
为了保持数学简单,我们经常使用占位符属性,其行为作为中介,允许我们实际执行设定健康值之前需要执行的所有数学运算。 这些中间值称为元属性。
普通属性通常会被复制,并且可以在服务器上更改并复制到客户端。
元属性不会被复制,它实际上只是一个临时占位符。 仅在服务器上使用这些来执行计算,执行这些计算后可以将更改并应用于重要的真实属性上。
例如,在损坏的情况下,我们可能有一个称为传入损坏的元属性。 元属性已经不再复制。 假设游戏效果将施加伤害。 假设这个游戏效果显示我对你造成 11 点伤害。 游戏效果将增加元属性生命值,而不是减去受害者的 11。 元属性执行所有修改器的计算。 然后得出最终值。 之后归零,开始对元属性做出响应。 例如弹出提示文字,眩晕玩家,减少健康值。
使用元属性伤害替代游戏效果对敌人直接造成的伤害。
由于元属性不会复制,因此它不会收到代表通知。 例如 OnRep_Health
在服务器上设置它们,然后在服务器上处理数据,然后更改任何复制的数据属性。
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
public:
/*
* Meta Attributes 元属性
*/
UPROPERTY(BlueprintReadOnly, Category = "Meta Attributes")
FGameplayAttributeData IncomingDamage;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, IncomingDamage);
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
UE_LOG(LogTemp, Warning, TEXT("Changed Health on %s, Health: %f"), *Props.TargetAvatarActor->GetName(), GetHealth());
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
}
}
}
打开 GE_Damage 伤害游戏效果
这是原效果,直接修改健康值: Modifiers-索引0-Attribute-AuraAttributeSet.Health Modifiers-索引0-Modifier Op-Add Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude- -10
使用使用传入的伤害,不直接修改健康值: Modifiers-索引0-Attribute-AuraAttributeSet.IncomingDamage Modifiers-索引0-Modifier Op-Add Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Scalable Float 需要的不是负值,而是正值。 Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude- 10
现在通过伤害元属性 IncomingDamage 来修改敌人的健康值。
伤害元属性首先被清零,每一帧都被消耗用于计算伤害。
这个大小将由创建技能效果规范的代码/蓝图显式设置
打开 GE_Damage
Modifiers-索引0-Attribute-AuraAttributeSet.Health Modifiers-索引0-Modifier Op-Add Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Set By Caller
调用者幅度设置的是它们的键值对, 需要一个游戏标签来根据调用者的大小来识别属性集合。 因此,首先要为伤害创建一个游戏标签。
Source/Aura/Public/AuraGameplayTags.h
public:
// 为伤害创建一个游戏标签
// 用于 Set By Caller 识别属性集
FGameplayTag Damage;
Source/Aura/Private/AuraGameplayTags.cpp
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
.......省略
GameplayTags.Damage = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Damage"),
FString("Damage")
);
}
只需要在应用时能够从游戏效果中访问该键值对
// 使用技能系统本身造成伤害,而非游戏效果
// Set By Caller 类型
// 硬编码伤害值,仅作学习
FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签,其值为 50
// 只需要在应用时能够从游戏效果中访问该键值对
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, 50.f);
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
#include "Aura/Public/AuraGameplayTags.h"
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
// 设置投射物旋转 方向
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
Rotation.Pitch = 0.f;
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo());
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, GetAbilityLevel(), SourceASC->MakeEffectContext());
// 使用技能系统本身造成伤害,而非游戏效果
// Set By Caller 类型
// 硬编码伤害值,仅作学习
FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签,其值为 50
// 只需要在应用时能够从游戏效果中访问该键值对
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, 50.f);
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
}
打开 GE_Damage
Modifiers-索引0-Attribute-AuraAttributeSet.Health Modifiers-索引0-Modifier Op-Add Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Set By Caller Set by Caller Magnitude 由调用者设置大小 Data Name Data Tag -Damage 选择伤害标签,这个修改器就会造成 50 点伤害
现在攻击敌人可造成50点伤害。
现在我们的游戏技能系统可以控制伤害的大小, 它不仅仅是在游戏效果本身中设置的,它现在是我们通过代码控制的东西。
Source/Aura/Public/AbilitySystem/Abilities/AuraGameplayAbility.h
public:
// 与等级关联的可扩展伤害
// FScalableFloat 表示可设置缩放值和曲线表
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Damage")
FScalableFloat Damage;
可以设置缩放值。 并且可以使用曲线表格使 Damage 可扩展,随等级变化
GA_FireBolt-细节-损害-Damage
新建文件 Data/CT_Damage.json
键为等级,值为伤害
[
{
"Name": "Abilities.FireBolt",
"1": 5,
"5": 10,
"10": 16,
"15": 27,
"20": 41,
"40": 120
}
]
导入 Data/CT_Damage.json
为曲线表格资产
选择 Data/CT_PrimaryAttributes_Warrior.json 文件
导入为-CurveTable 选择曲线插值类型-Cubic 应用
打开 GA_FireBolt
损害- Damage 1, CT_Damage,Abilities.FireBolt
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
const float ScaledDamage = Damage.GetValueAtLevel(GetAbilityLevel());
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, ScaledDamage);
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
// 设置投射物旋转 方向
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
Rotation.Pitch = 0.f;
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo());
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, GetAbilityLevel(), SourceASC->MakeEffectContext());
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
const float ScaledDamage = Damage.GetValueAtLevel(GetAbilityLevel());
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, ScaledDamage);
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
}
为敌人新增命中响应技能,该技能播放命中相应蒙太奇。 击中敌人时,为其附加原生游戏标签,触发敌人命中响应技能,触发蒙太奇动画。
基于 Gameplay Effect 创建游戏效果蓝图 GE_HitReact
Content/Blueprints/AbilitySystem/GameplayEffects/GE_HitReact.uasset
打开 GE_HitReact
持续时间-duration policy-Infinite 无限时间
Source/Aura/Public/AuraGameplayTags.h
public:
// 敌人命中反应效果游戏原生标签
FGameplayTag Effects_HitReact;
Source/Aura/Private/AuraGameplayTags.cpp
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
.......
GameplayTags.Effects_HitReact = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Effects.HitReact"),
FString("Tag granted when Hit Reacting")
);
}
细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Asset Tags Gameplay Effect Component
细节-Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Add Tags-Added-Effects.HitReact
游戏效果 GE_HitReact 将为目标授予 Effects.HitReact 效果标签。
Source/Aura/Public/Character/AuraEnemy.h
// 响应 Effects.HitReact 效果标签
void HitReactTagChanged(const FGameplayTag CallbackTag, int32 NewCount);
// 如果标签计数大于0则做出响应
UPROPERTY(BlueprintReadOnly, Category = "Combat")
bool bHitReacting = false;
// 基本步行速度
UPROPERTY(BlueprintReadOnly, Category = "Combat")
float BaseWalkSpeed = 250.f;
Source/Aura/Private/Character/AuraEnemy.cpp
#include "AuraGameplayTags.h"
#include "GameFramework/CharacterMovementComponent.h"
void AAuraEnemy::BeginPlay()
{
Super::BeginPlay();
//
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
InitAbilityActorInfo();
// 为健康条控件绑定控件控制器
// 敌人自身将成为健康条控件的控件控制器
if (UAuraUserWidget* AuraUserWidget = Cast<UAuraUserWidget>(HealthBar->GetUserWidgetObject()))
{
AuraUserWidget->SetWidgetController(this);
}
if (const UAuraAttributeSet* AuraAS = Cast<UAuraAttributeSet>(AttributeSet))
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
// 注册 Effects.HitReact 效果标签 的 NewOrRemoved 新增或移除标签事件
// 参数1-标签
// 参数2-标签事件类型
AbilitySystemComponent->RegisterGameplayTagEvent(FAuraGameplayTags::Get().Effects_HitReact,
EGameplayTagEventType::NewOrRemoved).AddUObject(
this,
&AAuraEnemy::HitReactTagChanged
);
// 广播初始值
OnHealthChanged.Broadcast(AuraAS->GetHealth());
OnMaxHealthChanged.Broadcast(AuraAS->GetMaxHealth());
}
}
// NewCount 新标签计数 Effects.HitReact 效果标签 的数量
void AAuraEnemy::HitReactTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
// 如果标签计数大于0则做出命中响应
bHitReacting = NewCount > 0;
// 做出命中响应时不能移动
GetCharacterMovement()->MaxWalkSpeed = bHitReacting ? 0.f : BaseWalkSpeed;
}
基于 GameplayAbility 创建 命中响应技能 蓝图 GA_HitReact
Content/Blueprints/AbilitySystem/GameplayAbilities/GA_HitReact.uasset
GA_HitReact 添加游戏效果 GE_HitReact 图表: 事件:event activeAbility applyGameplayEffectToOwner applyGameplayEffectToOwner-GameplayEffectClass 选择 GE_HitReact 与场景无关,可以硬编码 applyGameplayEffectToOwner-return value-提升为变量 Active GE Hit React 缓存引用以删除效果
激活效果后,按顺序删除该效果
get avatar actor from actor info cast to CombatInterface 使用接口,不再依赖具体蒙太奇动画
播放蒙太奇,不适合硬编码,不同模型动画不同 playMontageAndWait
Source/Aura/Public/Interaction/CombatInterface.h
class UAnimMontage;
public:
// 获取 命中相应蒙太奇指针
// BlueprintNativeEvent 蓝图本地事件,c++无需virtual 声明,C++ GetHitReactMontage_Implementation 实现
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
UAnimMontage* GetHitReactMontage();
Source/Aura/Public/Character/AuraCharacterBase.h
class UAnimMontage;
public:
// 继承 GetHitReactMontage
virtual UAnimMontage* GetHitReactMontage_Implementation() override;
private:
// 命中相应蒙太奇指针
UPROPERTY(EditAnywhere, Category = "Combat")
TObjectPtr<UAnimMontage> HitReactMontage;
Source/Aura/Private/Character/AuraCharacterBase.cpp
// 返回 命中相应蒙太奇指针
UAnimMontage* AAuraCharacterBase::GetHitReactMontage_Implementation()
{
return HitReactMontage;
}
图表:
cast to CombatInterface-as combat Interface 拖出 GetHitReactMontage
GetHitReactMontage 的return value 输出至 playMontageAndWait-Montage to play
基于动画序列 HitReact_Spear 创建蒙太奇动画 AM_HitReact_GoblinSpear
Content/Assets/Enemies/Goblin/Animations/Spear/AM_HitReact_GoblinSpear.uasset
基于动画序列 HitReact_Slingshot 创建蒙太奇动画 AM_HitReact_GoblinSlingshot
Content/Assets/Enemies/Goblin/Animations/Slingshot/AM_HitReact_GoblinSlingshot.uasset
打开 BP_Goblin_Spear combat-HitReactMontage-AM_HitReact_GoblinSpear
打开 BP_Goblin_Slingshot combat-HitReactMontage-AM_HitReact_GoblinSlingshot
在敌人受到伤害且为死亡时激活技能 GA_HitReact
职业信息由敌人共享,可以包含共享的技能
Source/Aura/Public/AbilitySystem/Data/CharacterClassInfo.h
class UGameplayAbility;
public:
// 各职业共享的技能组
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
TArray<TSubclassOf<UGameplayAbility>> CommonAbilities;
用以将初始技能,共享节能功能赋予角色
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
// 赋予初始技能组
UFUNCTION(BlueprintCallable, Category="AuraAbilitySystemLibrary|CharacterClassDefaults")
static void GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
void UAuraAbilitySystemLibrary::GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC)
{
AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if (AuraGameMode == nullptr) return;
UCharacterClassInfo* CharacterClassInfo = AuraGameMode->CharacterClassInfo;
for (TSubclassOf<UGameplayAbility> AbilityClass : CharacterClassInfo->CommonAbilities)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
ASC->GiveAbility(AbilitySpec);
}
}
// 为敌人添加初始/共享技能
UAuraAbilitySystemLibrary::GiveStartupAbilities(this, AbilitySystemComponent);
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::BeginPlay()
{
Super::BeginPlay();
//
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
InitAbilityActorInfo();
// 为敌人添加初始/共享技能
UAuraAbilitySystemLibrary::GiveStartupAbilities(this, AbilitySystemComponent);
// 为健康条控件绑定控件控制器
// 敌人自身将成为健康条控件的控件控制器
if (UAuraUserWidget* AuraUserWidget = Cast<UAuraUserWidget>(HealthBar->GetUserWidgetObject()))
{
AuraUserWidget->SetWidgetController(this);
}
if (const UAuraAttributeSet* AuraAS = Cast<UAuraAttributeSet>(AttributeSet))
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
// 注册 Effects.HitReact 效果标签 的 NewOrRemoved 新增或移除标签事件
// 参数1-标签
// 参数2-标签事件类型
AbilitySystemComponent->RegisterGameplayTagEvent(FAuraGameplayTags::Get().Effects_HitReact,
EGameplayTagEventType::NewOrRemoved).AddUObject(
this,
&AAuraEnemy::HitReactTagChanged
);
// 广播初始值
OnHealthChanged.Broadcast(AuraAS->GetHealth());
OnMaxHealthChanged.Broadcast(AuraAS->GetMaxHealth());
}
}
通过技能标签激活技能 更通用
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
if (!bFatal)
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
UE_LOG(LogTemp, Warning, TEXT("Changed Health on %s, Health: %f"), *Props.TargetAvatarActor->GetName(), GetHealth());
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
if (!bFatal)
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
}
}
}
打开 DA_CharacterClassInfo
Common Abilities-添加一个技能 GA_HitReact
通过标签激活技能 GA_HitReact 时,技能必须拥有该标签。
TargetASC->TryActivateAbilitiesByTag(TagContainer);
打开 GA_HitReact 细节-标签-Ability Tags-Effects.HitReact
对目标的健康属性集造成伤害,如果这种伤害不是致命的, 尝试使用包含 Effects_HitReact 标签的标签容器通过标签激活技能 GA_HitReact。 GA_HitReact 技能将其中的 GE_HitReact 效果应用在目标上。 然后获取,播放蒙太奇动画
现在攻击敌人,敌人将播放受击蒙太奇动画。
AM_HitReact_GoblinSlingshot,AM_HitReact_GoblinSpear 混合选项混入-混合时间-0.05 更快的混合 混出-混合时间-0.05 更快的混合
打开 GA_HitReact 技能 高级-Instancing Policy 实例化策略-Instanced Per Actor 每个参与者实例化 第一次激活时,只会实例化一次技能。后续使用缓存。
打开 GA_HitReact 技能 图表 通过句柄移除游戏效果 RemoveGameplayEffectFromOwnerWithHandle Active GE Hit React 输出至 RemoveGameplayEffectFromOwnerWithHandle-handle
然后再结束技能 end ability
在蒙太奇混出时也结束效果和技能,则可以使敌人播放完整动画,减少受击动画频次。 但是不使用混出时也结束效果更好。更符合直觉。
死亡时玩家和敌人的通用功能
Source/Aura/Public/Interaction/CombatInterface.h
virtual void Die() = 0;
布娃娃状态 使角色无需播放死亡动画 死亡必须使用特殊函数处理
Source/Aura/Public/Character/AuraCharacterBase.h
public:
// 只在服务器端执行
virtual void Die() override;
// 处理角色死亡时所有客户端发生的事情
// NetMulticast - 多播RPC
// Reliable -死亡必须可靠复制
// 实现- MulticastHandleDeath_Implementation
UFUNCTION(NetMulticast, Reliable)
virtual void MulticastHandleDeath();
Source/Aura/Private/Character/AuraCharacterBase.cpp
// 只在服务器端执行
void AAuraCharacterBase::Die()
{
// 分离武器 如果在服务器分离,则不必在客户端分离
Weapon->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepWorld, true));
// 调用多播
MulticastHandleDeath();
}
// 服务器,客户端执行
void AAuraCharacterBase::MulticastHandleDeath_Implementation()
{
// 启用模拟物理
Weapon->SetSimulatePhysics(true);
// 启用重力确保武器掉落
Weapon->SetEnableGravity(true);
// 启用碰撞,无查询,无重叠 【武器默认无碰撞】
Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 将网格体布娃娃化
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetEnableGravity(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 对世界静态类型阻止,网格体可以阻止其他物体
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
// 交胶囊体禁用碰撞 不会再阻挡其他物体通过
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
Source/Aura/Public/Character/AuraEnemy.h
public:
virtual void Die() override;
// 死亡后存在时间
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat")
float LifeSpan = 5.f;
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::Die()
{
// 设置寿命
SetLifeSpan(LifeSpan);
Super::Die();
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
#include "Interaction/CombatInterface.h"
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
UE_LOG(LogTemp, Warning, TEXT("Changed Health on %s, Health: %f"), *Props.TargetAvatarActor->GetName(), GetHealth());
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
CombatInterface->Die();
}
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
}
}
}
现在敌人健康为0时将死亡,成为布娃娃。5秒后消失。
M_DissolveEffect 溶解材质
将主框中的节点复制到其他材质,可使其具由溶解效果。
敌人溶解材质 MI_GoblinDissolve
动态材质实例 可以在运行时更改参数。
Source/Aura/Public/Character/AuraCharacterBase.h
protected:
/* Dissolve Effects 溶解特效*/
void Dissolve();
// 蓝图中实现
// 角色溶解与武器溶解必须是独立的2个时间轴
UFUNCTION(BlueprintImplementableEvent)
void StartDissolveTimeline(UMaterialInstanceDynamic* DynamicMaterialInstance);
UFUNCTION(BlueprintImplementableEvent)
void StartWeaponDissolveTimeline(UMaterialInstanceDynamic* DynamicMaterialInstance);
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TObjectPtr<UMaterialInstance> DissolveMaterialInstance;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TObjectPtr<UMaterialInstance> WeaponDissolveMaterialInstance;
Source/Aura/Private/Character/AuraCharacterBase.cpp
void AAuraCharacterBase::Dissolve()
{
if (IsValid(DissolveMaterialInstance))
{
// 创建动态材质实例
UMaterialInstanceDynamic* DynamicMatInst = UMaterialInstanceDynamic::Create(DissolveMaterialInstance, this);
// 设置网格体的材质索引 当前为单一材质 只有索引0
GetMesh()->SetMaterial(0, DynamicMatInst);
StartDissolveTimeline(DynamicMatInst);
}
if (IsValid(WeaponDissolveMaterialInstance))
{
UMaterialInstanceDynamic* DynamicMatInst = UMaterialInstanceDynamic::Create(WeaponDissolveMaterialInstance, this);
Weapon->SetMaterial(0, DynamicMatInst);
StartWeaponDissolveTimeline(DynamicMatInst);
}
}
void AAuraCharacterBase::MulticastHandleDeath_Implementation()
{
// 启用模拟物理
Weapon->SetSimulatePhysics(true);
// 启用重力确保武器掉落
Weapon->SetEnableGravity(true);
// 启用碰撞,无查询,无重叠 【武器默认无碰撞】
Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 将网格体布娃娃化
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetEnableGravity(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 对世界静态类型阻止,网格体可以阻止其他物体
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
// 交胶囊体禁用碰撞 不会再阻挡其他物体通过
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Dissolve();
}
打开 BP_EnemyBase
添加事件 StartDissolveTimeline 【此处实现基类的定义】
add timeline 重命名 DissolveTimeline
添加浮点型轨道 DissolveTrack 添加关键帧 时间:0 值:-0.1 表示溶解开始
添加关键帧 时间:3 值:0.55 表示完全溶解
ctrl 多选2个关键帧-右键-自动 使过渡平滑
StartWeaponDissolveTimeline-DynamicMaterialInstance 拖出 set scalar parameter value 获取材质实例 set scalar parameter value-parameter name-Dissolve parameter name-Dissolve 为材质实例上控制溶解的参数
DissolveTimeline -DissolveTrack 输出至 set scalar parameter value-value 时间轴输出的值控制溶解度Dissolve
DissolveTimeline-update 引脚-执行 set scalar parameter value DissolveTimeline 每次更新都调用 set scalar parameter value
事件图表 StartWeaponDissolveTimeline WeaponDissolveTimeline DissolveTrack set scalar parameter value-value
打开 BP_Goblin_Spear Dissolve Material Instance-MI_GoblinDissolve Weapon Dissolve Material Instance-MI_Spear_Dissolve
BP_Goblin_Slingshot Dissolve Material Instance-MI_GoblinDissolve Weapon Dissolve Material Instance-MI_Slingshot_Red_Dissolve
不需要控件控制器
右键-用户界面-控件蓝图-UserWidget
WBP_DamageText
Content/Blueprints/UI/FloatingText/WBP_DamageText.uasset
打开 WBP_DamageText 设计器: 大小为-所需 覆层-Overlay_Root
文本-Text_Damage 内容-文本-999 设为变量 水平居中对齐 垂直居中对齐
DamageAnim-添加轨道-Text_Damage Text_Damage-轨道加号-变换
第一帧-变换-平移-x 0,y -80 使文本上移
移动时间轴到0.2 变换-平移-x 90,y -80 使文本右移
移动时间轴到0.4 变换-平移-x 112,y -96
移动时间轴到0.7 变换-平移-x 88,y -212
移动时间轴到1 变换-平移-x -100,y -300
打开曲线编辑器 查看
设置缩放动画 移动时间轴到0.1 变换-缩放-x 2.25,y 2.25
移动时间轴到0.2 变换-缩放-x 1,y 1
移动时间轴到1 变换-缩放-x 0.4,y 0.4
设置不透明度动画 Text_Damage-轨道加号-渲染不透明度 移动时间轴到1 渲染不透明度 0
damage anim play animation
左侧添加函数 UpdateDamageText
UpdateDamageText-输入参数-增加 Damage 类型 浮点
Text_Damage setText(Text)
to text(float) to text(float)-Maximum Fractional Digits-3
基于 WidgetComponent 新建C++ DamageTextComponent
Source/Aura/Public/UI/Widget/DamageTextComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/WidgetComponent.h"
#include "DamageTextComponent.generated.h"
UCLASS()
class AURA_API UDamageTextComponent : public UWidgetComponent
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void SetDamageText(float Damage);
};
Source/Aura/Private/UI/Widget/DamageTextComponent.cpp
#include "UI/Widget/DamageTextComponent.h"
Content/Blueprints/UI/FloatingText/BP_DamageTextComponent.uasset
打开 BP_DamageTextComponent 设置控件类 细节-用户界面-控件类-WBP_DamageText
事件图表: 实现 SetDamageText 添加事件 event Set Damage Text
get user widget object 获取控件 cast to WBP_DamageText
UpdateDamageText
属性集的PostGameplayEffectExecute仅在服务器端执行
Source/Aura/Public/Player/AuraPlayerController.h
class UDamageTextComponent;
public:
// Client 客户端RPC
// Reliable 可靠复制
UFUNCTION(Client, Reliable)
void ShowDamageNumber(float DamageAmount, ACharacter* TargetCharacter);
private:
// 伤害提示文本控件组件类
UPROPERTY(EditDefaultsOnly)
TSubclassOf<UDamageTextComponent> DamageTextComponentClass;
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "GameFramework/Character.h"
#include "UI/Widget/DamageTextComponent.h"
// 对于服务器控制的角色,它只会在服务器上执行并且服务器控制角色会看到它。
// 但是对于客户端控制的角色,它将通过RPC在服务器上调用,但在客户端上执行的角色会看到它。
// 无论哪种方式,这个小控件都会被看到。
void AAuraPlayerController::ShowDamageNumber_Implementation(float DamageAmount, ACharacter* TargetCharacter)
{
// 目标可能在最后一帧被杀死,销毁自身 导致 TargetCharacter 为空
if (IsValid(TargetCharacter) && DamageTextComponentClass)
{
// 创建组件
UDamageTextComponent* DamageText = NewObject<UDamageTextComponent>(TargetCharacter, DamageTextComponentClass);
DamageText->RegisterComponent();//引擎中注册组件
// 伤害文字组件附加到目标收到伤害的位置 在此位置生成 开始播放动画
DamageText->AttachToComponent(TargetCharacter->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
// 从目标分离组件 自行动画
DamageText->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
DamageText->SetDamageText(DamageAmount);
}
}
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
private:
void ShowFloatingText(const FEffectProperties& Props, float Damage) const;
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
#include "Kismet/GameplayStatics.h"
#include "Player/AuraPlayerController.h"
void UAuraAttributeSet::SetEffectProperties(const FGameplayEffectModCallbackData& Data, FEffectProperties& Props) const
{
// Source = causer of the effect, Target = target of the effect (owner of this AS)
Props.EffectContextHandle = Data.EffectSpec.GetContext();
Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
if (IsValid(Props.SourceASC) && Props.SourceASC->AbilityActorInfo.IsValid() && Props.SourceASC->AbilityActorInfo->AvatarActor.IsValid())
{
Props.SourceAvatarActor = Props.SourceASC->AbilityActorInfo->AvatarActor.Get();
Props.SourceController = Props.SourceASC->AbilityActorInfo->PlayerController.Get();
if (Props.SourceController == nullptr && Props.SourceAvatarActor != nullptr)
{
if (const APawn* Pawn = Cast<APawn>(Props.SourceAvatarActor))
{
Props.SourceController = Pawn->GetController();
}
}
if (Props.SourceController)
{
// 修复之前的错误
Props.SourceCharacter = Cast<ACharacter>(Props.SourceController->GetPawn());
}
}
if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
{
Props.TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
Props.TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
Props.TargetCharacter = Cast<ACharacter>(Props.TargetAvatarActor);
Props.TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Props.TargetAvatarActor);
}
}
// 仅在服务器端执行
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
UE_LOG(LogTemp, Warning, TEXT("Changed Health on %s, Health: %f"), *Props.TargetAvatarActor->GetName(), GetHealth());
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
CombatInterface->Die();
}
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
ShowFloatingText(Props, LocalIncomingDamage);
}
}
}
void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, float Damage) const
{
// 对自己造成伤害时不显示文本
if (Props.SourceCharacter != Props.TargetCharacter)
{
if(AAuraPlayerController* PC = Cast<AAuraPlayerController>(UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)))
{
PC->ShowDamageNumber(Damage, Props.TargetCharacter);
}
}
}
打开 BP_AuraPlayerController Damage Text Component Class-BP_DamageTextComponent
打开 BP_DamageTextComponent 图表 delay destroy component 销毁组件,也会销毁控件
细节-用户界面-空间-屏幕 控件在屏幕中渲染,完全在场景之外,从不会被遮挡。
默认为 场景 控件在场景中被渲染为网格体,其能够像场景中的其他网格体一样被遮挡.
现在敌人受到伤害会显示伤害文本
打开 WBP_DamageText 动画- 确保缩放不超过1 所有的缩放关键帧的值都成被缩小。 缩放值:0.4 1 0.4 0.2
设计器: 将实际文本放大,不会像素化。
Text_Damage 控件-细节-外观-字体-尺寸-放大为 60
最定制化的计算方式:UGameplayEffectExecutionCaculation
Execution Calculation 执行计算: 游戏效果执行计算
Snapshotting (Source) 快照 (来源): Snapshotting captures the Attribute valuewhen the Gameplay Effect Spec is created 快照捕获创建游戏效果规格时的属性值
Not snapshotting captures the Attributevalue when the Gameplay Effect is applied 应用游戏效果时,非快照捕获属性值
From the Target, the value is captured on Effect Application only 从“目标”中,仅在效果应用程序上捕获该值
优点: Capture Attributes 捕获属性
Can change more than one Attribute 可以更改多个属性
Can have programmer logic 可以有程序员逻辑
缺点: No prediction 没有预测
Only Instant or Periodic Gameplay Effects 只有即时或周期性的游戏效果
Capturing doesn't run PreAttributeChange; 捕获属性不会运行PreAttributeChange; any clamping done there must be done again 在那里进行的任何夹紧都必须再次进行
Only executed on the Server from Gameplay Abilitieswith Local Predicted, Server Initiated, and Server Only Net Execution Policies 仅在服务器上执行本地预测、服务器启动和仅服务器网络执行策略的游戏播放能力
根据游戏效果执行计算定制的计算类。 这就是所谓的伤害执行计算。
基于 GameplayEffectExecutionCalculation 新建 C++ ExecCalc_Damage
Execute_Implementation 执行计算将如何影响任何其他属性的值 这属于游戏效果 通过设置一个自定义的计算类给游戏效果添加一个修改器 这个自定义计算类决定了当我们应用游戏效果时会发生什么 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
Source/Aura/Public/AbilitySystem/ExecCalc/ExecCalc_Damage.h
#pragma once
#include "CoreMinimal.h"
#include "GameplayEffectExecutionCalculation.h"
#include "ExecCalc_Damage.generated.h"
UCLASS()
class AURA_API UExecCalc_Damage : public UGameplayEffectExecutionCalculation
{
GENERATED_BODY()
public:
UExecCalc_Damage();
virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
};
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
#include "AbilitySystem/ExecCalc/ExecCalc_Damage.h"
#include "AbilitySystemComponent.h"
UExecCalc_Damage::UExecCalc_Damage()
{
}
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
const AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
const AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
}
捕获执行计算中的属性
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
#include "AbilitySystem/ExecCalc/ExecCalc_Damage.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
// 没有 F前缀,不会公开给蓝图和反射系统
// C++原始内部结构
struct AuraDamageStatics
{
// 属性捕获定义 捕获属性ArmorDef
// 创建游戏效果属性和属性捕获定义
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
// 构造函数
AuraDamageStatics()
{
// 创建并定义属性捕获定义 Armor
// 参数3:当前正在捕获Armor属性,进行伤害计算,目标受伤,需要目标的护甲Armor,非来源的护甲
// 参数4:是否快照
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, Armor, Target, false);
}
};
// 静态伤害函数
// 存储 属性捕获定义 Armor
static const AuraDamageStatics& DamageStatics()
{
// 静态伤害结构变量 只实例化一次 每次调用函数返回同一个 DStatics 实例
// 对象 非指针
// 函数结束时 也不会消失
static AuraDamageStatics DStatics;
return DStatics;
}
UExecCalc_Damage::UExecCalc_Damage()
{
// 将属性捕获定义添加到执行计算相关属性中 类似mmc,用于计算
// 告诉执行计算类 该属性捕获定义用于特定捕获属性
// 参数:属性捕获定义
RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
}
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
const AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
const AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
float Armor = 0.f;
// 尝试计算属性捕获修改器
// 参数1:属性捕获定义
// 参数2:执行计算参数
// 参数3:输出幅度值 传出捕获的属性的值
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, Armor);
Armor = FMath::Max<float>(0.f, Armor);
// 测试护甲效果 复杂计算的地方,计算护甲值
++Armor;
// 参数1:参与修改的游戏属性
// 参数2:修改器操作
// 参数3: 用以修改属性的值
// Additive 导致每次应用游戏效果到目标,其护甲值会累加上一次的护甲值 11.25 -> 22.5
const FGameplayModifierEvaluatedData EvaluatedData(DamageStatics().ArmorProperty, EGameplayModOp::Additive, Armor);
// 修改属性
// 捕获计算后的护甲值,传递到执行计算修改器中
// 可添加多个属性
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
打开 GE_Damage 伤害效果
删除 Gameplay Effect-Modifiers 下的修改器
Gameplay Effect-Executions 添加一个执行计算修改器 Gameplay Effect-Executions-Calculation Class 计算类-ExecCalc_Damage Gameplay Effect-Executions-Calculation Modifiers 计算修改器 Gameplay Effect-Executions-Conditional Gameplay Effects 游戏效果条件
属性集监测到的属性变更值,是经过执行计算修改器修改后的值 例如伤害值
执行计算修改器经过计算的伤害值,应用到IncomingDamage 属性上, 属性集收到修改后的IncomingDamage 属性,来计算健康值。
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
#include "AuraGameplayTags.h"
#include "AbilitySystem/ExecCalc/ExecCalc_Damage.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "AuraGameplayTags.h"
// 没有 F前缀,不会公开给蓝图和反射系统
// C++原始内部结构
struct AuraDamageStatics
{
// 属性捕获定义 捕获属性ArmorDef
// 创建游戏效果属性和属性捕获定义
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
// 定义格挡几率
DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
// 构造函数
AuraDamageStatics()
{
// 创建并定义属性捕获定义 Armor
// 参数3:当前正在捕获Armor属性,进行伤害计算,目标受伤,需要目标的护甲Armor,非来源的护甲
// 参数4:是否快照
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, Armor, Target, false);
// 捕获目标的格挡几率属性
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, BlockChance, Target, false);
}
};
// 静态伤害函数
// 存储 属性捕获定义 Armor
static const AuraDamageStatics& DamageStatics()
{
// 静态伤害结构变量 只实例化一次 每次调用函数返回同一个 DStatics 实例
// 对象 非指针
// 函数结束时 也不会消失
static AuraDamageStatics DStatics;
return DStatics;
}
UExecCalc_Damage::UExecCalc_Damage()
{
// 将属性捕获定义添加到执行计算相关属性中 类似mmc,用于计算
// 告诉执行计算类 该属性捕获定义用于特定捕获属性
// 参数:属性捕获定义
RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
RelevantAttributesToCapture.Add(DamageStatics().BlockChanceDef);
}
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
const AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
const AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// 根据调用者大小计算伤害值
// Get Damage Set by Caller Magnitude
float Damage = Spec.GetSetByCallerMagnitude(FAuraGameplayTags::Get().Damage);
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters, TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
Damage = bBlocked ? Damage / 2.f : Damage;
// 将计算后的伤害值,添加到 IncomingDamage 属性上
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(), EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
此时可以看到 ,敌人所受伤害在格挡成功后减半。 格挡未成功时,伤害:5 格挡成功时,伤害:2.5
Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/GE_SecondaryAttributes_TEST.uasset
拷贝 GE_SecondaryAttributes_Enemy 重命名为 GE_SecondaryAttributes_TEST
打开 GE_SecondaryAttributes_TEST
修改 AuraAttributeSet.BlockChance 属性
细节-Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.BlockChance 细节-Gameplay Effect-Modifiers--Modifier Op-Override Modifier-Modifier Magnitude-Magnitude calculation Type-Scalable Float 固定100% 的格挡几率 Modifier-Modifier Magnitude-Scalable Float Magnitude-100
打开 DA_CharacterClassInfo Common Class Defaults-Secondary Attributes- GE_SecondaryAttributes_TEST
此时固定百分百格挡,伤害为2.5
恢复使用原辅助效果 Common Class Defaults-Secondary Attributes- GE_SecondaryAttributes_Enemy
伤害计算顺序 -格挡几率-护甲穿透
护甲穿透抵消护甲的百分比
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
#include "AbilitySystem/ExecCalc/ExecCalc_Damage.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "AuraGameplayTags.h"
// 没有 F前缀,不会公开给蓝图和反射系统
// C++原始内部结构
struct AuraDamageStatics
{
// 属性捕获定义 捕获属性ArmorDef
// 创建游戏效果属性和属性捕获定义
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
DECLARE_ATTRIBUTE_CAPTUREDEF(ArmorPenetration);
// 定义格挡几率
DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
// 构造函数
AuraDamageStatics()
{
// 创建并定义属性捕获定义 Armor
// 参数3:当前正在捕获Armor属性,进行伤害计算,目标受伤,需要目标的护甲Armor,非来源的护甲
// 参数4:是否快照
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, Armor, Target, false);
// 目标的格挡几率属性
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, BlockChance, Target, false);
// 来源的,攻击者的护甲穿透
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, ArmorPenetration, Source, false);
}
};
// 静态伤害函数
// 存储 属性捕获定义 Armor
static const AuraDamageStatics& DamageStatics()
{
// 静态伤害结构变量 只实例化一次 每次调用函数返回同一个 DStatics 实例
// 对象 非指针
// 函数结束时 也不会消失
static AuraDamageStatics DStatics;
return DStatics;
}
UExecCalc_Damage::UExecCalc_Damage()
{
// 将属性捕获定义添加到执行计算相关属性中 类似mmc,用于计算
// 告诉执行计算类 该属性捕获定义用于特定捕获属性
// 参数:属性捕获定义
RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
RelevantAttributesToCapture.Add(DamageStatics().BlockChanceDef);
RelevantAttributesToCapture.Add(DamageStatics().ArmorPenetrationDef);
}
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
const AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
const AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// 根据调用者大小计算伤害值
// Get Damage Set by Caller Magnitude
float Damage = Spec.GetSetByCallerMagnitude(FAuraGameplayTags::Get().Damage);
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters, TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef, EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor *= ( 100 - SourceArmorPenetration * 0.25f ) / 100.f;
// Armor ignores a percentage of incoming Damage.
// 护甲忽略一定百分比的伤害。
Damage *= ( 100 - EffectiveArmor * 0.333f ) / 100.f;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(), EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
系数可能随等级变小。 在职业信息数字资产中保存每个系数的曲线表。 因为这是共享资产。
右键-其他-曲线表格-constant
CT_DamageCalculationCoefficients
没有平滑过渡
Content/Blueprints/AbilitySystem/Data/CT_DamageCalculationCoefficients.uasset
打开 CT_DamageCalculationCoefficients 默认曲线:ArmorPenetration 护甲穿透系数
添加2列 第1列列名-1 值0.25 第2列列名-10 值0.25 表示等级1-10 系数均为0.25 这会将每100点护甲穿透减少25点。 由于护甲穿透随等级提高,系数可以随等级降低。
第2列列名-10 值0.15
第3列列名-20 值0.085
第4列列名40 值0.035
阶梯状曲线图表示:
添加曲线:EffectiveArmor 有效护甲
1,0.333 10,0.25 20,0.15 40,0.085
Source/Aura/Public/AbilitySystem/Data/CharacterClassInfo.h
public:
// 伤害计算系数
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults|Damage")
TObjectPtr<UCurveTable> DamageCalculationCoefficients;
打开 职业信息 DA_CharacterClassInfo 数据资产 设置 伤害计算系数曲线表格
Common Class Defaults-Damage-Damage Calculation Coefficients-CT_DamageCalculationCoefficients
该数据资产存在于游戏模式中,如果可以访问游戏模式,那就可以访问数据资产。 不应该每次需要该数据资产的某种值时都必须获取游戏模式。
使用更简单的方法来获取该数据资产:技能系统蓝图函数库
先获取职业信息,再从其中获取曲线表资产数据
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
// 获取职业信息类
UFUNCTION(BlueprintCallable, Category="AuraAbilitySystemLibrary|CharacterClassDefaults")
static UCharacterClassInfo* GetCharacterClassInfo(const UObject* WorldContextObject);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
void UAuraAbilitySystemLibrary::InitializeDefaultAttributes(const UObject* WorldContextObject,
ECharacterClass CharacterClass, float Level,
UAbilitySystemComponent* ASC)
{
AActor* AvatarActor = ASC->GetAvatarActor();
UCharacterClassInfo* CharacterClassInfo = GetCharacterClassInfo(WorldContextObject);
FCharacterClassDefaultInfo ClassDefaultInfo = CharacterClassInfo->GetClassDefaultInfo(CharacterClass);
FGameplayEffectContextHandle PrimaryAttributesContextHandle = ASC->MakeEffectContext();
PrimaryAttributesContextHandle.AddSourceObject(AvatarActor);
const FGameplayEffectSpecHandle PrimaryAttributesSpecHandle = ASC->MakeOutgoingSpec(
ClassDefaultInfo.PrimaryAttributes, Level, PrimaryAttributesContextHandle);
ASC->ApplyGameplayEffectSpecToSelf(*PrimaryAttributesSpecHandle.Data.Get());
FGameplayEffectContextHandle SecondaryAttributesContextHandle = ASC->MakeEffectContext();
SecondaryAttributesContextHandle.AddSourceObject(AvatarActor);
const FGameplayEffectSpecHandle SecondaryAttributesSpecHandle = ASC->MakeOutgoingSpec(
CharacterClassInfo->SecondaryAttributes, Level, SecondaryAttributesContextHandle);
ASC->ApplyGameplayEffectSpecToSelf(*SecondaryAttributesSpecHandle.Data.Get());
FGameplayEffectContextHandle VitalAttributesContextHandle = ASC->MakeEffectContext();
VitalAttributesContextHandle.AddSourceObject(AvatarActor);
const FGameplayEffectSpecHandle VitalAttributesSpecHandle = ASC->MakeOutgoingSpec(
CharacterClassInfo->VitalAttributes, Level, VitalAttributesContextHandle);
ASC->ApplyGameplayEffectSpecToSelf(*VitalAttributesSpecHandle.Data.Get());
}
void UAuraAbilitySystemLibrary::GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC)
{
UCharacterClassInfo* CharacterClassInfo = GetCharacterClassInfo(WorldContextObject);
for (TSubclassOf<UGameplayAbility> AbilityClass : CharacterClassInfo->CommonAbilities)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
ASC->GiveAbility(AbilitySpec);
}
}
UCharacterClassInfo* UAuraAbilitySystemLibrary::GetCharacterClassInfo(const UObject* WorldContextObject)
{
AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if (AuraGameMode == nullptr) return nullptr;
return AuraGameMode->CharacterClassInfo;
}
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
#include "AbilitySystem/Data/CharacterClassInfo.h"
#include "Interaction/CombatInterface.h"
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// 根据调用者大小计算伤害值
// Get Damage Set by Caller Magnitude
float Damage = Spec.GetSetByCallerMagnitude(FAuraGameplayTags::Get().Damage);
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel());
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
现在,使用了系数曲线。 必须设置系数曲线资产,否则让游戏崩溃,更容易找到崩溃原因。
捕获一些属性并确定命中是否至关重要。 为此使用暴击几率, 并使用暴击抗性来降低有效暴击几率。 如果你受到暴击,则伤害加倍,并为暴击伤害添加暴击值。 添加致命一击抵抗系数计算系数的曲线表,调整您的致命一击抵抗力。
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
#include "AbilitySystem/ExecCalc/ExecCalc_Damage.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "AuraGameplayTags.h"
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
#include "AbilitySystem/Data/CharacterClassInfo.h"
#include "Interaction/CombatInterface.h"
// 没有 F前缀,不会公开给蓝图和反射系统
// C++原始内部结构
struct AuraDamageStatics
{
// 属性捕获定义 捕获属性ArmorDef
// 创建游戏效果属性和属性捕获定义
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
DECLARE_ATTRIBUTE_CAPTUREDEF(ArmorPenetration);
// 定义格挡几率
DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitResistance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitDamage);
// 构造函数
AuraDamageStatics()
{
// 创建并定义属性捕获定义 Armor
// 参数3:当前正在捕获Armor属性,进行伤害计算,目标受伤,需要目标的护甲Armor,非来源的护甲
// 参数4:是否快照
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, Armor, Target, false);
// 目标的格挡几率属性
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, BlockChance, Target, false);
// 来源的,攻击者的护甲穿透
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, ArmorPenetration, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitChance, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitResistance, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitDamage, Source, false);
}
};
// 静态伤害函数
// 存储 属性捕获定义 Armor
static const AuraDamageStatics& DamageStatics()
{
// 静态伤害结构变量 只实例化一次 每次调用函数返回同一个 DStatics 实例
// 对象 非指针
// 函数结束时 也不会消失
static AuraDamageStatics DStatics;
return DStatics;
}
UExecCalc_Damage::UExecCalc_Damage()
{
// 将属性捕获定义添加到执行计算相关属性中 类似mmc,用于计算
// 告诉执行计算类 该属性捕获定义用于特定捕获属性
// 参数:属性捕获定义
RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
RelevantAttributesToCapture.Add(DamageStatics().BlockChanceDef);
RelevantAttributesToCapture.Add(DamageStatics().ArmorPenetrationDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalHitChanceDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalHitResistanceDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalHitDamageDef);
}
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// 根据调用者大小计算伤害值
// Get Damage Set by Caller Magnitude
float Damage = Spec.GetSetByCallerMagnitude(FAuraGameplayTags::Get().Damage);
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel());
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef, EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef, EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef, EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(TargetCombatInterface->GetPlayerLevel());
// Critical Hit Resistance reduces Critical Hit Chance by a certain percentage
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance * CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// Double damage plus a bonus if critical hit
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
打开 CT_DamageCalculationCoefficients 添加曲线 CriticalHitResistance 系数随等级降低 0.15 0.1 0.08 0.06
执行计算中的暴击属性,无法在属性集变更中访问。
现在攻击敌人将有几率造成暴击。
魔法投射技能中,为游戏情景添加技能,源对象,投射物,技能命中结果。
弱对象指针是智能指针。 它们是可以存储指针的包装器,而弱对象指针有一个属性,就是不获取它所存储的指针的所有权。 即,它不参与引用计数。垃圾收集不会跟踪它。 弱引用指针就代表“我指向这东西,但这东西什么时候释放不关我事儿……”
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
// 设置投射物旋转 方向
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
Rotation.Pitch = 0.f;
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo());
// 为游戏情景添加技能,源对象,投射物,技能命中结果。
FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
EffectContextHandle.SetAbility(this);
EffectContextHandle.AddSourceObject(Projectile);
TArray<TWeakObjectPtr<AActor>> Actors;
Actors.Add(Projectile);
EffectContextHandle.AddActors(Actors);
FHitResult HitResult;
HitResult.Location = ProjectileTargetLocation;
EffectContextHandle.AddHitResult(HitResult);
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
const float ScaledDamage = Damage.GetValueAtLevel(GetAbilityLevel());
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, ScaledDamage);
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
}
用来存储 是否暴击,是否格挡等变量
GetScriptStruct: 返回一个新的class。 它是引擎中为派生的每个类生成的来自反射系统的新对象。 例如,如果您有一个新的 actor 类,则会为其生成一个用于反射的新类. 系统结构也有这样的版本。它称为脚本结构 ScriptStruct。 当您创建一个能够暴露给反射系统的结构体时,脚本结构体是为反射系统创建的。 将其视为该结构的反射,就像类是对象的反射一样。
StaticStruct 静态结构是一个函数,就像静态类一样,它返回游戏效果上下文。
Source/Aura/Public/AuraAbilityTypes.h
#pragma once
#include "GameplayEffectTypes.h"
#include "AuraAbilityTypes.generated.h"
USTRUCT(BlueprintType)
struct FAuraGameplayEffectContext : public FGameplayEffectContext
{
GENERATED_BODY()
public:
// 是否暴击
bool IsCriticalHit() const { return bIsCriticalHit; }
// 是否格挡
bool IsBlockedHit () const { return bIsBlockedHit; }
void SetIsCriticalHit(bool bInIsCriticalHit) { bIsCriticalHit = bInIsCriticalHit; }
void SetIsBlockedHit(bool bInIsBlockedHit) { bIsBlockedHit = bInIsBlockedHit; }
/** Returns the actual struct used for serialization, subclasses must override this! */
// 返回用于序列化的实际结构,子类必须重写此
virtual UScriptStruct* GetScriptStruct() const
{
return FGameplayEffectContext::StaticStruct();
}
/** Custom serialization, subclasses must override this */
// 自定义序列化,子类必须覆盖此
// 定义如何序列化该结构以在网络上传输
// 序列化效果情景的成员变量
virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
protected:
UPROPERTY()
bool bIsBlockedHit = false;
UPROPERTY()
bool bIsCriticalHit = false;
};
Source/Aura/Private/AuraAbilityTypes.cpp
#include "AuraAbilityTypes.h"
bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
return true;
}
左侧表示网络传输流,右侧表示布尔变量
存储- 从右向左执行 即 序列化布尔变量
加载- 从左向右 ,即 左侧被反序列化,存储在右侧布尔值中。
用来存储布尔值的紧凑方式: 为真则翻转指定位
RepBits=0;
if(A){
RepBits |= 1<<0;
}
RepBits |= 1<<1;
RepBits |= 1<<2;
Source/Aura/Private/AuraAbilityTypes.cpp
#include "AuraAbilityTypes.h"
bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
uint32 RepBits = 0;
// 存储
if (Ar.IsSaving())
{
if (bReplicateInstigator && Instigator.IsValid())
{
RepBits |= 1 << 0;
}
if (bReplicateEffectCauser && EffectCauser.IsValid() )
{
RepBits |= 1 << 1;
}
if (AbilityCDO.IsValid())
{
RepBits |= 1 << 2;
}
if (bReplicateSourceObject && SourceObject.IsValid())
{
RepBits |= 1 << 3;
}
if (Actors.Num() > 0)
{
RepBits |= 1 << 4;
}
if (HitResult.IsValid())
{
RepBits |= 1 << 5;
}
if (bHasWorldOrigin)
{
RepBits |= 1 << 6;
}
if (bIsBlockedHit)
{
// 如果格挡,翻转第7位-1
RepBits |= 1 << 7;
}
if (bIsCriticalHit)
{
// 如果暴击,翻转第8位-1
RepBits |= 1 << 8;
}
}
//序列化前9位
Ar.SerializeBits(&RepBits, 9);
if (RepBits & (1 << 0))
{
Ar << Instigator;
}
if (RepBits & (1 << 1))
{
Ar << EffectCauser;
}
if (RepBits & (1 << 2))
{
Ar << AbilityCDO;
}
if (RepBits & (1 << 3))
{
Ar << SourceObject;
}
if (RepBits & (1 << 4))
{
SafeNetSerializeTArray_Default<31>(Ar, Actors);
}
if (RepBits & (1 << 5))
{
if (Ar.IsLoading())
{
if (!HitResult.IsValid())
{
HitResult = TSharedPtr<FHitResult>(new FHitResult());
}
}
HitResult->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 6))
{
Ar << WorldOrigin;
bHasWorldOrigin = true;
}
else
{
bHasWorldOrigin = false;
}
if (RepBits & (1 << 7))
{
Ar << bIsBlockedHit;
}
if (RepBits & (1 << 8))
{
Ar << bIsCriticalHit;
}
if (Ar.IsLoading())
{
AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
}
bOutSuccess = true;
return true;
}
Source/Aura/Public/AuraAbilityTypes.h
#pragma once
#include "GameplayEffectTypes.h"
#include "AuraAbilityTypes.generated.h"
USTRUCT(BlueprintType)
struct FAuraGameplayEffectContext : public FGameplayEffectContext
{
GENERATED_BODY()
public:
// 是否暴击
bool IsCriticalHit() const { return bIsCriticalHit; }
// 是否格挡
bool IsBlockedHit () const { return bIsBlockedHit; }
void SetIsCriticalHit(bool bInIsCriticalHit) { bIsCriticalHit = bInIsCriticalHit; }
void SetIsBlockedHit(bool bInIsBlockedHit) { bIsBlockedHit = bInIsBlockedHit; }
/** Returns the actual struct used for serialization, subclasses must override this! */
// 返回用于序列化的实际结构,子类必须重写此
virtual UScriptStruct* GetScriptStruct() const
{
// 虚幻5.3中 只需要简单的返回静态结构,不需要完全限定
return StaticStruct();
// return FGameplayEffectContext::StaticStruct();
}
/** Creates a copy of this context, used to duplicate for later modifications */
// 创建此上下文的副本,用于网络复制以供以后修改
// 虚幻5.3中 需要返回自定义游戏效果情景结构 FAuraGameplayEffectContext
virtual FAuraGameplayEffectContext* Duplicate() const
{
FAuraGameplayEffectContext* NewContext = new FAuraGameplayEffectContext();
*NewContext = *this;
if (GetHitResult())
{
// Does a deep copy of the hit result
NewContext->AddHitResult(*GetHitResult(), true);
}
return NewContext;
}
/** Custom serialization, subclasses must override this */
// 自定义序列化,子类必须覆盖此
// 定义如何序列化该结构以在网络上传输
// 序列化效果情景的成员变量
virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
protected:
UPROPERTY()
bool bIsBlockedHit = false;
UPROPERTY()
bool bIsCriticalHit = false;
};
// 定义该结构的功能
// 启用序列化,网络复制
template<>
struct TStructOpsTypeTraits<FAuraGameplayEffectContext> : public TStructOpsTypeTraitsBase2<FAuraGameplayEffectContext>
{
enum
{
WithNetSerializer = true,
WithCopy = true
};
};
基于 AbilitySystemGlobals 新建 自定义Aura全局技能系统 C++ AuraAbilitySystemGlobals
存放需要全局访问的变量。
Source/Aura/Public/AbilitySystem/AuraAbilitySystemGlobals.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemGlobals.h"
#include "AuraAbilitySystemGlobals.generated.h"
UCLASS()
class AURA_API UAuraAbilitySystemGlobals : public UAbilitySystemGlobals
{
GENERATED_BODY()
// 创建全新的技能效果情景
virtual FGameplayEffectContext* AllocGameplayEffectContext() const override;
};
Source/Aura/Private/AbilitySystem/AuraAbilitySystemGlobals.cpp
#include "AbilitySystem/AuraAbilitySystemGlobals.h"
#include "AuraAbilityTypes.h"
FGameplayEffectContext* UAuraAbilitySystemGlobals::AllocGameplayEffectContext() const
{
// 返回自定义的游戏效果情景
return new FAuraGameplayEffectContext();
}
Config/DefaultGame.ini
新增内容
[/Script/GameplayAbilities.AbilitySystemGlobals]
+AbilitySystemGlobalsClassName="/Script/Aura.AuraAbilitySystemGlobals"
在 EffectContextHandle 效果情景上断点调试 查看其内容【调试模式】
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
SpawnProjectile()
{
EffectContextHandle.AddHitResult(HitResult);
}
玩家发射一个火球技能触发该函数。 以下变量为自定义Aura全局技能系统所添加的专有属性
bIsBlockedHit = {bool} false
bIsCriticalHit = {bool} false
可以在游戏任何地方使用。例如属性集变更处。
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
// 获取自定义效果情景的是否格挡变量
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static bool IsBlockedHit(const FGameplayEffectContextHandle& EffectContextHandle);
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static bool IsCriticalHit(const FGameplayEffectContextHandle& EffectContextHandle);
// 为自定义效果情景 设置是否格挡变量
// 会产生副作用 不设置为纯函数蓝图
// UPARAM(ref) 表示这是蓝图输入参数,非常量引用。否则 FGameplayEffectContextHandle 是输出。
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetIsBlockedHit(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInIsBlockedHit);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetIsCriticalHit(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInIsCriticalHit);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
#include "AuraAbilityTypes.h"
bool UAuraAbilitySystemLibrary::IsBlockedHit(const FGameplayEffectContextHandle& EffectContextHandle)
{
// static_cast 需要指针参数
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->IsBlockedHit();
}
return false;
}
bool UAuraAbilitySystemLibrary::IsCriticalHit(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->IsCriticalHit();
}
return false;
}
void UAuraAbilitySystemLibrary::SetIsBlockedHit(FGameplayEffectContextHandle& EffectContextHandle, bool bInIsBlockedHit)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetIsBlockedHit(bInIsBlockedHit);
}
}
void UAuraAbilitySystemLibrary::SetIsCriticalHit(FGameplayEffectContextHandle& EffectContextHandle,
bool bInIsCriticalHit)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetIsCriticalHit(bInIsCriticalHit);
}
}
在蓝图中,可以通过 : IsBlockedHit 函数,配合 Get Effect Context 参数,获取是否格挡变量。
FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
//为自定义效果情景设置是否格挡
UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
#include "AuraAbilityTypes.h"
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// 根据调用者大小计算伤害值
// Get Damage Set by Caller Magnitude
float Damage = Spec.GetSetByCallerMagnitude(FAuraGameplayTags::Get().Damage);
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
//
FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
//为自定义效果情景设置是否格挡
UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel());
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef,
EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef,
EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef,
EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(
TargetCombatInterface->GetPlayerLevel());
// Critical Hit Resistance reduces Critical Hit Chance by a certain percentage
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance *
CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// 为自定义效果情景设置是否暴击
UAuraAbilitySystemLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
// Double damage plus a bonus if critical hit
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
private:
void ShowFloatingText(const FEffectProperties& Props, float Damage, bool bBlockedHit, bool bCriticalHit) const;
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
// 仅在服务器端执行
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
UE_LOG(LogTemp, Warning, TEXT("Changed Health on %s, Health: %f"), *Props.TargetAvatarActor->GetName(), GetHealth());
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
CombatInterface->Die();
}
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
// 是否暴击,格挡,显示提示文本
const bool bBlock = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
ShowFloatingText(Props, LocalIncomingDamage, bBlock, bCriticalHit);
}
}
}
void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, float Damage, bool bBlockedHit, bool bCriticalHit) const
{
if (Props.SourceCharacter != Props.TargetCharacter)
{
if(AAuraPlayerController* PC = Cast<AAuraPlayerController>(UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)))
{
PC->ShowDamageNumber(Damage, Props.TargetCharacter);
}
}
}
Source/Aura/Public/UI/Widget/DamageTextComponent.h
public:
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void SetDamageText(float Damage, bool bBlockedHit, bool bCriticalHit);
Source/Aura/Public/Player/AuraPlayerController.h
public:
// Client 客户端RPC
// Reliable 可靠复制
UFUNCTION(Client, Reliable)
void ShowDamageNumber(float DamageAmount, ACharacter* TargetCharacter, bool bBlockedHit, bool bCriticalHit);
Source/Aura/Private/Player/AuraPlayerController.cpp
// 对于服务器控制的角色,它只会在服务器上执行并且服务器控制角色会看到它。
// 但是对于客户端控制的角色,它将通过RPC在服务器上调用,但在客户端上执行的角色会看到它。
// 无论哪种方式,这个小控件都会被看到。
void AAuraPlayerController::ShowDamageNumber_Implementation(float DamageAmount, ACharacter* TargetCharacter,
bool bBlockedHit, bool bCriticalHit)
{
// 目标可能在最后一帧被杀死,销毁自身 导致 TargetCharacter 为空
if (IsValid(TargetCharacter) && DamageTextComponentClass)
{
// 创建组件
UDamageTextComponent* DamageText = NewObject<UDamageTextComponent>(TargetCharacter, DamageTextComponentClass);
DamageText->RegisterComponent(); //引擎中注册组件
// 伤害文字组件附加到目标收到伤害的位置 在此位置生成 开始播放动画
DamageText->AttachToComponent(TargetCharacter->GetRootComponent(),
FAttachmentTransformRules::KeepRelativeTransform);
// 从目标分离组件 自行动画
DamageText->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
DamageText->SetDamageText(DamageAmount, bBlockedHit, bCriticalHit);
}
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, float Damage, bool bBlockedHit, bool bCriticalHit) const
{
if (Props.SourceCharacter != Props.TargetCharacter)
{
if(AAuraPlayerController* PC = Cast<AAuraPlayerController>(UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)))
{
PC->ShowDamageNumber(Damage, Props.TargetCharacter, bBlockedHit, bCriticalHit);
}
}
}
打开 WBP_DamageText
图表: 进入 Update Damage Text 函数: 为 Update Damage Text 函数添加输入参数: BlockedHit 布尔类型 CriticalHit 布尔类型
左侧新建 函数 GetColorBasedOnBlockAndCrit 为GetColorBasedOnBlockAndCrit输入添加输入参数: IsBlocked 布尔类型 IsCrit 布尔类型
IsBlocked 提升为变量 Blocked IsCrit 提升为变量 Crit
为GetColorBasedOnBlockAndCrit输入添加输出: Color: Slate颜色类型 提升为变量 Out Color
节点: 流程控制-branch
数学-布尔-and bool 数学-布尔-not bool
set Out Color
make SlateColor
流程控制-branch
流程控制-branch
流程控制-branch
格挡,未暴击-蓝色 暴击,未格挡-红色 暴击,格挡-黄色 未暴击,未格挡-白色
Get Color Based on Block and Crit Get Color Based on Block and Crit-color 提升为变量 Text Color
Text_Damage 外观-set color and opacity Text Color
打开 BP_DamageTextComponent 图表: 将时间获取的参数输出至 Update Damage Text 函数
添加控件: 文本:Text_HitMessage 设为变量 水平居中对齐 垂直居中对齐 将文本中对齐 轮廓大小:2 外观-字体-尺寸-78
新建动画:HitMessageAnim 添加轨道:Text_HitMessage 添加变换
时间轴 0: 平移:x 0, y -20 缩放:x 1, y 1
时间轴 0.05: 平移:x 0, y 30 缩放:x 0.6, y 0.6
时间轴 0.15: 平移:x 0, y -20
时间轴 1:x 0, y -35
添加渲染不透明度 时间轴 1:0
用户界面-动画-play animation 变量-动画-get HitMessageAnim
Text_HitMessage 控件-set text(text) 函数
左侧添加函数 GetHitMessageBasedOnBlockAndCrit GetHitMessageBasedOnBlockAndCrit 添加输入参数
IsBlocked 布尔类型 IsCrit 布尔类型
IsBlocked 提升为变量 Blocked_2 IsCrit 提升为变量 Crit_2
为GetColorBasedOnBlockAndCrit输入添加输出: Message 文本类型 提升为变量 OutMessage
从 Get Color Based on Block And Crit 拷贝所有条件分支
右键将拷贝的变量替换为本地变量
OutMessage
Update Damage Text 函数 的输入提升为本地变量 Blocked,Crit,Demage
GetHitMessageBasedOnBlockAndCrit GetHitMessageBasedOnBlockAndCrit-Message 提升为本地变量 Message
Text_HitMessage 外观-set color and opacity Text Color
打开 GA_HitReact Instancing Policy 实例化策略-Instanced Per Execution 每次执行实例化 受击立刻重新播放受击蒙太奇动画。 轻量级可以这样设置。
使用游戏标签识别伤害类型。 游戏标签可以在整个GAS中传递。
rpg游戏一般具有多种伤害,魔法伤害,物理伤害。
水属性伤害,火属性伤害,等。
不同的属性对不同的伤害的抗性也不同。魔法抗性,物理抗性。
Source/Aura/Public/AuraGameplayTags.h
public:
// 火属性伤害
FGameplayTag Damage_Fire;
// 伤害类型组
TArray<FGameplayTag> DamageTypes;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
GameplayTags.Damage_Fire = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Damage.Fire"),
FString("Fire Damage Type")
);
GameplayTags.DamageTypes.Add(GameplayTags.Damage_Fire);
}
AuraGameplayAbility 中存放通用的技能标签,例如 FGameplayTag StartupInputTag;
而 AuraDamageGameplayAbility 作为基类技能 AuraGameplayAbility 的子类。 将成为其他所有伤害型技能的父类。例如 作为 魔法投射物技能 AuraProjectileSpell 的父类。 存放伤害技能标签组,且包含更多信息。 所以 魔法投射物技能 AuraProjectileSpell 的 伤害型游戏效果属性 DamageEffectClass 可以放置到 AuraDamageGameplayAbility 类中。 所有 AuraDamageGameplayAbility 的子类技能才需要 伤害型游戏效果
可以在技能上有一个简单的游戏标签,称为伤害类型, 对游戏技能应用单一类型的伤害。
还可以让技能能够施加多种伤害类型。
在这种情况下,最好有一个 【游戏标签 :可扩展浮动值】 的map映射数据,以包含多种伤害类型。 每一组键值对对应一种伤害类型。 并且可扩展浮动值-例如曲线表 包含不同等级下的伤害值。 使技能可具有多种伤害类型,水属性伤害,火属性伤害。等。
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraGameplayAbility.h"
#include "AuraDamageGameplayAbility.generated.h"
UCLASS()
class AURA_API UAuraDamageGameplayAbility : public UAuraGameplayAbility
{
GENERATED_BODY()
protected:
// 伤害型游戏效果
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TSubclassOf<UGameplayEffect> DamageEffectClass;
// 包含多种伤害类型
// 每一组键值对对应一种伤害类型。
// 并且可扩展浮动值-例如曲线表 包含不同等级下的伤害值。
// 使技能可具有多种伤害类型,水属性伤害,火属性伤害
UPROPERTY(EditDefaultsOnly, Category = "Damage")
TMap<FGameplayTag, FScalableFloat> DamageTypes;
};
Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraDamageGameplayAbility.h"
#include "AuraProjectileSpell.generated.h"
class AAuraProjectile;
class UGameplayEffect;
UCLASS()
class AURA_API UAuraProjectileSpell : public UAuraDamageGameplayAbility
{
GENERATED_BODY()
protected:
// 激活技能
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
// 生成投射物 子类蓝图中调用
UFUNCTION(BlueprintCallable, Category = "Projectile")
void SpawnProjectile(const FVector& ProjectileTargetLocation);
// 投射物类 例如火球 Projectile BP_FireBolt
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSubclassOf<AAuraProjectile> ProjectileClass;
};
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
// 设置投射物旋转 方向
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
Rotation.Pitch = 0.f;
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo());
// 为游戏情景添加技能,源对象,投射物,技能命中结果。
FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
EffectContextHandle.SetAbility(this);
EffectContextHandle.AddSourceObject(Projectile);
TArray<TWeakObjectPtr<AActor>> Actors;
Actors.Add(Projectile);
EffectContextHandle.AddActors(Actors);
FHitResult HitResult;
HitResult.Location = ProjectileTargetLocation;
EffectContextHandle.AddHitResult(HitResult);
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
// 魔法投射物技能作为伤害型技能,可能拥有多种类型的伤害技能
for (auto& Pair : DamageTypes)
{
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
}
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
}
标签-伤害值 键值对在节能功能中设置,
在 UAuraProjectileSpell::SpawnProjectile
中填充。
// 根据调用者大小计算伤害值 累加
// DamageTypes 伤害型技能标签组包含所有伤害类型的标签
// DamageTypes 标签只存在于伤害型技能中
// Get Damage Set by Caller Magnitude
float Damage = 0.f;
for (FGameplayTag DamageTypeTag : FAuraGameplayTags::Get().DamageTypes)
{
// 游戏标签对应的的值由 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude 设置过【】
// 根据标签 后续可以使用不同的抗性属性,减少对应类型伤害的值 例如火抗 将 火属性伤害值减少一些
const float DamageTypeValue = Spec.GetSetByCallerMagnitude(DamageTypeTag);
Damage += DamageTypeValue;
}
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// 根据调用者大小计算伤害值 累加
// Get Damage Set by Caller Magnitude
// DamageTypes 伤害型技能标签组包含所有伤害类型的技能标签
// DamageTypes 标签只存在于伤害型技能中
float Damage = 0.f;
for (FGameplayTag DamageTypeTag : FAuraGameplayTags::Get().DamageTypes)
{
// 游戏标签对应的的值由 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude 设置过【】
// 根据标签 后续可以使用不同的抗性属性,减少对应类型伤害的值 例如火抗 将 火属性伤害值减少一些
const float DamageTypeValue = Spec.GetSetByCallerMagnitude(DamageTypeTag);
Damage += DamageTypeValue;
}
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
//
FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
//为自定义效果情景设置是否格挡
UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel());
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef,
EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef,
EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef,
EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(
TargetCombatInterface->GetPlayerLevel());
// Critical Hit Resistance reduces Critical Hit Chance by a certain percentage
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance *
CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// 为自定义效果情景设置是否暴击
UAuraAbilitySystemLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
// Double damage plus a bonus if critical hit
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
Source/Aura/Public/AbilitySystem/Abilities/AuraGameplayAbility.h
以下代码删除,Damage 现在由 UAuraDamageGameplayAbility 的 DamageEffectClass 替代
public:
// 与等级关联的可扩展伤害
// FScalableFloat 表示可设置缩放值和曲线表
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Damage")
FScalableFloat Damage;
完整代码
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "AuraGameplayAbility.generated.h"
UCLASS()
class AURA_API UAuraGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
// 游戏启动时的技能输入标签
// 不在运行时更新
UPROPERTY(EditDefaultsOnly, Category="Input")
FGameplayTag StartupInputTag;
};
打开 GA_FireBolt 损害-DamageTypes-添加一个伤害键值对 损害-DamageTypes-键-Damage.Fire 损害-DamageTypes-值-系数:1,曲线表格:CT_Damage ,曲线:Abilities.FireBolt
这替代了原先的可扩展类型Damage,但效果一样。 只是处理方式不同,并且新方式支持多个伤害叠加。
现在 火球术作为 伤害型技能的基类 可包含多种类型的伤害型标签。且包含可扩展伤害值。 现在已包含 Damage.Fire 标签,表示 火属性伤害。
火球术 的 DamageEffectClass 伤害技能效果类 为 GE_Damage , GE_Damage 使用了 执行计算 ExecCalc_Damage 修改器。 执行计算 ExecCalc_Damage 修改器 中 获取了 DamageTypes 包含的所有伤害标签例如 Damage.Fire, 对目标的健康值造成伤害。
之后可以为火球术 DamageTypes 添加 更多伤害类型的【标签-伤害曲线】键值对。 例如暗属性的伤害。 ExecCalc_Damage 修改器 都可以接收到。
有了伤害类型的概念。 就有与之对抗的抗性类型。
伤害-抗性组,为每种伤害类型技能标签映射一个抵抗类型属性标签:
将之前的 TArray<FGameplayTag> DamageTypes;
替换为 TMap<FGameplayTag, FGameplayTag> DamageTypesToResistances;
Source/Aura/Public/AuraGameplayTags.h
public:
// 辅助属性
// 与伤害相对的抗性类型属性标签
// 火抗
FGameplayTag Attributes_Resistance_Fire;
// 光抗
FGameplayTag Attributes_Resistance_Lightning;
// 秘抗
FGameplayTag Attributes_Resistance_Arcane;
// 物抗
FGameplayTag Attributes_Resistance_Physical;
// 定义所有的伤害类型技能标签
// 火属性伤害
FGameplayTag Damage_Fire;
// 光属性伤害
FGameplayTag Damage_Lightning;
// 秘属性伤害
FGameplayTag Damage_Arcane;
// 物理属性伤害 近战
FGameplayTag Damage_Physical;
// 伤害-抗性组,为每种伤害类型技能标签映射一个抵抗类型属性标签
TMap<FGameplayTag, FGameplayTag> DamageTypesToResistances;
Source/Aura/Private/AuraGameplayTags.cpp
#include "AuraGameplayTags.h"
#include "GameplayTagsManager.h"
FAuraGameplayTags FAuraGameplayTags::GameplayTags;
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
// 添加原生标签
/*
* Primary Attributes
*/
// 获取游戏标签管理器,Get() 返回唯一的游戏标签管理器
// 添加原生游戏标签 参数1-标签 参数2-注释
GameplayTags.Attributes_Primary_Strength = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Primary.Strength"),
FString("Increases physical damage 力量-增加物理伤害")
);
GameplayTags.Attributes_Primary_Intelligence = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Primary.Intelligence"),
FString("Increases magical damage 智力-增加魔法伤害")
);
GameplayTags.Attributes_Primary_Resilience = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Primary.Resilience"),
FString("Increases Armor and Armor Penetration 韧性-增加护甲和护甲穿透")
);
GameplayTags.Attributes_Primary_Vigor = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Primary.Vigor"),
FString("Increases Health 活力-增加生命值")
);
/*
* Secondary Attributes
*/
GameplayTags.Attributes_Secondary_Armor = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.Armor"),
FString("Reduces damage taken, improves Block Chance 护甲-减少受到的伤害,提高格挡几率")
);
GameplayTags.Attributes_Secondary_ArmorPenetration = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.ArmorPenetration"),
FString("Ignores Percentage of enemy Armor, increases Critical Hit Chance 护甲穿透-忽略敌方护甲百分比,增加暴击几率")
);
GameplayTags.Attributes_Secondary_BlockChance = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.BlockChance"),
FString("Chance to cut incoming damage in half 格挡几率-将受到的伤害减半的几率")
);
GameplayTags.Attributes_Secondary_CriticalHitChance = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.CriticalHitChance"),
FString("Chance to double damage plus critical hit bonus 暴击几率-有机会获得双倍伤害加暴击伤害加成")
);
GameplayTags.Attributes_Secondary_CriticalHitDamage = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.CriticalHitDamage"),
FString("Bonus damage added when a critical hit is scored 暴击伤害-获得暴击时增加的额外伤害")
);
GameplayTags.Attributes_Secondary_CriticalHitResistance = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.CriticalHitResistance"),
FString("Reduces Critical Hit Chance of attacking enemies 暴击抗性-降低敌人攻击的暴击几率")
);
GameplayTags.Attributes_Secondary_HealthRegeneration = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.HealthRegeneration"),
FString("Amount of Health regenerated every 1 second 健康回复-每1秒再生的生命值")
);
GameplayTags.Attributes_Secondary_ManaRegeneration = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.ManaRegeneration"),
FString("Amount of Mana regenerated every 1 second 魔力回复-每秒再生的魔法量")
);
GameplayTags.Attributes_Secondary_MaxHealth = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.MaxHealth"),
FString("Maximum amount of Health obtainable 最大健康值-可获得的最大健康量")
);
GameplayTags.Attributes_Secondary_MaxMana = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Secondary.MaxMana"),
FString("Maximum amount of Mana obtainable 最大魔力值-可获得的最大魔法量")
);
/*
* Input Tags 输入操作标签
*/
GameplayTags.InputTag_LMB = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.LMB"),
FString("Input Tag for Left Mouse Button 鼠标左键")
);
GameplayTags.InputTag_RMB = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.RMB"),
FString("Input Tag for Right Mouse Button 鼠标右键")
);
GameplayTags.InputTag_1 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.1"),
FString("Input Tag for 1 key")
);
GameplayTags.InputTag_2 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.2"),
FString("Input Tag for 2 key")
);
GameplayTags.InputTag_3 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.3"),
FString("Input Tag for 3 key")
);
GameplayTags.InputTag_4 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.4"),
FString("Input Tag for 4 key")
);
GameplayTags.Damage = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Damage"),
FString("Damage")
);
/*
* Damage Types 伤害类型技能
*/
GameplayTags.Damage_Fire = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Damage.Fire"),
FString("Fire Damage Type")
);
GameplayTags.Damage_Lightning = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Damage.Lightning"),
FString("Lightning Damage Type")
);
GameplayTags.Damage_Arcane = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Damage.Arcane"),
FString("Arcane Damage Type")
);
GameplayTags.Damage_Physical = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Damage.Physical"),
FString("Physical Damage Type")
);
/*
* Resistances 抗性属性
*/
GameplayTags.Attributes_Resistance_Arcane = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Resistance.Arcane"),
FString("Resistance to Arcane damage")
);
GameplayTags.Attributes_Resistance_Fire = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Resistance.Fire"),
FString("Resistance to Fire damage")
);
GameplayTags.Attributes_Resistance_Lightning = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Resistance.Lightning"),
FString("Resistance to Lightning damage")
);
GameplayTags.Attributes_Resistance_Physical = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Resistance.Physical"),
FString("Resistance to Physical damage")
);
/*
* Map of Damage Types to Resistances 伤害-抗性 map
*/
GameplayTags.DamageTypesToResistances.Add(GameplayTags.Damage_Arcane, GameplayTags.Attributes_Resistance_Arcane);
GameplayTags.DamageTypesToResistances.Add(GameplayTags.Damage_Lightning, GameplayTags.Attributes_Resistance_Lightning);
GameplayTags.DamageTypesToResistances.Add(GameplayTags.Damage_Physical, GameplayTags.Attributes_Resistance_Physical);
GameplayTags.DamageTypesToResistances.Add(GameplayTags.Damage_Fire, GameplayTags.Attributes_Resistance_Fire);
/*
* Effects 特效
*/
GameplayTags.Effects_HitReact = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Effects.HitReact"),
FString("Tag granted when Hit Reacting")
);
}
// 根据调用者大小计算伤害值 累加
// DamageTypesToResistances 伤害型标签-抗性属性 组包含所有伤害类型的标签 和与伤害关联的 抗性属性标签
// DamageTypesToResistances 标签只存在于伤害型技能中
// Get Damage Set by Caller Magnitude
float Damage = 0.f;
for (const TTuple<FGameplayTag, FGameplayTag>& Pair : FAuraGameplayTags::Get().DamageTypesToResistances)
{
// 游戏标签对应的的值由 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude 设置过【】
// Pair.Key 为伤害类型技能标签 Pair.Value 为抗性属性标签
// 根据标签 后续可以取出不同的抗性属性 Pair.Value,减少对应类型伤害的值 例如火抗 将 火属性伤害值减少一些
// 取出伤害类型技能的值
const float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
Damage += DamageTypeValue;
}
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// 根据调用者大小计算伤害值 累加
// DamageTypesToResistances 伤害型标签-抗性属性 组包含所有伤害类型的标签 和与伤害关联的 抗性属性标签
// DamageTypesToResistances 标签只存在于伤害型技能中
// Get Damage Set by Caller Magnitude
float Damage = 0.f;
for (const TTuple<FGameplayTag, FGameplayTag>& Pair : FAuraGameplayTags::Get().DamageTypesToResistances)
{
// 游戏标签对应的的值由 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude 设置过【】
// Pair.Key 为伤害类型技能标签 Pair.Value 为抗性属性标签
// 根据标签 后续可以取出不同的抗性属性 Pair.Value,减少对应类型伤害的值 例如火抗 将 火属性伤害值减少一些
// 取出伤害类型技能的值
const float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
Damage += DamageTypeValue;
}
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
//
FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
//为自定义效果情景设置是否格挡
UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel());
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef,
EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef,
EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef,
EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(
TargetCombatInterface->GetPlayerLevel());
// Critical Hit Resistance reduces Critical Hit Chance by a certain percentage
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance *
CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// 为自定义效果情景设置是否暴击
UAuraAbilitySystemLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
// Double damage plus a bonus if critical hit
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
public:
/*
* Resistance Attributes
*/
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_FireResistance, Category = "Resistance Attributes")
FGameplayAttributeData FireResistance;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, FireResistance);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_LightningResistance, Category = "Resistance Attributes")
FGameplayAttributeData LightningResistance;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, LightningResistance);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_ArcaneResistance, Category = "Resistance Attributes")
FGameplayAttributeData ArcaneResistance;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, ArcaneResistance);
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_PhysicalResistance, Category = "Resistance Attributes")
FGameplayAttributeData PhysicalResistance;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, PhysicalResistance);
UFUNCTION()
void OnRep_FireResistance(const FGameplayAttributeData& OldFireResistance) const;
UFUNCTION()
void OnRep_LightningResistance(const FGameplayAttributeData& OldLightningResistance) const;
UFUNCTION()
void OnRep_ArcaneResistance(const FGameplayAttributeData& OldArcaneResistance) const;
UFUNCTION()
void OnRep_PhysicalResistance(const FGameplayAttributeData& OldPhysicalResistance) const;
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
UAuraAttributeSet::UAuraAttributeSet()
{
//
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Strength, GetStrengthAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Intelligence, GetIntelligenceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Resilience, GetResilienceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Primary_Vigor, GetVigorAttribute);
/* Secondary Attributes */
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_Armor, GetArmorAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_ArmorPenetration, GetArmorPenetrationAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_BlockChance, GetBlockChanceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_CriticalHitChance, GetCriticalHitChanceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_CriticalHitResistance, GetCriticalHitResistanceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_CriticalHitDamage, GetCriticalHitDamageAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_HealthRegeneration, GetHealthRegenerationAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_ManaRegeneration, GetManaRegenerationAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_MaxHealth, GetMaxHealthAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Secondary_MaxMana, GetMaxManaAttribute);
/* Resistance Attributes */
TagsToAttributes.Add(GameplayTags.Attributes_Resistance_Arcane, GetArcaneResistanceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Resistance_Fire, GetFireResistanceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Resistance_Lightning, GetLightningResistanceAttribute);
TagsToAttributes.Add(GameplayTags.Attributes_Resistance_Physical, GetPhysicalResistanceAttribute);
}
void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 注册要复制的健康值 这是想要复制的任何内容所必需的。
// COND_None 条件,表示 不为这个变量的复制设置任何条件,我们总是想复制它,无条件地复制
// REPNOTIFY_Always 始终响应通知意味着如果在服务器上设置了该值,则复制它。在客户端上该值将被更新和设置。
// Primary Attributes 主要属性
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Strength, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Intelligence, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Resilience, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Vigor, COND_None, REPNOTIFY_Always);
// Secondary Attributes 次要/辅助属性
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Armor, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, ArmorPenetration, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, BlockChance, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, CriticalHitChance, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, CriticalHitDamage, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, CriticalHitResistance, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, HealthRegeneration, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, ManaRegeneration, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxMana, COND_None, REPNOTIFY_Always);
// Resistance Attributes
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, FireResistance, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, LightningResistance, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, ArcaneResistance, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, PhysicalResistance, COND_None, REPNOTIFY_Always);
// Vital Attributes 重要属性
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Mana, COND_None, REPNOTIFY_Always);
}
void UAuraAttributeSet::OnRep_FireResistance(const FGameplayAttributeData& OldFireResistance) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, FireResistance, OldFireResistance);
}
void UAuraAttributeSet::OnRep_LightningResistance(const FGameplayAttributeData& OldLightningResistance) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, LightningResistance, OldLightningResistance);
}
void UAuraAttributeSet::OnRep_ArcaneResistance(const FGameplayAttributeData& OldArcaneResistance) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, ArcaneResistance, OldArcaneResistance);
}
void UAuraAttributeSet::OnRep_PhysicalResistance(const FGameplayAttributeData& OldPhysicalResistance) const
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, PhysicalResistance, OldPhysicalResistance);
}
【可以单独新建抗性效果处理抗性计算,此处未使用该方式】 抗性可以依赖多种属性。
打开 GE_SecondaryAttributes
每1点火抗,可抵消 1% 的火属性伤害
细节-Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.FireResistance 细节-Gameplay Effect-Modifiers--Modifier Op-Override 细节-Gameplay Effect-Modifiers--Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Resistance Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.5 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-3
每1点智力,增加0.1火抗
细节-Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.FireResistance 细节-Gameplay Effect-Modifiers--Modifier Op-Add 细节-Gameplay Effect-Modifiers--Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Intelligence Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.1 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-0
每1点光抗,可抵消 1% 的光属性伤害
细节-Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.LightningResistance 细节-Gameplay Effect-Modifiers--Modifier Op-Override 细节-Gameplay Effect-Modifiers--Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Resistance Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.5 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-3
每1点秘抗,可抵消 1% 的秘属性伤害
细节-Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.ArcaneResistance 细节-Gameplay Effect-Modifiers--Modifier Op-Override 细节-Gameplay Effect-Modifiers--Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Resistance Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.5 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-3
每1点物抗,可抵消 1% 的物理属性伤害
细节-Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.PhysicalResistance 细节-Gameplay Effect-Modifiers--Modifier Op-Override 细节-Gameplay Effect-Modifiers--Modifier Magnitude-Magnitude calculation Type -Attribute Based
Modifier Magnitude-Attribute Based Magnitude-Backing Attribute 支持计算的属性 Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute to capture 捕获的游戏属性-AuraAttributeSet.Resistance Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Attribute Source 属性源-Target Modifier Magnitude-Attribute Based Magnitude-Backing Attribute-Snapshot 是否应该快照属性-不启用
Modifier Magnitude-Attribute Based Magnitude-Coefficient-0.5 Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value -0 Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value-3
打开 WBP_AttributeMenu 设计器 滚动框 控件下继续添加 WBP_TextValueRow
Row_FireResistance Row_LightningResistance Row_ArcaneResistance Row_PhysicalResistance 设为变量
图表: sequence
Row_FireResistance Row_LightningResistance Row_ArcaneResistance Row_PhysicalResistance
set attribute tag
分别使用标签: Attributes.Resistance.Fire Attributes.Resistance.Lightning Attributes.Resistance.Arcane Attributes.Resistance.Physical
优化蓝图:
分别折叠到函数: SetPrimaryAttributeTags SetSecondaryAttributeTags SetResistanceAttributeTags
打开 DA_AttributeInfo
新增4组属性 与控件中的抗性属性标签一致
Attribute Tag -Attributes.Resistance.Fire Attribute Name- 火抗 Attribute Description- 增加对火属性伤害的抗性
Attribute Tag -Attributes.Resistance.Lightning Attribute Name- 光抗 Attribute Description-增加对光属性伤害的抗性
Attribute Tag -Attributes.Resistance.Arcane Attribute Name- 秘抗 Attribute Description-增加对秘属性伤害的抗性
Attribute Tag -Attributes.Resistance.Physical Attribute Name- 物抗 Attribute Description-增加对物理属性伤害的抗性
现在属性菜单可看到抗性属性
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
#include "AbilitySystem/ExecCalc/ExecCalc_Damage.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "AuraGameplayTags.h"
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
#include "AbilitySystem/Data/CharacterClassInfo.h"
#include "Interaction/CombatInterface.h"
#include "AuraAbilityTypes.h"
// 没有 F前缀,不会公开给蓝图和反射系统
// C++原始内部结构
struct AuraDamageStatics
{
// 属性捕获定义 捕获属性ArmorDef
// 创建游戏效果属性和属性捕获定义
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
DECLARE_ATTRIBUTE_CAPTUREDEF(ArmorPenetration);
// 定义格挡几率
DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitResistance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitDamage);
// 抗性属性
DECLARE_ATTRIBUTE_CAPTUREDEF(FireResistance);
DECLARE_ATTRIBUTE_CAPTUREDEF(LightningResistance);
DECLARE_ATTRIBUTE_CAPTUREDEF(ArcaneResistance);
DECLARE_ATTRIBUTE_CAPTUREDEF(PhysicalResistance);
// 将捕获定义映射到属性标签
TMap<FGameplayTag, FGameplayEffectAttributeCaptureDefinition> TagsToCaptureDefs;
// 构造函数
AuraDamageStatics()
{
// 创建并定义属性捕获定义 Armor
// 参数3:当前正在捕获Armor属性,进行伤害计算,目标受伤,需要目标的护甲Armor,非来源的护甲
// 参数4:是否快照
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, Armor, Target, false);
// 目标的格挡几率属性
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, BlockChance, Target, false);
// 来源的,攻击者的护甲穿透
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, ArmorPenetration, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitChance, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitResistance, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitDamage, Source, false);
// 目标的抗性
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, FireResistance, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, LightningResistance, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, ArcaneResistance, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, PhysicalResistance, Target, false);
const FAuraGameplayTags& Tags = FAuraGameplayTags::Get();
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_Armor, ArmorDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_BlockChance, BlockChanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_ArmorPenetration, ArmorPenetrationDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitChance, CriticalHitChanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitResistance, CriticalHitResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitDamage, CriticalHitDamageDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, ArcaneResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Fire, FireResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Lightning, LightningResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Physical, PhysicalResistanceDef);
}
};
// 静态伤害函数
// 存储 属性捕获定义 Armor
static const AuraDamageStatics& DamageStatics()
{
// 静态伤害结构变量 只实例化一次 每次调用函数返回同一个 DStatics 实例
// 对象 非指针
// 函数结束时 也不会消失
static AuraDamageStatics DStatics;
return DStatics;
}
UExecCalc_Damage::UExecCalc_Damage()
{
// 将属性捕获定义添加到执行计算相关属性中 类似mmc,用于计算
// 告诉执行计算类 该属性捕获定义用于特定捕获属性
// 参数:属性捕获定义
RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
RelevantAttributesToCapture.Add(DamageStatics().BlockChanceDef);
RelevantAttributesToCapture.Add(DamageStatics().ArmorPenetrationDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalHitChanceDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalHitResistanceDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalHitDamageDef);
// 抗性
RelevantAttributesToCapture.Add(DamageStatics().FireResistanceDef);
RelevantAttributesToCapture.Add(DamageStatics().LightningResistanceDef);
RelevantAttributesToCapture.Add(DamageStatics().ArcaneResistanceDef);
RelevantAttributesToCapture.Add(DamageStatics().PhysicalResistanceDef);
}
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// 根据调用者大小计算伤害值 累加
// DamageTypesToResistances 伤害型标签-抗性属性 组包含所有伤害类型的标签 和与伤害关联的 抗性属性标签
// DamageTypesToResistances 标签只存在于伤害型技能中
// Get Damage Set by Caller Magnitude
float Damage = 0.f;
for (const TTuple<FGameplayTag, FGameplayTag>& Pair : FAuraGameplayTags::Get().DamageTypesToResistances)
{
// 游戏标签对应的的值由 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude 设置过【】
// Pair.Key 为伤害类型技能标签 Pair.Value 为抗性属性标签
// 根据标签 后续可以取出不同的抗性属性 Pair.Value,减少对应类型伤害的值 例如火抗 将 火属性伤害值减少一些
// 取出伤害类型技能的值
// const float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
// 伤害型技能标签
const FGameplayTag DamageTypeTag = Pair.Key;
// 抗性属性标签
const FGameplayTag ResistanceTag = Pair.Value;
checkf(AuraDamageStatics().TagsToCaptureDefs.Contains(ResistanceTag), TEXT("TagsToCaptureDefs doesn't contain Tag: [%s] in ExecCalc_Damage"), *ResistanceTag.ToString());
// 通过属性标签,找到相关联的捕获属性定义,当前只需要抗性捕获定义
// 定义在 TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, ArcaneResistanceDef);
const FGameplayEffectAttributeCaptureDefinition CaptureDef = AuraDamageStatics().TagsToCaptureDefs[ResistanceTag];
float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
// 计算捕获的目标的属性 通过 Resistance 传出
float Resistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(CaptureDef, EvaluationParameters, Resistance);
// 抗性最大抵消100%的伤害
Resistance = FMath::Clamp(Resistance, 0.f, 100.f);
// 每一点抗性抵消1%的伤害
DamageTypeValue *= ( 100.f - Resistance ) / 100.f;
Damage += DamageTypeValue;
}
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
//
FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
//为自定义效果情景设置是否格挡
UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel());
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef,
EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef,
EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef,
EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(
TargetCombatInterface->GetPlayerLevel());
// Critical Hit Resistance reduces Critical Hit Chance by a certain percentage
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance *
CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// 为自定义效果情景设置是否暴击
UAuraAbilitySystemLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
// Double damage plus a bonus if critical hit
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
打开 GE_SecondaryAttributes_TEST
每1点火抗,可抵消 1% 的火属性伤害
为方便测试使用 Scalable Float
细节-Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.FireResistance 细节-Gameplay Effect-Modifiers--Modifier Op-Override 细节-Gameplay Effect-Modifiers--Modifier Magnitude-Magnitude calculation Type -Scalable Float
火属性伤害减50%。 Modifier Magnitude-Scalable Float Magnitude-50
由于游戏模式仅存在于服务端,现在游戏在多人模式中,客户端游戏模式为空,将崩溃。
UAuraAbilitySystemLibrary::InitializeDefaultAttributes 内部用到了游戏模式
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::BeginPlay()
{
Super::BeginPlay();
//
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
InitAbilityActorInfo();
// 为敌人添加初始/共享技能 只在服务端执行
// GiveStartupAbilities 内部使用了游戏模式 ,在客户端为空
if (HasAuthority())
{
UAuraAbilitySystemLibrary::GiveStartupAbilities(this, AbilitySystemComponent);
}
// 为健康条控件绑定控件控制器
// 敌人自身将成为健康条控件的控件控制器
if (UAuraUserWidget* AuraUserWidget = Cast<UAuraUserWidget>(HealthBar->GetUserWidgetObject()))
{
AuraUserWidget->SetWidgetController(this);
}
if (const UAuraAttributeSet* AuraAS = Cast<UAuraAttributeSet>(AttributeSet))
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
// 注册 Effects.HitReact 效果标签 的 NewOrRemoved 新增或移除标签事件
// 参数1-标签
// 参数2-标签事件类型
AbilitySystemComponent->RegisterGameplayTagEvent(FAuraGameplayTags::Get().Effects_HitReact,
EGameplayTagEventType::NewOrRemoved).AddUObject(
this,
&AAuraEnemy::HitReactTagChanged
);
// 广播初始值
OnHealthChanged.Broadcast(AuraAS->GetHealth());
OnMaxHealthChanged.Broadcast(AuraAS->GetMaxHealth());
}
}
void AAuraEnemy::InitAbilityActorInfo()
{
// 初始技能参与者信息 服务器和客户端都在此设置
// 两者均为敌人类自身角色
AbilitySystemComponent->InitAbilityActorInfo(this, this);
Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent)->AbilityActorInfoSet();
// 为敌人基类临时添加初始化属性功能 仅作学习用
if (HasAuthority())
{
InitializeDefaultAttributes();
// 内部用到了游戏模式
}
}
属性集中的 IncomingDamage 属性 只在服务器执行获取,不会复制到客户端
// IncomingDamage 属性只在服务器执行获取,不会复制到客户端
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
必须保证客户端获取到真正的玩家控制器
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
// 由于 if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute()) 这里仅在服务端执行
void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, float Damage, bool bBlockedHit, bool bCriticalHit) const
{
if (Props.SourceCharacter != Props.TargetCharacter)
{
// 所有玩家控制器均存在于服务器端
// UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)) 如果在服务端执行,则获取的是服务器的玩家控制器0号,非客户端控制器
//if(AAuraPlayerController* PC = Cast<AAuraPlayerController>(UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)))
// 应当使用源角色,即造成伤害的来源角色 来获取玩家控制器
if(AAuraPlayerController* PC = Cast<AAuraPlayerController>(Props.SourceCharacter->Controller))
{
// 如果获取的是服务器的玩家控制器0,将只在服务器上显示提示文本控件
// PC->ShowDamageNumber 通过RPC在服务器执行,
// PC玩家控制器不能使用服务器的控制器0号,那不会存在于客户端。必须使用准确的客户端的控制器
PC->ShowDamageNumber(Damage, Props.TargetCharacter, bBlockedHit, bCriticalHit);
}
}
}
必须是本地玩家控制器
Source/Aura/Private/Player/AuraPlayerController.cpp
// 对于服务器控制的角色,它只会在服务器上执行并且服务器控制角色会看到它。
// 但是对于客户端控制的角色,它将通过RPC在服务器上调用,但在客户端上执行的角色会看到它。
// 无论哪种方式,这个小控件都会被看到。
void AAuraPlayerController::ShowDamageNumber_Implementation(float DamageAmount, ACharacter* TargetCharacter,
bool bBlockedHit, bool bCriticalHit)
{
// 目标可能在最后一帧被杀死,销毁自身 导致 TargetCharacter 为空
// 必须是本地玩家控制器
if (IsValid(TargetCharacter) && DamageTextComponentClass && IsLocalController())
{
// 创建组件
UDamageTextComponent* DamageText = NewObject<UDamageTextComponent>(TargetCharacter, DamageTextComponentClass);
DamageText->RegisterComponent(); //引擎中注册组件
// 伤害文字组件附加到目标收到伤害的位置 在此位置生成 开始播放动画
DamageText->AttachToComponent(TargetCharacter->GetRootComponent(),
FAttachmentTransformRules::KeepRelativeTransform);
// 从目标分离组件 自行动画
DamageText->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
DamageText->SetDamageText(DamageAmount, bBlockedHit, bCriticalHit);
}
}
Source/Aura/Private/Actor/AuraProjectile.cpp
void AAuraProjectile::Destroyed()
{
if (!bHit && !HasAuthority())
{
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
// 此时投射物可能已销毁 循环音效也将一同销毁
if (LoopingSoundComponent) LoopingSoundComponent->Stop();
}
Super::Destroyed();
}
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
// GetEffectCauser 效果引发者
// DamageEffectSpecHandle.Data 在客户端上无效
if (DamageEffectSpecHandle.Data.IsValid() && DamageEffectSpecHandle.Data.Get()->GetContext().GetEffectCauser() == OtherActor)
{
return;
}
// 命中之后不应多次播放音效
if (!bHit)
{
// 播放音效
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
// 撞击Niagara特效
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
// 此时投射物可能已销毁 循环音效也将一同销毁
if (LoopingSoundComponent) LoopingSoundComponent->Stop();
}
if (HasAuthority())
{
// 只在服务器上应用伤害效果即可
// 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端
// 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
TargetASC->ApplyGameplayEffectSpecToSelf(*DamageEffectSpecHandle.Data.Get());
}
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
else
{
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
bHit = true;
}
}
由于服务器与客户端的火球运行时间差异 不应将俯仰角归0,如果投射物生成位置高于敌人,投射物将高于敌人运行
去掉 Rotation.Pitch = 0.f;
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
// 设置投射物旋转 方向 直接瞄准敌人
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
// Rotation.Pitch = 0.f; // 由于服务器与客户端的火球运行时间差异 不应将俯仰角归0,如果投射物生成位置高于敌人,投射物将高于敌人运行
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo());
// 为游戏情景添加技能,源对象,投射物,技能命中结果。
FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
EffectContextHandle.SetAbility(this);
EffectContextHandle.AddSourceObject(Projectile);
TArray<TWeakObjectPtr<AActor>> Actors;
Actors.Add(Projectile);
EffectContextHandle.AddActors(Actors);
FHitResult HitResult;
HitResult.Location = ProjectileTargetLocation;
EffectContextHandle.AddHitResult(HitResult);
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
// 魔法投射物技能作为伤害型技能,可能拥有多种类型的伤害技能
for (auto& Pair : DamageTypes)
{
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
}
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
}
https://docs.unrealengine.com/5.3/zh-CN/artificial-intelligence-in-unreal-engine/ AI系统——一种可用于在项目中创建高真实度AI实体的系统。
Source/Aura/Aura.Build.cs
PrivateDependencyModuleNames.AddRange(new string[] { "NavigationSystem", "Niagara", "AIModule" });
AI控制器 包含黑板组件和行为树组件
Source/Aura/Public/AI/AuraAIController.h
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "AuraAIController.generated.h"
class UBlackboardComponent;
class UBehaviorTreeComponent;
UCLASS()
class AURA_API AAuraAIController : public AAIController
{
GENERATED_BODY()
public:
AAuraAIController();
protected:
//不需要创建黑板组件,只需要设置即可。因为AI控制器已创建该指针Blackboard
//UPROPERTY()
//TObjectPtr<UBlackboardComponent> BlackboardComponent;
UPROPERTY()
TObjectPtr<UBehaviorTreeComponent> BehaviorTreeComponent;
};
Source/Aura/Private/AI/AuraAIController.cpp
#include "AI/AuraAIController.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
AAuraAIController::AAuraAIController()
{
Blackboard = CreateDefaultSubobject<UBlackboardComponent>("BlackboardComponent");
check(Blackboard);
BehaviorTreeComponent = CreateDefaultSubobject<UBehaviorTreeComponent>("BehaviorTreeComponent");
check(BehaviorTreeComponent);
}
Source/Aura/Public/Character/AuraEnemy.h
class UBehaviorTree;
class AAuraAIController;
public:
virtual void PossessedBy(AController* NewController) override;
UPROPERTY(EditAnywhere, Category = "AI")
TObjectPtr<UBehaviorTree> BehaviorTree;
UPROPERTY()
TObjectPtr<AAuraAIController> AuraAIController;
Source/Aura/Private/Character/AuraEnemy.cpp
#include "AI/AuraAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
void AAuraEnemy::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
//if (!HasAuthority()) return;
AuraAIController = Cast<AAuraAIController>(NewController);
//AuraAIController->GetBlackboardComponent()->InitializeBlackboard(*BehaviorTree->BlackboardAsset);
//AuraAIController->RunBehaviorTree(BehaviorTree);
}
打开 BP_EnemyBase pawn-AI Controller class-BP_AuraAIController
右键-人工智能-黑板
BB_EnemyBlackboard
Content/Blueprints/AI/BB_EnemyBlackboard.uasset
右键-人工智能-行为树 BT_EnemyBehaviorTree
Content/Blueprints/AI/BT_EnemyBehaviorTree.uasset
行为树具由黑板资产
打开 BP_EnemyBase AI-Behavior Tree-BT_EnemyBehaviorTree
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// AI角色由服务器控制。
// 客户看到的任何东西都是复制的结果。
if (!HasAuthority()) return;
// 只在服务器上设置AI控制器。
AuraAIController = Cast<AAuraAIController>(NewController);
// 在黑板组件上初始化黑板。
AuraAIController->GetBlackboardComponent()->InitializeBlackboard(*BehaviorTree->BlackboardAsset);
// 运行行为树。
AuraAIController->RunBehaviorTree(BehaviorTree);
}
打开 BT_EnemyBehaviorTree 行为树: 拖出节点: selector 选择器 选择器会从左至右按顺序执行其并列的子节点
tasks-paly animation paly animation-细节-节点-要播放的动画-Attack_Spear
运行游戏,敌人将播放攻击动画 Attack_Spear。
删除测试节点。
selector 选择器会从左至右按顺序执行其并列的子节点。 一个播放动画节点完成,它将转下一个节点。 行为树中的节点具有成功或失败的概念, 节点是否成功或失败取决于节点如何定义成功或失败。 如果一个节点成功或失败,它会将成功或失败信息返回给其父节点, 然后父节点将决定如何处理该信息。
选择器将依次执行其子级,直到它的一个子级成功了,它将成功返回到ROOT。 一旦成功返回到根,那么整棵树就会重新开始。
具有三个子级的选择器将首先执行其第一个子级。 如果那个子级成功了,其他子级就永远不会成功。 一旦它成功返回根并重新开始。 如果它再次成功,那么将永远不会尝试执行其他两个子节点。 只要第一个节点保持成功,它就会一直执行。 一旦失败,选择器将尝试执行其下一个子节点并继续从那里开始这个过程。
附加到选择器的这些类型的节点之一称为服务。
右键单击选择器,可以添加装饰器和服务。 装饰器可以附加到节点,服务可以附加到节点。
选择器被认为是一个分支,即使它有多个子级连接到它, 选择器及其所有子级视为一个分支。
一个服务一旦附加到节点(例如选择器),只要分支正在被执行。 则附加到它的任何服务都将被执行,并且服务以我们指定的频率执行。
一个服务 获取所有玩家控制的角色并找到最近的玩家,这样它就可以去攻击该玩家。
由于服务经常在行为树上使用,并且拥有该节点的行为树组件具有它自己的主人。 可以访问拥有该特定节点的参与者和控制器。
Source/Aura/Public/AI/BTService_FindNearestPlayer.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/Services/BTService_BlueprintBase.h"
#include "BTService_FindNearestPlayer.generated.h"
UCLASS()
class AURA_API UBTService_FindNearestPlayer : public UBTService_BlueprintBase
{
GENERATED_BODY()
protected:
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
Source/Aura/Private/AI/BTService_FindNearestPlayer.cpp
#include "AI/BTService_FindNearestPlayer.h"
#include "AIController.h"
// 由于服务经常在行为树上使用,并且拥有该节点的行为树组件具有它自己的主人。
// 可以访问拥有该特定节点的参与者和控制器。
void UBTService_FindNearestPlayer::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
GEngine->AddOnScreenDebugMessage(1, 1.f, FColor::Red, *AIOwner->GetName());
GEngine->AddOnScreenDebugMessage(2, 1.f, FColor::Green, *ActorOwner->GetName());
}
Content/Blueprints/AI/BTS_FindNearestPlayer.uasset
打开 BTS_FindNearestPlayer 事件图表:
左侧可以重载事件函数。
细节面板:
描述-节点名称-FindNearestPlayer
左侧重载函数 接收Tick AI
打开 BT_EnemyBehaviorTree 行为树: 选择器-右键-添加服务-BT Enemy Behavior Tree
现在让我们使用蓝图版本 BT 查找最近的玩家,注意它显示“FindNearestPlayer”。 该名称在服务中0节点名称 定义.
选择 带有该服务的选择器的 FindNearestPlayer 部分 细节-服务-间隔 0.5 表示执行频率
运行游戏:
BTS_FindNearestPlayer 服务 的 AIOwner ,ActorOwner 都是 BP_AuraAIController
服务和行为树不存储变量。 服务将收集的信息存储在黑板键资产中。例如最近的玩家。
BB_EnemyBlackboard 有默认关键帧 SelfActor
行为树可以访问与其关联的黑板。 在行为树中,可以访问这些值并根据这些值进行行为。
打开 BTS_FindNearestPlayer 新增变量 公开变量 SelfActorKey 类型-黑板键选择器
打开 BT_EnemyBehaviorTree 选择 FindNearestPlayer 细节-默认-SelfActorKey-SelfActor
SelfActor 是行为树的黑板的默认键 这样 ,服务可以获取到行为树的黑板的黑板键
打开BTS_FindNearestPlayer
屏幕打印出各个敌人的蓝图名称 :BP_Goblin_Spear 表示 行为树的黑板的黑板键 SelfActor 自动设置为 BP_Goblin_Spear 敌人角色
这因为: 敌人类 包含行为树,包含AI控制器。 行为树包含了黑板。包含了服务。 服务可以获取到行为树的黑板的黑板键。【黑板的黑板键 SelfActor 自动设置为 BP_Goblin_Spear 敌人角色】
删除测试节点和SelfActorKey变量。
打开 BB_EnemyBlackboard
名称 TargetToFollow
TargetToFollow-细节-键类型-基类-Actor 这用来限制 TargetToFollow 的类型
TargetToFollow-细节-实例已同步-不启用 如设为true,则此域将在此黑板的所有实例之间同步。
如果有多个敌人都使用这个黑板,即时同步将使他们之间共享变量。 如果其中一个敌人设置了它,那么所有的人都可以访问同一个变量。
如果未选中,则黑板的每个实例都有自己的该变量版本。
到目标的距离 在找到最近的目标后立即设置它, 然后我们可以根据距离采取行动。
Source/Aura/Public/AI/BTService_FindNearestPlayer.h
protected:
// 黑板键选择器类型
// C++类和黑板上的任何键之间创建链接
// 后续将其与黑板蓝图上存在的实际键关联起来
UPROPERTY(BlueprintReadOnly, EditAnywhere)
FBlackboardKeySelector TargetToFollowSelector;
UPROPERTY(BlueprintReadOnly, EditAnywhere)
FBlackboardKeySelector DistanceToTargetSelector;
Source/Aura/Private/AI/BTService_FindNearestPlayer.cpp
#include "AI/BTService_FindNearestPlayer.h"
#include "AIController.h"
#include "Kismet/GameplayStatics.h"
// 由于服务经常在行为树上使用,并且拥有该节点的行为树组件具有它自己的主人。
// 可以访问拥有该特定节点的参与者和控制器。
void UBTService_FindNearestPlayer::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
APawn* OwningPawn = AIOwner->GetPawn();
// 目标标签名
// 如果自身有Player标签 表示玩家或宠物,那么目标标签是敌人,
// 如果自身有Enemy标签,表示敌人,那么目标标签是玩家
const FName TargetTag = OwningPawn->ActorHasTag(FName("Player")) ? FName("Enemy") : FName("Player");
// 获取带有该标签的所有Actor
TArray<AActor*> ActorsWithTag;
UGameplayStatics::GetAllActorsWithTag(OwningPawn, TargetTag, ActorsWithTag);
}
打开 BP_EnemyBase actor-高级-标签-Enemy
打开 BP_AuraCharacter actor-高级-标签-Player
打开 BT_EnemyBehaviorTree
选择 FindNearestPlayer 服务-
可以看到 继承自 BTService_FindNearestPlayer C++ 基类中的属性: TargetToFollowSelector DistanceToTargetSelector
分别设置值为黑板中的键 TargetToFollow,DistanceToTarget
TargetToFollowSelector - TargetToFollow DistanceToTargetSelector - DistanceToTarget
此时服务可获取并修改黑板的键。
Source/Aura/Private/AI/BTService_FindNearestPlayer.cpp
#include "BehaviorTree/BTFunctionLibrary.h"
#include "AI/BTService_FindNearestPlayer.h"
#include "AIController.h"
#include "Kismet/GameplayStatics.h"
#include "BehaviorTree/BTFunctionLibrary.h"
// 由于服务经常在行为树上使用,并且拥有该节点的行为树组件具有它自己的主人。
// 可以访问拥有该特定节点的参与者和控制器。
void UBTService_FindNearestPlayer::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
APawn* OwningPawn = AIOwner->GetPawn();
// 目标标签名
// 如果自身有Player标签 表示玩家或宠物,那么目标标签是敌人,
// 如果自身有Enemy标签,表示敌人,那么目标标签是玩家
const FName TargetTag = OwningPawn->ActorHasTag(FName("Player")) ? FName("Enemy") : FName("Player");
// 获取带有该标签的所有Actor
TArray<AActor*> ActorsWithTag;
UGameplayStatics::GetAllActorsWithTag(OwningPawn, TargetTag, ActorsWithTag);
// 最近的目标距离初始化为一个最大的浮点值
float ClosestDistance = TNumericLimits<float>::Max();
// 最近的目标Actor
AActor* ClosestActor = nullptr;
for (AActor* Actor : ActorsWithTag)
{
GEngine->AddOnScreenDebugMessage(-1, .5f, FColor::Orange, *Actor->GetName());
if (IsValid(Actor) && IsValid(OwningPawn))
{
const float Distance = OwningPawn->GetDistanceTo(Actor);
if (Distance < ClosestDistance)
{
ClosestDistance = Distance;
ClosestActor = Actor;
}
}
}
UBTFunctionLibrary::SetBlackboardValueAsObject(this,TargetToFollowSelector, ClosestActor);
UBTFunctionLibrary::SetBlackboardValueAsFloat(this, DistanceToTargetSelector, ClosestDistance);
}
打开行为树 BT_EnemyBehaviorTree 运行游戏
行为树的黑板键会实时显示其值:每0.5秒更新一次。
添加节点: Move To Move To-细节-黑板-黑板键-TargetToFollow
敌人将跟随目标玩家
打开 BP_EnemyBase 旋转全部不启用。
使用角色移动组件控制移动 选择 角色移动组件 角色移动(旋转设置)-使用控制器所需的旋转-启用 如为tue,朝控制器的理想旋转(通常为Controler->ControlRotation)平滑地旋转角色,将RotationRate用作旋转的变化率被OrentRotationToMovement覆盖 通常需要确保将其他设置清除,如角色上的bUseControllerRotationYaw。
// 使用控制器所需的旋转
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
bUseControllerRotationYaw = false;
GetCharacterMovement()->bUseControllerDesiredRotation = true;
Source/Aura/Private/Character/AuraEnemy.cpp
AAuraEnemy::AAuraEnemy()
{
// 设置敌人基类的网格体组件的碰撞预设为 custom,检测响应-Visibility-阻挡,
// 使光标跟踪生效,因为光标跟踪Visibility通道。
// GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
// 构造敌人类的技能系统组件
AbilitySystemComponent = CreateDefaultSubobject<UAuraAbilitySystemComponent>("AbilitySystemComponent");
// 设置为网络复制
AbilitySystemComponent->SetIsReplicated(true);
// 设置复制模式 游戏效果不重复。游戏提示和游戏标签复制到所有客户端。
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
// 使用控制器所需的旋转
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
bUseControllerRotationYaw = false;
GetCharacterMovement()->bUseControllerDesiredRotation = true;
// 构造敌人类的属性集
AttributeSet = CreateDefaultSubobject<UAuraAttributeSet>("AttributeSet");
// 构造健康条控件
HealthBar = CreateDefaultSubobject<UWidgetComponent>("HealthBar");
// 将健康条控件附加到根组件
HealthBar->SetupAttachment(GetRootComponent());
}
Source/Aura/Public/Actor/AuraEffectActor.h
protected:
// 效果应用后是否销毁Actor
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
bool bDestroyOnEffectApplication = false;
// 是否对敌人应用效果
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
bool bApplyEffectsToEnemies = false;
Source/Aura/Private/Actor/AuraEffectActor.cpp
#include "Actor/AuraEffectActor.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
AAuraEffectActor::AAuraEffectActor()
{
PrimaryActorTick.bCanEverTick = false;
SetRootComponent(CreateDefaultSubobject<USceneComponent>("SceneRoot"));
}
void AAuraEffectActor::BeginPlay()
{
Super::BeginPlay();
}
void AAuraEffectActor::ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass)
{
// 如果目标是敌人,且不对敌人应用效果 则返回
if (TargetActor->ActorHasTag(FName("Enemy")) && !bApplyEffectsToEnemies) return;
// 获取Target【例如玩家角色】的技能系统组件
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
// 如果目标【例如玩家角色】没有技能系统组件 则什么都不做
// 例如红药水与玩家重叠
if (TargetASC == nullptr) return;
// 游戏效果类必须有效,无论目标是否具由技能系统组件。否则崩溃
check(GameplayEffectClass);
// 制作游戏效果情景句柄【轻量级指针】,与游戏效果相关的东西,包含 背景,效果目标,谁造成的效果,效果是什么
// 句柄是一个轻量级包装器,它将实际效果上下文存储为指针。
// 它有能力清除该指针。有办法获取影响上下文的任何游戏标签它有很多实用程序
FGameplayEffectContextHandle EffectContextHandle = TargetASC->MakeEffectContext();
// 添加导致此游戏效果的来源【例如红药水】
EffectContextHandle.AddSourceObject(this);
// 制作游戏效果规范句柄
// 参数1:效果类
// 参数2:效果等级
// 参数3: 效果情景
const FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(
GameplayEffectClass, ActorLevel, EffectContextHandle);
// 应用游戏效果规格句柄的数据【游戏效果】到Target自身【例如玩家角色自身】
// 参数2:预测,补偿
// Get 返回原始指针
// * 星号取消这个原始指针 获取游戏效果
// 一旦您应用了游戏效果,该游戏效果就会变为活动状态,并且这些应用功能返回该效果的句柄 ActiveEffectHandle。
// 所以我们以后总是可以使用该句柄ActiveEffectHandle ,例如如果它是无限时间游戏效果,则将其效果删除。
const FActiveGameplayEffectHandle ActiveEffectHandle = TargetASC->ApplyGameplayEffectSpecToSelf(
*EffectSpecHandle.Data.Get());
// 效果的持续时间类型
const bool bIsInfinite = EffectSpecHandle.Data.Get()->Def.Get()->DurationPolicy ==
EGameplayEffectDurationType::Infinite;
if (bIsInfinite && InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
{
// 存储无限时间游戏效果的 游戏效果规格句柄+技能系统组件 键值对 用以删除无限时间游戏效果
// 其他类型效果不需要存储,因为他们自动删除自己
ActiveEffectHandles.Add(ActiveEffectHandle, TargetASC);
}
if (!bIsInfinite)
{
Destroy();
}
}
void AAuraEffectActor::OnOverlap(AActor* TargetActor)
{
if (TargetActor->ActorHasTag(FName("Enemy")) && !bApplyEffectsToEnemies) return;
// 重叠开始时策略
// 即时和持续时间效果应用策略
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 (TargetActor->ActorHasTag(FName("Enemy")) && !bApplyEffectsToEnemies) return;
// 重叠结束时策略
// 即时和持续时间效果应用策略
if (InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
{
ApplyEffectToTarget(TargetActor, InstantGameplayEffectClass);
}
if (DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
{
ApplyEffectToTarget(TargetActor, DurationGameplayEffectClass);
}
if (InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
{
ApplyEffectToTarget(TargetActor, InfiniteGameplayEffectClass);
}
// 如果需要在重叠结束时删除无限效果,那么开始删除对应的无限效果数组的每一项
if (InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
{
// 获取目标的技能系统组件
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
if (!IsValid(TargetASC)) return;
TArray<FActiveGameplayEffectHandle> HandlesToRemove;
for (TTuple<FActiveGameplayEffectHandle, UAbilitySystemComponent*> HandlePair : ActiveEffectHandles)
{
// 如果找到了当前技能组件系统对应的键值对
if (TargetASC == HandlePair.Value)
{
// 移除该技能组件系统上的活跃效果
// 参数1:活跃效果句柄
// 参数2:要移除的堆栈 默认为 -1,表示全部移除。同类型效果全部失效。1表示只移除一个堆栈
TargetASC->RemoveActiveGameplayEffect(HandlePair.Key, 1);
HandlesToRemove.Add(HandlePair.Key);
}
}
for (FActiveGameplayEffectHandle& Handle : HandlesToRemove)
{
ActiveEffectHandles.FindAndRemoveChecked(Handle);
}
}
}
Applied Effects-Destroy On Effect Application-不启用 Applied Effects-Apply Effects To Enemies-启用
Applied Effects-Destroy On Effect Application-启用 Applied Effects-Apply Effects To Enemies-不启用
图表 删除 destroy actor 节点
https://docs.unrealengine.com/5.3/zh-CN/unreal-engine-behavior-tree-node-reference-decorators/
装饰器节点(在其他行为树系统中也称为条件语句)连接到合成(Composite)或任务(Task)节点,并定义树中的分支,甚至单个节点是否可以执行。
黑板(Blackboard) 节点将检查给定的 黑板键(Blackboard Key) 上是否设置了值。
属性 | 描述 | ||||
---|---|---|---|---|---|
通知观察者(Notify Observer) | 结果改变时(On Result Change)仅在条件改变时进行重新计算。值改变时(On Value Change)仅在观察到的黑板键改变时进行重新计算。 | 结果改变时(On Result Change) | 仅在条件改变时进行重新计算。 | 值改变时(On Value Change) | 仅在观察到的黑板键改变时进行重新计算。 |
结果改变时(On Result Change) | 仅在条件改变时进行重新计算。 值改变时(On Value Change) | 仅在观察到的黑板键改变时进行重新计算。 观察者中止(Observer Aborts) | 无(None)不中止执行。自身(Self)中止此节点自身和在其下运行的所有子树。低优先级(Lower Priority)中止此节点右侧的所有节点。两者(Both)中止此节点自身和在其下运行的所有子树,以及此节点右侧的所有节点。 | 无(None) | 不中止执行。 | 自身(Self) | 中止此节点自身和在其下运行的所有子树。 | 低优先级(Lower Priority) | 中止此节点右侧的所有节点。 | 两者(Both) | 中止此节点自身和在其下运行的所有子树,以及此节点右侧的所有节点。
无(None) | 不中止执行。 自身(Self) | 中止此节点自身和在其下运行的所有子树。 低优先级(Lower Priority) | 中止此节点右侧的所有节点。 两者(Both) | 中止此节点自身和在其下运行的所有子树,以及此节点右侧的所有节点。 黑板键(Blackboard Key) | 装饰器将运行的黑板键。 键查询(Key Query) | 已经设置(Is Set)数值是否已设置?尚未设置(Is Not Set)数值是否尚未设置? | 已经设置(Is Set) | 数值是否已设置? | 尚未设置(Is Not Set) | 数值是否尚未设置?
已经设置(Is Set) | 数值是否已设置? 尚未设置(Is Not Set) | 数值是否尚未设置? 节点名称(Node Name) | 节点应该在行为树图表中显示的名称。
行为树有一个找到最近玩家的服务,它正在设置几个黑板键的值,每半秒左右一次。
可以使用这些黑板键值来确定下一步应该做什么。
装饰器也可以附加到节点上,它们可以为我们提供条件。像 if 语句, 根据黑板键值,允许执行给定的分支或阻止执行给定分支。
删除 move to 节点
添加节点: selector selector-右键-添加装饰器-blackBoard
该装饰器选择器使用黑板键作为条件。
选择选择器的 装饰器选择器 部分
细节-黑板-黑板键-TargetToFollow 细节-黑板-键查询-已设置 如果它被设置为已设置,则必须设置要遵循的目标【黑板键-TargetToFollow】才能执行此选择器。 因此,如果我有子类连接到选择器,那么只有在跟随目标时它们才会被执行。 即 找到了最近的玩家时,才会执行。黑板键-TargetToFollow 有值。
黑板 添加键 HitReacting 布尔类型
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// AI角色由服务器控制。
// 客户看到的任何东西都是复制的结果。
if (!HasAuthority()) return;
// 只在服务器上设置AI控制器。
AuraAIController = Cast<AAuraAIController>(NewController);
// 在黑板组件上初始化黑板。
AuraAIController->GetBlackboardComponent()->InitializeBlackboard(*BehaviorTree->BlackboardAsset);
// 运行行为树。
AuraAIController->RunBehaviorTree(BehaviorTree);
//
AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("HitReacting"), false);
}
// NewCount 新标签计数 Effects.HitReact 效果标签 的数量
void AAuraEnemy::HitReactTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
// 如果标签计数大于0则做出命中响应
bHitReacting = NewCount > 0;
// 做出命中响应时不能移动
GetCharacterMovement()->MaxWalkSpeed = bHitReacting ? 0.f : BaseWalkSpeed;
//
AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("HitReacting"), bHitReacting);
}
打开 BT_EnemyBehaviorTree 继续添加黑板装饰器到选择器上 细节-黑板-黑板键-HitReact 细节-黑板-键查询-未设置
远程攻击
添加节点 sequence 表示 RangedAttacker 远程攻击 sequence 表示 MeleeAttacker 近战攻击 sequence 表示 Move to target
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// AI角色由服务器控制。
// 客户看到的任何东西都是复制的结果。
if (!HasAuthority()) return;
// 只在服务器上设置AI控制器。
AuraAIController = Cast<AAuraAIController>(NewController);
// 在黑板组件上初始化黑板。
AuraAIController->GetBlackboardComponent()->InitializeBlackboard(*BehaviorTree->BlackboardAsset);
// 运行行为树。
AuraAIController->RunBehaviorTree(BehaviorTree);
//
AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("HitReacting"), false);
// 如果不是战士,则设置黑板键远程攻击为false
AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("RangedAttacker"), CharacterClass != ECharacterClass::Warrior);
}
行为树
添加黑板装饰器 细节-黑板-黑板键-RangedAttacker 细节-黑板-键查询-已设置 节点名称-我是远程攻击吗
只有 RangedAttacker 为真,才表示是远程攻击,才执行此分支,否则执行下一个节点。
添加黑板装饰器 细节-黑板-黑板键-DistanceToTarget 细节-黑板-键查询-小于或等于 细节-黑板-键值-600 节点名称-到达攻击距离了吗 远程攻击也有距离限制
添加黑板装饰器 细节-黑板-黑板键-DistanceToTarget 细节-黑板-键查询-小于 细节-黑板-键值-500 节点名称-足够近以攻击
目标距离小于500时,表示近战攻击,则执行此分支,否则执行下一个节点。
前2各都失败时执行此分支 与目标距离在500-4000时,开始移动至目标。
1-足够接近目标时,才向目标移动 添加黑板装饰器 细节-黑板-黑板键-DistanceToTarget 细节-黑板-键查询-小于或等于 细节-黑板-键值-4000 节点名称-足够近以攻击
2-如果太近,则失败,重新开始整个节点 添加黑板装饰器 细节-黑板-黑板键-DistanceToTarget 细节-黑板-键查询-大于或等于 细节-黑板-键值-500 节点名称-足够远以攻击
3-添加节点 wait wait -等待-等待时间-0 wait -等待-随即偏差-0.5 约0.5秒的延迟后执行后面的节点
4-move to move to-黑板-黑板键-TargetToFollow move to-节点-可接受半径-50 添加到目标到达测试中A[和目标位置之间闯值的固定距离
Source/Aura/Public/AI/BTTask_Attack.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlueprintBase.h"
#include "BTTask_Attack.generated.h"
UCLASS()
class AURA_API UBTTask_Attack : public UBTTask_BlueprintBase
{
GENERATED_BODY()
// 执行任务
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
Source/Aura/Private/AI/BTTask_Attack.cpp
#include "AI/BTTask_Attack.h"
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
return Super::ExecuteTask(OwnerComp, NodeMemory);
}
Content/Blueprints/AI/BTT_Attack.uasset
打开 BTT_Attack 重载 接收执行AI event receive execute AI
事件图表: ai-行为树-finish execute finish execute-success-启用 任务必须返回成功或失败
controlled pawn 拖出 get object name print string
打开 BT_EnemyBehaviorTree
近战节点下拖出节点 BTT_Attack
近战攻击时,打印 controlled pawn -受控pawn名称 BP_Goblin_Spear 近战攻击时,打印 owner controller 为 BP_AuraAIController
BP_Goblin_Spear-Character Class-ranger 游侠 BP_Goblin_Spear-Character Class-elementailst 魔法师 随意添加一个敌人设置为 warrior 战士
打开 BTT_Attack
变换-get actor location 渲染-调试-draw debug sphere
此时只有近战职业会显示调试球。
目标距离大于等于500时中止自身节点和子类节点。不再继续跟踪。返回行为树。 这样敌人跟踪时,最终会停在500距离处,不会无限贴近目标。
拖出节点 move to
细节-黑板-黑板键-TargetToFollow 节点-可接受半径-20 节点名称-接近 近战攻击战士,在距离目标20单位时,才开始攻击。
BTT_Attack-节点名称-Attack
近战攻击者会贴近玩家,远程攻击者会在远处。 敌人之间会互相妨碍。远程敌人可能会阻止近战敌人行走。 敌人脚下没有生成孔洞。
防止动画切换时的抖动 打开 ABP_Goblin_Spear 找到使用的混合空间 BS_GoblinSpear_IdleRun 打开 BS_GoblinSpear_IdleRun
取样平滑-权重速度-4 表示在4分之一秒内切换动画
如果大于0,这是允许取样权重变化的速度。 速度为1意味着取样权重可以在一秒内从0变为1(或从1变为0)。 速度为2意味着这需要半秒钟。 这允许混合空间切换到新参数而不经过中间状态有效地混合它原来的位置和新目标的位置。举例而言,想象我们有一个运动的混合空间,向左、向前和向右移动。现在如果内插混合空间本身的输入从一个极端到另一个极端,你会从左向前、向右。作为另一种选择,将此取样权重速度值设为高于0它将直接从左到右,而不需要先向前移动。 值越小,样本权重调整越慢,因此更平滑然而,0值将完全禁用此平滑。
打开 BT_EnemyBehaviorTree 黑板-添加 向量 黑板键-MoveToLocation
行为树-新建任务-BTTask_Blueprint-BTT_GoAroundTarget
Content/Blueprints/AI/BTT_GoAroundTarget.uasset
基于蓝图的任务节点的基类。不可用来创建本地C++类!
任务接收到中止事件时,所有与此实例相关的所有延迟操作将被移除这可以防止Execute执行继续操作,但不处理外部事件请谨慎使用(终止时注销),存疑时调用IsTaskExecuting()
。
重载 接收执行AI -event receive execute AI ai-行为树-finish execute success 启用
新建 黑板键选择器 类型的变量 NewLocation :新位置 公开变量
新建 黑板键选择器 类型的变量 Target :目标 公开变量
拖出Target get blackboard value as actor 因为目标是actor类型 工具-is Valid
get actor location
在目标位置指定半径附近生成一个随机位置 确保新位置在导航网格内范围内 AI-导航-get random location in navigable radius radius 提升为变量 Radius Radius 默认值300 公开 Radius
设置新位置给 NewLocation NewLocation AI-行为树-set blackboard value as vector
打开 BT_EnemyBehaviorTree
1 melee attacker 拖出任务 wait 等待-等待时间-1 等待-随即偏差-0.5
2 melee attacker 拖出任务 BTT_GoAroundTarget BTT_GoAroundTarget-默认-NewLocation-MoveToLocation BTT_GoAroundTarget-默认-Target-TargetToFollow 节点名称-在目标周围寻找新位置
3 melee attacker 拖出任务 move to move to-黑板-黑板键-MoveToLocation move to 右键添加 TimeLimit 装饰器 TimeLimit 装饰器-时间限制-2 使敌人在玩家周围移动时不能停留太久,超时则中止。 防止被其他敌人卡住位置无法接近目标。
最终,近战敌人接近目标后开始攻击,等待1秒左右,移动到目标周围的新位置,不断循环。再次开始攻击。 移动到目标周围的新位置时如果被物体卡住,最多等待2秒,重新找新位置。
https://docs.unrealengine.com/5.3/zh-CN/environment-query-system-in-unreal-engine/
这些数据可给人工智能提供决策数据,用于后续动作的决策过程。
场景查询系统(EQS) 是虚幻引擎5(UE5) AI系统的一个功能,可将其用于从环境中收集数据。在EQS中,可以通过不同种类的测试向收集的数据提问,这些测试会根据提出问题的类型来生成最适合的项目。
可以从行为树中调用EQS查询,并根据测试的结果将其用于后续操作的决定。EQS查询主要由生成器节点(用于生成将被测试及加权的位置或Actor)和情境节点(被用作各种测试和生成器引用的框架)组成。可以用EQS查询指引AI角色找到能够发现玩家并发起攻击的最佳位置、找到距离最近的体力值或弹药拾取物,或找到最近的掩体(以及其他可进行的动作)。
使用场景查询系统对目标位置过滤,找出合适的位置。
重新分组 AIController BehaviorTree Services Tasks EQS
寻找远程攻击位置
右键-人工智能-场景查询-EQ_FindRangedAttackPosition
Content/Blueprints/AI/EQS/EQ_FindRangedAttackPosition.uasset
专门用来测试场景查询
E:/Unreal Projects 532/Aura/Content/Blueprints/AI/EQS/BP_EQSTestingPawn.uasset
Content/Maps/EQS_TestingMap.umap
拖入 BP_EQSTestingPawn
BP_EQSTestingPawn-EQS-查询模板-EQ_FindRangedAttackPosition
打开 EQ_FindRangedAttackPosition 查询利用生成器生成一堆actor或位置的项目,并且我们可以在我们的环境中使用这些位置, 根据我们设置的任何规则进行查询。
围绕查询目标pawn 在其周围生成点 pawn 通常为运行此场景查询的pawn。
网格半大小-500 之间的空间-100
场景中选中 BP_EQSTestingPawn, 可以看到 在半径1000的正方体范围内,每隔100单位生成一个点
场景中选中 BP_EQSTestingPawn,
测试将获取这些点,对其评估。
打开 EQ_FindRangedAttackPosition points:pathing-右键-添加测试-trace trace将跟踪每个点到选择的某个目的地,默认情况下,它会跟踪可见性通道。 测试目的 决定了我们对每个点的处理方式。 如果我们追踪某个目标,我们能否击中该目标,或者是否有什么东西阻碍了我们? 可以选择过滤掉所有无法追踪到给定目标或相反目标的点。
测试目的-仅过滤
Content/Blueprints/AI/EQS/EQS_PlayerContext.uasset
需要这个场景查询情景能够准确地确定我们应该能够追踪并尝试达到的目标。
打开 EQS_PlayerContext 重载 提供Actor集 provide actors set 返回Actor对象引用的数组
get all actors of class get all actors of class-actor class-BP_AuraCharacter 这将获取 BP_AuraCharacter 类的所有角色
打开 EQ_FindRangedAttackPosition pathing grid 的 trace 测试 检测-情景-EQS_PlayerContext 将追踪当前玩家的情景可见性
即,这些点如果可以看到 玩家,则为蓝色,否则红色。
将 BP_EQSTestingPawn 上移超过地板。防止被地板挡住。
默认地图没有玩家 BP_AuraCharacter 红色
地图拖入 玩家 BP_AuraCharacter 蓝色
pathing grid 的 trace 测试 布尔匹配 这个布尔值来确定是否要过滤掉或保留基于此跟踪的点。 赋予“ScoringFActo分数所需要匹配的布尔值。如不匹配此值,分数将不会发生改变
如果不启用,即时关卡有 BP_AuraCharacter,所有点也是红色。
pathing grid 的 trace 测试 布尔匹配-不启用
关卡拖入障碍物 看不到玩家的点为蓝色,看得到玩家的点为红色。
布尔匹配-启用 看不到玩家的点为红色,看得到玩家的点为蓝色。
我们只想要可以看到玩家的点,可以畅通无阻地追踪到玩家的点,所以 布尔匹配-不启用
需要为障碍物添加 打开障碍物网格体SM_Block2x2x1
显示简单碰撞 添加盒体简单碰撞
NavMeshBoundsVolume 放大至包裹整个关卡
选择 NavMeshBoundsVolume 按P键显示导航网格
障碍物已经不再导航网格内。
保存 EQ_FindRangedAttackPosition 使其重新计算。 现在障碍物内部不再生成点。
路径与寻路有关,而寻路与人工智能有关,人工智能使用导航网格边界体积。 因此,路径网格不会给我们任何不可导航的点,这使人工智能角色无法继续行走。
测试出最好的点
打开 EQ_FindRangedAttackPosition
pathing grid 节点右键-添加测试-distance 距离允许我们计算每个点到查询或我们的查询中的距离. case 是测试池或某些查询上下文。 测试目的-仅得分 默认:得分是每个点到我们在这里设置的情景的距离
距离-到此距离-EQS_PlayerContext 得分因数- -1【负一】 距离玩家越近,得分越高。
得分因数- 1 【正一】 距离玩家越远,得分越高。
距离-到此距离-EnwQueryContext_Querier EnwQueryContext_Querier 表示 场景查询情景的所有点的中心。 这使游侠呆在场景查询情景的所有点的中心
得分因数- -1【负一】 这使游侠不会被障碍物挡住。
打开 BT_EnemyBehaviorTree
1 RangedAttacker 远程攻击节点-拖出 run EQS query run EQS query-EQS-EQS请求-查询模板-EQ_FindRangedAttackPosition 场景查询 节点名称-获取攻击位置 黑板-黑板键-MoveToLocation 运行查询后,它将设置我们指定的黑板键之一 MoveToLocation 的值为 EQ_FindRangedAttackPosition 场景查询 查询过滤后的点的位置之一。
2 RangedAttacker 拖出 move to
移动到该位置
move to-黑板-黑板键-MoveToLocation 节点名称-到达攻击位置
3 RangedAttacker 拖出 BTT attack
4 RangedAttacker 拖出 wait 等待时间-1 随即偏差-0.5
回到主关卡
远程攻击敌人将始终在远处准备攻击,并且视线不会被障碍物挡住。
Source/Aura/Private/Player/AuraPlayerController.cpp
void AAuraPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)
{
// 如果释放的不是鼠标左键,而是其他键,则激活释放对应键的技能
// 此时一定不是自动奔跑或鼠标释放左键技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())GetASC()->AbilityInputTagReleased(InputTag);
return;
}
// 如果释放的是鼠标左键,且跟踪到敌人,则执行释放鼠标左键的技能
if (GetASC()) GetASC()->AbilityInputTagReleased(InputTag);
// 如果是鼠标左键释放,并且鼠标没有跟踪到敌人目标,并且没有按下shift ,则自动奔跑至目的地
if (!bTargeting && !bShiftKeyDown)
{
const APawn* ControlledPawn = GetPawn();
// 如果鼠标跟随时间小于短按阙值,表示是短按
if (FollowTime <= ShortPressThreshold && ControlledPawn)
{
// 通过受控pawn位置和目的地位置,同步查找位置路径,生成导航路径
if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(
this, ControlledPawn->GetActorLocation(), CachedDestination))
{
//清除样条线的点
Spline->ClearSplinePoints();
// 循环导航路径的点
for (const FVector& PointLoc : NavPath->PathPoints)
{
//向样条曲线添加点
Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
//DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Green, false, 5.f);
}
// 修复玩家自动寻路错误
if (NavPath->PathPoints.Num() > 0)
{
// 减去目的地路径导航点的最后一个点,防止目标点为障碍物中心时,玩家永远无法到达而不停奔跑
CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
// 正在自动奔跑为真
bAutoRunning = true;
}
}
}
//自动奔跑时重置跟随时间
FollowTime = 0.f;
// 自动奔跑时没有跟踪敌人
bTargeting = false;
}
}
选中障碍物-碰撞
Source/Aura/Public/AbilitySystem/Abilities/AuraMeleeAttack.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraDamageGameplayAbility.h"
#include "AuraMeleeAttack.generated.h"
UCLASS()
class AURA_API UAuraMeleeAttack : public UAuraDamageGameplayAbility
{
GENERATED_BODY()
};
Source/Aura/Private/AbilitySystem/Abilities/AuraMeleeAttack.cpp
#include "AbilitySystem/Abilities/AuraMeleeAttack.h"
Source/Aura/Public/AuraGameplayTags.h
public:
// 技能攻击父标签
FGameplayTag Abilities_Attack;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
/*
* Abilities 技能攻击
*/
GameplayTags.Abilities_Attack = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Attack"),
FString("Attack Ability Tag")
);
}
Source/Aura/Public/AbilitySystem/Data/CharacterClassInfo.h
// 包含每个职业的所有信息的结构
USTRUCT(BlueprintType)
struct FCharacterClassDefaultInfo
{
GENERATED_BODY()
// 一个游戏效果来应用到主要属性。
// 一个能够存储新游戏效果的子类
UPROPERTY(EditDefaultsOnly, Category = "Class Defaults")
TSubclassOf<UGameplayEffect> PrimaryAttributes;
// 每种职业可以具由一些独有的默认技能
// 默认技能不一定立即赋予
UPROPERTY(EditDefaultsOnly, Category = "Class Defaults")
TArray<TSubclassOf<UGameplayAbility>> StartupAbilities;
};
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
// 赋予初始技能组
// 使用职业类枚举
UFUNCTION(BlueprintCallable, Category="AuraAbilitySystemLibrary|CharacterClassDefaults")
static void GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC, ECharacterClass CharacterClass);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
#include "Interaction/CombatInterface.h"
void UAuraAbilitySystemLibrary::GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC,
ECharacterClass CharacterClass)
{
UCharacterClassInfo* CharacterClassInfo = GetCharacterClassInfo(WorldContextObject);
//职业通用技能
if (CharacterClassInfo == nullptr) return;
for (TSubclassOf<UGameplayAbility> AbilityClass : CharacterClassInfo->CommonAbilities)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
ASC->GiveAbility(AbilitySpec);
}
//职业独有的初始技能
const FCharacterClassDefaultInfo& DefaultInfo = CharacterClassInfo->GetClassDefaultInfo(CharacterClass);
for (TSubclassOf<UGameplayAbility> AbilityClass : DefaultInfo.StartupAbilities)
{
if (ICombatInterface* CombatInterface = Cast<ICombatInterface>(ASC->GetAvatarActor()))
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, CombatInterface->GetPlayerLevel());
ASC->GiveAbility(AbilitySpec);
}
}
}
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::BeginPlay()
{
Super::BeginPlay();
//
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
InitAbilityActorInfo();
// 为敌人添加初始/共享技能 只在服务端执行
// GiveStartupAbilities 内部使用了游戏模式 ,在客户端为空
if (HasAuthority())
{
UAuraAbilitySystemLibrary::GiveStartupAbilities(this, AbilitySystemComponent, CharacterClass);
}
// 为健康条控件绑定控件控制器
// 敌人自身将成为健康条控件的控件控制器
if (UAuraUserWidget* AuraUserWidget = Cast<UAuraUserWidget>(HealthBar->GetUserWidgetObject()))
{
AuraUserWidget->SetWidgetController(this);
}
if (const UAuraAttributeSet* AuraAS = Cast<UAuraAttributeSet>(AttributeSet))
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
// 注册 Effects.HitReact 效果标签 的 NewOrRemoved 新增或移除标签事件
// 参数1-标签
// 参数2-标签事件类型
AbilitySystemComponent->RegisterGameplayTagEvent(FAuraGameplayTags::Get().Effects_HitReact,
EGameplayTagEventType::NewOrRemoved).AddUObject(
this,
&AAuraEnemy::HitReactTagChanged
);
// 广播初始值
OnHealthChanged.Broadcast(AuraAS->GetHealth());
OnMaxHealthChanged.Broadcast(AuraAS->GetMaxHealth());
}
}
Content/Blueprints/AbilitySystem/GameplayAbilities/GA_MeleeAttack.uasset
打开 GA_MeleeAttack
通过技能标签激活技能
细节-tags-ability tag-Abilities.Attack
打开 DA_CharacterClassInfo warrior- Startup Abilities-GA_MeleeAttack
使用攻击技能标签激活技能
打开 BTT_Attack 从 controlled pawn 获取技能系统组件 get ability system component 使用 技能系统组件 尝试通过技能标签激活技能 try activate abilities by tag
需要标签容器 make gameplay tag container from array
新建 gameplay标签 变量 AttackTag 默认值-Abilities.Attack
AttackTag nake array 输出至 make gameplay tag container from array
打开 GA_MeleeAttack 事件图表: get avatar actor from actor info get actor location draw debug sphere end ability
细节-高级-Instancing Policy-Instanced Per Actor 只会实例一次,不会每次执行都实例化。
近战攻击绘制绿色调试球
Content/Assets/Enemies/Goblin/Animations/Spear/AM_Attack_GoblinSpear.uasset
默认通知轨道改为-MotionWarping MotionWarping轨道-右键-添加通知状态-Motion Warping
控制 MotionWarping 通知条的长度,覆盖 攻击开始到结束
选择 MotionWarping 通知-Root Motion Modifier-Warp Target Name-FacingTarget FacingTarget 将用在敌人扭曲运动事件的 add or update warp target from location-warp target name
选择 MotionWarping 通知-Root Motion Modifier-Warp Translation-不启用 只有扭曲旋转
Root Motion Modifier-Rotation Type-Facing 旋转以面对该目标
打开 GA_MeleeAttack play montage and wait play montage and wait-montage to play-AM_Attack_GoblinSpear
蒙太奇完成时结束技能 end ability
打开 BP_EnemyBase 添加 MotionWarping 组件
事件图表
添加事件 -event update facing target
MotionWarping add or update warp target from location
add or update warp target from location-warp target name-FacingTarget
还需要为敌人添加攻击目标用于扭曲旋转面向目标
打开 Attack_Spear 动画序列
根运动-启用根运动-启用
Source/Aura/Public/Character/AuraEnemy.h
public:
// 设置攻击目标 蓝图本机事件
virtual void SetCombatTarget_Implementation(AActor* InCombatTarget) override;
// 获取攻击目标 蓝图本机事件
virtual AActor* GetCombatTarget_Implementation() const override;
// 攻击目标
UPROPERTY(BlueprintReadWrite, Category = "Combat")
TObjectPtr<AActor> CombatTarget;
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::SetCombatTarget_Implementation(AActor* InCombatTarget)
{
CombatTarget = InCombatTarget;
}
AActor* AAuraEnemy::GetCombatTarget_Implementation() const
{
return CombatTarget;
}
Source/Aura/Public/Interaction/EnemyInterface.h
public:
// 设置攻击目标
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
void SetCombatTarget(AActor* InCombatTarget);
// 获取攻击目标
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
AActor* GetCombatTarget() const;
实际目标来自行为树。
打开 BTT_Attack
新增 黑板键选择器 变量 CombatTargetSelector 公开变量
CombatTargetSelector get blackboard value as actor 工具-is valid 攻击目标有效时才激活技能
类-enemy interface-set Combat Target
打开 BT_EnemyBehaviorTree
MeleeAttacker 的 attack 分支-默认- Combat Target Selector-TargetToFollow 节点名称-近战攻击
RangedAttacker 的 BTT_attack 分支-默认- Combat Target Selector-TargetToFollow 节点名称-远程攻击
打开 GA_MeleeAttack
get avatar actor from actor info enemy interface-get combat target 攻击目标应当设置为玩家,用来更新扭曲目标 【可以通过 get object name 测试,这里不需要】 cast to combatInterface update facing target get actor location 面向目标扭曲
现在敌人近战攻击玩家时,始终面向玩家。
使用近战攻击蒙太奇向演员发送游戏事件标签。
将获取攻击插槽位置事件改为蓝图版本
Source/Aura/Public/Interaction/CombatInterface.h
public:
// 技能激活时生成投射物需要变换,位置信息
// 例如武器上的插槽位置
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
FVector GetCombatSocketLocation();
删除 GetCombatSocketLocation 默认实现
Source/Aura/Private/Interaction/CombatInterface.cpp
#include "Interaction/CombatInterface.h"
int32 ICombatInterface::GetPlayerLevel()
{
return 0;
}
Source/Aura/Public/Character/AuraCharacterBase.h
protected:
// 获取用于技能投射物生成的插槽位置
virtual FVector GetCombatSocketLocation_Implementation() override;
Source/Aura/Private/Character/AuraCharacterBase.cpp
// 通过武器插槽名称获取插槽位置
// 提供欸技能投射物生成位置用
FVector AAuraCharacterBase::GetCombatSocketLocation_Implementation()
{
check(Weapon);
return Weapon->GetSocketLocation(WeaponTipSocketName);
}
// 参数:实现该接口的 actor :GetAvatarActorFromActorInfo()
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(GetAvatarActorFromActorInfo());
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
// 参数:实现该接口的 actor :GetAvatarActorFromActorInfo()
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(GetAvatarActorFromActorInfo());
// 设置投射物旋转 方向 直接瞄准敌人
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
// Rotation.Pitch = 0.f; // 由于服务器与客户端的火球运行时间差异 不应将俯仰角归0,如果投射物生成位置高于敌人,投射物将高于敌人运行
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(
GetAvatarActorFromActorInfo());
// 为游戏情景添加技能,源对象,投射物,技能命中结果。
FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
EffectContextHandle.SetAbility(this);
EffectContextHandle.AddSourceObject(Projectile);
TArray<TWeakObjectPtr<AActor>> Actors;
Actors.Add(Projectile);
EffectContextHandle.AddActors(Actors);
FHitResult HitResult;
HitResult.Location = ProjectileTargetLocation;
EffectContextHandle.AddHitResult(HitResult);
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(
DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
// 魔法投射物技能作为伤害型技能,可能拥有多种类型的伤害技能
for (auto& Pair : DamageTypes)
{
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
}
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
不需要从c++访问,所以可以在项目设置中添加 添加 Event.Montage.Attack.Melee
打开 AM_Attack_GoblinSpear 添加 Events 通知轨道 在武器刺出的时间点上添加通知 右键-添加通知-AN_MontageEvent 【自定义的通知】
选择 AN_MontageEvent -动画通知-event tag-Event.Montage.Attack.Melee 用以在游戏中监听
wait gameplay event wait gameplay event-event tag-Event.Montage.Attack.Melee Get Combat Socket Location [基类中自定义蓝图事件] 获取攻击时武器上的插槽位置 ability-get avatar actor from actor info draw debug sphere
监听到攻击事件后在武器插槽位置处画出调试球。
此时,武器插槽位置错误,因为没有设置插槽名称
根据 BP_Goblin_Spear 的 weapon 组件定位到武器 SKM_Spear 打开 SKM_Spear 插槽名-TipSocket
打开 BP_Goblin_Spear
Combat-Weapon Tip Socket Name-TipSocket
此时位置正确
Source/Aura/Public/Interaction/CombatInterface.h
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
bool IsDead() const;
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
AActor* GetAvatar();
Source/Aura/Public/Character/AuraCharacterBase.h
public:
/** Combat Interface */
// 继承 GetHitReactMontage
virtual UAnimMontage* GetHitReactMontage_Implementation() override;
// 只在服务器端执行
virtual void Die() override;
// 获取用于技能投射物生成的插槽位置
virtual FVector GetCombatSocketLocation_Implementation() override;
virtual bool IsDead_Implementation() const override;
virtual AActor* GetAvatar_Implementation() override;
/** end Combat Interface */
protected:
bool bDead = false;
Source/Aura/Public/Character/AuraCharacterBase.h
完整
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AbilitySystemInterface.h"
#include "Interaction/CombatInterface.h"
#include "AuraCharacterBase.generated.h"
class UAbilitySystemComponent;
class UAttributeSet;
class UGameplayEffect;
class UGameplayAbility;
class UAnimMontage;
UCLASS(Abstract)
class AURA_API AAuraCharacterBase : public ACharacter, public IAbilitySystemInterface, public ICombatInterface
{
GENERATED_BODY()
public:
AAuraCharacterBase();
// 获取技能系统组件
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
// 获取属性集
UAttributeSet* GetAttributeSet() const { return AttributeSet; }
// 添加技能
void AddCharacterAbilities();
/** Combat Interface */
// 继承 GetHitReactMontage
virtual UAnimMontage* GetHitReactMontage_Implementation() override;
// 只在服务器端执行
virtual void Die() override;
// 获取用于技能投射物生成的插槽位置
virtual FVector GetCombatSocketLocation_Implementation() override;
virtual bool IsDead_Implementation() const override;
virtual AActor* GetAvatar_Implementation() override;
/** end Combat Interface */
// 处理角色死亡时所有客户端发生的事情
// NetMulticast - 多播RPC
// Reliable -死亡必须可靠复制
// 实现- MulticastHandleDeath_Implementation
UFUNCTION(NetMulticast, Reliable)
virtual void MulticastHandleDeath();
protected:
virtual void BeginPlay() override;
//TObjectPtr 与原始指针相似 但有附加功能:访问跟踪指针,可选的延迟加载资产
UPROPERTY(EditAnywhere,Category="Combat")
TObjectPtr<USkeletalMeshComponent> Weapon;
// 技能系统组件
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
// 为技能投射物提供生成位置信息的武器插槽名称
UPROPERTY(EditAnywhere, Category = "Combat")
FName WeaponTipSocketName;
bool bDead = false;
// 属性集
UPROPERTY()
TObjectPtr<UAttributeSet> AttributeSet;
virtual void InitAbilityActorInfo();
// 主属性默认值
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Attributes")
TSubclassOf<UGameplayEffect> DefaultPrimaryAttributes;
// 次属性默认值
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Attributes")
TSubclassOf<UGameplayEffect> DefaultSecondaryAttributes;
// 重要属性默认值
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Attributes")
TSubclassOf<UGameplayEffect> DefaultVitalAttributes;
// 应用游戏效果
void ApplyEffectToSelf(TSubclassOf<UGameplayEffect> GameplayEffectClass, float Level) const;
virtual void InitializeDefaultAttributes() const;
/* Dissolve Effects 溶解特效*/
void Dissolve();
// 蓝图中实现
// 角色溶解与武器溶解必须是独立的2个时间轴
UFUNCTION(BlueprintImplementableEvent)
void StartDissolveTimeline(UMaterialInstanceDynamic* DynamicMaterialInstance);
UFUNCTION(BlueprintImplementableEvent)
void StartWeaponDissolveTimeline(UMaterialInstanceDynamic* DynamicMaterialInstance);
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TObjectPtr<UMaterialInstance> DissolveMaterialInstance;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TObjectPtr<UMaterialInstance> WeaponDissolveMaterialInstance;
private:
// 这些将是从游戏一开始就应该赋予的技能列表
UPROPERTY(EditAnywhere, Category = "Abilities")
TArray<TSubclassOf<UGameplayAbility>> StartupAbilities;
// 命中相应蒙太奇指针
UPROPERTY(EditAnywhere, Category = "Combat")
TObjectPtr<UAnimMontage> HitReactMontage;
};
Source/Aura/Private/Character/AuraCharacterBase.cpp
// 服务器,客户端执行
void AAuraCharacterBase::MulticastHandleDeath_Implementation()
{
// 启用模拟物理
Weapon->SetSimulatePhysics(true);
// 启用重力确保武器掉落
Weapon->SetEnableGravity(true);
// 启用碰撞,无查询,无重叠 【武器默认无碰撞】
Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 将网格体布娃娃化
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetEnableGravity(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 对世界静态类型阻止,网格体可以阻止其他物体
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
// 交胶囊体禁用碰撞 不会再阻挡其他物体通过
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Dissolve();
bDead = true;
}
bool AAuraCharacterBase::IsDead_Implementation() const
{
return bDead;
}
AActor* AAuraCharacterBase::GetAvatar_Implementation()
{
return this;
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
// 获取攻击武器插槽位置半径内的存活actor
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayMechanics")
static void GetLivePlayersWithinRadius(const UObject* WorldContextObject, TArray<AActor*>& OutOverlappingActors,
const TArray<AActor*>& ActorsToIgnore, float Radius,
const FVector& SphereOrigin);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
void UAuraAbilitySystemLibrary::GetLivePlayersWithinRadius(const UObject* WorldContextObject,
TArray<AActor*>& OutOverlappingActors,
const TArray<AActor*>& ActorsToIgnore, float Radius,
const FVector& SphereOrigin)
{
FCollisionQueryParams SphereParams;
SphereParams.AddIgnoredActors(ActorsToIgnore);
// EGetWorldErrorMode::LogAndReturnNull 无法获取世界时的错误处理模式 记录,返回空指针
if (const UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject,
EGetWorldErrorMode::LogAndReturnNull))
{
// 创建不可见的球体,找到与该球体重叠的物体 传出到 Overlaps
// 参数2:球心
// 参数3:球体旋转
// 参数4:碰撞参数 所有动态类型物体
// 参数5:使用半径制作球体
// 参数6:
TArray<FOverlapResult> Overlaps;
World->OverlapMultiByObjectType(Overlaps, SphereOrigin, FQuat::Identity,
FCollisionObjectQueryParams(
FCollisionObjectQueryParams::InitType::AllDynamicObjects),
FCollisionShape::MakeSphere(Radius), SphereParams);
for (FOverlapResult& Overlap : Overlaps)
{
// 从重叠的结果中获取所有存活 actor
// 是否实现战斗接口,且未死亡
if (Overlap.GetActor()->Implements<UCombatInterface>() && !ICombatInterface::Execute_IsDead(
Overlap.GetActor()))
{
OutOverlappingActors.AddUnique(ICombatInterface::Execute_GetAvatar(Overlap.GetActor()));
}
}
}
}
打开 GA_MeleeAttack Get Live Players Within Radius for each loop 变换-get actor location
现在 ,敌人攻击玩家时,在敌人武器插槽位置半径处,使用不可见球体查询与球体重叠的动态类型actor,获取存活actor,在actor中心渲染调试球体。即在玩家中心绘制球体。敌人也会被重叠查询到。
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
public:
// 造成伤害
UFUNCTION(BlueprintCallable)
void CauseDamage(AActor* TargetActor);
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
#include "AbilitySystem/Abilities/AuraDamageGameplayAbility.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
void UAuraDamageGameplayAbility::CauseDamage(AActor* TargetActor)
{
FGameplayEffectSpecHandle DamageSpecHandle = MakeOutgoingGameplayEffectSpec(DamageEffectClass, 1.f);
for (TTuple<FGameplayTag, FScalableFloat> Pair : DamageTypes)
{
// 为伤害技能标签分配可扩展伤害值
const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(DamageSpecHandle, Pair.Key, ScaledDamage);
}
// 将该伤害效果赋予actor
GetAbilitySystemComponentFromActorInfo()->ApplyGameplayEffectSpecToTarget(
*DamageSpecHandle.Data.Get(), UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor));
}
打开 CT_Damage 添加曲线 Abilities.Melee 1,5 2,7.5 40,50
自动平滑
打开 GA_MeleeAttack 细节-damage effect class-GE_Damage
GE_Damage 具由即时效果,使用执行计算,遍历所有伤害类型的效果。
细节-damage-damage types: key-Damage.Physical value-1.0 , CT_Damage, Abilities.Melee
事件图表: make outgoing gameplay effect spec damage effect class Assign Tag Set by Caller Magnitude Assign Tag Set by Caller Magnitude-data tag-Damage.Physical
以上为蓝图版本的使用伤害效果。此处仅作展示,需要删除这3个节点。 使用C++定义的 CauseDamage 替代。
CauseDamage 对重叠中的存活目标循环使用 造成伤害函数 应用伤害【伤害标签-伤害曲线值】
此时,近战敌人近战攻击玩家,将使玩家健康值减少。
可以改变 敌人的 level 等级,应用不同等级的伤害
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
// 由于 if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute()) 这里仅在服务端执行
void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, float Damage, bool bBlockedHit, bool bCriticalHit) const
{
if (Props.SourceCharacter != Props.TargetCharacter)
{
// 所有玩家控制器均存在于服务器端
// UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)) 如果在服务端执行,则获取的是服务器的玩家控制器0号,非客户端控制器
//if(AAuraPlayerController* PC = Cast<AAuraPlayerController>(UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)))
// 应当使用源角色,即造成伤害的来源角色 来获取玩家控制器
// 来源是玩家 【玩家攻击敌人】
if(AAuraPlayerController* PC = Cast<AAuraPlayerController>(Props.SourceCharacter->Controller))
{
PC->ShowDamageNumber(Damage, Props.TargetCharacter, bBlockedHit, bCriticalHit);
return;
}
// 目标是玩家 【来源是敌人,敌人攻击玩家】
if(AAuraPlayerController* PC = Cast<AAuraPlayerController>(Props.TargetCharacter->Controller))
{
// 如果获取的是服务器的玩家控制器0,将只在服务器上显示提示文本控件
// PC->ShowDamageNumber 通过RPC在服务器执行,
// PC玩家控制器不能使用服务器的控制器0号,那不会存在于客户端。必须使用准确的客户端的控制器
PC->ShowDamageNumber(Damage, Props.TargetCharacter, bBlockedHit, bCriticalHit);
}
}
}
Source/Aura/Private/Character/AuraEnemy.cpp
// NewCount 新标签计数 Effects.HitReact 效果标签 的数量
// 服务端和客户端均调用
void AAuraEnemy::HitReactTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
// 如果标签计数大于0则做出命中响应
bHitReacting = NewCount > 0;
// 做出命中响应时不能移动
GetCharacterMovement()->MaxWalkSpeed = bHitReacting ? 0.f : BaseWalkSpeed;
// AuraAIController 仅在服务器端有效
if (AuraAIController && AuraAIController->GetBlackboardComponent())
{
AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("HitReacting"), bHitReacting);
}
}
添加蒙太奇标签
Source/Aura/Public/AuraGameplayTags.h
public:
// 蒙太奇标签
FGameplayTag Montage_Attack_Weapon;
FGameplayTag Montage_Attack_RightHand;
FGameplayTag Montage_Attack_LeftHand;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
/*
* Montage
*/
GameplayTags.Montage_Attack_Weapon = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Montage.Attack.Weapon"),
FString("Weapon")
);
GameplayTags.Montage_Attack_RightHand = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Montage.Attack.RightHand"),
FString("Right Hand")
);
GameplayTags.Montage_Attack_LeftHand = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Montage.Attack.LeftHand"),
FString("Left Hand")
);
}
标签关联关联蒙太奇。 标签可指代事件,插槽。
Source/Aura/Public/Interaction/CombatInterface.h
#include "GameplayTagContainer.h"
// 一个连接蒙太奇和游戏标签的结构.
USTRUCT(BlueprintType)
struct FTaggedMontage
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
UAnimMontage* Montage = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag MontageTag;
};
public:
// 获取 标记蒙太奇组
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
TArray<FTaggedMontage>
Source/Aura/Public/Character/AuraCharacterBase.h
public:
// 攻击标记蒙太奇组
UPROPERTY(EditAnywhere, Category = "Combat")
TArray<FTaggedMontage> AttackMontages;
// 获取 标记蒙太奇组
virtual TArray<FTaggedMontage> GetAttackMontages_Implementation() override;
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "AuraGameplayTags.h"
TArray<FTaggedMontage> AAuraCharacterBase::GetAttackMontages_Implementation()
{
return AttackMontages;
}
打开 BP_Goblin_Spear combat-Attack Montages-添加一组映射: montage-AM_Attack_GoblinSpear montage tag-Montage.Attack.Weapon
不再硬编码要播放的蒙太奇动画名称
打开 GA_MeleeAttack get attack montages 可以获取 标记蒙太奇组
需要参数 get avatar actor from actor info
随机选择一个蒙太奇 length -1 random integer in range
get(a ref) break TaggedMontage
打开 AM_Attack_GoblinSpear 选择通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.Weapon
技能中将监听该事件标签。【wait gemeplay event】【但是此标签从角色中标记蒙太奇组获取的】 由于角色已设置该蒙太奇与该标签的关联,所以此处标签必须一致。
通过标签找到插槽
例如攻击插槽:武器插槽,左手插槽,右手插槽
Source/Aura/Public/Interaction/CombatInterface.h
public:
// 技能激活时生成投射物需要变换,位置信息
// 例如武器上的插槽位置
// 通过蒙太奇标签获取插槽
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
FVector GetCombatSocketLocation(const FGameplayTag& MontageTag);
Source/Aura/Public/Character/AuraCharacterBase.h
public:
// 获取用于技能投射物生成的插槽位置
virtual FVector GetCombatSocketLocation_Implementation(const FGameplayTag& MontageTag) override;
protected:
// 左手攻击时的插槽
UPROPERTY(EditAnywhere, Category = "Combat")
FName LeftHandSocketName;
// 右手攻击时的插槽
UPROPERTY(EditAnywhere, Category = "Combat")
FName RightHandSocketName;
Source/Aura/Private/Character/AuraCharacterBase.cpp
// 通过蒙太奇标签获取插槽名称
// 通过武器/左手/右手 插槽名称获取插槽位置
// 提供欸技能投射物生成位置用
FVector AAuraCharacterBase::GetCombatSocketLocation_Implementation(const FGameplayTag& MontageTag)
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
if (MontageTag.MatchesTagExact(GameplayTags.Montage_Attack_Weapon) && IsValid(Weapon))
{
return Weapon->GetSocketLocation(WeaponTipSocketName);
}
if (MontageTag.MatchesTagExact(GameplayTags.Montage_Attack_LeftHand))
{
return GetMesh()->GetSocketLocation(LeftHandSocketName);
}
if (MontageTag.MatchesTagExact(GameplayTags.Montage_Attack_RightHand))
{
return GetMesh()->GetSocketLocation(RightHandSocketName);
}
return FVector();
}
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
// 参数1:实现该接口的 actor :GetAvatarActorFromActorInfo()
// 参数2:蒙太奇标签
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
FAuraGameplayTags::Get().Montage_Attack_Weapon);
// 设置投射物旋转 方向 直接瞄准敌人
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
// Rotation.Pitch = 0.f; // 由于服务器与客户端的火球运行时间差异 不应将俯仰角归0,如果投射物生成位置高于敌人,投射物将高于敌人运行
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(
GetAvatarActorFromActorInfo());
// 为游戏情景添加技能,源对象,投射物,技能命中结果。
FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
EffectContextHandle.SetAbility(this);
EffectContextHandle.AddSourceObject(Projectile);
TArray<TWeakObjectPtr<AActor>> Actors;
Actors.Add(Projectile);
EffectContextHandle.AddActors(Actors);
FHitResult HitResult;
HitResult.Location = ProjectileTargetLocation;
EffectContextHandle.AddHitResult(HitResult);
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(
DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
// 魔法投射物技能作为伤害型技能,可能拥有多种类型的伤害技能
for (auto& Pair : DamageTypes)
{
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
}
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
打开GA_MeleeAttack
打开 SK_Ghoul 骨骼
Wrist-L:LeftHandSocket
Wrist-R:RightHandSocket
移动插槽到手部
新建文件夹 Content/Blueprints/Character/Ghoul
Source/Aura/Public/Character/AuraEnemy.h
public:
// 基本步行速度
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat")
float BaseWalkSpeed = 250.f;
右键-动画-混合空间-SK_Ghoul 骨骼
BS_IdleWalk
Content/Assets/Enemies/Ghoul/Animations/BS_IdleWalk.uasset
打开 BS_IdleWalk
Axis settings-水平坐标-名称-Speed 将 Idle 动画拖入0位置 speed-0 none-50
将 Walk 动画拖入100位置 speed-100 none-50
取样平滑-权重速度-4
基于 Assets/Enemies/Ghoul/Animations/HitReact.HitReact'
动画序列制作动画蒙太奇 AM_HitReact_Ghoul
Content/Assets/Enemies/Ghoul/Animations/AM_HitReact_Ghoul.uasset
打开 AM_HitReact_Ghoul
Content/Blueprints/Character/Ghoul/BP_Ghoul.uasset
打开 BP_Ghoul 网格体 mesh 组件-骨骼网格体资产-SKM_Ghoul
调整网格体 面向前方,
选择主组件BP_Ghoul-combat-life span-5 combat-Left Hand Socket name-LeftHandSocket combat-Right Hand Socket name-RightHandSocket hit react montage-AM_HitReact_Ghoul
character class-warrior
attributes 分类下的属性已不再使用,改用职业信息资产中的设置
调整胶囊体组件 半高,半径 适应网格体大小
选择 骨骼 SK_Ghoul
Content/Blueprints/Character/Ghoul/ABP_Ghoul.uasset
资产覆盖编辑器-混合空间播放器-BS_IdleWalk
打开 ABP_Enemy 网格体组件: 细节-动画-动画类-ABP_Ghoul
BaseWalkSpeed-125
角色移动-旋转速率-0,0,150
打开 GA_MeleeAttack
branch end ability
打开 Attack_L,Attack_R 根运动-启用根运动-启用
基于动画序列 E:/Unreal Projects 532/Aura/Content/Assets/Enemies/Ghoul/Animations/Attack_R.uasset
创建动画蒙太奇 AM_Ghoul_Attack_R
Content/Assets/Enemies/Ghoul/Animations/AM_Ghoul_Attack_R.uasset
打开 AM_Ghoul_Attack_R
默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人的右手开始攻击伸出手 - 到 攻击结束 挥舞手结束
选择 MotionWarping 通知-Root Motion Modifier-Warp Target Name-FacingTarget FacingTarget 将用在敌人扭曲运动事件的 add or update warp target from location-warp target name
选择 MotionWarping 通知-Root Motion Modifier-Warp Translation-不启用 只有扭曲旋转
Root Motion Modifier-Rotation Type-Facing 旋转以面对该目标
缩短 Motion Warping 扭曲动画时间
添加通知轨道:Events
右键-添加通知-AN_MontageEvent 【自定义的通知】 在预计击中位置 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.RightHand 用以在游戏中监听
基于动画序列 E:/Unreal Projects 532/Aura/Content/Assets/Enemies/Ghoul/Animations/Attack_L.uasset
创建动画蒙太奇 AM_Ghoul_Attack_L
Content/Assets/Enemies/Ghoul/Animations/AM_Ghoul_Attack_L.uasset
打开 AM_Ghoul_Attack_L 默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人的右手开始攻击伸出手 - 到 攻击结束 挥舞手结束
选择 MotionWarping 通知-Root Motion Modifier-Warp Target Name-FacingTarget FacingTarget 将用在敌人扭曲运动事件的 add or update warp target from location-warp target name
选择 MotionWarping 通知-Root Motion Modifier-Warp Translation-不启用 只有扭曲旋转
Root Motion Modifier-Rotation Type-Facing 旋转以面对该目标
缩短 Motion Warping 扭曲动画时间
添加通知轨道:Events
右键-添加通知-AN_MontageEvent 【自定义的通知】 在预计击中位置 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.LeftHand 用以在游戏中监听
打开 BP_Ghoul attack montages-添加2组 montage-AM_Ghoul_Attack_L montage tag-AM_Ghoul_Attack_L
montage-AM_Ghoul_Attack_R montage tag-AM_Ghoul_Attack_R
打开 GA_MeleeAttack
随机数提升为变量 TaggedMontage 使随机数固定。
删除 break TaggedMontage 从 TaggedMontage 拖出 TaggedMontage
play montage and wait -Stop when Ability Ends-不启用 防止攻击动画意外停止,不再循环
在攻击蒙太奇完成,中断,取消时结束技能。
打开 GA_HitReact
细节-标签-Cancel Abilities with Tag-Ability.Attack 执行此技能时,具有这些标签的技能会取消。 GA_HitReact 技能表示敌人受击,当敌人受击时,敌人如果同时具由一个带 Ability.Attack 标签的技能,那么这个技能将自动结束,不会执行。 敌人攻击时,中途被击打导致受击,则敌人的攻击技能结束。防止出现技能无法结束的情况。 GA_MeleeAttack 技能拥有标签 Ability.Attack
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
// 友军判断
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayMechanics")
static bool IsNotFriend(AActor* FirstActor, AActor* SecondActor);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
bool UAuraAbilitySystemLibrary::IsNotFriend(AActor* FirstActor, AActor* SecondActor)
{
const bool bBothArePlayers = FirstActor->ActorHasTag(FName("Player")) && SecondActor->ActorHasTag(FName("Player"));
const bool bBothAreEnemies = FirstActor->ActorHasTag(FName("Enemy")) && SecondActor->ActorHasTag(FName("Enemy"));
const bool bFriends = bBothArePlayers || bBothAreEnemies;
return !bFriends;
}
打开 GA_MeleeAttack
IsNotFriend get avatar actor from actor info branch
GE_SecondaryAttributes_Enemy 系数改为0.25
复制材质 M_DissolveEffect 自发光颜色和不透明蒙版处的节点
拷贝 M_Ghoul 材质为 M_GhoulDissolve 打开 M_GhoulDissolve 选中主输出节点-细节-材质0混合模式-已遮罩
To Emissive Color 连接到 自发光颜色
To Opacity Mask 连接到 不透明蒙版
Dissolve 值改为 -2 每种网格体都不一样,需要通过材质实例测试得出完全不溶解的值
基于 M_GhoulDissolve 创建材质实例 MI_GhoulDissolve
打开 BP_Ghoul Dissolve Material Instance-MI_GhoulDissolve
打开 BP_EnemyBase 细节-角色移动:回避-使用RVD回避-启用 设置后组件将使用RVO回避。这只在服务器上运行。仅适用与人工智能。 敌人将会互相回避。但不会在旋转时定向。
胶囊体组件 启用碰撞 处理碰撞功能,但忽略技能抛射物。
网格体组件 纯查询 处理常见的世界对象的阻挡,处理与技能抛射物的重叠。
武器 无碰撞 处理阻挡,忽略技能抛射物的重叠。因为武器产生技能抛射物
Sphere 组件 纯查询 处理与 世界物体,玩家敌人的重叠。 其他都忽略。
球体半径-12 过大则会与玩家自身重叠触发事件,伤害自身。 【也可以使用标签判断,新增通道等方式避免重叠事件】
石块网格体不应该和任何物体触发重叠事件,因为这由石块的Sphere组件触发
整理文件夹
基于 AuraProjectileSpell 创建远程攻击技能蓝图 GA_RangedAttack
Content/Blueprints/AbilitySystem/Enemy/Abilities/GA_RangedAttack.uasset
Content/Blueprints/AbilitySystem/Enemy/Abilities/BP_SlingshotRock.uasset
选中石块的 静态网格体 SM_SlingshotRock
打开 BP_SlingshotRock 添加静态网格体组件(BP_SlingshotRock) 名称-RockMesh 这会自动将石块网格体设置为静态网格体组件的-静态网格体
ProjectileMovement 组件: 细节-抛射物-初始速度-1000 最大速度-1000 发射物重力范围-1
打开 GA_RangedAttack
Aura Projectile Spell-Projectile Class-BP_SlingshotRock 【石块抛射物】
Aura Damage Gameplay Ability-Damage Effect Class-GE_Damage
GE_Damage的伤害取决于调用者幅度的设置
需要包含 DamageTypes
示例:
来自此技能基类 AuraProjectileSpell
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
// 魔法投射物技能作为伤害型技能,可能拥有多种类型的伤害技能
for (auto& Pair : DamageTypes)
{
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
}
损害-Damage Types-
输入-Startup Input Tag-
输入-Replicate Input Directly-
打开 CT_Damage 曲线表格
添加 Abilities.Ranged 远程伤害曲线 1,7.5 40,35
全选,自动平滑
打开 GA_RangedAttack
损害-Damage Types-添加一组 gameplay tag-Damage.Physical 表示物理伤害
scaleable float-1, CT_Damage,Abilities.Ranged
基类将遍历 Damage Types 将伤害值赋予当前技能使用的技能效果 GE_Damage
当远程攻击被授予技能 GA_RangedAttack 行为树将根据技能标签激活攻击任务。
行为树通过技能标签激活当前技能
细节-tags-ability tag-Abilities.Attack
行为树的激活技能处示例:
打开DA_CharacterClassInfo ranger-startup abilities-添加一组初始技能 :GA_RangedAttack 远程攻击技能
打开 GA_RangedAttack 远程攻击技能 事件图表: event ActivateAbility get avatar actor from actor info get actor location draw debug sphere
添加手拿弹弓的敌人 BP_Goblin_Slingshot character class -Ranger 游侠职业 游戏敌人在到达攻击范围内停下,开始攻击,绘制调试球。
打开 Attack_Slingshot 根运动-启用根运动-启用
Content/Assets/Enemies/Goblin/Animations/Slingshot/AM_Attack_Goblin_Slingshot.uasset
打开 AM_Attack_Goblin_Slingshot
默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人的拉弹弓开始攻击 - 到 拉弹弓到最大攻击结束
选择 MotionWarping 通知-Root Motion Modifier-Warp Target Name-FacingTarget FacingTarget 将用在敌人扭曲运动事件的 add or update warp target from location-warp target name
选择 MotionWarping 通知-Root Motion Modifier-Warp Translation-不启用 只有扭曲旋转
Root Motion Modifier-Rotation Type-Facing 旋转以面对该目标
添加通知轨道:Events 决定发射弹弓的时机
右键-添加通知-AN_MontageEvent 【自定义的通知】 在预计弹弓发射的位置 【弹弓拉到最长】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.Weapon 用以在游戏中监听 标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇武器上的插槽位置,生成抛射物。 播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.Weapon。 后续通过标签监听此事件。
打开 BP_Goblin_Slingshot
combat-attack Montages -添加一组标签蒙太奇映射 montage-AM_Attack_Goblin_Slingshot montage tag-Montage.Attack.Weapon 这与之前创建的蒙太奇,以及蒙太奇的标签一致
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
#include "Interaction/CombatInterface.h"
protected:
// 从标签蒙太奇组中随机选取一对标签蒙太奇对返回
UFUNCTION(BlueprintPure)
FTaggedMontage GetRandomTaggedMontageFromArray(const TArray<FTaggedMontage>& TaggedMontages) const;
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
FTaggedMontage UAuraDamageGameplayAbility::GetRandomTaggedMontageFromArray(const TArray<FTaggedMontage>& TaggedMontages) const
{
if (TaggedMontages.Num() > 0)
{
const int32 Selection = FMath::RandRange(0, TaggedMontages.Num() - 1);
return TaggedMontages[Selection];
}
return FTaggedMontage();
}
打开 GA_MeleeAttack 事件图表: GetRandomTaggedMontageFromArray
打开 BP_EnemyBase combat-weapon tip socket name-TipSocket
这样,无需在敌人子类中设置插槽名,除非需要在子类中覆盖。 例如 BP_Goblin_Slingshot
打开 SK_Slingshot Pouch 骨骼添加插槽 PouchSocket
打开 BP_Goblin_Slingshot combat-weapon tip socket name-PouchSocket
打开 GA_RangedAttack
事件图表: get avatar actor from actor info get combat target get actor location update facing target get avatar actor from actor info get attack montages GetRandomTaggedMontageFromArray 提升为变量 TaggedMontage play montage and wait break TaggedMontage play montage and wait -Stop when Ability Ends-不启用 防止攻击动画意外停止,不再循环 ability-tasks-wait gameplay event 具由该技能并且已激活的actor 将监听 动画蒙太奇的通知事件 Get Combat Socket Location [基类中自定义蓝图事件] 获取攻击时武器上的插槽位置 [需要 BP_Goblin_Slingshot 设置武器插槽名称 combat-weapon tip socket name] ability-get avatar actor from actor info draw debug sphere 这将在拉弹弓时在弹弓处绘制调试球
打开 GA_RangedAttack 事件图表: spawn projectile
get avatar actor from actor info enemy interface-get combat target get actor location
世界场景设置-游戏模式重载-BP_AuraGameMode 删除场景的角色 拖入 player start 玩家出生点 拖入 BP_Goblin_Slingshot 职业 ranger 现在游侠可以抛射石块伤害玩家
打开 SM_SlingshotRock 打开 石头主材质 M_SlingshotRock 添加节点 : fresnel multiply multiply 向量3 输出至 自发光颜色
打开 BP_SlingshotRock 石块抛射物 开始播放时设置随机旋转量
事件图表: Event beginplay random float in range -500 至 500 提升为变量 YawRotationRate 和 PitchRotationRate 和 RollRotationRate
每一帧添加局部旋转量
Event Tick Rock Mesh add local Rotation delta rotation 分割结构体引脚 YawRotationRate PitchRotationRat RollRotationRate
multiply
防止友军伤害也将阻止PVP功能。
// 技能抛射物不能伤害友军
if (!UAuraAbilitySystemLibrary::IsNotFriend(DamageEffectSpecHandle.Data.Get()->GetContext().GetEffectCauser(),
OtherActor))
{
return;
}
Source/Aura/Private/Actor/AuraProjectile.cpp
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
// GetEffectCauser 效果引发者
// DamageEffectSpecHandle.Data 在客户端上无效
if (DamageEffectSpecHandle.Data.IsValid() && DamageEffectSpecHandle.Data.Get()->GetContext().GetEffectCauser() ==
OtherActor)
{
return;
}
// 技能抛射物不能伤害友军
if (!UAuraAbilitySystemLibrary::IsNotFriend(DamageEffectSpecHandle.Data.Get()->GetContext().GetEffectCauser(),
OtherActor))
{
return;
}
// 命中之后不应多次播放音效
if (!bHit)
{
// 播放音效
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
// 撞击Niagara特效
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
// 此时投射物可能已销毁 循环音效也将一同销毁
if (LoopingSoundComponent) LoopingSoundComponent->Stop();
}
if (HasAuthority())
{
// 只在服务器上应用伤害效果即可
// 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端
// 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
TargetASC->ApplyGameplayEffectSpecToSelf(*DamageEffectSpecHandle.Data.Get());
}
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
else
{
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
bHit = true;
}
}
Content/Blueprints/Character/Goblin_Slingshot/ABP_Slingshot.uasset
打开 ABP_Slingshot
添加状态机 Main 动画-状态机-state machine
进入 状态机 Main 添加 Slingshot_Idle
进入主状态 使用默认插槽运行动画 Main 拖出 slot defaultSlot
使用动画蓝图控制骨骼位置 希望我的骨骼位置能够跟随持有弹弓的妖精的骨骼位置。 需要知道妖精手的变换,位置,旋转。 在妖精的右手上至少有一个插槽。
打开 BP_Goblin_Slingshot 找到 其骨骼 SK_Goblin 打开 SK_Goblin
将右手的Hand-RSocket插槽重命名为 RightHandSocket
打开 动画蓝图 ABP_Slingshot 事件图表:
event blueprint initialize animation try get pawn owner cast to character 变量-角色-get mesh 网格体提升为变量 OwnerMesh
event blueprint update animation OwnerMesh get socket transform 按右手插槽名称获取插槽 get socket transform-in socket name-RightHandSocket 提升为变量 HandSocketTransform 防止每一帧都获取插槽
transform (Modify) Bone
添加 HandSocketTransform 后分割结构体引脚 用来修改弹弓骨骼的位置和旋转
指定要修改的弹弓骨骼 选择 transform (Modify) Bone -细节- 骨骼控制-要修改的骨骼-Pouch 平移-平移模式-替换现有项 平移空间-世界场景空间 【因为事件图表中是在世界空间中变换插槽 get socket transform】
旋转-旋转模式-替换现有项 旋转空间-世界场景空间
缩放-缩放模式-忽略 缩放-缩放引脚-取消选择公开为引脚 用以不公开缩放引脚
透明度-透明度引脚-取消选择公开为引脚 用以不公开透明度引脚
通过主状态变换弹弓骨骼
打开 BP_Goblin_Slingshot Weapon组件-细节-动画-动画模式-使用动画蓝图 -动画-动画类-ABP_Slingshot
这样 弹弓的Pouch骨骼将实时使用敌人右手的RightHandSocket插槽位置
网格体组件-细节-动画-动画模式-使用动画蓝图 -动画-动画类-AM_Attack_Goblin_Slingshot
打开 ABP_Goblin_Slingshot 添加布尔变量 HoldingPouch 表示正在拉紧弹弓准备弹射 默认为true
HoldingPouch 为true时,不修改骨骼姿势,而使用默认空闲动画姿势
主动画: 添加 动画 Slingshot_Idle blend poses by bool HoldingPouch
在 WeaponHandSocket 插槽处添加预览资产 SKM_Slingsshot
新增通知轨道Rock
在释放弹弓处添加通知 ReleaseRock
在抓住弹弓皮带起始处 添加通知 GrabPouch da
Source/Aura/Public/Character/AuraCharacterBase.h
protected:
//TObjectPtr 与原始指针相似 但有附加功能:访问跟踪指针,可选的延迟加载资产
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Combat")
TObjectPtr<USkeletalMeshComponent> Weapon;
E:/Unreal Projects 532/Aura/Content/Assets/Enemies/Goblin/Slingshot/Centered/AM_Slingshot_Attack.uasset
打开 ABP_Goblin_Slingshot 事件图表:
event blueprint initialize animation 在父级事件上也要调用改动画 event blueprint initialize animation-右键-将调用添加到父函数 Parent: blueprint initialize animation 用于初始化在父级也进行
try get pawn owner cast to BP_Goblin_Slingshot combat-get weapon get anim instance cast to ABP_Slingshot 提升为变量 ABP Slingshot 这获取了弹弓武器动画
右键 ReleaseRock 添加 动画通知事件 Event AnimNotify_ReleaseRock ABP_Slingshot ABP_Slingshot 转换为有效get
设置 ABP_Slingshot 的变量 HoldingPouch set HoldingPouch 如果释放石块时不再拉紧弹弓 设置 HoldingPouch 为false
ABP_Slingshot 拉弹弓动画获取太早时获取不到该动画蓝图,此时重新执行获取 ABP_Slingshot 流程
释放石块时,播放弹弓射出石块动画蒙太奇AM_Slingshot_Attack ABP Slingshot montage play montage play-montage to play-AM_Slingshot_Attack
右键 GrabPouch 添加 动画通知事件 Event AnimNotify_GrabPouch ABP_Slingshot ABP_Slingshot 转换为有效get
设置 ABP_Slingshot 的变量 HoldingPouch set HoldingPouch 如果攻击刚开始 拉紧弹弓 设置 HoldingPouch 为true 此时 ABP_Slingshot 动画蓝图一定有效
完整:
此时游戏敌人可以正确播放弹弓投射石块动画。 这个概念可用于任务武器运动动画。
打开 BP_SlingshotRock RockMesh组件-碰撞-碰撞预设-无碰撞 NoCollision
职业 Elementalist 魔法师
Content/Blueprints/Character/Shaman/BP_Shaman.uasset
Content/Blueprints/Character/Shaman/ABP_Shaman.uasset
打开 BP_Shaman 网格体组件-骨骼网格体资产-SKM_Shaman
调整胶囊体组件 半高 52,半径 和位置
调整网格体组件面向前方
weapon 组件 骨骼网格体资产-SKM_ShamanStaff
打开 SK_Shaman 将 左手的 插槽 LeftHandSocket 重命名为 WeaponHandSocket
右键-=动画-混合空间-SK_Shaman BS_IdleWalk
Content/Assets/Enemies/Shaman/Animations/BS_IdleWalk.uasset
打开 BS_IdleWalk
水平坐标名称-Speed
最大幅值-75
拖入 Shaman_Idle 到 0,50 位置
拖入 Shaman_Walk 到 75,50
细节-取样平滑-权重速度-4
打开 ABP_Shaman 资产覆盖编辑器-混合空间播放器-BS_IdleWalk
网格体-动画-动画类-ABP_Shaman
细节-character class -Elementalist 这样魔法师将使用 Elementalist 职业 的属性值和节能初始化自身。
细节 -根运动-启用根运动
Content/Assets/Enemies/Shaman/Animations/AM_Attack_Shaman.uasset
打开 AM_Attack_Shaman
默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人的魔杖抬起开始攻击 - 到 魔杖落下攻击结束 选择 MotionWarping 通知-Root Motion Modifier-Warp Target Name-FacingTarget FacingTarget 将用在敌人扭曲运动事件的 add or update warp target from location-warp target name
选择 MotionWarping 通知-Root Motion Modifier-Warp Translation-不启用 只有扭曲旋转
Root Motion Modifier-Rotation Type-Facing 旋转以面对该目标
添加通知轨道:Events 决定魔杖生成魔法球的时机
右键-添加通知-AN_MontageEvent 【自定义的通知】 在预计弹弓发射的位置 【弹弓拉到最长】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.Weapon 用以在游戏中监听
标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇武器上的插槽位置,生成抛射物。 播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.Weapon。 后续通过标签监听此事件。
打开 BP_Shaman
combat-attack Montages -添加一组标签蒙太奇映射 montage-AM_Attack_Shaman montage tag-Montage.Attack.Weapon 这与之前创建的蒙太奇,以及蒙太奇的标签一致
打开 SKM_ShamanStaff
添加插槽 TipSocket
调整插槽到法杖尖端
Content/Blueprints/AbilitySystem/Enemy/Abilities/GA_EnemyFireBolt.uasset
打开 GA_EnemyFireBolt
投射物类 Projectile calss-BP_FireBolt
damage types-gameplay tag -Damage.Fire scalable float-1, CT_Damage,Abilities.FireBolt
其他设置使用继承来的默认值
现在敌人有了发射火球的技能
打开 DA_CharacterClassInfo Elementalist -startup abilities- GA_EnemyFireBolt
打开 BP_Shaman combat-base walk speed-75
角色移动组件-细节- 角色移动-旋转速率-0,0,200
打开 AM_Attack_Shaman 添加通知轨道 Sound 在火球生成处 添加通知-播放音效- 选中 PlaySound 通知-细节-动画通知-音效-sfx_FireBolt
打开 AM_Attack_Goblin_Slingshot 添加通知轨道 Sound 在石块发射处 添加通知-播放音效- 选中 PlaySound 通知-细节-动画通知-音效-sfx_Slingshot_Fire
行为树检查敌人是否死亡 防止敌人死亡后仍然发射技能
打开 BB_EnemyBlackboard 添加 布尔类型 Dead 黑板键
打开 BT_EnemyBehaviorTree 重命名节点 我是否没有激活 受击技能 我是否有攻击目标
添加blackboard黑板装饰器 名称-我是否存活 我是否存活-黑板-黑板键-Dead 我是否存活-黑板-键查询-未设置
如果死亡,则中止一切行为 我是否存活-流控制-通知观察者-值改变时 我是否存活-流控制-观察器中止-both 死亡时,整个行为树中止。
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::Die()
{
// 设置寿命
SetLifeSpan(LifeSpan);
// 设置黑板键Dead
if (AuraAIController) AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("Dead"), true);
Super::Die();
}
// GetEffectCauser 效果引发者
// DamageEffectSpecHandle.Data 在客户端上无效
if (!DamageEffectSpecHandle.Data.IsValid() || DamageEffectSpecHandle.Data.Get()->GetContext().GetEffectCauser() ==
OtherActor)
{
return;
}
Source/Aura/Private/Actor/AuraProjectile.cpp
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
// GetEffectCauser 效果引发者
// DamageEffectSpecHandle.Data 在客户端上无效
if (!DamageEffectSpecHandle.Data.IsValid() || DamageEffectSpecHandle.Data.Get()->GetContext().GetEffectCauser() ==
OtherActor)
{
return;
}
// 技能抛射物不能伤害友军
if (!UAuraAbilitySystemLibrary::IsNotFriend(DamageEffectSpecHandle.Data.Get()->GetContext().GetEffectCauser(),
OtherActor))
{
return;
}
// 命中之后不应多次播放音效
if (!bHit)
{
// 播放音效
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
// 撞击Niagara特效
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
// 此时投射物可能已销毁 循环音效也将一同销毁
if (LoopingSoundComponent) LoopingSoundComponent->Stop();
}
if (HasAuthority())
{
// 只在服务器上应用伤害效果即可
// 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端
// 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
TargetASC->ApplyGameplayEffectSpecToSelf(*DamageEffectSpecHandle.Data.Get());
}
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
else
{
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
bHit = true;
}
}
打开 sfx_Footsteps_quite VolumeMultiplier-默认-0.05
打开 Run_Spear_fix
默认通知轨道-Sound 添加通知-播放音效 音效-sfx_Footstepssfx_Footsteps_quite
拷贝 sfx_Template_multi 到 E:/Unreal Projects 532/Aura/Content/Assets/Sounds/Enemies/Goblin/Swoosh/sfx_Template_multi.uasset
打开 sfx_Template_multi InputArray-细节-默认值- 将 Content/Assets/Sounds/Enemies/Goblin/Swoosh 目录下的音效全部拖入
VolumeMultiplier-0.5
重命名为 sfx_Swoosh
打开 AM_Attack_GoblinSpear
默认通知轨道-Sounds 添加通知-播放音效 音效-sfx_Swoosh
长矛刺中目标时产生粒子特效和音效 在长毛尖端处生成特效【不太准确的方式】
Source/Aura/Public/Interaction/CombatInterface.h
class UNiagaraSystem;
class UAnimMontage;
public:
// 获取撞击的血液粒子特效
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
UNiagaraSystem* GetBloodEffect();
因为 撞击音效与蒙太奇动画关联
Source/Aura/Public/Interaction/CombatInterface.h
// 一个连接蒙太奇和游戏标签的结构.
USTRUCT(BlueprintType)
struct FTaggedMontage
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
UAnimMontage* Montage = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag MontageTag;
// 撞击音效与蒙太奇动画关联
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
USoundBase* ImpactSound = nullptr;
};
Source/Aura/Public/Character/AuraCharacterBase.h
class UNiagaraSystem;
public:
// 获取血液粒子特效
virtual UNiagaraSystem* GetBloodEffect_Implementation() override;
protected:
// 撞击血液粒子特效
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat")
UNiagaraSystem* BloodEffect;
Source/Aura/Private/Character/AuraCharacterBase.cpp
UNiagaraSystem* AAuraCharacterBase::GetBloodEffect_Implementation()
{
return BloodEffect;
}
打开 BP_AuraCharacter combat-BloodEffect-NS_BloodImpact
打开 BP_EnemyBase combat-BloodEffect-NS_BloodImpact
拷贝 sfx_Template_multi 为 sfx_Swipe
Content/Assets/Sounds/Enemies/Ghoul/Swipe/sfx_Swipe.uasset
打开 sfx_Swipe
选中 InputArray 默认值拖入 /Script/Engine.SoundWave'/Game/Assets/Sounds/Enemies/Ghoul/Swipe/SFX_Swipe_03.SFX_Swipe_03'3个音效 VolumeMultiplier-0.25
长矛刺中目标的音效
打开 BP_Goblin_Spear cpmbat-Acttack Montages ImpactSound -sfx_Swipe
打开 GA_MeleeAttack 新建布尔变量 HasHitTarget 造成伤害时设为true 技能刚激活时设为false HasHitTarget
get combat socket location 提升为变量 CombatSocketLocation
循环刺中 的每个目标时,刺中完成时播放撞击音效 需要已命中目标 HasHitTarget 为true Branch 撞击音效来自TaggedMontage 的标签蒙太奇音效组
play sound at location 音效位置来自武器上生成的检测重叠目标的重叠球的插槽 TaggedMontage 分割结构体引脚 CombatSocketLocation
对每个目标造成伤害时播放血液粒子 get BloodEffect niagara-spawn system at location CombatSocketLocation
敌人攻击玩家时,客户端无法产生击中粒子和音效。 这是由因为技能GA_MeleeAttack在服务器端运行AI行为树产生。 技能只复制到拥有该技能的客户端。 而没有任何玩家可以拥有由人工智能控制的角色。
所以AI控制的技能不会复制到客户端。 技能中的粒子和音效也不会复制到客户端。
技能系统使用gameplay cue 来复制AI控制的粒子和音效到客户端。
https://docs.unrealengine.com/5.3/en-US/BlueprintAPI/Ability/GameplayCue/
游戏性Cue显示: 游戏性Cue 是可通过游戏性技能系统控制的管理装饰效果(例如,粒子或音效)的方法,它可以节约网络资源。游戏性技能和游戏性效果可以触发它们,它们通过四个可在本地或蓝图代码中覆盖的主函数来产生作用:On Active、While Active、Removed及Executed(仅由游戏性效果使用)。所有游戏性Cue必须与"GameplayCue"开头的游戏性标记相关联,例如"GameplayCue.ElectricalSparks"或"GameplayCue.WaterSplash.Big"。
游戏性Cue管理器 执行游戏性Cue。Actor可通过实现IGameplayCueInterface
并具有名称与游戏性Cue标记相匹配的函数来对游戏性Cue作出反应。独立的 游戏性Cue通知 蓝图也可以对游戏性Cue作出反应。
游戏性Cue显示 有多种类型
GameplayCueNotify_Static 不会实例化
Content/Blueprints/AbilitySystem/Enemy/Cues/GC_MeleeImpact.uasset
打开 GC_MeleeImpact 指定执行cue发生什么 重载函数 on execute 同时调用父函数,类似 super
拆分 参数parameters
执行带有参数的游戏性cue显示,我们就可以访问这些参数。 当从游戏效果执行此游戏性cue显示时,其中一些会自动填充. 但手动执行这个游戏性cue显示,就必须手动填写这些参数。
现在想做的是近战攻击时播放声音并产生粒子。
play sound at location play sound at location-Sound-sfx_Swipe
niagara-spawn system at location spawn system at location- system template-NS_BloodImpact CombatSocketLocation
游戏性Cue显示标签 必须与"GameplayCue"开头的游戏性标记相关联,且GameplayCue必须是顶层标签。
项目设置中添加标签 GameplayCue.MeleeImpact
类默认值-gameplay cue-gameplay cue tag-GameplayCue.MeleeImpact
打开 GA_MeleeAttack
execute gameplayCueWithParams on owner execute gameplayCueWithParams on owner-gameplay cue tag-GameplayCue.MeleeImpact
make Gameplay Cue Parameters CombatSocketLocation
现在,服务端和客户端都可以显示硬编码的粒子和音效。 这些效果都可以复制到客户端。
将战斗目标作为来源参数传入 Cue。 cue 中就可以通过来源参数获取血液粒子。
get combat target ability-get avatar actor from actor info
打开 GC_MeleeImpact get blood effect 现在血液粒子不再硬编码。
复制 NS_BloodImpact 为 NS_BloodImpact_green 打开 NS_BloodImpact_green Initialize particle-color-绿色
打开 BP_AuraCharacter combat - blood effect-NS_BloodImpact_green 换回 NS_BloodImpact
打开 GA_MeleeAttack 删除原来的额播放音效节点和生成粒子节点
通过标签从标签蒙太奇音效组中获取音效 TaggedMontage break TaggedMontage make gameplay tag container from tag
打开 GC_MeleeImpact 循环 aggregated source tags 标签 for each loop get debug string from gameplay tag container print string
蒙太奇标签组需要通过插槽添加不同类型的标签。区分蒙太奇标签和音效标签。
将
FGameplayTag Montage_Attack_Weapon;
FGameplayTag Montage_Attack_RightHand;
FGameplayTag Montage_Attack_LeftHand;
重命名为
FGameplayTag CombatSocket_Weapon;
FGameplayTag CombatSocket_RightHand;
FGameplayTag CombatSocket_LeftHand;
Source/Aura/Public/AuraGameplayTags.h
public:
// 蒙太奇标签
// 用来识别战斗插槽位置,与蒙太奇关联,提供给技能投射物生成位置用
// 用以生成抛射物或直接生成攻击目标重叠检测球
FGameplayTag CombatSocket_Weapon;
FGameplayTag CombatSocket_RightHand;
FGameplayTag CombatSocket_LeftHand;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
/*
* Montage
*/
GameplayTags.CombatSocket_Weapon = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("CombatSocket.Weapon"),
FString("Weapon")
);
GameplayTags.CombatSocket_RightHand = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("CombatSocket.RightHand"),
FString("Right Hand")
);
GameplayTags.CombatSocket_LeftHand = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("CombatSocket.LeftHand"),
FString("Left Hand")
);
}
Source/Aura/Private/Character/AuraCharacterBase.cpp
// 通过蒙太奇标签获取插槽名称
// 通过武器/左手/右手 插槽名称获取插槽位置
// 提供给技能投射物生成位置用
FVector AAuraCharacterBase::GetCombatSocketLocation_Implementation(const FGameplayTag& MontageTag)
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
if (MontageTag.MatchesTagExact(GameplayTags.CombatSocket_Weapon) && IsValid(Weapon))
{
return Weapon->GetSocketLocation(WeaponTipSocketName);
}
if (MontageTag.MatchesTagExact(GameplayTags.CombatSocket_LeftHand))
{
return GetMesh()->GetSocketLocation(LeftHandSocketName);
}
if (MontageTag.MatchesTagExact(GameplayTags.CombatSocket_RightHand))
{
return GetMesh()->GetSocketLocation(RightHandSocketName);
}
return FVector();
}
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家或敌人武器上的插座的位置
// 参数1:实现该接口的 actor :GetAvatarActorFromActorInfo()
// 参数2:蒙太奇标签
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
FAuraGameplayTags::Get().CombatSocket_Weapon);
// 设置投射物旋转 方向 直接瞄准敌人
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
// Rotation.Pitch = 0.f; // 由于服务器与客户端的火球运行时间差异 不应将俯仰角归0,如果投射物生成位置高于敌人,投射物将高于敌人运行
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(
GetAvatarActorFromActorInfo());
// 为游戏情景添加技能,源对象,投射物,技能命中结果。
FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
EffectContextHandle.SetAbility(this);
EffectContextHandle.AddSourceObject(Projectile);
TArray<TWeakObjectPtr<AActor>> Actors;
Actors.Add(Projectile);
EffectContextHandle.AddActors(Actors);
FHitResult HitResult;
HitResult.Location = ProjectileTargetLocation;
EffectContextHandle.AddHitResult(HitResult);
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(
DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
// 魔法投射物技能作为伤害型技能,可能拥有多种类型的伤害技能
for (auto& Pair : DamageTypes)
{
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
}
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
Source/Aura/Public/AuraGameplayTags.h
public:
// 用来识别蒙太奇的蒙太奇标签,攻击蒙太奇标签
FGameplayTag Montage_Attack_1;
FGameplayTag Montage_Attack_2;
FGameplayTag Montage_Attack_3;
FGameplayTag Montage_Attack_4;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
/*
* Montage Tags
*/
GameplayTags.Montage_Attack_1 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Montage.Attack.1"),
FString("Attack 1")
);
GameplayTags.Montage_Attack_2 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Montage.Attack.2"),
FString("Attack 2")
);
GameplayTags.Montage_Attack_3 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Montage.Attack.3"),
FString("Attack 3")
);
GameplayTags.Montage_Attack_4 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Montage.Attack.4"),
FString("Attack 4")
);
}
Source/Aura/Public/Interaction/CombatInterface.h
// 一个连接蒙太奇和游戏标签的结构.
USTRUCT(BlueprintType)
struct FTaggedMontage
{
GENERATED_BODY()
// 蒙太奇动画
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
UAnimMontage* Montage = nullptr;
// 蒙太奇战斗标签
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag MontageTag;
// 战斗插槽位置
// 例如用来生成技能抛射物的位置
// 用以生成抛射物或直接生成攻击目标重叠检测球
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag SocketTag;
// 撞击音效与蒙太奇动画关联
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
USoundBase* ImpactSound = nullptr;
};
用来监听蒙太奇战斗事件通知 和 用作生成抛射物用的位置 打开GA_MeleeAttack
combat-attack montages-
左手攻击的 蒙太奇,蒙太奇关联的蒙太奇攻击标签,战斗插槽位置标签 【用以生成抛射物或直接生成攻击目标重叠检测球】 montage-AM_Ghoul_Attack_L montage tag-Montage.Attack.1 Socket tag-CombatSocket.LeftHand
右手攻击 montage-AM_Ghoul_Attack_R montage tag-Montage.Attack.2 Socket tag-CombatSocket.RightHand
combat-attack montages-
montage-AM_Attack_Goblin_Slingshot montage tag-Montage.Attack.1 Socket tag-CombatSocket.Weapon
combat-attack montages-
montage-AM_Attack_GoblinSpear montage tag-Montage.Attack.1 Socket tag-CombatSocket.Weapon Impact Sound-sfx_Swipe
combat-attack montages-
montage-AM_Attack_Shaman montage tag-Montage.Attack.1 Socket tag-CombatSocket.Weapon
打开 GA_MeleeAttack 蒙太奇标签组break taggedMontage 的 montage tag 输出至 wait gamepley event 的 event tag
选中自定义事件通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.1
选中自定义事件通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.2
选中自定义事件通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.1
选中自定义事件通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.1
选中自定义事件通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.1
Source/Aura/Public/Interaction/CombatInterface.h
public:
// 获取指定的蒙太奇标签组
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
FTaggedMontage GetTaggedMontageByTag(const FGameplayTag& MontageTag);
Source/Aura/Public/Character/AuraCharacterBase.h
public:
// 获取指定的蒙太奇标签组
virtual FTaggedMontage GetTaggedMontageByTag_Implementation(const FGameplayTag& MontageTag) override;
Source/Aura/Private/Character/AuraCharacterBase.cpp
FTaggedMontage AAuraCharacterBase::GetTaggedMontageByTag_Implementation(const FGameplayTag& MontageTag)
{
for (FTaggedMontage TaggedMontage : AttackMontages)
{
if (TaggedMontage.MontageTag == MontageTag)
{
return TaggedMontage;
}
}
return FTaggedMontage();
}
打开GC_MeleeImpact
循环来源获取的标签容器,获取每一个标签. 根据标签获取到每个蒙太奇标签组 从蒙太奇标签组获取到其中的撞击音效 ImpactSound
GetTaggedMontageByTag break gameplay tag container for each loop break taggedMontage
break gameplay cue parameters -effect causer 提升为局部变量-EffectCauser 提升为局部变量【否则报错 该变量只读,无法设置新值】 break gameplay cue parameters -SourceObject 提升为局部变量-SourceObject break gameplay cue parameters -Location 提升为局部变量-Location
Parent:on Execute -return value 提升为局部变量-RetVal
EffectCauser 输出至 play sound at location 的world context object
get blood effect 从效果引起者 EffectCauser 获取血液粒子 EffectCauser 输出至 get blood effect 的 target
EffectCauser 输出至 spawn system at location 的 world context object
现在 服务端和客户端的AI敌人都可以播放撞击音效和血液粒子。
复制 sfx_Template_multi 为 sfx_GoblinHurt
Content/Assets/Sounds/Enemies/Goblin/Hurt/sfx_GoblinHurt.uasset
打开 sfx_GoblinHurt
使用 E:/Unreal Projects 532/Aura/Content/Assets/Sounds/Enemies/Goblin/Hurt/SFX_Goblin_Hurt_03.uasset
3个音效
配置 inputArray
VolumeMultiplier-0.25
默认通知轨道-Sounds 添加通知-播放音效- sfx_GoblinHurt
Source/Aura/Public/Character/AuraCharacterBase.h
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat")
USoundBase* DeathSound;
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "Kismet/GameplayStatics.h"
// 服务器,客户端执行
void AAuraCharacterBase::MulticastHandleDeath_Implementation()
{
UGameplayStatics::PlaySoundAtLocation(this, DeathSound, GetActorLocation(), GetActorRotation());
// 启用模拟物理
Weapon->SetSimulatePhysics(true);
// 启用重力确保武器掉落
Weapon->SetEnableGravity(true);
// 启用碰撞,无查询,无重叠 【武器默认无碰撞】
Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 将网格体布娃娃化
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetEnableGravity(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 对世界静态类型阻止,网格体可以阻止其他物体
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
// 交胶囊体禁用碰撞 不会再阻挡其他物体通过
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Dissolve();
bDead = true;
}
复制 sfx_Template_multi 为 sfx_GoblinDeath
Content/Assets/Sounds/Enemies/Goblin/Death/sfx_GoblinDeath.uasset
打开 sfx_GoblinHurt 使用 同目录下3个音效 配置 inputArray VolumeMultiplier-0.25
打开 BP_Goblin_Spear 细节-combat-Death Sound-sfx_GoblinDeath
默认轨道-Sounds 添加通知-播放音效-sfx_Footsteps_quite
默认轨道-Sounds 添加通知-播放音效-sfx_GoblinHurt
打开 BP_Goblin_Spear 细节-combat-Death Sound-sfx_GoblinDeath
复制 sfx_Template_multi 为 sfx_RockHit
Content/Assets/Sounds/Enemies/Goblin/RockHit/sfx_RockHit.uasset
打开 sfx_RockHit 使用 同目录下3个音效 配置 inputArray VolumeMultiplier-0.08
BP_SlingshotRock impact sound-sfx_RockHit
地板对 Projectile 抛射物通道产生重叠响应 地板生成重叠事件
BP_SlingshotRock impact effect-NS_SlingshotImpact
Source/Aura/Private/Actor/AuraProjectile.cpp
void AAuraProjectile::Destroyed()
{
if (!bHit && !HasAuthority())
{
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
// 此时投射物可能已销毁 循环音效也将一同销毁
if (LoopingSoundComponent) LoopingSoundComponent->Stop();
bHit = true;
}
Super::Destroyed();
}
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
// GetEffectCauser 效果引发者
// DamageEffectSpecHandle.Data 在客户端上无效
if (!DamageEffectSpecHandle.Data.IsValid() || DamageEffectSpecHandle.Data.Get()->GetContext().GetEffectCauser() ==
OtherActor)
{
return;
}
// 技能抛射物不能伤害友军
if (!UAuraAbilitySystemLibrary::IsNotFriend(DamageEffectSpecHandle.Data.Get()->GetContext().GetEffectCauser(),
OtherActor))
{
return;
}
// 命中之后不应多次播放音效
if (!bHit)
{
// 播放音效
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
// 撞击Niagara特效
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
// 此时投射物可能已销毁 循环音效也将一同销毁
if (LoopingSoundComponent) LoopingSoundComponent->Stop();
bHit = true;
}
if (HasAuthority())
{
// 只在服务器上应用伤害效果即可
// 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端
// 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
TargetASC->ApplyGameplayEffectSpecToSelf(*DamageEffectSpecHandle.Data.Get());
}
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
else
{
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
bHit = true;
}
}
默认轨道-Sounds 添加通知-播放音效-sfx_Footsteps
Content/Assets/Enemies/Shaman/Animations/AM_Shaman_HitReact.uasset
打开 AM_Shaman_HitReact 默认轨道-Sounds 添加通知-播放音效-sfx_GoblinHurt
打开 BP_Shaman
Hit React montage-AM_Shaman_HitReact
打开 BP_Shaman 细节-combat-Death Sound-sfx_GoblinDeath
默认轨道-Sounds 添加通知-播放音效-sfx_Footsteps
复制 sfx_Template_multi 为 sfx_GhoulAttack
Content/Assets/Sounds/Enemies/Ghoul/Attack/sfx_GhoulAttack.uasset
VolumeMultiplier-0.15
添加轨道-Sounds 添加通知-播放音效- sfx_GhoulAttack
打开 BP_Ghoul impact sound-sfx_Swipe
攻击音效可以早于撞击音效发生。
复制 sfx_Template_multi 为 sfx_GhoulAttack
Content/Assets/Sounds/Enemies/Ghoul/Growl/sfx_GhoulHurt.uasset
VolumeMultiplier-0.18
默认轨道-Sounds 添加通知-播放音效- sfx_GhoulHurt
复制 sfx_Template_multi 为 sfx_GhoulDeath
Content/Assets/Sounds/Enemies/Demon/Death/sfx_GhoulDeath.uasset
与demon共用音效
VolumeMultiplier-0.25
BP_Ghoul death sound-sfx_GhoulDeath
打开 骨骼 SK_Ghoul
在骨骼 Wrist-R 添加插槽 RightTrailSocket 在骨骼 Wrist-L 添加插槽 LeftTrailSocket
插槽 RightTrailSocket ,LeftTrailSocket 移动到指尖
添加轨道 Trail 右键-添加通知状态-定时Niagara效果 覆盖攻击动作时间范围 Niagara系统-NS_CombatTrail 插槽命名-LeftTrailSocket
添加轨道 Trail 右键-添加通知状态-定时Niagara效果 覆盖攻击动作时间范围 Niagara系统-NS_CombatTrail 插槽命名-RightTrailSocket
Content/Blueprints/Character/Demon/BP_Demon.uasset
Content/Blueprints/Character/Demon/ABP_Demon.uasset
BP_Demon-网格体组件-骨骼网格体资产-SKM_Demon 调整位置,面向前方
胶囊体组件-半高-56,半径 26
打开 ABP_Demon 资产覆盖编辑器-混合空间播放器-BS_Demon_IdleRun
打开 BP_Demon 网格体组件-动画-动画模式-使用蓝图动画 动画类-ABP_Demon
打开 SK_Demon 在尾巴末端骨骼 添加插槽 TailSocket
BP_Demon weapon tip socket name-TailSocket
base walk speed-175
Content/Assets/Enemies/Demon/Animations/AM_Attack_Demon_L.uasset
Content/Assets/Enemies/Demon/Animations/AM_Attack_Demon_R.uasset
打开 AM_Attack_Demon_L 默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人的尾巴开始攻击 - 到 尾巴攻击结束
选择 MotionWarping 通知-Root Motion Modifier-Warp Target Name-FacingTarget FacingTarget 将用在敌人扭曲运动事件的 add or update warp target from location-warp target name
选择 MotionWarping 通知-Root Motion Modifier-Warp Translation-不启用 只有扭曲旋转
Root Motion Modifier-Rotation Type-Facing 旋转以面对该目标
添加通知轨道:Events 决定尾巴攻击的时机
右键-添加通知-AN_MontageEvent 【自定义的通知】 在尾巴击中目标的位置 【尾巴旋转范围扇形的最长,敌人完全背对目标时】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.1 用以在游戏中监听 需要调整,使尾巴尖击中目标的位置在目标身体中心位置
标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇尾巴上的插槽位置,生成尾巴重叠虚拟球。 播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.1 后续通过标签监听此事件。
添加通知轨道:Sounds 攻击音效
添加通知-播放音效- sfx_GhoulAttack
打开 AM_Attack_Demon_R 默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人的尾巴开始攻击 - 到 尾巴攻击结束
选择 MotionWarping 通知-Root Motion Modifier-Warp Target Name-FacingTarget FacingTarget 将用在敌人扭曲运动事件的 add or update warp target from location-warp target name
选择 MotionWarping 通知-Root Motion Modifier-Warp Translation-不启用 只有扭曲旋转
Root Motion Modifier-Rotation Type-Facing 旋转以面对该目标
添加通知轨道:Events 决定尾巴攻击的时机
右键-添加通知-AN_MontageEvent 【自定义的通知】 在尾巴击中目标的位置 【尾巴旋转范围扇形的最长,敌人完全背对目标时】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.2 用以在游戏中监听 需要调整,使尾巴尖击中目标的位置在目标身体中心位置
标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇尾巴上的插槽位置,生成尾巴重叠虚拟球。 播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.2 后续通过标签监听此事件。
添加通知轨道:Sounds 攻击音效
添加通知-播放音效- sfx_GhoulAttack
Socket tag:CombatSocket.Weapon 默认由weapon组件用来获取武器上的插槽。 不能用于恶魔的尾巴攻击
Source/Aura/Public/AuraGameplayTags.h
public:
// 用于恶魔的尾巴攻击的插槽
FGameplayTag CombatSocket_Tail;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
GameplayTags.CombatSocket_Tail = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("CombatSocket.Tail"),
FString("Tail")
);
}
Source/Aura/Public/Character/AuraCharacterBase.h
protected:
// 尾巴攻击时的插槽
UPROPERTY(EditAnywhere, Category = "Combat")
FName TailSocketName;
Source/Aura/Private/Character/AuraCharacterBase.cpp
// 通过蒙太奇标签获取插槽名称
// 通过武器/左手/右手/尾巴 插槽名称获取插槽位置
// 提供给技能投射物生成位置用
FVector AAuraCharacterBase::GetCombatSocketLocation_Implementation(const FGameplayTag& MontageTag)
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
if (MontageTag.MatchesTagExact(GameplayTags.CombatSocket_Weapon) && IsValid(Weapon))
{
return Weapon->GetSocketLocation(WeaponTipSocketName);
}
if (MontageTag.MatchesTagExact(GameplayTags.CombatSocket_LeftHand))
{
return GetMesh()->GetSocketLocation(LeftHandSocketName);
}
if (MontageTag.MatchesTagExact(GameplayTags.CombatSocket_RightHand))
{
return GetMesh()->GetSocketLocation(RightHandSocketName);
}
if (MontageTag.MatchesTagExact(GameplayTags.CombatSocket_Tail))
{
return GetMesh()->GetSocketLocation(TailSocketName);
}
return FVector();
}
Tail Socket Name-TailSocket
BP_Demon 添加2组,左旋攻击,右旋攻击 combat-attack montages-
montage-AM_Attack_Demon_L montage tag-Montage.Attack.1 Socket tag-CombatSocket.Tail impact sound-sfx_Swipe
montage-AM_Attack_Demon_R montage tag-Montage.Attack.2 Socket tag-CombatSocket.Tail impact sound-sfx_Swipe
draw debug sphere
character class-warrior
打开 BS_Demon_IdleRun 取样平滑-权重速度-4
打开 Demon_Throw 根运动-启用根运动-启用
AM_Demon_Throw
Content/Assets/Enemies/Demon/Animations/AM_Demon_Throw.uasset
打开 AM_Demon_Throw 默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人投掷动作的开始攻击 - 到 投掷动作攻击结束
选择 MotionWarping 通知-Root Motion Modifier-Warp Target Name-FacingTarget FacingTarget 将用在敌人扭曲运动事件的 add or update warp target from location-warp target name
选择 MotionWarping 通知-Root Motion Modifier-Warp Translation-不启用 只有扭曲旋转
Root Motion Modifier-Rotation Type-Facing 旋转以面对该目标
添加通知轨道:Events 决定扔出石块攻击的时机
右键-添加通知-AN_MontageEvent 【自定义的通知】 在尾巴击中目标的位置 【尾巴旋转范围扇形的最长,敌人完全背对目标时】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.1 用以在游戏中监听
标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇尾巴上的插槽位置,生成尾巴重叠虚拟球。 播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.1 后续通过标签监听此事件。
添加通知轨道:Sounds 攻击音效
添加通知-播放音效- sfx_Swipe
近战 恶魔 BP_Demon 重命名为 BP_Demon_Warrior
Content/Blueprints/Character/Demon/BP_Demon_Warrior.uasset
复制 BP_Demon_Warrior 为 BP_Demon_Ranger
Content/Blueprints/Character/Demon/BP_Demon_Ranger.uasset
直接复制容易出现问题,部分设置不生效。 应当新建。
打开BP_Demon_Ranger character class-ranger
SK_Demon 在骨骼 Wrist-L处添加插槽 LeftHandSocket 调整位置到掌心
BP_Demon_Ranger 删除原标签组 添加新组 combat-attack montages-
montage-AM_Demon_Throw montage tag-Montage.Attack.1 Socket tag-CombatSocket.LeftHand impact sound-sfx_Swipe
Left Hand Socket Name-LeftHandSocket
默认只在 CombatSocket_Weapon 武器位置生成投掷物, 应该从角色指定的插槽位置处生成投掷物。
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家指定的插槽的位置,例如武器,左右手,尾巴尖等插槽
// 参数1:实现该接口的 actor :GetAvatarActorFromActorInfo()
// 参数2:蒙太奇标签
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
SocketTag);
Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h
struct FGameplayTag;
protected:
// 在指定得到插槽处生成投射物 子类蓝图中调用
UFUNCTION(BlueprintCallable, Category = "Projectile")
void SpawnProjectile(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag);
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家指定的插槽的位置,例如武器,左右手,尾巴尖等插槽
// 参数1:实现该接口的 actor :GetAvatarActorFromActorInfo()
// 参数2:蒙太奇标签
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
SocketTag);
// 设置投射物旋转 方向 直接瞄准敌人
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
// Rotation.Pitch = 0.f; // 由于服务器与客户端的火球运行时间差异 不应将俯仰角归0,如果投射物生成位置高于敌人,投射物将高于敌人运行
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(
GetAvatarActorFromActorInfo());
// 为游戏情景添加技能,源对象,投射物,技能命中结果。
FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
EffectContextHandle.SetAbility(this);
EffectContextHandle.AddSourceObject(Projectile);
TArray<TWeakObjectPtr<AActor>> Actors;
Actors.Add(Projectile);
EffectContextHandle.AddActors(Actors);
FHitResult HitResult;
HitResult.Location = ProjectileTargetLocation;
EffectContextHandle.AddHitResult(HitResult);
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(
DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
// 魔法投射物技能作为伤害型技能,可能拥有多种类型的伤害技能
for (auto& Pair : DamageTypes)
{
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
}
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
删除 get combat socket location
break taggedMontage -Socket tag 输出至 spawn projectile-Socket Tag
火球术传入硬编码的插槽即可
make literal gameplay tag make literal gameplay tag-value-CombatSocket.Weapon
打开 BP_Demon_Ranger
材质-Mesh-元素0-M_DemonDark
该技能不生成投掷物,不需要伤害变量。所以不基于 AuraProjectileSpell。 而是基于 AuraGameplayAbility
Source/Aura/Public/AbilitySystem/Abilities/AuraSummonAbility.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraGameplayAbility.h"
#include "AuraSummonAbility.generated.h"
// 召唤技能
UCLASS()
class AURA_API UAuraSummonAbility : public UAuraGameplayAbility
{
GENERATED_BODY()
public:
// 获取生成的小兵的位置的数组
UFUNCTION(BlueprintCallable)
TArray<FVector> GetSpawnLocations();
// 生成小兵的数量
UPROPERTY(EditDefaultsOnly, Category = "Summoning")
int32 NumMinions = 5;
// 生成的小兵的职业种类数组
UPROPERTY(EditDefaultsOnly, Category = "Summoning")
TArray<TSubclassOf<APawn>> MinionClasses;
// 主人和小兵之间的最小距离
UPROPERTY(EditDefaultsOnly, Category = "Summoning")
float MinSpawnDistance = 50.f;
// 主人和小兵之间的最大距离
UPROPERTY(EditDefaultsOnly, Category = "Summoning")
float MaxSpawnDistance = 250.f;
// 生成小兵位于主人前方的角度范围
UPROPERTY(EditDefaultsOnly, Category = "Summoning")
float SpawnSpread = 90.f;
};
Source/Aura/Private/AbilitySystem/Abilities/AuraSummonAbility.cpp
#include "AbilitySystem/Abilities/AuraSummonAbility.h"
#include "NiagaraBakerSettings.h"
#include "Kismet/KismetSystemLibrary.h"
TArray<FVector> UAuraSummonAbility::GetSpawnLocations()
{
// actor 前向向量
const FVector Forward = GetAvatarActorFromActorInfo()->GetActorForwardVector();
// actor 位置
const FVector Location = GetAvatarActorFromActorInfo()->GetActorLocation();
const float DeltaSpread = SpawnSpread / NumMinions;
// 左边界角度:将前向向量 绕Z轴 FVector::UpVector 左旋,左旋角度为生成小兵角度范围的一半
const FVector LeftOfSpread = Forward.RotateAngleAxis(-SpawnSpread / 2.f, FVector::UpVector);
// 从左边界开始生成小兵
TArray<FVector> SpawnLocations;
for (int32 i = 0; i < NumMinions; i++)
{
const FVector Direction = LeftOfSpread.RotateAngleAxis(DeltaSpread * i, FVector::UpVector);
const FVector ChosenSpawnLocation = Location + Direction * FMath::FRandRange(MinSpawnDistance, MaxSpawnDistance);
SpawnLocations.Add(ChosenSpawnLocation);
DrawDebugSphere(GetWorld(), ChosenSpawnLocation, 18.f, 12, FColor::Cyan, false, 3.f );
UKismetSystemLibrary::DrawDebugArrow(GetAvatarActorFromActorInfo(), Location, Location + Direction * MaxSpawnDistance, 4.f, FLinearColor::Green, 3.f );
DrawDebugSphere(GetWorld(), Location + Direction * MinSpawnDistance, 5.f, 12, FColor::Red, false, 3.f );
DrawDebugSphere(GetWorld(), Location + Direction * MaxSpawnDistance, 5.f, 12, FColor::Red, false, 3.f );
}
return SpawnLocations;
}
Content/Blueprints/AbilitySystem/Enemy/Abilities/GA_SummonAbility.uasset
打开 GA_SummonAbility
事件图表: 技能激活时获取召唤物位置数组 event activateAbility GetSpawnLocations
打开 GA_SummonAbility 类默认值-细节 -标签-ability tags-Abilities.Attack
这可以使其与行为树攻击任务一起使用。
打开职业信息数据资产 DA_CharacterClassInfo
Elementalist-startup abilites- 将技能 GA_EnemyFireBolt 火球术替换为 GA_SummonAbility
事件图表: 将 召唤物位置组提升为变量 SpawnLocations
新建整数类型 召唤物位置索引变量 SpawnLocationIndex
branch SpawnLocationIndex less SpawnLocations length draw debug sphere SpawnLocations get(a copy) SpawnLocationIndex ++ delay shuffle
Source/Aura/Private/AbilitySystem/Abilities/AuraSummonAbility.cpp
#include "AbilitySystem/Abilities/AuraSummonAbility.h"
TArray<FVector> UAuraSummonAbility::GetSpawnLocations()
{
// actor 前向向量
const FVector Forward = GetAvatarActorFromActorInfo()->GetActorForwardVector();
// actor 位置
const FVector Location = GetAvatarActorFromActorInfo()->GetActorLocation();
const float DeltaSpread = SpawnSpread / NumMinions;
// 左边界角度:将前向向量 绕Z轴 FVector::UpVector 左旋,左旋角度为生成小兵角度范围的一半
const FVector LeftOfSpread = Forward.RotateAngleAxis(-SpawnSpread / 2.f, FVector::UpVector);
// 从左边界开始生成小兵
TArray<FVector> SpawnLocations;
for (int32 i = 0; i < NumMinions; i++)
{
const FVector Direction = LeftOfSpread.RotateAngleAxis(DeltaSpread * i, FVector::UpVector);
FVector ChosenSpawnLocation = Location + Direction * FMath::FRandRange(MinSpawnDistance, MaxSpawnDistance);
FHitResult Hit;
// 确保召唤物位置在地面上
// 将位置上移后向下跟踪
GetWorld()->LineTraceSingleByChannel(Hit, ChosenSpawnLocation + FVector(0.f, 0.f, 400.f),
ChosenSpawnLocation - FVector(0.f, 0.f, 400.f), ECC_Visibility);
if (Hit.bBlockingHit)
{
ChosenSpawnLocation = Hit.ImpactPoint;
}
SpawnLocations.Add(ChosenSpawnLocation);
}
return SpawnLocations;
}
地板需要简单盒体碰撞
选中部分地板-右键-组 旋转使该部分地板倾斜
时间图表: for each loop spawn system at location spawn system at location-system template-NS_GroundSummon delay
打开 GA_SummonAbility 召唤技能 类默认设置-细节-Minion Classes 添加2组职业 BP_Demon_Ranger BP_Demon_Warrior
Source/Aura/Public/AbilitySystem/Abilities/AuraSummonAbility.h
public:
// 从技能设置的职业中随机选择用以召唤物
UFUNCTION(BlueprintPure, Category="Summoning")
TSubclassOf<APawn> GetRandomMinionClass();
Source/Aura/Private/AbilitySystem/Abilities/AuraSummonAbility.cpp
TSubclassOf<APawn> UAuraSummonAbility::GetRandomMinionClass()
{
const int32 Selection = FMath::RandRange(0, MinionClasses.Num() - 1);
return MinionClasses[Selection];
}
打开 GA_SummonAbility 召唤技能 时间图表: GetRandomMinionClass spawnActor from class spawnActor from class 固定生成,忽略碰撞
提高生成位置,防止陷入地板 add
手动在世界中生成的actor,不会获取到分配给他的控制器,所以直接生成的敌人不会攻击玩家。
分配使用默认控制器 pawn-spawn default controller
Content/Assets/Enemies/Shaman/Animations/AM_Shaman_Summon.uasset
默认轨道-Events
右键-添加通知-AN_MontageEvent 【自定义的通知】 在动作将结束的位置 【施法结束】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.1 用以在游戏中监听
注意:AN_MontageEvent 通知必须放在首帧,在动画开始即刻发出通知。 否则会导致BTT_Attack_Elementalist多次执行萨满召唤技能。知道收到动画通知后才执行递增IncremenetMinionCount。 这会导致瞬间召唤大量AI。
标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。
播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.1 后续通过标签监听此事件。
GA_SummonAbility 事件图表: PlayMontageAndWait PlayMontageAndWait-Montage to play-AM_Shaman_Summon
通过标签 Montage.Attack.1 监听事件 wait gameplay event wait gameplay event-event tag-Montage.Attack.1 监听到事件后继续执行 wait gameplay event-event received
调整召唤物的朝向,与召唤者一致 get avatar actor from actor info 变换-get actor location find look at rotation
Source/Aura/Public/Interaction/CombatInterface.h
public:
// 获取召唤物数量
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
int32 GetMinionCount();
Source/Aura/Public/AuraGameplayTags.h
public:
// 召唤类型的技能标签
FGameplayTag Abilities_Summon;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
GameplayTags.Abilities_Summon = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Summon"),
FString("Summon Ability Tag")
);
}
Source/Aura/Public/Character/AuraCharacterBase.h
public:
// 获取召唤物数量
virtual int32 GetMinionCount_Implementation() override;
protected:
/* Minions 召唤物数量*/
int32 MinionCount = 0;
Source/Aura/Private/Character/AuraCharacterBase.cpp
int32 AAuraCharacterBase::GetMinionCount_Implementation()
{
return MinionCount;
}
表明不是攻击技能,而是召唤技能
GA_SummonAbility 细节- ability tags-Abilities.Summon
之前的 BT_EnemyBehaviorTree 行为树中的 BTT_Attack 任务使用攻击类型标签Abilities.Attack来激活攻击技能, 没有使用 Abilities.Summon 标签 来激活召唤技能。
Content/Blueprints/AI/BehaviorTree/BT_EnemyBehaviorTree_Elementalist.uasset
打开 BP_Shaman AI-Behavior Tree-BT_EnemyBehaviorTree_Elementalist
打开 DA_CharacterClassInfo Elementalist-startup abilites-2个技能 GA_EnemyFireBolt 火球术 GA_SummonAbility 召唤术
Source/Aura/Public/Actor/AuraProjectile.h
protected:
// 投射物球体碰撞组件
// 投射物球体碰撞组件
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<USphereComponent> Sphere;
设置 BP_FireBolt sphere组件默认无碰撞,不检测重叠 打开BP_FireBolt sphere组件-碰撞-碰撞预设-custom 碰撞已启用-无碰撞 对象类型-Projectile
火球发射后短暂延时后再启用查询 时间图表: event beginplay delay Sphere Sphere 右键 转换为有效get 碰撞-set collision enabled set collision enabled-new type-纯查询(无物理碰撞) draw debug sphere get actor location sphere get sphere radius
在小兵低于一定数量时,才激活召唤技能,开始召唤小兵。 双击 BTT_Attack 打开 BTT_Attack 任务
Content/Blueprints/AI/Tasks/BTT_Attack_Elementalist.uasset
打开 BTT_Attack_Elementalist
事件图表: event receive execute AI-controlled pawn 提升为变量 ControlledPawn
添加 gameplay 标签 变量 SummonTag 表示召唤 公开 SummonTag 默认值 -Abilities. Summon
添加 gameplay 标签 变量 AbilityTag
检查条件根据小兵数量后设置 AbilityTag
GetMinionCount ControlledPawn
添加整数变量 MinionSpawnThreshold 小兵数量最低值 默认值 2 表示小兵数量小于2时,激活召唤技能
less MinionSpawnThreshold branch set AbilityTag SummonTag
如果小兵数量不少于2,AbilityTag 设为攻击标签 Attack Tag,表示攻击技能
最终使用AbilityTag的技能标签激活技能
通过传递负数,表示减少数量
Source/Aura/Public/Interaction/CombatInterface.h
public:
// 每次召唤时递增小兵数量
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
void IncremenetMinionCount(int32 Amount);
Source/Aura/Public/Character/AuraCharacterBase.h
public:
// 每次召唤时递增小兵数量
virtual void IncremenetMinionCount_Implementation(int32 Amount) override;
Source/Aura/Private/Character/AuraCharacterBase.cpp
void AAuraCharacterBase::IncremenetMinionCount_Implementation(int32 Amount)
{
MinionCount += Amount;
}
打开GA_SummonAbility 事件图表: IncremenetMinionCount get avatar actor from actor info
RangedAttacker 远程攻击下的 BTT_Attack 替换为 BTT_Attack_Elementalist
BTT_Attack_Elementalist分支-默认- Combat Target Selector-TargetToFollow 节点名称-魔法师攻击
MeleeAttacker 近战攻击下的 BTT_Attack 替换为 BTT_Attack_Elementalist
BTT_Attack_Elementalist分支-默认- Combat Target Selector-TargetToFollow 节点名称-魔法师攻击
打开 GA_SummonAbility 在生成小兵后,监听其销毁事件 事件图表: assign on destroyed [有一点延迟] IncremenetMinionCount
打开 BP_SlingshotRock 在开始时禁用碰撞
sphere组件-碰撞-碰撞预设-custom 碰撞已启用-无碰撞 对象类型-Projectile
事件图表: Sphere【可省略,由以上设置代替】 set collision enabled-无碰撞 【可省略,由以上设置代替】 延迟一秒后启用查询 delay Sphere set collision enabled-纯查询(无物理碰撞)
Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h
protected:
// 在指定得到插槽处生成投射物 子类蓝图中调用
// 指定抛射角度
UFUNCTION(BlueprintCallable, Category = "Projectile")
void SpawnProjectile(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag,
bool bOverridePitch = false, float PitchOverride = 0.f);
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag,
bool bOverridePitch, float PitchOverride)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家指定的插槽的位置,例如武器,左右手,尾巴尖等插槽
// 参数1:实现该接口的 actor :GetAvatarActorFromActorInfo()
// 参数2:蒙太奇标签
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
SocketTag);
// 设置投射物旋转 方向 直接瞄准敌人
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
// Rotation.Pitch = 0.f; // 由于服务器与客户端的火球运行时间差异 不应将俯仰角归0,如果投射物生成位置高于敌人,投射物将高于敌人运行
if (bOverridePitch)
{
Rotation.Pitch = PitchOverride;
}
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(
GetAvatarActorFromActorInfo());
// 为游戏情景添加技能,源对象,投射物,技能命中结果。
FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
EffectContextHandle.SetAbility(this);
EffectContextHandle.AddSourceObject(Projectile);
TArray<TWeakObjectPtr<AActor>> Actors;
Actors.Add(Projectile);
EffectContextHandle.AddActors(Actors);
FHitResult HitResult;
HitResult.Location = ProjectileTargetLocation;
EffectContextHandle.AddHitResult(HitResult);
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(
DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
// 魔法投射物技能作为伤害型技能,可能拥有多种类型的伤害技能
for (auto& Pair : DamageTypes)
{
// 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// 只需要在应用时能够从游戏效果中访问该键值对
const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
}
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
SpawnProjectile-OverridePitch-true SpawnProjectile-PitchOverride-35
召唤物生成时 出现缩放动画。
event beginPlay add timeline 名称 SpawnTimeline
添加浮点型轨道 ScaleTrack
添加关键帧: 0,0 0.1,0
全选2个关键帧-自动平滑
调整过渡角度为山峰曲线,最高点到1.5
添加关键帧: 0.2,0 0.3,0 全选2个关键帧-自动平滑 调整过渡 全选首末之外的所有点,使最低点为1,末点为1
使用时间轴的曲线值设置网格体的缩放。
make vector mesh set relative scale 3d
GA_EnemyFireBolt 火球术默认未设置 火球角度,导致使用了默认值的角度,过高。 需要为 基类 GA_RangedAttack 设置角度默认为false
打开 GA_RangedAttack
spawn projectile -OverridePitch 提升为变量 ShouldOverridePitch 默认为false
spawn projectile -PitchOverride提升为变量PitchOverride
默认 AAuraProjectile::OnSphereOverlap 击中目标重叠时产生的石块和粒子不断累计,越来越多。
wait gameplay event-only trigger once-启用
这样,每次投掷石块蒙太奇动画触发一次事件。 否则一次蒙太奇会触发多次通知事件。
默认情况: 粒子更新-particle state-无限 表示粒子生成后将一直存在
粒子更新-particle state-Kill Particles When Lifetime Has Elapsed 当生命周期已过时系死粒子 -启用 此时无限符号消失。 2处都需要启用。
当前项目属性不是通用的。
// 参数2 :未找到相关属性时是否警告
float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key, false);
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// 根据调用者大小计算伤害值 累加
// DamageTypesToResistances 伤害型标签-抗性属性 组包含所有伤害类型的标签 和与伤害关联的 抗性属性标签
// DamageTypesToResistances 标签只存在于伤害型技能中
// Get Damage Set by Caller Magnitude
float Damage = 0.f;
for (const TTuple<FGameplayTag, FGameplayTag>& Pair : FAuraGameplayTags::Get().DamageTypesToResistances)
{
// 游戏标签对应的的值由 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude 设置过【】
// Pair.Key 为伤害类型技能标签 Pair.Value 为抗性属性标签
// 根据标签 后续可以取出不同的抗性属性 Pair.Value,减少对应类型伤害的值 例如火抗 将 火属性伤害值减少一些
// 取出伤害类型技能的值
// const float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
// 伤害型技能标签
const FGameplayTag DamageTypeTag = Pair.Key;
// 抗性属性标签
const FGameplayTag ResistanceTag = Pair.Value;
checkf(AuraDamageStatics().TagsToCaptureDefs.Contains(ResistanceTag), TEXT("TagsToCaptureDefs doesn't contain Tag: [%s] in ExecCalc_Damage"), *ResistanceTag.ToString());
// 通过属性标签,找到相关联的捕获属性定义,当前只需要抗性捕获定义
// 定义在 TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, ArcaneResistanceDef);
const FGameplayEffectAttributeCaptureDefinition CaptureDef = AuraDamageStatics().TagsToCaptureDefs[ResistanceTag];
// 参数2 :未找到相关属性时是否警告
float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key, false);
// 计算捕获的目标的属性 通过 Resistance 传出
float Resistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(CaptureDef, EvaluationParameters, Resistance);
// 抗性最大抵消100%的伤害
Resistance = FMath::Clamp(Resistance, 0.f, 100.f);
// 每一点抗性抵消1%的伤害
DamageTypeValue *= ( 100.f - Resistance ) / 100.f;
Damage += DamageTypeValue;
}
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
//
FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
//为自定义效果情景设置是否格挡
UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel());
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef,
EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef,
EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef,
EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(
TargetCombatInterface->GetPlayerLevel());
// Critical Hit Resistance reduces Critical Hit Chance by a certain percentage
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance *
CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// 为自定义效果情景设置是否暴击
UAuraAbilitySystemLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
// Double damage plus a bonus if critical hit
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
删除默认地板 向上移动 DirectionalLight SkyAtmosphere SkyLight VolumetricCloud
拖入SM_Tile_3x3_A 地板 location 0,0,0
拖入 SM_Tile_3x3_B地板
所有地板和建筑物添加简单碰撞
捕捉网格启用, 开始复制制作地板
世界场景设置-游戏模式重载-BP_AuraGameMode
添加 玩家出生点
光源-强度-1 lux 光源颜色-蓝紫 使用色温-启用 温度-6400
光源颜色-蓝 光源-强度范围-50
后期处理体积设置-无限范围-启用
渲染功能-后期处理材质-资产引用 PP_Highlight
镜头-exposure-min EV100-0 镜头-exposure-max EV100-0 使光照恒定,不模拟眼睛
颜色分级-temperature- 色温类型-色温 色温-7500 着色-0.1
颜色分级-global-
颜色分级-shadows-
胶片粒度
光源颜色-金黄
它的出现是提示场景中的 Texture 使用的内存超出了为它们分配的额定内存(UE4 默认是1000MB),因此引擎已经开始通过降低纹理的质量以进行补偿。同时你还会看到场景里有些材质现实质量会变得很差,很模糊。
https://docs.unrealengine.com/5.3/zh-CN/texture-streaming-in-unreal-engine/ 纹理流送用于在运行时在内存中加载和卸载纹理的系统。
需要简单碰撞盒
覆盖整个关卡
为障碍物添加碰撞盒可阻止导航体积
打开 纹理 Tileset1_low_DefaultMaterial_BaseColor
压缩-高级-最大纹理尺寸-512 此时纹理会变小
过滤 tile 纹理,批量压缩
DefaultEngine.ini
[/Script/Engine.RenderSettings]
r.TextureStreaming=True
r.Streaming.PoolSize=1000
Content/Blueprints/Actor/FlamePillar/BP_FlamePillar.uasset
BP_FlamePillar
添加 静态网格体 Pillar 作为根组件 使用 SM_Pillar
添加 Niagara :Flame 使用 NS_Fire
添加 PointLight 点光源组件 FireLight 调整位置到火中心 光源颜色-金黄
BP_FlamePillar-actor tick-启用tick并开始-不启用
FireLight-细节-移动性-固定
事件图表: add timeline :FlameIntensityTimeline_1
进入时间轴 长度-5 添加浮点型轨道-FlameIntensity 自动平滑
事件图表: FireLight set Intensity set Intensity-new Intensity 提升为变量-BaseIntensity 默认值5000 ,为灯光的强度 multiply 折叠到函数 ScaleFlameIntensity 输入参数改为 Intensity
add timeline :FlameIntensityTimeline_2
进入时间轴 长度-3
添加浮点型轨道-FlameIntensity 关键帧与第一个不同 自动平滑
使不同的火焰柱子火光变化不一样
添加自定义事件 custom event :StartTimeline_1 添加自定义事件 custom event :StartTimeline_2。 添加自定义事件 custom event :ChooseTimeline
random integer in range switch on int StartTimeline_1 StartTimeline_2 ChooseTimeline
官方 projectile 为阻挡, 实际需要忽略。
所有障碍物碰撞预设为忽略相机 防止相机缩放
Content/Blueprints/Actor/FadeActor/BP_FadeActor.uasset
添加 静态网格体 Mesh
使用 SM_Beacon
打开 材质 M_Beacon_f 选择 主节点 细节-混合模式-已遮罩 已遮罩 比不透明 消耗更少
添加 标量1 转换为参数Fade 默认1 DitherTemporalAA [抖动时间用于溶解纹理] 控制不透明度
基于 M_Beacon_f 创建材质实例 MI_Beacon_f
打开 事件 construction script Mesh get materials 提升为变量 OriginalMaterials for each loop create dynamic material instance 新建 材质动态实例数组类型变量 DynamicMaterialInstance DynamicMaterialInstance add unique
DynamicMaterialInstance clear
event beginplay DynamicMaterialInstance for each loop Mesh set material 折叠到函数 SetMaterialToDynamicInstance
DynamicMaterialInstance for each loop set scalar parameter value set scalar parameter value-parameter name-Fade set scalar parameter value-value-0.5
新建 材质实例数组类型变量 FadeMaterialInstance
打开 事件 construction script FadeMaterialInstance
add timeline: FadeTimeline 添加浮点类型轨道 Fade 0,1 1,0 自动平滑
添加自定义事件 custom event : FadeOut 添加自定义事件 custom event : FadeIn
= branch
OriginalMaterials for each loop Mesh set material 折叠到函数 ResetMaterials
event beginPlay FadeOut delay FadeIn
Mesh set collision response to channel channel 可视性,new response 阻挡
branch <= Mesh set collision response to channel channel 可视性,new response 忽略
折叠到节点 FadeFinished
完整:
Content/Blueprints/Actor/FadeActor/BI_FadeInterface.uasset
添加函数 FadeOut FadeIn
打开 BP_FadeActor 类设置-已实现的接口 BI_FadeInterface
事件图表: 删除 自身的 FadeOut,FadeIn 使用接口的 event FadeOut,event FadeIn
SpringArm 组件下添加 Box 碰撞盒
Box 碰撞盒 要比胶囊体组件更窄
Box 碰撞盒 -盒体范围-217,22,32
Box 碰撞盒-碰撞预设-custom 生成叠事件 碰撞已启用-纯查询 对象类型-WorldDynamic 只重叠 WorldStatic 其他全部忽略
事件图表: on component begin overlap(Box) does implement interface does implement interface-interface-BI_FadeInterface branch FadeOut
on component end overlap(Box) does implement interface does implement interface-interface-BI_FadeInterface branch FadeIn
官方projectile 为阻挡。
删除 FadeFinished 函数的 设置通道节点
BP_FadeActor 的 Mesh 组件 细节-光照-投射阴影-不启用
Content/Blueprints/Actor/FadeActor/FA_Tile_3x3x2.uasset
打开 FA_Tile_3x3x2 Mesh 组件使用原障碍物的网格体即可 配置 Fade Material Instance 属性 为淡入淡出版本得到对应材质实例即可 多个材质实例,顺序要保持一致
替换场景中的障碍物为淡入淡出版本。
打开BP_FadeActor 打开 FadeFinished 函数
Mesh set collision response to channel channel 可视性,new response 阻挡
Mesh set collision response to channel channel 可视性,new response 忽略
branch 添加布尔变量 BlockVisibility branch BlockVisibility branch BlockVisibility
BP_FadeActor-细节-BlockVisibility-启用
mesh组件-碰撞预设-Visibility-阻挡
这样,才可以在点击该淡入淡出版本的网格体时,进行移动导航。
Content/Blueprints/UI/Overlay/Subwidget/WBP_HealthManaSpells.uasset
填充屏幕-自定义 宽:1140 高:216
设计器: 覆层:Overlay_Root
horizontal box 水平框:BaseBox 水平填充,垂直填充
BaseBox 子级:
horizontal box 水平框:HealthBox 插槽-尺寸-填充 0.2
horizontal box 水平框:CentralBox 插槽-尺寸-填充 0.6
horizontal box 水平框:ManaBox 插槽-尺寸-填充 0.2
填充使3个水平框按自定义比例占用父空间
CentralBox 子级: vertical box 垂直框:OffensiveBox 攻击技能 插槽-尺寸-填充 0.9
vertical box 垂直框:PassiveBox 被动技能 插槽-尺寸-填充 0.1
OffensiveBox 子级: vertical box 垂直框:AboveIconBox 技能图标 插槽-尺寸-填充 0.3
horizontal box 水平框:SpellGlobesBox 技能 插槽-尺寸-填充 0.45
horizontal box 水平框:SpaceBox 插槽-尺寸-填充 0.3
AboveIconBox 子级: horizontal box 水平框:OffensiveText 插槽-尺寸-填充 1
horizontal box 水平框:InputText 插槽-尺寸-填充1
HealthBox 子级: WBP Health Globe 插槽-尺寸-填充1
ManaBox 子级: WBP Mana Globe 插槽-尺寸-填充1
OffensiveText 子级: 文本:Text_Offensive 插槽-尺寸-填充1 水平居中对齐 垂直向下对齐 尺寸 14 黄色 轮廓大小-1
PassiveBox 子级: vertical box 垂直框:PassiveText 插槽-尺寸-填充 0.2
vertical box 垂直框:PassiveBoxes 插槽-尺寸-填充 0.8
PassiveText 子级: 文本:Text_Passive 插槽-尺寸-填充1 水平居中对齐 垂直向下对齐 将文本中对齐 尺寸 14 黄色 轮廓大小-1
InputText 子级: vertical box 垂直框: 插槽-尺寸-填充 1
vertical box 垂直框: 插槽-尺寸-填充 1
vertical box 垂直框: 插槽-尺寸-填充 1
vertical box 垂直框: 插槽-尺寸-填充 1
vertical box 垂直框: 插槽-尺寸-填充 1
vertical box 垂直框: 插槽-尺寸-填充 1
vertical box 子级: 文本:Text_LMB 插槽-尺寸-填充1 水平居中对齐 垂直向下对齐 尺寸 14 白色 轮廓大小-1
文本:Text_RMB 文本:Text_1 文本:Text_2 文本:Text_3 文本:Text_4
SpellGlobesBox 子级: vertical box 垂直框:SpellBox_LMB 插槽-尺寸-填充 1
vertical box 垂直框:SpellBox_RMB 插槽-尺寸-填充 1
vertical box 垂直框:SpellBox_1 插槽-尺寸-填充 1
vertical box 垂直框:SpellBox_2 插槽-尺寸-填充 1
vertical box 垂直框:SpellBox_3 插槽-尺寸-填充 1
vertical box 垂直框:SpellBox_4 插槽-尺寸-填充 1
PassiveBoxes 子级: vertical box 垂直框:PassiveBox_1 插槽-尺寸-填充 1
vertical box 垂直框:PassiveBox_2 插槽-尺寸-填充 1
vertical box 垂直框:PassiveBox_Spacer 插槽-尺寸-填充 0.6
完整:
Content/Blueprints/UI/SpellGlobes/WBP_SpellGlobe.uasset
设计器: SizeBox 尺寸框 :SizeBox_Root 填充屏幕-所需 子布局-宽100 高100
图表: 添加变量 BoxWidth,BoxHeight 浮点 分类 GlobelProperties 默认100
event pre construct SizeBox_Root 布局-尺寸框-set width override
SizeBox_Root 布局-尺寸框-set height override
BoxWidth,BoxHeight
折叠导函数 UpdateBoxSize
设计器: SizeBox_Root 子级: 覆层:Overlay_Root
Overlay_Root 子级: 图像:Image_Glass 水平填充,垂直填充
图像:Image_Ring 水平填充,垂直填充
图表: 添加变量 RingBrush 类型 Slate笔刷 分类 GlobelProperties 默认图像 SkillRing_1
Image_Ring set brush RingBrush 折叠导函数 UpdateRingBrush
设计器: Image_Glass 笔刷-图像-MI_EmptyGlobe 插槽-填充-7
图表: Image_Glass slot as overlay slot set padding make margin 添加变量 GlassPadding 分类 GlobelProperties 默认值 7
折叠到函数 UpdateGlobePadding
设计器: Overlay_Root 子级: 图像:Image_Background 水平填充,垂直填充 笔刷-图像-MI_FireSkillBG 插槽-填充-7
图表 UpdateGlobePadding 内:
Image_Background slot as overlay slot set padding
设计器: Overlay_Root 子级: 图像:Image_SpellIcon 水平填充,垂直填充 笔刷-图像-FireBolt 插槽-填充-7
图表 UpdateGlobePadding 内: Image_SpellIcon slot as overlay slot set padding
主图表: 添加变量 SpellIconBrush 类型 Slate笔刷 分类 GlobelProperties 默认图像 FireBolt
Image_SpellIcon set brush SpellIconBrush 折叠到函数 UpdateSpellIconBrush
设计器: Overlay_Root 子级: 文本:Text_Cooldown 水平居中对齐,垂直居中对齐 文本 空 尺寸 24 轮廓大小1
显示冷却倒计时时,图标背景变暗
主图表: Image_Background set brush tint color make slatecolor 灰色 make LinearColor A为1
折叠到函数 SetBackgroundTint 输入:Tint 浮点
添加变量 TransparentBrush 类型 Slate笔刷 分类 GlobelProperties 默认值-着色-A 为0 全透明
添加函数 ClearGlobe ClearGlob打开 Image_SpellIcon Image_Background set brush TransparentBrush
主图表: ClearGlobe
添加函数 SetIconAndBackground
输入1 IconBrush 类型 Slate笔刷 输入2 BackgroundBrush 类型 Slate笔刷
Image_SpellIcon Image_Background set brush set brush
主图表:
SpellBox_LMB 到 SpellBox_4 子类均为 WBP_SpellGlobe 尺寸-填充 1 插槽-填充-左2.5 右 2.5
PassiveBox_1,PassiveBox_2 子类 均为 WBP_SpellGlobe 尺寸-填充 1 插槽-填充-2 WBP Spell Globe-细节-GlobelProperties-Glass Padding-5
事件图表: 删除 健康/魔力进度球控件 部分节点
设计器: 移除 健康,魔力 进度球
添加 WBP_HealthManaSpells 插槽-尺寸x 1140 尺寸y 218 锚点 下居中 插槽-对齐- 0.5,0 插槽-位置X- 0 插槽-位置y- -240
事件图表: WBP_HealthManaSpells set widget controller get widget controller
时间图表 event widget controller set sequence
获取 健康,魔力 控件控制器,设置给控件
WBP_HealthGlobe WBP_ManaGlobe set widget controller get widget controller
关卡中添加 BP_FireArea 测试健康进度控件 此时健康控件正常。
Content/Blueprints/UI/ProgressBar/WBP_XPBar.uasset
设计器: 填充屏幕-自定义 宽度 880 高度 50
覆层:Overlay_Root
Overlay_Root 子级: 图像: 水平填充,垂直填充 笔刷-图像-xp_frame
进度条 progress bar 水平填充,垂直填充 插槽-填充-21 9.5 21 21 样式-背景图-着色-0,0,0,0.5
样式-填充图-图像-xp_bar
进度-百分比-0.5
设计器: WBP_XPBar
插槽-锚点-下中 插槽-尺寸x-880 插槽-尺寸y-50 插槽-对齐-0.5,0 插槽-位置x-0 插槽-位置y- -45
代码编辑器 aura-右键-添加文件
Source/Aura/AuraLogChannels.h
#pragma once
#include "CoreMinimal.h"
#include "Logging/LogMacros.h"
// 声明日志类别 LogAura
DECLARE_LOG_CATEGORY_EXTERN(LogAura, Log, All);
Source/Aura/AuraLogChannels.cpp
#include "AuraLogChannels.h"
// 定义日志类别 LogAura
DEFINE_LOG_CATEGORY(LogAura);
Source/Aura/Private/AbilitySystem/Data/AttributeInfo.cpp
#include "Aura/AuraLogChannels.h"
#include "AbilitySystem/Data/AttributeInfo.h"
#include "Aura/AuraLogChannels.h"
FAuraAttributeInfo UAttributeInfo::FindAttributeInfoForTag(const FGameplayTag& AttributeTag, bool bLogNotFound) const
{
for (const FAuraAttributeInfo& Info : AttributeInformation)
{
if (Info.AttributeTag.MatchesTagExact(AttributeTag))
{
return Info;
}
}
if (bLogNotFound)
{
UE_LOG(LogAura, Error, TEXT("Can't find Info for AttributeTag [%s] on AttributeInfo [%s]."),
*AttributeTag.ToString(), *GetNameSafe(this));
}
return FAuraAttributeInfo();
}
Source/Aura/Public/AbilitySystem/Data/AbilityInfo.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "AbilityInfo.generated.h"
// 技能信息数据数据结构
USTRUCT(BlueprintType)
struct FAuraAbilityInfo
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag AbilityTag = FGameplayTag();
// 输入操作标签
// 不应公开给蓝图,应在代码中设置,通过技能获取,可以运行时改变
UPROPERTY(BlueprintReadOnly)
FGameplayTag InputTag = FGameplayTag();
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<const UTexture2D> Icon = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<const UMaterialInterface> BackgroundMaterial = nullptr;
};
UCLASS()
class AURA_API UAbilityInfo : public UDataAsset
{
GENERATED_BODY()
public:
// 技能信息组
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "AbilityInformation")
TArray<FAuraAbilityInfo> AbilityInformation;
FAuraAbilityInfo FindAbilityInfoForTag(const FGameplayTag& AbilityTag, bool bLogNotFound = false) const;
};
Source/Aura/Private/AbilitySystem/Data/AbilityInfo.cpp
#include "AbilitySystem/Data/AbilityInfo.h"
#include "Aura/AuraLogChannels.h"
FAuraAbilityInfo UAbilityInfo::FindAbilityInfoForTag(const FGameplayTag& AbilityTag, bool bLogNotFound) const
{
for (const FAuraAbilityInfo& Info : AbilityInformation)
{
if (Info.AbilityTag == AbilityTag)
{
return Info;
}
}
if (bLogNotFound)
{
UE_LOG(LogAura, Error, TEXT("Can't find info for AbilityTag [%s] on AbilityInfo [%s]"), *AbilityTag.ToString(), *GetNameSafe(this));
}
return FAuraAbilityInfo();
}
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
class UAbilityInfo;
protected:
// 技能信息数据资产
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Widget Data")
TObjectPtr<UAbilityInfo> AbilityInfo;
右键-其他-数据资产-AbilityInfo
DA_AbilityInfo
Content/Blueprints/AbilitySystem/Data/DA_AbilityInfo.uasset
打开 BP_OverlayWidgetController Ability Info-DA_AbilityInfo
Source/Aura/Public/AuraGameplayTags.h
public:
// 火球术技能标签
FGameplayTag Abilities_Fire_FireBolt;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
GameplayTags.Abilities_Fire_FireBolt = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Fire.FireBolt"),
FString("FireBolt Ability Tag")
);
}
AbilityTag-Abilities.Fire.FireBolt InputTag // 不应公开给蓝图,应在代码中设置,通过技能获取,可以运行时改变 Icon-FireBolt BackgroundMaterial-MI_FireSkillBG
现在控件控制器可以通过技能标签查找技能信息数据,然后广播给控件。
使用委托,使技能系统组件 和 覆层控件控制器的通信,传递技能信息
赋予技能时,广播一个事件
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
// 声明赋予技能委托
DECLARE_MULTICAST_DELEGATE_OneParam(FAbilitiesGiven, UAuraAbilitySystemComponent*);
public:
// 赋予技能委托
FAbilitiesGiven AbilitiesGivenDelegate;
// 是否赋予初始技能
bool bStartupAbilitiesGiven = false;
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
// 添加技能
// 仅在服务端运行,不会复制
void UAuraAbilitySystemComponent::AddCharacterAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupAbilities)
{
for (const TSubclassOf<UGameplayAbility> AbilityClass : StartupAbilities)
{
// 为每个技能类创建一个技能规格 暂时使用技能等级1
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
if (const UAuraGameplayAbility* AuraAbility = Cast<UAuraGameplayAbility>(AbilitySpec.Ability))
{
// 将初始技能输入标签动态加入技能规格的动态技能标签中
// 动态技能标签可在运行时修改
AbilitySpec.DynamicAbilityTags.AddTag(AuraAbility->StartupInputTag);
// 赋予技能
GiveAbility(AbilitySpec);
}
}
// 赋予技能后开始广播赋予技能委托
// 表明初始技能已赋予
// 仅在服务端运行,不会复制
bStartupAbilitiesGiven = true;
AbilitiesGivenDelegate.Broadcast(this);
}
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
class UAuraAbilitySystemComponent;
protected:
// 监听技能系统组件的初始化初始技能委托
void OnInitializeStartupAbilities(UAuraAbilitySystemComponent* AuraAbilitySystemComponent);
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
void UOverlayWidgetController::BindCallbacksToDependencies()
{
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性更改多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnManaChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxManaChanged.Broadcast(Data.NewValue);
}
);
// 资产标签响应函数
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent))
{
// 如果技能系统组件初始化了初始技能,开始在控制器中初始化初始技能,广播给控件
if (AuraASC->bStartupAbilitiesGiven)
{
OnInitializeStartupAbilities(AuraASC);
}
// 否则 监听技能系统组件的赋予技能委托
else
{
AuraASC->AbilitiesGivenDelegate.AddUObject(this, &UOverlayWidgetController::OnInitializeStartupAbilities);
}
// 响应技能系统组件的委托时获取 传入的资产标签容器参数 AssetTags
// 传入this参数,可使用当前类中的属性和方法
AuraASC->EffectAssetTags.AddLambda(
[this](const FGameplayTagContainer& AssetTags)
{
for (const FGameplayTag& Tag : AssetTags)
{
// 查找标签是否包含 Message 字符,是表示消息游戏标签
// For example, say that Tag = Message.HealthPotion
// "Message.HealthPotion".MatchesTag("Message") will return True, "Message".MatchesTag("Message.HealthPotion") will return False
FGameplayTag MessageTag = FGameplayTag::RequestGameplayTag(FName("Message"));
if (Tag.MatchesTag(MessageTag))
{
// 通过行名称/标签名称查找对应的文本消息等数据。
const FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
// 将数据表的一行数据广播
MessageWidgetRowDelegate.Broadcast(*Row);
//之后在控件蓝图时间中绑定覆盖该事件以接受该行数据
}
}
}
);
}
}
void UOverlayWidgetController::OnInitializeStartupAbilities(UAuraAbilitySystemComponent* AuraAbilitySystemComponent)
{
// TODO获取所有给定技能的信息,查找其技能信息,并将其广播到控件。
//TODO Get information about all given abilities, look up their Ability Info, and broadcast it to widgets.
if (!AuraAbilitySystemComponent->bStartupAbilitiesGiven) return;
}
直接循环可激活的所有技能 在技能系统中执行回调,替代在控件控制器中执行回调
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
// 声明循环委托 参数为技能规格
DECLARE_DELEGATE_OneParam(FForEachAbility, const FGameplayAbilitySpec&);
public:
// 循环每个委托
void ForEachAbility(const FForEachAbility& Delegate);
static FGameplayTag GetAbilityTagFromSpec(const FGameplayAbilitySpec& AbilitySpec);
static FGameplayTag GetInputTagFromSpec(const FGameplayAbilitySpec& AbilitySpec);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
#include "Aura/AuraLogChannels.h"
void UAuraAbilitySystemComponent::ForEachAbility(const FForEachAbility& Delegate)
{
// 通过引用系统组件锁定可激活的技能
// 因为技能在运行时可能不再激活,或被标签阻止
FScopedAbilityListLock ActiveScopeLock(*this);
// 遍历所有可激活的技能
for (const FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
// 如果委托绑定了回调,则执行该回调,参数为当前技能规格
// 如果没绑定回调,则执行返回false,记录日志
if (!Delegate.ExecuteIfBound(AbilitySpec))
{
UE_LOG(LogAura, Error, TEXT("Failed to execute delegate in %hs"), __FUNCTION__);
}
}
}
FGameplayTag UAuraAbilitySystemComponent::GetAbilityTagFromSpec(const FGameplayAbilitySpec& AbilitySpec)
{
if (AbilitySpec.Ability)
{
for (FGameplayTag Tag : AbilitySpec.Ability.Get()->AbilityTags)
{
if (Tag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("Abilities"))))
{
return Tag;
}
}
}
return FGameplayTag();
}
FGameplayTag UAuraAbilitySystemComponent::GetInputTagFromSpec(const FGameplayAbilitySpec& AbilitySpec)
{
for (FGameplayTag Tag : AbilitySpec.DynamicAbilityTags)
{
if (Tag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("InputTag"))))
{
return Tag;
}
}
return FGameplayTag();
}
回调函数中为技能信息资产设置输入标签,然后广播技能信息资产
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
struct FAuraAbilityInfo;
//
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAbilityInfoSignature, const FAuraAbilityInfo&, Info);
public:
UPROPERTY(BlueprintAssignable, Category="GAS|Messages")
FAbilityInfoSignature AbilityInfoDelegate;
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
#include "AbilitySystem/Data/AbilityInfo.h"
void UOverlayWidgetController::OnInitializeStartupAbilities(UAuraAbilitySystemComponent* AuraAbilitySystemComponent)
{
// TODO获取所有给定技能的信息,查找其技能信息,并将其广播到控件。
//TODO Get information about all given abilities, look up their Ability Info, and broadcast it to widgets.
if (!AuraAbilitySystemComponent->bStartupAbilitiesGiven) return;
FForEachAbility BroadcastDelegate;
BroadcastDelegate.BindLambda([this, AuraAbilitySystemComponent](const FGameplayAbilitySpec& AbilitySpec)
{
//TODO need a way to figure out the ability tag for a given ability spec.
// 需要一种方法来计算给定技能规范的技能标签。
// 在技能信息资产中根据标签查找指定技能信息
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AuraAbilitySystemComponent->GetAbilityTagFromSpec(AbilitySpec));
// 为技能信息指定输入标签
Info.InputTag = AuraAbilitySystemComponent->GetInputTagFromSpec(AbilitySpec);
// 广播该技能信息 控件中可以监听该广播
AbilityInfoDelegate.Broadcast(Info);
});
// 技能系统组件 为每个技能规格执行该委托绑定的函数
AuraAbilitySystemComponent->ForEachAbility(BroadcastDelegate);
}
覆层控件监听 来自控件控制器的技能信息广播
打开 WBP_HealthManaSpells 设计器: 将各技能栏设置为与 InputTag 一致的名称,并设为变量用以访问
主动技能 SpellGlobe_LMB SpellGlobe_RMB SpellGlobe_1 SpellGlobe_2 SpellGlobe_3 SpellGlobe_4
被动技能 SpellGlobe_Passive_1 SpellGlobe_Passive_2
事件图表: 添加函数 SetSpellGlobeInputTags 为每个技能控件设置输入标签
sequence
SpellGlobe_LMB InputTag.LMB SpellGlobe_RMB InputTag.RMB SpellGlobe_1 InputTag.1 SpellGlobe_2 InputTag.2 SpellGlobe_3 InputTag.3 SpellGlobe_4 InputTag.4
set InputTag
主事件: event pre construct SetSpellGlobeInputTags
打开 WBP_SpellGlobe 施法球控件 事件图表: 设置控件控制器事件 event widget controller set sequence get widget controller cast to BP_OverlayWidgetController 提升为变量 BPOverlayWidgetController
监听控件控制器的 AbilityInfoDelegate 委托,以接收技能信息,用以设置UI BPOverlayWidgetController assign AbilityInfoDelegate break AuraAbilityInfo
获取,检测 被父控件 WBP_HealthManaSpells 设置过的 InputTag 变量 将 InputTag 与监听获取的技能信息中的 InputTag 比较 如果一致,使用对应的技能信息的图表,材质设置该技能球UI InputTag Matches Tag branch SetIconAndBackground make slateBrush
监听事件折叠到函数 ReceiveAbilityInfo
添加函数 SetGlobeWidgetControllers 将自身的控件控制器 设置为各 WBP_SpellGlobe 子控件 的控件控制器
SpellGlobe_LMB
SpellGlobe_RMB
SpellGlobe_1
SpellGlobe_2
SpellGlobe_3
SpellGlobe_4
get widget controller set widget controller
WBP_SpellGlobe 子控件 的输入标签要早于控制器设置,因为 WBP_SpellGlobe 子控件自身在通过标签获取技能信息。 所以 WBP_SpellGlobe 子控件 在预构建事件中设置了输入标签。
主事件: SetGlobeWidgetControllers
打开 GA_FireBolt 标签-ability tags-Abilities.Fire.FireBolt
因为 UAuraAbilitySystemComponent::AddCharacterAbilities 仅在服务端运行,所以客户端无法显示技能信息UI.
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
protected:
// 客户端复制 激活技能事件 包含技能规格
virtual void OnRep_ActivateAbilities() override;
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::OnRep_ActivateAbilities()
{
Super::OnRep_ActivateAbilities();
// 只在第一次复制激活技能时广播
if (!bStartupAbilitiesGiven)
{
bStartupAbilitiesGiven = true;
// 激活技能后在客户端广播赋予的技能信息
AbilitiesGivenDelegate.Broadcast(this);
}
}
现在客户端技能栏也可以获取到技能信息
打开 GA_FireBolt 当前 输入-startup inputt tag-InputTag.LMB 标签-ability tags-Abilities.Fire.FireBolt
表示 InputTag.LMB 左键 触发技能 当前技能 Abilities.Fire.FireBolt 所以技能栏显示为 LMB 显示火球术。
如果把输入标签设置为 InputTag.RMB 输入-startup inputt tag-InputTag.RMB, 则技能栏 RMB显示为火球术,只能用右键触发火球术
灵活的系统。 可以在运行时更改 输入-startup inputt tag 的标签
打开 GA_FireBolt 游戏技能消耗和冷却 通过技能效果实现 costs-cost gameplay effect class-
Content/Blueprints/AbilitySystem/Aura/Abilities/Fire/FireBolt/GE_Cost_FireBolt.uasset
持续时间-Duration Policy-Instant 即时
GameplayEffect -Modifiers: attribute-AuraAttributeSet.Mana 表示释放该火球术技能时,消耗 AuraAttributeSet.Mana 魔力属性的值
Modifier Op-Add
Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifier Magnitude-Scalable Float Magnitude- -20
每次释放火球术,消耗魔力20点。
costs-cost gameplay effect class-GE_Cost_FireBolt
事件图表: avant activateAbility commitAbility commitAbility 返回布尔值 提交技能需要有对应的消耗火冷却资源,否则会失败,导致技能激活也失败。
现在每次施放火球术时,立即消耗20点魔力,如果魔力不足,将无法施放技能。激活技能事件将撤销。
右键-其他-曲线表格-constant
CT_Cost
Content/Blueprints/AbilitySystem/Data/CT_Cost.uasset
默认曲线 Fire.FireBolt
1,20 2,25 3,35 4,50 5,70, 6,90 7,120 8,150 9,180 10,200
GameplayEffect -Modifiers: attribute-AuraAttributeSet.Mana 表示释放该火球术技能时,消耗 AuraAttributeSet.Mana 魔力属性的值
Modifier Op-Add
Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifier Magnitude-Scalable Float Magnitude- -1,CT_Cost,Fire.FireBolt
Source/Aura/Public/AuraGameplayTags.h
public:
// 火球术技能冷却标签
FGameplayTag Cooldown_Fire_FireBolt;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
/*
* Cooldown
*/
GameplayTags.Cooldown_Fire_FireBolt = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Cooldown.Fire.FireBolt"),
FString("FireBolt Cooldown Tag")
);
}
Content/Blueprints/AbilitySystem/Aura/Abilities/Fire/FireBolt/GE_Cooldown_FireBolt.uasset
提交技能时,可以将技能冷却效果 GE_Cooldown_FireBolt 效果应用到技能上。
Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Target Tags Gameplay Effect Component
-Add Tags-Cooldown.Fire.FireBolt
当把GE_Cooldown_FireBolt设置为火球术技能的技能冷却效果时, 游戏效果 GE_Cooldown_FireBolt 将为目标授予Cooldown.Fire.FireBolt 冷却 效果标签。
细节-持续时间-Duration Policy-Has Duration 有持续时间 细节-持续时间-Duration Magnitude-Magnitude calculation Type-scalable float 细节-持续时间-Duration Magnitude-Scalable Float Magnitude-1 【该效果持续1秒,然后自行消失】
打开 GA_FireBolt Cooldowns- Cooldown Gameplay Effect Class-GE_Cooldown_FireBolt
事件图表: 移除delay延时节点
但这会立即结束技能,使客户端在产生火球之前就结束了技能,所以客户端将不会触发技能。
应该在施法蒙太奇动画 的几个事件后结束技能。这确保生成火球。
使技能结束后依然可以继续播放火球术蒙太奇动画。 如果再次激活技能,将重新播放蒙太奇动画。 打开 GA_FireBolt
play montage and wait-Stop when Ability Ends-取消
冷却倒计时
了解冷却的开始时间,持续时间,结束时间,自身运行定时器逻辑。 不需要每一帧都查询技能系统组件。 这需要异步任务。类似播放蒙太奇。
在冷却开始,结束时得到通知。
Source/Aura/Public/AbilitySystem/AsyncTasks/WaitCooldownChange.h
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "GameplayTagContainer.h"
#include "ActiveGameplayEffectHandle.h"
#include "WaitCooldownChange.generated.h"
class UAbilitySystemComponent;
struct FGameplayEffectSpec;
// 广播将成为异步任务的执行引脚
// 广播冷却的剩余持续时间
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCooldownChangeSignature, float, TimeRemaining);
UCLASS()
class AURA_API UWaitCooldownChange : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
// 冷却开始委托 一旦广播将执行该引脚
UPROPERTY(BlueprintAssignable)
FCooldownChangeSignature CooldownStart;
// 冷却结束委托
UPROPERTY(BlueprintAssignable)
FCooldownChangeSignature CooldownEnd;
// 静态函数构造此类的实例
// 参数1-技能系统组件
// 参数2-冷却游戏标签
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UWaitCooldownChange* WaitForCooldownChange(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayTag& InCooldownTag);
// 控件销毁时 清理资源
UFUNCTION(BlueprintCallable)
void EndTask();
protected:
// 存储构造实例时获取的技能系统组件
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> ASC;
// 存储构造实例时获取的冷却技能标签
FGameplayTag CooldownTag;
// 技能系统组件的冷却事件标签的回调
void CooldownTagChanged(const FGameplayTag InCooldownTag, int32 NewCount);
void OnActiveEffectAdded(UAbilitySystemComponent* TargetASC, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveEffectHandle);
};
Source/Aura/Private/AbilitySystem/AsyncTasks/WaitCooldownChange.cpp
#include "AbilitySystem/AsyncTasks/WaitCooldownChange.h"
#include "AbilitySystemComponent.h"
UWaitCooldownChange* UWaitCooldownChange::WaitForCooldownChange(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayTag& InCooldownTag)
{
UWaitCooldownChange* WaitCooldownChange = NewObject<UWaitCooldownChange>();
WaitCooldownChange->ASC = AbilitySystemComponent;
WaitCooldownChange->CooldownTag = InCooldownTag;
if (!IsValid(AbilitySystemComponent) || !InCooldownTag.IsValid())
{
WaitCooldownChange->EndTask();
return nullptr;
}
// To know when a cooldown has ended (Cooldown Tag has been removed)
// 订阅技能系统组件的冷却事件标签被删除,新增事件
// AddUObject 只是指针,需要订阅目标 WaitCooldownChange
AbilitySystemComponent->RegisterGameplayTagEvent(
InCooldownTag,
EGameplayTagEventType::NewOrRemoved).AddUObject(
WaitCooldownChange,
&UWaitCooldownChange::CooldownTagChanged);
// To know when a cooldown effect has been applied
// 每当添加基于duraton的GE,都会在客户端和服务器上调用(例如,即时GE不会触发此操作),没有RPC
AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(WaitCooldownChange, &UWaitCooldownChange::OnActiveEffectAdded);
return WaitCooldownChange;
}
void UWaitCooldownChange::EndTask()
{
if (!IsValid(ASC)) return;
// 获取技能系统组件,从其委托中删除回调
ASC->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved).RemoveAll(this);
// 在游戏实例中取消注册
SetReadyToDestroy();
// 垃圾收集
MarkAsGarbage();
}
void UWaitCooldownChange::CooldownTagChanged(const FGameplayTag InCooldownTag, int32 NewCount)
{
// 冷却标签计数为0,倒计时结束
if (NewCount == 0)
{
CooldownEnd.Broadcast(0.f);
}
}
void UWaitCooldownChange::OnActiveEffectAdded(UAbilitySystemComponent* TargetASC, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveEffectHandle)
{
FGameplayTagContainer AssetTags;
SpecApplied.GetAllAssetTags(AssetTags);
FGameplayTagContainer GrantedTags;
SpecApplied.GetAllGrantedTags(GrantedTags);
if (AssetTags.HasTagExact(CooldownTag) || GrantedTags.HasTagExact(CooldownTag))
{
// 通过冷却的标签查询冷却效果
// GetSingleTagContainer 带有单个标签的容器
FGameplayEffectQuery GameplayEffectQuery = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTag.GetSingleTagContainer());
TArray<float> TimesRemaining = ASC->GetActiveEffectsTimeRemaining(GameplayEffectQuery);
if (TimesRemaining.Num() > 0)
{
float TimeRemaining = TimesRemaining[0];
for (int32 i = 0; i < TimesRemaining.Num(); i++)
{
if (TimesRemaining[i] > TimeRemaining)
{
TimeRemaining = TimesRemaining[i];
}
}
// 广播剩余时间
CooldownStart.Broadcast(TimeRemaining);
}
}
}
事件图表
WaitCooldownChange
接收到技能信息资产时,可获取技能冷却标签 break AuraAbilityInfo
也可以 临时硬编码一个冷却标签,用以关联冷却效果 make literal gameplay tag-Cooldown.Fire.FireBolt
BPOverlayWidgetController get ability system component
Source/Aura/Public/AbilitySystem/AsyncTasks/WaitCooldownChange.h
// AsyncTask 冷却函数返回已完成任务的引用,用以结束
UCLASS(BlueprintType, meta = (ExposedAsyncProxy = "AsyncTask"))
class AURA_API UWaitCooldownChange : public UBlueprintAsyncActionBase
事件图表 AsyncTask 提升为变量 WaitCooldownChangeTask
在WaitCooldownChange的各异步任务调用之前调用其结束任务,防止覆盖导致内存泄露。
WaitCooldownChangeTask 转换为有效get EndTask
Source/Aura/Public/AbilitySystem/Data/AbilityInfo.h
// 技能信息数据数据结构
USTRUCT(BlueprintType)
struct FAuraAbilityInfo
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag AbilityTag = FGameplayTag();
// 冷却标签
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag CooldownTag = FGameplayTag();
// 输入操作标签
// 不应公开给蓝图,应在代码中设置,通过技能获取,可以运行时改变
UPROPERTY(BlueprintReadOnly)
FGameplayTag InputTag = FGameplayTag();
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<const UTexture2D> Icon = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<const UMaterialInterface> BackgroundMaterial = nullptr;
};
CooldownTag - Cooldown.Fire.FireBolt
事件图表 ReceiveAbilityInfo 函数中 CooldownTag 提升为变量 CooldownTag 只在当前技能栏操作控件与技能的输入标签匹配时才设置变量。
主事件: 接收到技能信息后: sequence 检查 WaitCooldownChange ,然后结束它 然后再开始 WaitCooldownChange 内的 冷却开始,结束事件。 防止多次执行委托事件。 CooldownTag
事件图表 冷却开始时 TimeRemaining 提升为变量 TimeRemaining
SetBackgroundTint Tint-0.05 SetBackgroundTint-Tint 提升为变量 CooldownTint Text_Cooldown set render opacity In Opacity -1
折叠到函数 SetCooldownState
SetBackgroundTint Tint-1 Text_Cooldown set render opacity In Opacity -0 折叠到函数 SetDefaultState
使用计时器更新冷却时间 set timer by event set timer by event-Looping-启用 否则只更新一次 set timer by event-Time 提升为变量 TimerFrequency 默认值 0.1 set timer by event return 提升为变量 CooldownTimerHandle
add custom event:UpdateTimer
TimerFrequency
Text_Cooldown SetText(Text)
clamp(Float) to Text(Float) Minimum Fractional Digits-1 Maximum Fractional Digits-1
branch TimeRemaining <=0 CooldownTimerHandle Clear and Invalidate Timer by Handle SetDefaultState
折叠到函数 UpdateCooldownTimer
设计器: Text_Cooldown-渲染变换-渲染-渲染不透明度-1
事件图表: Text_Cooldown set render opacity 折叠到函数HideCooldownText
完整:
经验数据资产中包含升级需要得到经验,奖励,属性点奖励,技能点奖励。
数学公式等级实现跨级比较复杂。 经验数据资产更适合实现跨级升级。
经验放置在玩家状态上。
Source/Aura/Public/AbilitySystem/Data/LevelUpInfo.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "LevelUpInfo.generated.h"
USTRUCT(BlueprintType)
struct FAuraLevelUpInfo
{
GENERATED_BODY()
// 升级条件
UPROPERTY(EditDefaultsOnly)
int32 LevelUpRequirement = 0;
UPROPERTY(EditDefaultsOnly)
int32 AttributePointAward = 1;
UPROPERTY(EditDefaultsOnly)
int32 SpellPointAward = 1;
};
UCLASS()
class AURA_API ULevelUpInfo : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly)
TArray<FAuraLevelUpInfo> LevelUpInformation;
// 找到经验值对应的等级
int32 FindLevelForXP(int32 XP) const;
};
Source/Aura/Private/AbilitySystem/Data/LevelUpInfo.cpp
#include "AbilitySystem/Data/LevelUpInfo.h"
int32 ULevelUpInfo::FindLevelForXP(int32 XP) const
{
int32 Level = 1;
bool bSearching = true;
while (bSearching)
{
// LevelUpInformation[1] = Level 1 Information
// LevelUpInformation[2] = Level 1 Information
if (LevelUpInformation.Num() - 1 <= Level) return Level;
if (XP >= LevelUpInformation[Level].LevelUpRequirement)
{
++Level;
}
else
{
bSearching = false;
}
}
return Level;
}
右键-其他-数据资产-LevelUpInfo
DA_LevelUpInfo
Content/Blueprints/AbilitySystem/Data/DA_LevelUpInfo.uasset
手动设置经验值比数据公式更容易管理
添加组: 第1组 0级,无意义,仅占位:
LevelUpRequirement-0 AttributePointAward-1 SpellPointAward-1
第2组1级
LevelUpRequirement-300 AttributePointAward-1 SpellPointAward-1
第3组2级
索引0 - 10
等级 | 经验 | 属性点奖励 | 技能点奖励 |
---|---|---|---|
0 | 0 | 1 | 1 |
1 | 300 | 1 | 1 |
2 | 900 | 1 | 1 |
3 | 2700 | 1 | 1 |
4 | 6400 | 1 | 1 |
5 | 14500 | 2 | 1 |
6 | 20000 | 1 | 1 |
7 | 35000 | 1 | 1 |
8 | 50000 | 1 | 1 |
9 | 65000 | 1 | 1 |
10 | 85000 | 1 | 1 |
玩家状态上的 经验值在服务端执行,复制到客户端
Source/Aura/Public/Player/AuraPlayerState.h
// 定义玩家状态统计数据变更委托
DECLARE_MULTICAST_DELEGATE_OneParam(FOnPlayerStatChanged, int32 /*StatValue*/)
public:
// 经验值变更委托
FOnPlayerStatChanged OnXPChangedDelegate;
// 等级变更委托
FOnPlayerStatChanged OnLevelChangedDelegate;
FORCEINLINE int32 GetXP() const { return XP; }
// 增加经验值
void AddToXP(int32 InXP);
// 增加等级
void AddToLevel(int32 InLevel);
void SetXP(int32 InXP);
void SetLevel(int32 InLevel);
private:
UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_XP)
int32 XP = 1;
UFUNCTION()
void OnRep_XP(int32 OldXP);
Source/Aura/Private/Player/AuraPlayerState.cpp
// 注册Level,XP 变量以进行复制
void AAuraPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AAuraPlayerState, Level);
DOREPLIFETIME(AAuraPlayerState, XP);
}
void AAuraPlayerState::AddToXP(int32 InXP)
{
XP += InXP;
OnXPChangedDelegate.Broadcast(XP);
}
void AAuraPlayerState::AddToLevel(int32 InLevel)
{
Level += InLevel;
OnLevelChangedDelegate.Broadcast(Level);
}
void AAuraPlayerState::SetXP(int32 InXP)
{
XP = InXP;
OnXPChangedDelegate.Broadcast(XP);
}
void AAuraPlayerState::SetLevel(int32 InLevel)
{
Level = InLevel;
OnLevelChangedDelegate.Broadcast(Level);
}
//
void AAuraPlayerState::OnRep_Level(int32 OldLevel)
{
OnLevelChangedDelegate.Broadcast(Level);
}
void AAuraPlayerState::OnRep_XP(int32 OldXP)
{
OnXPChangedDelegate.Broadcast(XP);
}
Source/Aura/Public/Player/AuraPlayerState.h
class ULevelUpInfo;
public:
// 等级升级信息
UPROPERTY(EditDefaultsOnly)
TObjectPtr<ULevelUpInfo> LevelUpInfo;
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
public:
// 监听经验百分比变化
UPROPERTY(BlueprintAssignable, Category="GAS|XP")
FOnAttributeChangedSignature OnXPPercentChangedDelegate;
protected:
void OnXPChanged(int32 NewXP) const;
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
void UOverlayWidgetController::BindCallbacksToDependencies()
{
// 为玩家状态的经验值变更委托绑定回调
AAuraPlayerState* AuraPlayerState = CastChecked<AAuraPlayerState>(PlayerState);
AuraPlayerState->OnXPChangedDelegate.AddUObject(this, &UOverlayWidgetController::OnXPChanged);
完整
#include "AbilitySystem/Data/LevelUpInfo.h"
#include "Player/AuraPlayerState.h"
void UOverlayWidgetController::BindCallbacksToDependencies()
{
// 为玩家状态的经验值变更委托绑定回调
AAuraPlayerState* AuraPlayerState = CastChecked<AAuraPlayerState>(PlayerState);
AuraPlayerState->OnXPChangedDelegate.AddUObject(this, &UOverlayWidgetController::OnXPChanged);
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性更改多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnManaChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxManaChanged.Broadcast(Data.NewValue);
}
);
// 资产标签响应函数
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent))
{
// 如果技能系统组件初始化了初始技能,开始在控制器中初始化初始技能,广播给控件
if (AuraASC->bStartupAbilitiesGiven)
{
OnInitializeStartupAbilities(AuraASC);
}
// 否则 监听技能系统组件的赋予技能委托
else
{
AuraASC->AbilitiesGivenDelegate.AddUObject(this, &UOverlayWidgetController::OnInitializeStartupAbilities);
}
// 响应技能系统组件的委托时获取 传入的资产标签容器参数 AssetTags
// 传入this参数,可使用当前类中的属性和方法
AuraASC->EffectAssetTags.AddLambda(
[this](const FGameplayTagContainer& AssetTags)
{
for (const FGameplayTag& Tag : AssetTags)
{
// 查找标签是否包含 Message 字符,是表示消息游戏标签
// For example, say that Tag = Message.HealthPotion
// "Message.HealthPotion".MatchesTag("Message") will return True, "Message".MatchesTag("Message.HealthPotion") will return False
FGameplayTag MessageTag = FGameplayTag::RequestGameplayTag(FName("Message"));
if (Tag.MatchesTag(MessageTag))
{
// 通过行名称/标签名称查找对应的文本消息等数据。
const FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
// 将数据表的一行数据广播
MessageWidgetRowDelegate.Broadcast(*Row);
//之后在控件蓝图时间中绑定覆盖该事件以接受该行数据
}
}
}
);
}
}
void UOverlayWidgetController::OnXPChanged(int32 NewXP) const
{
const AAuraPlayerState* AuraPlayerState = CastChecked<AAuraPlayerState>(PlayerState);
const ULevelUpInfo* LevelUpInfo = AuraPlayerState->LevelUpInfo;
checkf(LevelUpInfo, TEXT("Unabled to find LevelUpInfo. Please fill out AuraPlayerState Blueprint"));
const int32 Level = LevelUpInfo->FindLevelForXP(NewXP);
const int32 MaxLevel = LevelUpInfo->LevelUpInformation.Num();
if (Level <= MaxLevel && Level > 0)
{
const int32 LevelUpRequirement = LevelUpInfo->LevelUpInformation[Level].LevelUpRequirement;
const int32 PreviousLevelUpRequirement = LevelUpInfo->LevelUpInformation[Level - 1].LevelUpRequirement;
const int32 DeltaLevelRequirement = LevelUpRequirement - PreviousLevelUpRequirement;
const int32 XPForThisLevel = NewXP - PreviousLevelUpRequirement;
const float XPBarPercent = static_cast<float>(XPForThisLevel) / static_cast<float>(DeltaLevelRequirement);
OnXPPercentChangedDelegate.Broadcast(XPBarPercent);
}
}
奖励XP 1.XP奖励敌人 (和一种获得它的方法) 2.传入XP元属性 3.被动游戏技能,GA监听事件 (并授予它) a.响应事件应用的游戏效果 4.当伤害致命时从属性集奖励经验值 5.在属性集中处理传入的XP,并在玩家状态下增加XP
敌人拥有攻击者在杀死敌人时获得的 XP 值。
1.创建XP奖励曲线表 2.每个职业的可缩放浮动值【XP奖励】 3.技能系统组件中获取角色的职业和等级 4.在战斗接口中添加GetCharacterClass函数 BlueprintNativeEvent和BlueprintCallable)
右键-其他-曲线表格 -Cubic
CT_XP_Reward
Content/Blueprints/AbilitySystem/Data/CT_XP_Reward.uasset
3个职业曲线:
魔法师 Elementalist
战士 Warrior
游侠 Ranger
Warrior曲线: 1,20 40,1000
自动平滑
Ranger曲线: 1,25 40,1500 自动平滑
Elementalist曲线: 1,35 40,2500 自动平滑
Source/Aura/Public/AbilitySystem/Data/CharacterClassInfo.h
#include "ScalableFloat.h"
// 包含每个职业的所有信息的结构
USTRUCT(BlueprintType)
struct FCharacterClassDefaultInfo
{
GENERATED_BODY()
// 一个游戏效果来应用到主要属性。
// 一个能够存储新游戏效果的子类
UPROPERTY(EditDefaultsOnly, Category = "Class Defaults")
TSubclassOf<UGameplayEffect> PrimaryAttributes;
// 每种职业可以具由一些独有的默认技能
// 默认技能不一定立即赋予
UPROPERTY(EditDefaultsOnly, Category = "Class Defaults")
TArray<TSubclassOf<UGameplayAbility>> StartupAbilities;
// XP奖励曲线
UPROPERTY(EditDefaultsOnly, Category = "Class Defaults")
FScalableFloat XPReward = FScalableFloat();
};
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
// 通过职业和等级获取 XP奖励值
static int32 GetXPRewardForClassAndLevel(const UObject* WorldContextObject, ECharacterClass CharacterClass,
int32 CharacterLevel);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
int32 UAuraAbilitySystemLibrary::GetXPRewardForClassAndLevel(const UObject* WorldContextObject,
ECharacterClass CharacterClass, int32 CharacterLevel)
{
UCharacterClassInfo* CharacterClassInfo = GetCharacterClassInfo(WorldContextObject);
if (CharacterClassInfo == nullptr) return 0;
const FCharacterClassDefaultInfo& Info = CharacterClassInfo->GetClassDefaultInfo(CharacterClass);
const float XPReward = Info.XPReward.GetValueAtLevel(CharacterLevel);
return static_cast<int32>(XPReward);
}
Source/Aura/Public/Interaction/CombatInterface.h
#include "AbilitySystem/Data/CharacterClassInfo.h"
public:
// 获取职业
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
ECharacterClass GetCharacterClass();
Source/Aura/Public/Character/AuraEnemy.h
删除职业类代码
// 职业类 删除
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character Class Defaults")
ECharacterClass CharacterClass = ECharacterClass::Warrior;
Source/Aura/Public/Character/AuraCharacterBase.h
#include "AbilitySystem/Data/CharacterClassInfo.h"
public:
virtual ECharacterClass GetCharacterClass_Implementation() override;
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character Class Defaults")
ECharacterClass CharacterClass = ECharacterClass::Warrior;
Source/Aura/Private/Character/AuraCharacterBase.cpp
ECharacterClass AAuraCharacterBase::GetCharacterClass_Implementation()
{
return CharacterClass;
}
Source/Aura/Private/Character/AuraCharacter.cpp
AAuraCharacter::AAuraCharacter()
{
// 获取角色运动组件
// 启用:方向旋转到运动
GetCharacterMovement()->bOrientRotationToMovement = true;
// 可以通过获取角色移动旋转速率来控制旋转速率。
// 角色就会以这个速度400,在偏航旋转方向上运动,角色运动可以迫使我们将运动限制在一个平面上。
// yaw():航向,将物体绕Y轴旋转
GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f);
// 角色被捕捉到平面
GetCharacterMovement()->bConstrainToPlane = true;
// 在开始时捕捉到平面
GetCharacterMovement()->bSnapToPlaneAtStart = true;
// 角色本身不应该使用控制器的旋转
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
bUseControllerRotationYaw = false;
CharacterClass = ECharacterClass::Elementalist;
}
魔法师Elementalist XPReward-1,CT_XP_Reward,Elementalist
战士Warrior XPReward-1,CT_XP_Reward,Warrior
游侠Ranger XPReward-1,CT_XP_Reward,Ranger
属性集中设置XP元属性 元属性不会被复制。
当前只打印测试。 本地存储XP元属性副本, 原XP元属性归零。
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
public:
// 设置传入XP元属性
UPROPERTY(BlueprintReadOnly, Category = "Meta Attributes")
FGameplayAttributeData IncomingXP;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, IncomingXP);
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
#include "Aura/AuraLogChannels.h"
// 仅在服务器端执行
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
// IncomingDamage 属性只在服务器执行获取,不会复制到客户端
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
CombatInterface->Die();
}
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
// 是否暴击,格挡,显示提示文本
const bool bBlock = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
ShowFloatingText(Props, LocalIncomingDamage, bBlock, bCriticalHit);
}
}
if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
{
// 本地存储XP元属性副本
const float LocalIncomingXP = GetIncomingXP();
// 原XP元属性归零
SetIncomingXP(0.f);
UE_LOG(LogAura, Log, TEXT("Incoming XP: %f"), LocalIncomingXP);
}
}
只在服务器运行,不需要复制到客户端。
激活后将一直运行。
Content/Blueprints/AbilitySystem/Aura/Abilities/Passive_Startup/GA_ListenForEvent.uasset
高级-Instancing Policy-Instanced Per Actor 每个actor实例一个技能
Net Execution Policy-Server Only 仅服务器运行,不复制到客户端
激活技能时,开始监听游戏事件
事件图表:
event activateAbility
ability-tasks-wait gameplay event
wait gameplay event-event tag-留空 用以监听 任意 标签事件
wait gameplay event-only match exact-取消
wait gameplay event-only trigger once-取消 这在整个游戏中持续监听。
wait gameplay event-payload 分割数据结构 Break GameplayEventData 可以获取其中的标签和值,来创建游戏效果。
Source/Aura/Public/AuraGameplayTags.h
public:
FGameplayTag Attributes_Meta_IncomingXP;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
/*
* Meta Attributes
*/
GameplayTags.Attributes_Meta_IncomingXP = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Attributes.Meta.IncomingXP"),
FString("Incoming XP Meta Attribute")
);
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
// 添加启动就拥有的被动技能
void AddCharacterPassiveAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupPassiveAbilities);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::AddCharacterPassiveAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupPassiveAbilities)
{
for (const TSubclassOf<UGameplayAbility> AbilityClass : StartupPassiveAbilities)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
GiveAbilityAndActivateOnce(AbilitySpec);
}
}
Source/Aura/Public/Character/AuraCharacterBase.h
private:
// 这些将是从游戏一开始就应该赋予的技能列表
// 初始被动技能
UPROPERTY(EditAnywhere, Category = "Abilities")
TArray<TSubclassOf<UGameplayAbility>> StartupPassiveAbilities;
Source/Aura/Private/Character/AuraCharacterBase.cpp
void AAuraCharacterBase::AddCharacterAbilities()
{
UAuraAbilitySystemComponent* AuraASC = CastChecked<UAuraAbilitySystemComponent>(AbilitySystemComponent);
// 只能由服务器端添加节能
if (!HasAuthority()) return;
AuraASC->AddCharacterAbilities(StartupAbilities);
AuraASC->AddCharacterPassiveAbilities(StartupPassiveAbilities);
}
Content/Blueprints/AbilitySystem/Aura/Abilities/Passive_Startup/GE_EventBasedEffect.uasset
持续时间-Duration Policy-Instant 即时
Gameplay Effect-Modifiers-索引0-Attribute-AuraAttributeSet.IncomingXP Gameplay Effect-Modifiers-索引0-Modifier Op-Add Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type -Set By Caller Set by Caller Magnitude 由调用者设置大小 Data Name Data Tag -Attributes.Meta.IncomingXP 使带此效果的技能可以监听带此标签的事件 通过在技能中使用 wait gameplay event 监听该事件。
打开 GA_ListenForEvent 时间图表: 添加变量 EventBasedEffectClass 类型 GameplayEffect-类引用 默认值-GE_EventBasedEffect
使用技能系统组件应用该效果 get ability system component from actor info make outgoing spec make outgoing spec-level-1 make effect context EventBasedEffectClass
assign tag set by caller Magnitude 设置当前技能的技能效果的set by caller修改器的具体的值 Spec->SetSetByCallerMagnitude(DataTag, Magnitude);
get ability system component from actor info
ApplyGameplayEffectSpecToSelf
打开 BP_AuraCharacter Abilities-Startup Passive Abilities-GA_ListenForEvent
玩家将具由监听事件技能。该被动技能只在服务端激活,只在服务端监听。不会复制。 现在可以监听游戏事件,只其他地方等待发送游戏事件。
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
private:
// 发送XP经验事件
void SendXPEvent(const FEffectProperties& Props);
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
// 仅在服务器端执行
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
// IncomingDamage 属性只在服务器执行获取,不会复制到客户端
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
CombatInterface->Die();
}
// 死亡后发送XP事件
SendXPEvent(Props);
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
// 是否暴击,格挡,显示提示文本
const bool bBlock = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
ShowFloatingText(Props, LocalIncomingDamage, bBlock, bCriticalHit);
}
}
if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
{
// 本地存储XP元属性副本
const float LocalIncomingXP = GetIncomingXP();
// 原XP元属性归零
SetIncomingXP(0.f);
UE_LOG(LogAura, Log, TEXT("Incoming XP: %f"), LocalIncomingXP);
}
}
void UAuraAttributeSet::SendXPEvent(const FEffectProperties& Props)
{
if (ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetCharacter))
{
// 奖励来自伤害的目标
const int32 TargetLevel = CombatInterface->GetPlayerLevel();
// Execute_GetCharacterClass 调用蓝图本机版本方法 需要 Execute_ 前缀
const ECharacterClass TargetClass = ICombatInterface::Execute_GetCharacterClass(Props.TargetCharacter);
const int32 XPReward = UAuraAbilitySystemLibrary::GetXPRewardForClassAndLevel(Props.TargetCharacter, TargetClass, TargetLevel);
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
FGameplayEventData Payload;
Payload.EventTag = GameplayTags.Attributes_Meta_IncomingXP;
Payload.EventMagnitude = XPReward;
// 发送事件给伤害的来源角色
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Props.SourceCharacter, GameplayTags.Attributes_Meta_IncomingXP, Payload);
}
}
现在,敌人死亡时会发送 Attributes.Meta.IncomingXP事件,玩家被动技能会监听到该事件,并获取事件中的标签和具体的经验值,设置到技能规格中。
之后需要在if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
中实际设置玩家的经验值。
然后广播到控件。
玩家状态接收经验值。 属性集发送经验值事件。 属性集不应依赖玩家状态。 所以需要接口。
Source/Aura/Public/Interaction/PlayerInterface.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "PlayerInterface.generated.h"
UINTERFACE(MinimalAPI)
class UPlayerInterface : public UInterface
{
GENERATED_BODY()
};
class AURA_API IPlayerInterface
{
GENERATED_BODY()
public:
// 增加经验值
UFUNCTION(BlueprintNativeEvent)
void AddToXP(int32 InXP);
};
Source/Aura/Private/Interaction/PlayerInterface.cpp
#include "Interaction/PlayerInterface.h"
Source/Aura/Public/Character/AuraCharacter.h
#include "Interaction/PlayerInterface.h"
class AURA_API AAuraCharacter : public AAuraCharacterBase, public IPlayerInterface
public:
/** Players Interface */
virtual void AddToXP_Implementation(int32 InXP) override;
/** end Player Interface */
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::AddToXP_Implementation(int32 InXP)
{
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
AuraPlayerState->AddToXP(InXP);
}
if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
{
// 本地存储XP元属性副本
const float LocalIncomingXP = GetIncomingXP();
// 原XP元属性归零
SetIncomingXP(0.f);
//TODO: 是否应该升级
// 为伤害来源添加经验
if (Props.SourceCharacter->Implements<UPlayerInterface>())
{
IPlayerInterface::Execute_AddToXP(Props.SourceCharacter, LocalIncomingXP);
}
}
完整:
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
#include "Interaction/PlayerInterface.h"
// 仅在服务器端执行
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
// IncomingDamage 属性只在服务器执行获取,不会复制到客户端
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
CombatInterface->Die();
}
// 死亡后发送XP事件
SendXPEvent(Props);
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
// 是否暴击,格挡,显示提示文本
const bool bBlock = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
ShowFloatingText(Props, LocalIncomingDamage, bBlock, bCriticalHit);
}
}
if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
{
// 本地存储XP元属性副本
const float LocalIncomingXP = GetIncomingXP();
// 原XP元属性归零
SetIncomingXP(0.f);
//TODO: 是否应该升级
// 为伤害来源添加经验
if (Props.SourceCharacter->Implements<UPlayerInterface>())
{
IPlayerInterface::Execute_AddToXP(Props.SourceCharacter, LocalIncomingXP);
}
}
}
监听到后设置 WBP_XPBar 进度百分比
打开 WBP_Overlay 事件:
WBP_XPBar set widget controller get widget controller
设计器: 进度条名称:ProgressBar_XP
打开 WBP_XPBar 事件: Event Widget Controller Set sequence
获取控件控制器: get widget controller cast to BP_OverlayWidgetController 提升为变量 BPOverlayWidgetController
监听控件控制器: BPOverlayWidgetController Assign On XPPercent Changed Delegate ProgressBar_XP set percent
打开BP_AuraPlayerState Level Up Info-DA_LevelUpInfo
现在杀死一个敌人将提升经验百分比。
Source/Aura/Public/Interaction/PlayerInterface.h
public:
// 升级
UFUNCTION(BlueprintNativeEvent)
void LevelUp();
Source/Aura/Public/Character/AuraCharacter.h
public:
virtual void LevelUp_Implementation() override;
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::LevelUp_Implementation()
{
}
可以判断是否实现接口。Implements 用来检查是否实现接口。 在没有实现接口的时候,可以设置默认值。
Source/Aura/Public/Interaction/CombatInterface.h
删除 virtual int32 GetPlayerLevel();
public:
UFUNCTION(BlueprintNativeEvent)
int32 GetPlayerLevel();
Source/Aura/Private/Interaction/CombatInterface.cpp
删除 int32 ICombatInterface::GetPlayerLevel()
#include "Interaction/CombatInterface.h"
Source/Aura/Public/Character/AuraCharacter.h
删除 virtual int32 GetPlayerLevel() override;
public:
virtual int32 GetPlayerLevel_Implementation() override;
Source/Aura/Private/Character/AuraCharacter.cpp
int32 AAuraCharacter::GetPlayerLevel_Implementation()
{
const AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
return AuraPlayerState->GetPlayerLevel();
}
Source/Aura/Public/Character/AuraEnemy.h
删除virtual int32 GetPlayerLevel() override;
public:
virtual int32 GetPlayerLevel_Implementation() override;
Source/Aura/Private/Character/AuraEnemy.cpp
int32 AAuraEnemy::GetPlayerLevel_Implementation()
{
return Level;
}
Source/Aura/Private/AbilitySystem/ModMagCalc/MMC_MaxHealth.cpp
float UMMC_MaxHealth::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
// 从源和目标收集标签
// Gather tags from source and target
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
float Vigor = 0.f;
// 捕获属性 VigorDef 的值,通过 Vigor 传出
GetCapturedAttributeMagnitude(VigorDef, Spec, EvaluationParameters, Vigor);
// 限制属性为非负
Vigor = FMath::Max<float>(Vigor, 0.f);
// Spec.GetContext().GetSourceObject() 是玩家角色或敌人角色
// 从战斗接口获取玩家等级 玩家状态和敌人角色都实现了战斗接口
//ICombatInterface* CombatInterface = Cast<ICombatInterface>(Spec.GetContext().GetSourceObject());
//const int32 PlayerLevel = CombatInterface->GetPlayerLevel();
// 如果没有实现接口,则使用 PlayerLevel = 1;
// Implements 用来检查是否实现接口
int32 PlayerLevel = 1;
if (Spec.GetContext().GetSourceObject()->Implements<UCombatInterface>())
{
PlayerLevel = ICombatInterface::Execute_GetPlayerLevel(Spec.GetContext().GetSourceObject());
}
return 80.f + 2.5f * Vigor + 10.f * PlayerLevel;
}
Source/Aura/Private/AbilitySystem/ModMagCalc/MMC_MaxMana.cpp
float UMMC_MaxMana::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
// Gather tags from source and target
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
float Int = 0.f;
GetCapturedAttributeMagnitude(IntDef, Spec, EvaluationParameters, Int);
Int = FMath::Max<float>(Int, 0.f);
//ICombatInterface* CombatInterface = Cast<ICombatInterface>(Spec.GetContext().GetSourceObject());
//const int32 PlayerLevel = CombatInterface->GetPlayerLevel();
// 如果没有实现接口,则使用 PlayerLevel = 1;
// Implements 用来检查是否实现接口
int32 PlayerLevel = 1;
if (Spec.GetContext().GetSourceObject()->Implements<UCombatInterface>())
{
PlayerLevel = ICombatInterface::Execute_GetPlayerLevel(Spec.GetContext().GetSourceObject());
}
return 50.f + 2.5f * Int + 15.f * PlayerLevel;
}
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
//ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
//ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
int32 SourcePlayerLevel = 1;
if (SourceAvatar->Implements<UCombatInterface>())
{
SourcePlayerLevel = ICombatInterface::Execute_GetPlayerLevel(SourceAvatar);
}
int32 TargetPlayerLevel = 1;
if (TargetAvatar->Implements<UCombatInterface>())
{
TargetPlayerLevel = ICombatInterface::Execute_GetPlayerLevel(TargetAvatar);
}
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// 根据调用者大小计算伤害值 累加
// DamageTypesToResistances 伤害型标签-抗性属性 组包含所有伤害类型的标签 和与伤害关联的 抗性属性标签
// DamageTypesToResistances 标签只存在于伤害型技能中
// Get Damage Set by Caller Magnitude
float Damage = 0.f;
for (const TTuple<FGameplayTag, FGameplayTag>& Pair : FAuraGameplayTags::Get().DamageTypesToResistances)
{
// 游戏标签对应的的值由 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude 设置过【】
// Pair.Key 为伤害类型技能标签 Pair.Value 为抗性属性标签
// 根据标签 后续可以取出不同的抗性属性 Pair.Value,减少对应类型伤害的值 例如火抗 将 火属性伤害值减少一些
// 取出伤害类型技能的值
// const float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
// 伤害型技能标签
const FGameplayTag DamageTypeTag = Pair.Key;
// 抗性属性标签
const FGameplayTag ResistanceTag = Pair.Value;
checkf(AuraDamageStatics().TagsToCaptureDefs.Contains(ResistanceTag),
TEXT("TagsToCaptureDefs doesn't contain Tag: [%s] in ExecCalc_Damage"), *ResistanceTag.ToString());
// 通过属性标签,找到相关联的捕获属性定义,当前只需要抗性捕获定义
// 定义在 TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, ArcaneResistanceDef);
const FGameplayEffectAttributeCaptureDefinition CaptureDef = AuraDamageStatics().TagsToCaptureDefs[
ResistanceTag];
// 参数2 :未找到相关属性时是否警告
float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key, false);
// 计算捕获的目标的属性 通过 Resistance 传出
float Resistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(CaptureDef, EvaluationParameters, Resistance);
// 抗性最大抵消100%的伤害
Resistance = FMath::Clamp(Resistance, 0.f, 100.f);
// 每一点抗性抵消1%的伤害
DamageTypeValue *= (100.f - Resistance) / 100.f;
Damage += DamageTypeValue;
}
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
//
FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
//为自定义效果情景设置是否格挡
UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourcePlayerLevel);
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetPlayerLevel);
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef,
EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef,
EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef,
EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(TargetPlayerLevel);
// Critical Hit Resistance reduces Critical Hit Chance by a certain percentage
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance *
CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// 为自定义效果情景设置是否暴击
UAuraAbilitySystemLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
// Double damage plus a bonus if critical hit
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::SendXPEvent(const FEffectProperties& Props)
{
if (Props.TargetCharacter->Implements<UCombatInterface>())
{
// 奖励来自伤害的目标
const int32 TargetLevel = ICombatInterface::Execute_GetPlayerLevel(Props.TargetCharacter);
// Execute_GetCharacterClass 调用蓝图本机版本方法 需要 Execute_ 前缀
const ECharacterClass TargetClass = ICombatInterface::Execute_GetCharacterClass(Props.TargetCharacter);
const int32 XPReward = UAuraAbilitySystemLibrary::GetXPRewardForClassAndLevel(Props.TargetCharacter, TargetClass, TargetLevel);
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
FGameplayEventData Payload;
Payload.EventTag = GameplayTags.Attributes_Meta_IncomingXP;
Payload.EventMagnitude = XPReward;
// 发送事件给伤害的来源角色
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Props.SourceCharacter, GameplayTags.Attributes_Meta_IncomingXP, Payload);
}
}
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
void UAuraAbilitySystemLibrary::GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC,
ECharacterClass CharacterClass)
{
UCharacterClassInfo* CharacterClassInfo = GetCharacterClassInfo(WorldContextObject);
//职业通用技能
if (CharacterClassInfo == nullptr) return;
for (TSubclassOf<UGameplayAbility> AbilityClass : CharacterClassInfo->CommonAbilities)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
ASC->GiveAbility(AbilitySpec);
}
//职业独有的初始技能
const FCharacterClassDefaultInfo& DefaultInfo = CharacterClassInfo->GetClassDefaultInfo(CharacterClass);
for (TSubclassOf<UGameplayAbility> AbilityClass : DefaultInfo.StartupAbilities)
{
if (ASC->GetAvatarActor()->Implements<UCombatInterface>())
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass,
ICombatInterface::Execute_GetPlayerLevel(
ASC->GetAvatarActor()));
ASC->GiveAbility(AbilitySpec);
}
}
}
Source/Aura/Public/Interaction/PlayerInterface.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "PlayerInterface.generated.h"
UINTERFACE(MinimalAPI)
class UPlayerInterface : public UInterface
{
GENERATED_BODY()
};
class AURA_API IPlayerInterface
{
GENERATED_BODY()
public:
// 根据总经验获取对应的等级
UFUNCTION(BlueprintNativeEvent)
int32 FindLevelForXP(int32 InXP) const;
UFUNCTION(BlueprintNativeEvent)
int32 GetXP() const;
UFUNCTION(BlueprintNativeEvent)
int32 GetAttributePointsReward(int32 Level) const;
UFUNCTION(BlueprintNativeEvent)
int32 GetSpellPointsReward(int32 Level) const;
// 增加经验值
UFUNCTION(BlueprintNativeEvent)
void AddToXP(int32 InXP);
// 升级
UFUNCTION(BlueprintNativeEvent)
void LevelUp();
UFUNCTION(BlueprintNativeEvent)
void AddToPlayerLevel(int32 InPlayerLevel);
UFUNCTION(BlueprintNativeEvent)
void AddToAttributePoints(int32 InAttributePoints);
UFUNCTION(BlueprintNativeEvent)
void AddToSpellPoints(int32 InSpellPoints);
};
Source/Aura/Public/Character/AuraCharacter.h
public:
virtual void AddToXP_Implementation(int32 InXP) override;
virtual void LevelUp_Implementation() override;
virtual int32 GetXP_Implementation() const override;
virtual int32 FindLevelForXP_Implementation(int32 InXP) const override;
virtual int32 GetAttributePointsReward_Implementation(int32 Level) const override;
virtual int32 GetSpellPointsReward_Implementation(int32 Level) const override;
virtual void AddToPlayerLevel_Implementation(int32 InPlayerLevel) override;
virtual void AddToAttributePoints_Implementation(int32 InAttributePoints) override;
virtual void AddToSpellPoints_Implementation(int32 InSpellPoints) override;
Source/Aura/Private/Character/AuraCharacter.cpp
#include "AbilitySystem/Data/LevelUpInfo.h"
int32 AAuraCharacter::GetXP_Implementation() const
{
const AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
return AuraPlayerState->GetXP();
}
int32 AAuraCharacter::FindLevelForXP_Implementation(int32 InXP) const
{
const AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
return AuraPlayerState->LevelUpInfo->FindLevelForXP(InXP);
}
int32 AAuraCharacter::GetAttributePointsReward_Implementation(int32 Level) const
{
const AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
return AuraPlayerState->LevelUpInfo->LevelUpInformation[Level].AttributePointAward;
}
int32 AAuraCharacter::GetSpellPointsReward_Implementation(int32 Level) const
{
const AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
return AuraPlayerState->LevelUpInfo->LevelUpInformation[Level].SpellPointAward;
}
void AAuraCharacter::AddToPlayerLevel_Implementation(int32 InPlayerLevel)
{
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
AuraPlayerState->AddToLevel(InPlayerLevel);
}
void AAuraCharacter::AddToAttributePoints_Implementation(int32 InAttributePoints)
{
//TODO: Add AttributePoints to PlayerState
}
void AAuraCharacter::AddToSpellPoints_Implementation(int32 InSpellPoints)
{
//TODO: Add SpellPoints to PlayerState
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
// 仅在服务器端执行
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
// IncomingDamage 属性只在服务器执行获取,不会复制到客户端
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
CombatInterface->Die();
}
// 死亡后发送XP事件
SendXPEvent(Props);
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
// 是否暴击,格挡,显示提示文本
const bool bBlock = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
ShowFloatingText(Props, LocalIncomingDamage, bBlock, bCriticalHit);
}
}
if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
{
// 本地存储XP元属性副本
const float LocalIncomingXP = GetIncomingXP();
// 原XP元属性归零
SetIncomingXP(0.f);
//TODO: 是否应该升级
// 为伤害来源添加经验
// Source Character is the owner, since GA_ListenForEvents applies GE_EventBasedEffect, adding to IncomingXP
if (Props.SourceCharacter->Implements<UPlayerInterface>() && Props.SourceCharacter->Implements<UCombatInterface>())
{
const int32 CurrentLevel = ICombatInterface::Execute_GetPlayerLevel(Props.SourceCharacter);
const int32 CurrentXP = IPlayerInterface::Execute_GetXP(Props.SourceCharacter);
const int32 NewLevel = IPlayerInterface::Execute_FindLevelForXP(Props.SourceCharacter, CurrentXP + LocalIncomingXP);
const int32 NumLevelUps = NewLevel - CurrentLevel;
if (NumLevelUps > 0)
{
const int32 AttributePointsReward = IPlayerInterface::Execute_GetAttributePointsReward(Props.SourceCharacter, CurrentLevel);
const int32 SpellPointsReward = IPlayerInterface::Execute_GetSpellPointsReward(Props.SourceCharacter, CurrentLevel);
IPlayerInterface::Execute_AddToPlayerLevel(Props.SourceCharacter, NumLevelUps);
IPlayerInterface::Execute_AddToAttributePoints(Props.SourceCharacter, AttributePointsReward);
IPlayerInterface::Execute_AddToSpellPoints(Props.SourceCharacter, SpellPointsReward);
SetHealth(GetMaxHealth());
SetMana(GetMaxMana());
IPlayerInterface::Execute_LevelUp(Props.SourceCharacter);
}
IPlayerInterface::Execute_AddToXP(Props.SourceCharacter, LocalIncomingXP);
}
}
}
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
// 定义 玩家统计数据变更委托
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerStatChangedSignature, int32, NewValue);
public:
// 监听等级变更
UPROPERTY(BlueprintAssignable, Category="GAS|Level")
FOnPlayerStatChangedSignature OnPlayerLevelChangedDelegate;
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
// 为等级委托绑定回调
AuraPlayerState->OnLevelChangedDelegate.AddLambda(
[this](int32 NewLevel)
{
OnPlayerLevelChangedDelegate.Broadcast(NewLevel);
}
);
完整:
void UOverlayWidgetController::BindCallbacksToDependencies()
{
// 为玩家状态的经验值变更委托绑定回调
AAuraPlayerState* AuraPlayerState = CastChecked<AAuraPlayerState>(PlayerState);
AuraPlayerState->OnXPChangedDelegate.AddUObject(this, &UOverlayWidgetController::OnXPChanged);
// 为等级委托绑定回调
AuraPlayerState->OnLevelChangedDelegate.AddLambda(
[this](int32 NewLevel)
{
OnPlayerLevelChangedDelegate.Broadcast(NewLevel);
}
);
const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性更改多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnManaChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxManaChanged.Broadcast(Data.NewValue);
}
);
// 资产标签响应函数
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent))
{
// 如果技能系统组件初始化了初始技能,开始在控制器中初始化初始技能,广播给控件
if (AuraASC->bStartupAbilitiesGiven)
{
OnInitializeStartupAbilities(AuraASC);
}
// 否则 监听技能系统组件的赋予技能委托
else
{
AuraASC->AbilitiesGivenDelegate.AddUObject(this, &UOverlayWidgetController::OnInitializeStartupAbilities);
}
// 响应技能系统组件的委托时获取 传入的资产标签容器参数 AssetTags
// 传入this参数,可使用当前类中的属性和方法
AuraASC->EffectAssetTags.AddLambda(
[this](const FGameplayTagContainer& AssetTags)
{
for (const FGameplayTag& Tag : AssetTags)
{
// 查找标签是否包含 Message 字符,是表示消息游戏标签
// For example, say that Tag = Message.HealthPotion
// "Message.HealthPotion".MatchesTag("Message") will return True, "Message".MatchesTag("Message.HealthPotion") will return False
FGameplayTag MessageTag = FGameplayTag::RequestGameplayTag(FName("Message"));
if (Tag.MatchesTag(MessageTag))
{
// 通过行名称/标签名称查找对应的文本消息等数据。
const FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
// 将数据表的一行数据广播
MessageWidgetRowDelegate.Broadcast(*Row);
//之后在控件蓝图时间中绑定覆盖该事件以接受该行数据
}
}
}
);
}
}
复制 WBP_SpellGlobe 为 WBP_ValueGlobe
Content/Blueprints/UI/SpellGlobes/WBP_ValueGlobe.uasset
打开 WBP_ValueGlobe
Text_Cooldown 改为 Text_Value
Text_Value-渲染-渲染不透明度-1
文本-1
SetIconAndBackground 改为 UpdateBackground
SpellIconBrush 改为 BackgroundBrush 图像-MI_LockedBG
BPOverlayWidgetController Assign On Player Level Changed Delegate Text_Value set text(text)
![BPGraphScreenshot_2024Y-01M-25D-23h-12m-42s-317_00]
(https://github.com/WangShuXian6/blog/assets/30850497/22ace08c-23fc-408e-b82f-0d1a55cf8127)
打开 WBP_Overlay 设计器: WBP_ValueGlobe :ValueGlobe_Level
图表: ValueGlobe_Level get widget controller set widget controller
打开 CT_XP_Reward 增大一级的经验奖励用来测试等级UI
现在,UI将实时显示等级,并可以升级。
复制 WBP_GlobeProgressBar 为 WBP_PictureFrame
Content/Blueprints/UI/Overlay/Subwidget/WBP_PictureFrame.uasset
Source/Aura/Public/Character/AuraCharacter.h
class UNiagaraComponent;
class UCameraComponent;
class USpringArmComponent;
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<UNiagaraComponent> LevelUpNiagaraComponent;
private:
UPROPERTY(VisibleAnywhere)
TObjectPtr<UCameraComponent> TopDownCameraComponent;
UPROPERTY(VisibleAnywhere)
TObjectPtr<USpringArmComponent> CameraBoom;
// 网络多播,可靠,会复制到客户端
UFUNCTION(NetMulticast, Reliable)
void MulticastLevelUpParticles() const;
Source/Aura/Private/Character/AuraCharacter.cpp
#include "NiagaraComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
AAuraCharacter::AAuraCharacter()
{
CameraBoom = CreateDefaultSubobject<USpringArmComponent>("CameraBoom");
CameraBoom->SetupAttachment(GetRootComponent());
CameraBoom->SetUsingAbsoluteRotation(true);
CameraBoom->bDoCollisionTest = false;
TopDownCameraComponent = CreateDefaultSubobject<UCameraComponent>("TopDownCameraComponent");
TopDownCameraComponent->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
TopDownCameraComponent->bUsePawnControlRotation = false;
LevelUpNiagaraComponent = CreateDefaultSubobject<UNiagaraComponent>("LevelUpNiagaraComponent");
LevelUpNiagaraComponent->SetupAttachment(GetRootComponent());
// 防止自动激活,必须手动激活
LevelUpNiagaraComponent->bAutoActivate = false;
// 获取角色运动组件
// 启用:方向旋转到运动
GetCharacterMovement()->bOrientRotationToMovement = true;
// 可以通过获取角色移动旋转速率来控制旋转速率。
// 角色就会以这个速度400,在偏航旋转方向上运动,角色运动可以迫使我们将运动限制在一个平面上。
// yaw():航向,将物体绕Y轴旋转
GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f);
// 角色被捕捉到平面
GetCharacterMovement()->bConstrainToPlane = true;
// 在开始时捕捉到平面
GetCharacterMovement()->bSnapToPlaneAtStart = true;
// 角色本身不应该使用控制器的旋转
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
bUseControllerRotationYaw = false;
CharacterClass = ECharacterClass::Elementalist;
}
// 默认只在服务端运行,不会复制
void AAuraCharacter::LevelUp_Implementation()
{
// 会复制到客户端
MulticastLevelUpParticles();
}
void AAuraCharacter::MulticastLevelUpParticles_Implementation() const
{
if (IsValid(LevelUpNiagaraComponent))
{
const FVector CameraLocation = TopDownCameraComponent->GetComponentLocation();
const FVector NiagaraSystemLocation = LevelUpNiagaraComponent->GetComponentLocation();
const FRotator ToCameraRotation = (CameraLocation - NiagaraSystemLocation).Rotation();
// Niagara 朝向相机旋转 使粒子特效面向相机,屏幕
LevelUpNiagaraComponent->SetWorldRotation(ToCameraRotation);
// 激活
LevelUpNiagaraComponent->Activate(true);
}
}
Niagara - Niagara 系统资产-NS_LevelUp
将盒体碰撞移动到 CameraBoom 弹簧臂子级
删除蓝图添加的弹簧臂 SpringArm 和相机组件 Camera
SpringArm-目标臂长度-800 绝对旋转-0,-45,0 位置-0,0,0 否则弹簧臂将抖动
右键-用户界面-控件蓝图-AuraUserWidget WBP_LevelUpMessage
Content/Blueprints/UI/Overlay/Subwidget/WBP_LevelUpMessage.uasset
设计器:
内容自动换行
外观-尺寸-1900,150
插槽-填充-填充空白空间-启用
插槽-填充-填充空白空间-启用
水平居中对齐 垂直对齐
将文本中对齐
水平居中对齐 垂直对齐 插槽-填充-填充空白空间-启用 强制换新行-启用
插槽-填充-填充空白空间-不启用 强制换新行-启用
水平向右对齐 垂直对齐
将文本中对齐
插槽-填充-填充空白空间-不启用 强制换新行-不启用
水平向左对齐 垂直对齐
将文本中对齐
打开 WBP_Overlay
图表: 订阅升级广播
BPOverlayWidgetController Assign On Player Level Changed Delegate OnPlayerLevelChangedDelegate_事件 重命名为 OnLevelChanged
创建控件 create widget 选择类 WBP_LevelUpMessage 提升为变量 LevelUpWidget 用以在创建之前检查,如果存在,则销毁 BPOverlayWidgetController get player controller get Text_Level setText(Text)
LevelUpWidget 创建之前检查,如果存在,则销毁 LevelUpWidget 转换为有效get remove from parent
添加到视口 LevelUpWidget add to viewport
打开 WBP_LevelUpMessage event construct delay remove from parent
Assets/Sounds/LevelUp/sfx_Template_single.sfx_Template_single'
sfx_Template_single 重命名为 sfx_LevelUpSound
play sound 2D play sound 2D-sound-sfx_LevelUpSound
添加动画 MessageAnimation 添加轨道 VerticalBox_Message
VerticalBox_Message 添加变换
缩放: 时间轴0:x 0, y 0 时间轴0。5:x 1 , y 1
play animation get MessageAnimation
Source/Aura/Public/Player/AuraPlayerState.h
public:
// 属性点变更委托
FOnPlayerStatChanged OnAttributePointsChangedDelegate;
// 技能点变更委托
FOnPlayerStatChanged OnSpellPointsChangedDelegate;
FORCEINLINE int32 GetAttributePoints() const { return AttributePoints; }
FORCEINLINE int32 GetSpellPoints() const { return SpellPoints; }
void AddToAttributePoints(int32 InPoints);
void AddToSpellPoints(int32 InPoints);
private:
UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_AttributePoints)
int32 AttributePoints = 0;
UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_SpellPoints)
int32 SpellPoints = 1;
UFUNCTION()
void OnRep_AttributePoints(int32 OldAttributePoints);
UFUNCTION()
void OnRep_SpellPoints(int32 OldSpellPoints);
Source/Aura/Private/Player/AuraPlayerState.cpp
// 注册Level,XP ,属性点,技能点变量以进行复制
void AAuraPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AAuraPlayerState, Level);
DOREPLIFETIME(AAuraPlayerState, XP);
DOREPLIFETIME(AAuraPlayerState, AttributePoints);
DOREPLIFETIME(AAuraPlayerState, SpellPoints);
}
void AAuraPlayerState::OnRep_AttributePoints(int32 OldAttributePoints)
{
OnAttributePointsChangedDelegate.Broadcast(AttributePoints);
}
void AAuraPlayerState::OnRep_SpellPoints(int32 OldSpellPoints)
{
OnSpellPointsChangedDelegate.Broadcast(SpellPoints);
}
void AAuraPlayerState::AddToAttributePoints(int32 InPoints)
{
AttributePoints += InPoints;
OnAttributePointsChangedDelegate.Broadcast(AttributePoints);
}
void AAuraPlayerState::AddToSpellPoints(int32 InPoints)
{
SpellPoints += InPoints;
OnSpellPointsChangedDelegate.Broadcast(SpellPoints);
}
将 Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
// 定义 玩家统计数据变更委托
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerStatChangedSignature, int32, NewValue);
移动到基类 Source/Aura/Public/UI/WidgetController/AuraWidgetController.h
用以共享
Source/Aura/Public/UI/WidgetController/AttributeMenuWidgetController.h
public:
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnPlayerStatChangedSignature AttributePointsChangedDelegate;
Source/Aura/Private/UI/WidgetController/AttributeMenuWidgetController.cpp
#include "Player/AuraPlayerState.h"
void UAttributeMenuWidgetController::BindCallbacksToDependencies()
{
UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
check(AttributeInfo);
for (auto& Pair : AS->TagsToAttributes)
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Pair.Value()).AddLambda(
[this, Pair](const FOnAttributeChangeData& Data)
{
BroadcastAttributeInfo(Pair.Key, Pair.Value());
}
);
}
AAuraPlayerState* AuraPlayerState = CastChecked<AAuraPlayerState>(PlayerState);
AuraPlayerState->OnAttributePointsChangedDelegate.AddLambda(
[this](int32 Points)
{
AttributePointsChangedDelegate.Broadcast(Points);
}
);
}
Source/Aura/Private/UI/WidgetController/AttributeMenuWidgetController.cpp
void UAttributeMenuWidgetController::BroadcastInitialValues()
{
// 属性集
UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
// 检查属性信息资产在蓝图中是否设置
check(AttributeInfo);
for (auto& Pair : AS->TagsToAttributes)
{
BroadcastAttributeInfo(Pair.Key, Pair.Value());
}
AAuraPlayerState* AuraPlayerState = CastChecked<AAuraPlayerState>(PlayerState);
AttributePointsChangedDelegate.Broadcast(AuraPlayerState->GetAttributePoints());
}
复制 WBP_TextValueRow 为 WBP_AttributePointsRow
Content/Blueprints/UI/AttributeMenu/WBP_AttributePointsRow.uasset
打开 WBP_AttributePointsRow
设计器: 删除 命名的插槽
TextBlock_label: 内容-文本:属性点
图表: 删除 订阅属性标签事件,接受符合当前栏的标签的属性,更新属性名称和值 删除 AttributeTag
Event Widget Controller Set get widget controller
cast to BP_AttributeMenuWidgetController 提升为变量 BPAttributeMenuWidgetController
assign Attribute Points Changed Delegate
WBP_FramedValue get TextBlock_Value setText(Text)
设计器: 属性下的控件 WBP Text Value Button Row 替换为 WBP_AttributePointsRow
图表: 在广播初始值之前需要先为 WBP_AttributePointsRow 设置控件控制器, 才能使 WBP_AttributePointsRow 监听到初始值
WBP_AttributePointsRow set widget controller
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::AddToAttributePoints_Implementation(int32 InAttributePoints)
{
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
AuraPlayerState->AddToAttributePoints(InAttributePoints);
}
void AAuraCharacter::AddToSpellPoints_Implementation(int32 InSpellPoints)
{
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
AuraPlayerState->AddToSpellPoints(InSpellPoints);
}
现在每次升级都会获取属性点。
图表: 添加函数 SetButtonEnabled 输入参数 Enabled 布尔
WBP_Button get button set is enabled
只需要在一个地方监听属性点变更,控制所有 WBP_TextValueButtonRow 按钮状态。 替代 在 每个 WBP_TextValueButtonRow 中监听。
打开 WBP_AttributeMenu 图表:
get Attribute Menu Widget Controller 提升为变量 AttributeMenuWidgetController AttributeMenuWidgetController AttributeMenuWidgetController
广播初始值将放置到最末端。
AttributeMenuWidgetController 监听属性点变更 assign Attribute Points Changed Delegate 事件重命名为 AttributePointsChanged
添加函数接收监听返回值 SetButtonEnabled 输入参数 AttributePoints 整数 branch
主属性按钮 Row_Strength Row_Intelligence Row_Resilience Row_Vigor
Row_Strength-SetButtonEnabled
主事件:
现在升级后,属性点不为0,主属性按钮将可用
为属性增加按钮创建蓝图可调用函数
点击按钮后,发送按钮的游戏标签信息到控件控制器,附带按钮信息。 控件控制器使用技能系统组件通过标签更新对应的属性值。 因为技能系统组件能够执行与属性相关的操作,例如应用效果或发送游戏事件。 所以我要在技能系统组件上创建一个函数,我们可以调用它来升级属性。
当我们在小部件控制器中单击此按钮时,会调用系统组件的函数UpgradeAttribute, 我们可能在客户端,也可能在服务器上。 如果在客户端。要确保服务器上也执行此函数内的功能。 发送游戏事件,之后通过被动技能监听该事件,为效果设置 set by caller 修改器的值,应用到技能。 更新标签对应的属性点。 同时更新属性点总数。 在我们说向服务器发送 RPC 之前,我们应该在升级属性中检查一下确保我们有足够的属性点。 技能系统组件不应依赖于玩家状态等事务。 应依赖玩家接口。
因为技能系统组件不应依赖于玩家状态
Source/Aura/Public/Interaction/PlayerInterface.h
public:
UFUNCTION(BlueprintNativeEvent)
int32 GetAttributePoints() const;
UFUNCTION(BlueprintNativeEvent)
int32 GetSpellPoints() const;
因为技能系统组件不应依赖于玩家状态
Source/Aura/Public/Character/AuraCharacter.h
public:
virtual int32 GetAttributePoints_Implementation() const override;
virtual int32 GetSpellPoints_Implementation() const override;
Source/Aura/Private/Character/AuraCharacter.cpp
int32 AAuraCharacter::GetAttributePoints_Implementation() const
{
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
return AuraPlayerState->GetAttributePoints();
}
int32 AAuraCharacter::GetSpellPoints_Implementation() const
{
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
return AuraPlayerState->GetSpellPoints();
}
如果actor实现了玩家接口,玩家接口检查属性点是否大于0
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
// 客户端 通过标签更新对应属性的值
// 要确保服务器上也执行此函数内的功能。
void UpgradeAttribute(const FGameplayTag& AttributeTag);
// 服务器上执行 更新标签对应的属性点,并更新玩家状态上的总属性点
UFUNCTION(Server, Reliable)
void ServerUpgradeAttribute(const FGameplayTag& AttributeTag);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
#include "AbilitySystemBlueprintLibrary.h"
#include "Interaction/PlayerInterface.h"
void UAuraAbilitySystemComponent::UpgradeAttribute(const FGameplayTag& AttributeTag)
{
if (GetAvatarActor()->Implements<UPlayerInterface>())
{
if (IPlayerInterface::Execute_GetAttributePoints(GetAvatarActor()) > 0)
{
ServerUpgradeAttribute(AttributeTag);
}
}
}
void UAuraAbilitySystemComponent::ServerUpgradeAttribute_Implementation(const FGameplayTag& AttributeTag)
{
FGameplayEventData Payload;
Payload.EventTag = AttributeTag;
Payload.EventMagnitude = 1.f;
// 发送游戏事件,之后通过被动技能监听该事件,为效果设置 set by caller 修改器的值,应用到技能
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(GetAvatarActor(), AttributeTag, Payload);
if (GetAvatarActor()->Implements<UPlayerInterface>())
{
IPlayerInterface::Execute_AddToAttributePoints(GetAvatarActor(), -1);
}
}
点击按钮后,发送按钮的游戏标签信息到控件控制器,附带按钮信息。 控件控制器使用技能系统组件通过标签更新对应的属性值。 因为技能系统组件能够执行与属性相关的操作,例如应用效果或发送游戏事件。 所以我要在技能系统组件上创建一个函数,我们可以调用它来升级属性。
Source/Aura/Public/UI/WidgetController/AttributeMenuWidgetController.h
struct FGameplayTag;
public:
// 通过标签更新指定属性
UFUNCTION(BlueprintCallable)
void UpgradeAttribute(const FGameplayTag& AttributeTag);
Source/Aura/Private/UI/WidgetController/AttributeMenuWidgetController.cpp
#include "AbilitySystem/AuraAbilitySystemComponent.h"
void UAttributeMenuWidgetController::UpgradeAttribute(const FGameplayTag& AttributeTag)
{
UAuraAbilitySystemComponent* AuraASC = CastChecked<UAuraAbilitySystemComponent>(AbilitySystemComponent);
AuraASC->UpgradeAttribute(AttributeTag);
}
此技能的技能效果中需使用set by caller 指定修改器类型。
每个 Set By Caller修改器的值 将有调用者在技能中通过监听对应标签事件,获取值,设置值。 增加按钮调用UpgradeAttribute触发件控制器事件。
1 Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.Strength Gameplay Effect-Modifiers--Modifier Op-Add Gameplay Effect-Modifiers--Modifier Magnitude-Magnitude calculation Type -Set By Caller Set by Caller Magnitude 由调用者设置大小 Data Name Data Tag -Attributes.Primary.Strength 使带此效果的技能可以监听带此标签的事件 通过在技能中使用 wait gameplay event 监听该事件。
2 Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.Intelligence Gameplay Effect-Modifiers--Modifier Op-Add Gameplay Effect-Modifiers--Modifier Magnitude-Magnitude calculation Type -Set By Caller Set by Caller Magnitude 由调用者设置大小 Data Name Data Tag -Attributes.Primary.Intelligence 使带此效果的技能可以监听带此标签的事件 通过在技能中使用 wait gameplay event 监听该事件。
3 Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.Resilience Gameplay Effect-Modifiers--Modifier Op-Add Gameplay Effect-Modifiers--Modifier Magnitude-Magnitude calculation Type -Set By Caller Set by Caller Magnitude 由调用者设置大小 Data Name Data Tag -Attributes.Primary.Resilience 使带此效果的技能可以监听带此标签的事件 通过在技能中使用 wait gameplay event 监听该事件。
4 Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.Vigor Gameplay Effect-Modifiers--Modifier Op-Add Gameplay Effect-Modifiers--Modifier Magnitude-Magnitude calculation Type -Set By Caller Set by Caller Magnitude 由调用者设置大小 Data Name Data Tag -Attributes.Primary.Vigor 使带此效果的技能可以监听带此标签的事件 通过在技能中使用 wait gameplay event 监听该事件。
打开 WBP_TextValueButtonRow
图表: sequence get Attribute Menu Widget Controller 提升为变量 AttributeMenuWidgetController AttributeMenuWidgetController
WBP_Button get Button assign on clicked
AttributeMenuWidgetController UpgradeAttribute get Attribute Tag
现在 点击增加按钮可以消耗总属性点,增加指定主属性的点。 对应的辅助属性也会增加。
游戏后处理效果完成后,等级才会实际增加,此时获取的最大健康值依然不是最新的值
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
public:
// 游戏属性后执行,在游戏属性改变后执行,用以获取最新属性
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;
private:
// 是否设置最大健康值,魔力值的标签
// 仅在升级时可以设置最大值
// 其他效果引起的最大健康值变更不加满健康
bool bTopOffHealth = false;
bool bTopOffMana = false;
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
// 仅在服务器端执行
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 获取发生改变的属性-Health
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
}
//
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 正确夹紧属性
// 这发生在游戏效果应用之后
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 打印日志
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
// IncomingDamage 属性只在服务器执行获取,不会复制到客户端
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
CombatInterface->Die();
}
// 死亡后发送XP事件
SendXPEvent(Props);
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
// 是否暴击,格挡,显示提示文本
const bool bBlock = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
ShowFloatingText(Props, LocalIncomingDamage, bBlock, bCriticalHit);
}
}
if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
{
// 本地存储XP元属性副本
const float LocalIncomingXP = GetIncomingXP();
// 原XP元属性归零
SetIncomingXP(0.f);
//TODO: 是否应该升级
// 为伤害来源添加经验
// Source Character is the owner, since GA_ListenForEvents applies GE_EventBasedEffect, adding to IncomingXP
if (Props.SourceCharacter->Implements<UPlayerInterface>() && Props.SourceCharacter->Implements<UCombatInterface>())
{
const int32 CurrentLevel = ICombatInterface::Execute_GetPlayerLevel(Props.SourceCharacter);
const int32 CurrentXP = IPlayerInterface::Execute_GetXP(Props.SourceCharacter);
const int32 NewLevel = IPlayerInterface::Execute_FindLevelForXP(Props.SourceCharacter, CurrentXP + LocalIncomingXP);
const int32 NumLevelUps = NewLevel - CurrentLevel;
if (NumLevelUps > 0)
{
const int32 AttributePointsReward = IPlayerInterface::Execute_GetAttributePointsReward(Props.SourceCharacter, CurrentLevel);
const int32 SpellPointsReward = IPlayerInterface::Execute_GetSpellPointsReward(Props.SourceCharacter, CurrentLevel);
// 游戏后处理效果完成后,等级才会实际增加,此时获取的最大健康值依然不是最新的值
IPlayerInterface::Execute_AddToPlayerLevel(Props.SourceCharacter, NumLevelUps);
IPlayerInterface::Execute_AddToAttributePoints(Props.SourceCharacter, AttributePointsReward);
IPlayerInterface::Execute_AddToSpellPoints(Props.SourceCharacter, SpellPointsReward);
// 升级后,设置可以设置健康值,魔力值为最大值
bTopOffHealth = true;
bTopOffMana = true;
IPlayerInterface::Execute_LevelUp(Props.SourceCharacter);
}
IPlayerInterface::Execute_AddToXP(Props.SourceCharacter, LocalIncomingXP);
}
}
}
void UAuraAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
{
Super::PostAttributeChange(Attribute, OldValue, NewValue);
// 此时获取到了最新的最大健康值,魔力值
// 仅在升级时可以设置为最大值,加满健康值,魔力值
if (Attribute == GetMaxHealthAttribute() && bTopOffHealth)
{
SetHealth(GetMaxHealth());
bTopOffHealth = false;
}
if (Attribute == GetMaxManaAttribute() && bTopOffMana)
{
SetMana(GetMaxMana());
bTopOffMana = false;
}
}
否则或提示异常 未设置 Set By Caller。 C++具由设置Set By Caller默认值选项,蓝图没有。
打开 GA_ListenForEvent 添加 gameplay 标签数组变量 EventTags
EventTags 默认值 添加5个标签
Attributes.Meta.IncomingXP Attributes.Primary.Strength; Attributes.Primary.Intelligence; Attributes.Primary.Resilience; Attributes.Primary.Vigor;
监听到标签事件后循环该数组,未接受的标签设置 Set By Caller 为 0
收到的Event Tag提升为变量 Event Tag
收到的Event Magnitude 修改器值提升为变量 Event Magnitude
提升技能规格为 变量 Spec
循环 EventTags for each loop branch matches tag EventTag
Spec EventTag Event Magnitude
Assign Tag Set by Caller Magnitude Spec
Spec
设计器: 选中-Button 添加 点击时,悬浮时 事件
图表: On Clicked (Button) play sound 2D play sound 2D-sound-使用音效 SFX_UI_ButtonClick_01
play sound 2D-sound 提升为变量 OnClickedSound 公开
On Hovered (Button) play sound 2D play sound 2D-sound-使用音效 SFX_UI_Hover_01
play sound 2D-sound 提升为变量 OnHoveredSound 公开
设计器: AttributeMenuButton-细节-On Clicked Sound-SFX_UI_ButtonClick_01
On Hovered Sound-SFX_UI_Hover_01
复制 WBP_ValueGlobe 为 WBP_SpellGlobe_Button
Content/Blueprints/UI/SpellGlobes/WBP_SpellGlobe_Button.uasset
设计器: Image_Background-默认 MI_LockedBG
图表: UpdateBackground 函数 BackgroundBrush-默认 MI_LockedBG
设计器: 添加图像控件:Image_Icon 水平填充,垂直填充 默认Locked
添加 按钮控件:Button_Ring 水平填充,垂直填充
外观-样式-普通-绘制为-图像 图像-SkillRing_1 因为默认的盒体有边框
外观-样式-已悬停-绘制为-图像 图像-SkillRing_1_Glow
外观-样式-已按压-绘制为-图像 图像-SkillRing_1_Glow 着色-变暗
外观-样式-已禁用-绘制为-图像 图像-SkillRing_1
添加图像控件:Image_Selection 水平填充,垂直填充 图像-SelectionCircle 填充-4,2,4,2 渲染-渲染不透明度-0
添加动画 SelectAnimation 添加轨道 Image_Selection 轨道添加变换
缩放: 0 - x 1.55 ,y 1.5
0.05 - x 1.3 ,y 1.25
WBP_OffensiveSpellTree
Content/Blueprints/UI/SpellMenu/WBP_OffensiveSpellTree.uasset
设计器: 填充屏幕-所需
尺寸框:SizeBox_Root
图表: Event pre construct: 更改变量时触发 SizeBox_Root 布局-尺寸框-set width override
SizeBox_Root 布局-尺寸框-set height override
In Width Override 提升为变量 BoxWidth 默认525 In Height Override 提升为变量 BoxHeight 默认350 分组 SpellTreeProperties
折叠到函数 UpdateBoxSize
设计器: 添加控件 包裹框:WrapBox_Root 水平居中对齐,垂直居中对齐
内部布局-朝向-垂直
添加控件 包裹框:WrapBox_Col_1 添加控件 包裹框:WrapBox_Col_2 添加控件 包裹框:WrapBox_Col_3 水平居中对齐,垂直居中对齐 内部布局-朝向-垂直
添加控件 WBP_SpellGlobe_Button *9
添加控件图像 *6 默认图像-Line 图像大小-32,15
添加控件间隔区 *2 尺寸-50,100
打开 WBP_Overlay
添加控件 WBP_OffensiveSpellTree
复制 WBP_OffensiveSpellTree 为
Content/Blueprints/UI/SpellMenu/WBP_PassiveSpellTree.uasset
SizeBox_Root 525 * 90
BoxHeight ,默认值 90
添加控件 WBP_SpellGlobe_Button *3 BoxWidth-60 BoxHeight-60
glass padding -4.5
添加控件间隔区 *2 尺寸-100,50
打开 WBP_Overlay
添加控件 WBP_PassiveSpellTree
打开 WBP_SpellGlobe_Button 将 Button_Ring 置于末端,最顶层
复制 WBP_PassiveSpellTree 为 WBP_EquippedSpellRow
Content/Blueprints/UI/SpellMenu/WBP_EquippedSpellRow.uasset
设计器: 添加控件 包裹框:WrapBox_Root
添加控件 vertical box垂直框:VerticalBox_Offensive
添加控件 包裹框:WrapBox_Offensive
添加控件间隔区 尺寸-12,1
添加控件 vertical box垂直框:VerticalBox_Passive
添加控件 包裹框:WrapBox_Passive 内部布局-朝向-垂直
添加控件 vertical box垂直框:Box_LMB
添加控件 文本: 文本-内容-LMB 尺寸-10 将文本中对齐
添加控件 WBP_SpellGlobe_Button BoxWidth-50 BoxHeight-50 glass padding -4
复制 Box_LMB 全部为 Box_RMB 复制 Box_LMB 全部为 Box_1 复制 Box_LMB 全部为 Box_2 复制 Box_LMB 全部为 Box_3 复制 Box_LMB 全部为 Box_4
添加控件间隔区 *5 尺寸-5,1
添加控件 WBP_SpellGlobe_Button *2 BoxWidth-35 BoxHeight-35 glass padding -3
打开 WBP_Overlay
添加控件 WBP_EquippedSpellRow
复制 WBP_AttributeMenu 为 WBP_SpellMenu
Content/Blueprints/UI/SpellMenu/WBP_SpellMenu.uasset
打开 WBP_Overlay
添加控件 WBP_SpellMenu 删除测试控件
WBP_Overlay 删除测试控件
点击主界面的技能按钮时,将技能按钮禁用,创建技能控件,显示在视口。
添加 WBP Wide Button:SpellMenuButton 图表: SpellMenuButton get button assign on clicked:OnSpellMenuButtonClicked add custom event:SpellMenuButtonClicked SpellMenuButton get button set is enabled get player controller create widget-class-WBP_SpellMenu add to viewport set position in viewport set position in viewport-y 提升为变量MenuPadding 默认25 get SizeBox_Root get width override get height override get viewport size break vector 2D
关闭按钮重命名 CloseButton
点击技能控件关闭按钮时,销毁技能控件 图表: event construct get CloseButton get button assign on clicked:OnSpellMenuButtonClicked remove from parent
添加事件分发器 SpellMenuClosed
触发一个技能控件关闭事件 event destruct call SpellMenuClosed
监听技能控件关闭事件 SpellMenuClosed 图表: assign SpellMenuClosed SpellMenuButton get button set is enabled
图表: 添加表示属性菜单是否打开的变量 AttributeMenuOpen 布尔 默认false 添加表示技能菜单是否打开的变量 SpellMenuOpen 布尔 默认 false
set AttributeMenuOpen set SpellMenuOpen
属性菜单按钮触发时,将输入模式设置为UI。 这将阻止任何输入。
get player controller set input mode UI only
所有菜单都关闭时才恢复输入模式。 branch SpellMenuOpen get player controller set input mode game and UI set input mode game and UI-hide cursor during capture-取消
branch AttributeMenuOpen get player controller set input mode game and UI set input mode game and UI-hide cursor during capture-取消
用以将选择的技能装备到技能栏
设计器: 添加控件 WBP Wide Button
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
#pragma once
#include "CoreMinimal.h"
#include "UI/WidgetController/AuraWidgetController.h"
#include "SpellMenuWidgetController.generated.h"
UCLASS()
class AURA_API USpellMenuWidgetController : public UAuraWidgetController
{
GENERATED_BODY()
public:
virtual void BroadcastInitialValues() override;
virtual void BindCallbacksToDependencies() override;
};
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
#include "UI/WidgetController/SpellMenuWidgetController.h"
void USpellMenuWidgetController::BroadcastInitialValues()
{
}
void USpellMenuWidgetController::BindCallbacksToDependencies()
{
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
DECLARE_MULTICAST_DELEGATE_OneParam(FAbilitiesGiven, UAuraAbilitySystemComponent*);
改为
// 声明赋予技能委托
DECLARE_MULTICAST_DELEGATE(FAbilitiesGiven);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
AbilitiesGivenDelegate.Broadcast(this);
改为
AbilitiesGivenDelegate.Broadcast();
// 添加技能
// 仅在服务端运行,不会复制
void UAuraAbilitySystemComponent::AddCharacterAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupAbilities)
{
for (const TSubclassOf<UGameplayAbility> AbilityClass : StartupAbilities)
{
// 为每个技能类创建一个技能规格 暂时使用技能等级1
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
if (const UAuraGameplayAbility* AuraAbility = Cast<UAuraGameplayAbility>(AbilitySpec.Ability))
{
// 将初始技能输入标签动态加入技能规格的动态技能标签中
// 动态技能标签可在运行时修改
AbilitySpec.DynamicAbilityTags.AddTag(AuraAbility->StartupInputTag);
// 赋予技能
GiveAbility(AbilitySpec);
}
}
// 赋予技能后开始广播赋予技能委托
// 表明初始技能已赋予
// 仅在服务端运行,不会复制
bStartupAbilitiesGiven = true;
AbilitiesGivenDelegate.Broadcast();
}
void UAuraAbilitySystemComponent::OnRep_ActivateAbilities()
{
Super::OnRep_ActivateAbilities();
// 只在第一次复制激活技能时广播
if (!bStartupAbilitiesGiven)
{
bStartupAbilitiesGiven = true;
// 激活技能后在客户端广播赋予的技能信息
AbilitiesGivenDelegate.Broadcast();
}
}
Source/Aura/Public/UI/WidgetController/AuraWidgetController.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAbilityInfoSignature, const FAuraAbilityInfo&, Info);
class AAuraPlayerController;
class AAuraPlayerState;
class UAuraAbilitySystemComponent;
class UAuraAttributeSet;
class UAbilityInfo;
public:
UPROPERTY(BlueprintAssignable, Category="GAS|Messages")
FAbilityInfoSignature AbilityInfoDelegate;
void BroadcastAbilityInfo();
protected:
// 技能信息数据资产
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Widget Data")
TObjectPtr<UAbilityInfo> AbilityInfo;
Source/Aura/Private/UI/WidgetController/AuraWidgetController.cpp
#include "Player/AuraPlayerController.h"
#include "Player/AuraPlayerState.h"
#include "AbilitySystem/AuraAbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "AbilitySystem/Data/AbilityInfo.h"
void UAuraWidgetController::BroadcastAbilityInfo()
{
if (!GetAuraASC()->bStartupAbilitiesGiven) return;
FForEachAbility BroadcastDelegate;
BroadcastDelegate.BindLambda([this](const FGameplayAbilitySpec& AbilitySpec)
{
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AuraAbilitySystemComponent->GetAbilityTagFromSpec(AbilitySpec));
Info.InputTag = AuraAbilitySystemComponent->GetInputTagFromSpec(AbilitySpec);
AbilityInfoDelegate.Broadcast(Info);
});
GetAuraASC()->ForEachAbility(BroadcastDelegate);
}
AAuraPlayerController* UAuraWidgetController::GetAuraPC()
{
if (AuraPlayerController == nullptr)
{
AuraPlayerController = Cast<AAuraPlayerController>(PlayerController);
}
return AuraPlayerController;
}
AAuraPlayerState* UAuraWidgetController::GetAuraPS()
{
if (AuraPlayerState == nullptr)
{
AuraPlayerState = Cast<AAuraPlayerState>(PlayerState);
}
return AuraPlayerState;
}
UAuraAbilitySystemComponent* UAuraWidgetController::GetAuraASC()
{
if (AuraAbilitySystemComponent == nullptr)
{
AuraAbilitySystemComponent = Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent);
}
return AuraAbilitySystemComponent;
}
UAuraAttributeSet* UAuraWidgetController::GetAuraAS()
{
if (AuraAttributeSet == nullptr)
{
AuraAttributeSet = Cast<UAuraAttributeSet>(AttributeSet);
}
return AuraAttributeSet;
}
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
删除以下代码
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAbilityInfoSignature, const FAuraAbilityInfo&, Info);
UPROPERTY(BlueprintAssignable, Category="GAS|Messages")
FAbilityInfoSignature AbilityInfoDelegate;
// 技能信息数据资产
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Widget Data")
TObjectPtr<UAbilityInfo> AbilityInfo;
// 监听技能系统组件的初始化初始技能委托
void OnInitializeStartupAbilities(UAuraAbilitySystemComponent* AuraAbilitySystemComponent);
void OnXPChanged(int32 NewXP) const;
改为 void OnXPChanged(int32 NewXP)
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
#include "UI/WidgetController/OverlayWidgetController.h"
#include "AbilitySystem/AuraAbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
#include "AbilitySystem/Data/AbilityInfo.h"
#include "AbilitySystem/Data/LevelUpInfo.h"
#include "Player/AuraPlayerState.h"
void UOverlayWidgetController::BroadcastInitialValues()
{
OnHealthChanged.Broadcast(GetAuraAS()->GetHealth());
OnMaxHealthChanged.Broadcast(GetAuraAS()->GetMaxHealth());
OnManaChanged.Broadcast(GetAuraAS()->GetMana());
OnMaxManaChanged.Broadcast(GetAuraAS()->GetMaxMana());
}
void UOverlayWidgetController::BindCallbacksToDependencies()
{
// 为玩家状态的经验值变更委托绑定回调
GetAuraPS()->OnXPChangedDelegate.AddUObject(this, &UOverlayWidgetController::OnXPChanged);
// 为等级委托绑定回调
GetAuraPS()->OnLevelChangedDelegate.AddLambda(
[this](int32 NewLevel)
{
OnPlayerLevelChangedDelegate.Broadcast(NewLevel);
}
);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性变更多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(GetAuraAS()->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(GetAuraAS()->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(GetAuraAS()->GetManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnManaChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(GetAuraAS()->GetMaxManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxManaChanged.Broadcast(Data.NewValue);
}
);
// 资产标签响应函数
if (GetAuraASC())
{
// 如果技能系统组件初始化了初始技能,开始在控制器中初始化初始技能,广播给控件
if (GetAuraASC()->bStartupAbilitiesGiven)
{
BroadcastAbilityInfo();
}
// 否则 监听技能系统组件的赋予技能委托
else
{
GetAuraASC()->AbilitiesGivenDelegate.AddUObject(this, &UOverlayWidgetController::BroadcastAbilityInfo);
}
// 响应技能系统组件的委托时获取 传入的资产标签容器参数 AssetTags
// 传入this参数,可使用当前类中的属性和方法
GetAuraASC()->EffectAssetTags.AddLambda(
[this](const FGameplayTagContainer& AssetTags)
{
for (const FGameplayTag& Tag : AssetTags)
{
// 查找标签是否包含 Message 字符,是表示消息游戏标签
// For example, say that Tag = Message.HealthPotion
// "Message.HealthPotion".MatchesTag("Message") will return True, "Message".MatchesTag("Message.HealthPotion") will return False
FGameplayTag MessageTag = FGameplayTag::RequestGameplayTag(FName("Message"));
if (Tag.MatchesTag(MessageTag))
{
// 通过行名称/标签名称查找对应的文本消息等数据。
const FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
// 将数据表的一行数据广播
MessageWidgetRowDelegate.Broadcast(*Row);
//之后在控件蓝图时间中绑定覆盖该事件以接受该行数据
}
}
}
);
}
}
void UOverlayWidgetController::OnXPChanged(int32 NewXP)
{
const ULevelUpInfo* LevelUpInfo = GetAuraPS()->LevelUpInfo;
checkf(LevelUpInfo, TEXT("Unabled to find LevelUpInfo. Please fill out AuraPlayerState Blueprint"));
const int32 Level = LevelUpInfo->FindLevelForXP(NewXP);
const int32 MaxLevel = LevelUpInfo->LevelUpInformation.Num();
if (Level <= MaxLevel && Level > 0)
{
const int32 LevelUpRequirement = LevelUpInfo->LevelUpInformation[Level].LevelUpRequirement;
const int32 PreviousLevelUpRequirement = LevelUpInfo->LevelUpInformation[Level - 1].LevelUpRequirement;
const int32 DeltaLevelRequirement = LevelUpRequirement - PreviousLevelUpRequirement;
const int32 XPForThisLevel = NewXP - PreviousLevelUpRequirement;
const float XPBarPercent = static_cast<float>(XPForThisLevel) / static_cast<float>(DeltaLevelRequirement);
OnXPPercentChangedDelegate.Broadcast(XPBarPercent);
}
}
Source/Aura/Private/UI/WidgetController/AttributeMenuWidgetController.cpp
void UAttributeMenuWidgetController::BindCallbacksToDependencies()
{
UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
check(AttributeInfo);
for (auto& Pair : AS->TagsToAttributes)
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Pair.Value()).AddLambda(
[this, Pair](const FOnAttributeChangeData& Data)
{
BroadcastAttributeInfo(Pair.Key, Pair.Value());
}
);
}
GetAuraPS()->OnAttributePointsChangedDelegate.AddLambda(
[this](int32 Points)
{
AttributePointsChangedDelegate.Broadcast(Points);
}
);
}
void UAttributeMenuWidgetController::BroadcastInitialValues()
{
// 检查属性信息资产在蓝图中是否设置
check(AttributeInfo);
for (auto& Pair : GetAuraAS()->TagsToAttributes)
{
BroadcastAttributeInfo(Pair.Key, Pair.Value());
}
AAuraPlayerState* AuraPlayerState = CastChecked<AAuraPlayerState>(PlayerState);
AttributePointsChangedDelegate.Broadcast(AuraPlayerState->GetAttributePoints());
}
void UAttributeMenuWidgetController::BroadcastInitialValues()
{
// 检查属性信息资产在蓝图中是否设置
check(AttributeInfo);
for (auto& Pair : GetAuraAS()->TagsToAttributes)
{
BroadcastAttributeInfo(Pair.Key, Pair.Value());
}
AttributePointsChangedDelegate.Broadcast(GetAuraPS()->GetAttributePoints());
}
Source/Aura/Public/UI/HUD/AuraHUD.h
class USpellMenuWidgetController;
public:
USpellMenuWidgetController* GetSpellMenuWidgetController(const FWidgetControllerParams& WCParams);
private:
// 技能菜单
UPROPERTY()
TObjectPtr<USpellMenuWidgetController> SpellMenuWidgetController;
UPROPERTY(EditAnywhere)
TSubclassOf<USpellMenuWidgetController> SpellMenuWidgetControllerClass;
Source/Aura/Private/UI/HUD/AuraHUD.cpp
#include "UI/WidgetController/SpellMenuWidgetController.h"
USpellMenuWidgetController* AAuraHUD::GetSpellMenuWidgetController(const FWidgetControllerParams& WCParams)
{
if (SpellMenuWidgetController == nullptr)
{
SpellMenuWidgetController = NewObject<USpellMenuWidgetController>(this, SpellMenuWidgetControllerClass);
SpellMenuWidgetController->SetWidgetControllerParams(WCParams);
SpellMenuWidgetController->BindCallbacksToDependencies();
}
return SpellMenuWidgetController;
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
class USpellMenuWidgetController;
struct FWidgetControllerParams;
public:
// AAuraHUD*& 对指针的引用
// DefaultToSelf = "WorldContextObject" WorldContextObject 参数的默认值为调用者自身
UFUNCTION(BlueprintPure, Category="AuraAbilitySystemLibrary|WidgetController", meta = (DefaultToSelf = "WorldContextObject"))
static bool MakeWidgetControllerParams(const UObject* WorldContextObject, FWidgetControllerParams& OutWCParams, AAuraHUD*& OutAuraHUD);
// 返回覆盖控件控制器
// 静态函数可以直接调用.
// 因为静态函数所属的类本身可能不存在于世界上。
// 静态函数无法访问世界上存在的任何对象。
// 所以需要一个世界上下文对象。
// BlueprintPure:纯函数蓝图。它不需要执行引脚或类似的东西。它只是执行某种操作并返回结果。
UFUNCTION(BlueprintPure, Category="AuraAbilitySystemLibrary|WidgetController", meta = (DefaultToSelf = "WorldContextObject"))
static UOverlayWidgetController* GetOverlayWidgetController(const UObject* WorldContextObject);
// 返回属性菜单控件控制器
UFUNCTION(BlueprintPure, Category="AuraAbilitySystemLibrary|WidgetController", meta = (DefaultToSelf = "WorldContextObject"))
static USpellMenuWidgetController* GetSpellMenuWidgetController(const UObject* WorldContextObject);
UFUNCTION(BlueprintPure, Category="AuraAbilitySystemLibrary|WidgetController", meta = (DefaultToSelf = "WorldContextObject"))
static UAttributeMenuWidgetController* GetAttributeMenuWidgetController(const UObject* WorldContextObject);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
bool UAuraAbilitySystemLibrary::MakeWidgetControllerParams(const UObject* WorldContextObject, FWidgetControllerParams& OutWCParams, AAuraHUD*& OutAuraHUD)
{
if (APlayerController* PC = UGameplayStatics::GetPlayerController(WorldContextObject, 0))
{
OutAuraHUD = Cast<AAuraHUD>(PC->GetHUD());
if (OutAuraHUD)
{
AAuraPlayerState* PS = PC->GetPlayerState<AAuraPlayerState>();
UAbilitySystemComponent* ASC = PS->GetAbilitySystemComponent();
UAttributeSet* AS = PS->GetAttributeSet();
OutWCParams.AttributeSet = AS;
OutWCParams.AbilitySystemComponent = ASC;
OutWCParams.PlayerState = PS;
OutWCParams.PlayerController = PC;
return true;
}
}
return false;
}
UOverlayWidgetController* UAuraAbilitySystemLibrary::GetOverlayWidgetController(const UObject* WorldContextObject)
{
FWidgetControllerParams WCParams;
AAuraHUD* AuraHUD = nullptr;
if (MakeWidgetControllerParams(WorldContextObject, WCParams, AuraHUD))
{
return AuraHUD->GetOverlayWidgetController(WCParams);
}
return nullptr;
}
UAttributeMenuWidgetController* UAuraAbilitySystemLibrary::GetAttributeMenuWidgetController(
const UObject* WorldContextObject)
{
FWidgetControllerParams WCParams;
AAuraHUD* AuraHUD = nullptr;
if (MakeWidgetControllerParams(WorldContextObject, WCParams, AuraHUD))
{
return AuraHUD->GetAttributeMenuWidgetController(WCParams);
}
return nullptr;
}
USpellMenuWidgetController* UAuraAbilitySystemLibrary::GetSpellMenuWidgetController(const UObject* WorldContextObject)
{
FWidgetControllerParams WCParams;
AAuraHUD* AuraHUD = nullptr;
if (MakeWidgetControllerParams(WorldContextObject, WCParams, AuraHUD))
{
return AuraHUD->GetSpellMenuWidgetController(WCParams);
}
return nullptr;
}
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
UCLASS(BlueprintType, Blueprintable)
配置技能信息资产 ability info-DA_AbilityInfo
Content/Blueprints/UI/WidgetController/BP_SpellMenuWidgetController.uasset
配置技能信息资产 ability info-DA_AbilityInfo
SpellMenuWidgetControllerClass-BP_SpellMenuWidgetController
现在可以构建 技能控件,设置他的控件控制器。
图表: sequence get SpellMenuWidgetController set widget controller
虽然被动技能不通过输入操作来激活。
Source/Aura/Public/AuraGameplayTags.h
public:
FGameplayTag InputTag_Passive_1;
FGameplayTag InputTag_Passive_2;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
GameplayTags.InputTag_Passive_1 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.Passive.1"),
FString("Input Tag Passive Ability 1")
);
GameplayTags.InputTag_Passive_2 = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("InputTag.Passive.2"),
FString("Input Tag Passive Ability 2")
);
}
复制 WBP_SpellGlobe_Button 为 WBP_EquippedRow_Button
Content/Blueprints/UI/SpellGlobes/WBP_EquippedRow_Button.uasset
打开WBP_EquippedSpellRow WBP Spell Globe Button 替换为 WBP_EquippedRow_Button
为每个按钮重命名 Globe_LMB Globe_RMB Globe_1 Globe_2 Globe_3 Globe_4
Globe_Passive_1 Globe_Passive_2
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
void USpellMenuWidgetController::BroadcastInitialValues()
{
BroadcastAbilityInfo();
}
图表:
设置技能菜单控件控制器,设置装备栏控件控制器
get WBP_EquippedSpellRow set widget controller get widget controller
cast to BP_SpellMenuWidgetController 提升为变量 BPSpellMenuWidgetController
BPSpellMenuWidgetController
广播初始值 技能信息
BPSpellMenuWidgetController Broadcast Initial values
图表: Event Widget Controller Set
Globe_LMB Globe_RMB Globe_1 Globe_2 Globe_3 Globe_4
Globe_Passive_1 Globe_Passive_2
set widget controller get widget controller
折叠到函数 SetWidgetControllers
一旦设置了控件控制器,就可以将事件分配给技能信息委托。
图表: 控件控制器设置 get widget controller cast to BP_SpellMenuWidgetController 提升为变量 BPSpellMenuWidgetController
sequence
添加 监听事件 BPSpellMenuWidgetController assign ability info delegate
根据回调的技能信息设置技能图标 break AuraAbilituInfo
装备栏关注技能按钮的输入操作信息。不关注技能。 装备栏按钮每个技能的输入操作固定,技能不固定。 用以为技能分配输入操作。
技能栏关注技能按钮的技能信息。不关注输入操作。 因为技能栏按钮每个技能的位置固定。输入操作不固定。
添加 gameplay标签 变量 InputTag 公开。 用以在父级装备栏为每个技能按钮设置输入操作标签名
设计器: Globe_LMB-细节-InputTag-InputTag.LMB
Globe_LMB-细节-InputTag-InputTag.LMB Globe_RMB-细节-InputTag-InputTag.RMB Globe_1-细节-InputTag-InputTag.1 Globe_2-细节-InputTag-InputTag.2 Globe_3-细节-InputTag-InputTag.3 Globe_4-细节-InputTag-InputTag.4
Globe_Passive_1-细节-InputTag-InputTag.Passive.1 Globe_Passive_2-细节-InputTag-InputTag.Passive.2
通过自身的技能输入标签,在回调技能信息中通过 InputTag 找到技能信息
图表: matches tag matches tag-exact match -启用 InputTag
branch Image_Icon set brush from texture
Image_Background set brush from material
此时装备栏显示已装备的技能信息:
改变火球术技能的输入标签,此处也会改变位置。
图表:
ClearGlobe函数
Image_Icon
未装备技能的按钮显示为空。 ClearGlobe
主图表: set brush set brush make slateBrush
折叠到函数 ReceiveAbilityInfo
Source/Aura/Public/AuraGameplayTags.h
public:
// 技能状态和类型标签
// 通用技能,非施法技能
FGameplayTag Abilities_HitReact;
// 技能状态
FGameplayTag Abilities_Status_Locked;
FGameplayTag Abilities_Status_Eligible;
FGameplayTag Abilities_Status_Unlocked;
FGameplayTag Abilities_Status_Equipped;
// 技能类型
// 攻击技能类型
FGameplayTag Abilities_Type_Offensive;
// 被动技能类型
FGameplayTag Abilities_Type_Passive;
// 通用技能类型 例如死亡,受击Abilities_HitReact
FGameplayTag Abilities_Type_None;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
GameplayTags.Abilities_HitReact = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.HitReact"),
FString("Hit React Ability")
);
GameplayTags.Abilities_Status_Eligible = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Status.Eligible"),
FString("Eligible Status")
);
GameplayTags.Abilities_Status_Equipped = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Status.Equipped"),
FString("Equipped Status")
);
GameplayTags.Abilities_Status_Locked = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Status.Locked"),
FString("Locked Status")
);
GameplayTags.Abilities_Status_Unlocked = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Status.Unlocked"),
FString("Unlocked Status")
);
GameplayTags.Abilities_Type_None = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Type.None"),
FString("Type None")
);
GameplayTags.Abilities_Type_Offensive = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Type.Offensive"),
FString("Type Offensive")
);
GameplayTags.Abilities_Type_Passive = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Type.Passive"),
FString("Type Passive")
);
}
一个技能只能有一个技能状态标签
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
static FGameplayTag GetStatusFromSpec(const FGameplayAbilitySpec& AbilitySpec);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
#include "AuraGameplayTags.h"
// 添加技能
// 仅在服务端运行,不会复制
void UAuraAbilitySystemComponent::AddCharacterAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupAbilities)
{
for (const TSubclassOf<UGameplayAbility> AbilityClass : StartupAbilities)
{
// 为每个技能类创建一个技能规格 暂时使用技能等级1
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
if (const UAuraGameplayAbility* AuraAbility = Cast<UAuraGameplayAbility>(AbilitySpec.Ability))
{
// 将初始技能输入标签动态加入技能规格的动态技能标签中
// 动态技能标签可在运行时修改
AbilitySpec.DynamicAbilityTags.AddTag(AuraAbility->StartupInputTag);
AbilitySpec.DynamicAbilityTags.AddTag(FAuraGameplayTags::Get().Abilities_Status_Equipped);
// 赋予技能
GiveAbility(AbilitySpec);
}
}
// 赋予技能后开始广播赋予技能委托
// 表明初始技能已赋予
// 仅在服务端运行,不会复制
bStartupAbilitiesGiven = true;
AbilitiesGivenDelegate.Broadcast();
}
FGameplayTag UAuraAbilitySystemComponent::GetStatusFromSpec(const FGameplayAbilitySpec& AbilitySpec)
{
for (FGameplayTag StatusTag : AbilitySpec.DynamicAbilityTags)
{
if (StatusTag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("Abilities.Status"))))
{
return StatusTag;
}
}
return FGameplayTag();
}
技能树的每个技能需要知道自己分配的技能信息。状态,非输入标签。
Source/Aura/Public/AbilitySystem/Data/AbilityInfo.h
// 技能信息数据数据结构
USTRUCT(BlueprintType)
struct FAuraAbilityInfo
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag AbilityTag = FGameplayTag();
// 冷却标签
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag CooldownTag = FGameplayTag();
// 输入操作标签
// 不应公开给蓝图,应在代码中设置,通过技能获取,可以运行时改变
UPROPERTY(BlueprintReadOnly)
FGameplayTag InputTag = FGameplayTag();
// 技能状态标签
UPROPERTY(BlueprintReadOnly)
FGameplayTag StatusTag = FGameplayTag();
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<const UTexture2D> Icon = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<const UMaterialInterface> BackgroundMaterial = nullptr;
};
Source/Aura/Private/UI/WidgetController/AuraWidgetController.cpp
void UAuraWidgetController::BroadcastAbilityInfo()
{
if (!GetAuraASC()->bStartupAbilitiesGiven) return;
// TODO获取所有给定技能的信息,查找其技能信息,并将其广播到控件。
FForEachAbility BroadcastDelegate;
BroadcastDelegate.BindLambda([this](const FGameplayAbilitySpec& AbilitySpec)
{
// 需要一种方法来计算给定技能规范的技能标签。
// 在技能信息资产中根据标签查找指定技能信息
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(
AuraAbilitySystemComponent->GetAbilityTagFromSpec(AbilitySpec));
// 为技能信息指定输入标签
Info.InputTag = AuraAbilitySystemComponent->GetInputTagFromSpec(AbilitySpec);
Info.StatusTag = AuraAbilitySystemComponent->GetStatusFromSpec(AbilitySpec);
// 广播该技能信息 控件中可以监听该广播
AbilityInfoDelegate.Broadcast(Info);
});
// 技能系统组件 为每个技能规格执行该委托绑定的函数
GetAuraASC()->ForEachAbility(BroadcastDelegate);
}
图表: 添加技能标签变量识别相关技能信息 AbilityTag [gameplay标签] 公开
打开 WBP_OffensiveSpellTree
设计器: 第一列:火属性技能 第二列:光属性技能 第三列:暗属性技能
从下到上开始
第一列第一个技能 WBP Spell Globe Button 细节-AbilityTag-Abilities.Fire.FireBolt
图表: get WBP_OffensiveSpellTree set widget controller get widget controller
get WBP_PassiveSpellTree set widget controller get widget controller
图表:
Event Widget Controller Set WBP_SpellGlobe_Button 所有的技能按钮 set widget controller get widget controller
折叠到函数 SetWidgetControllers
图表: 删除覆层控件控制器 Event Widget Controller Set get widget controller cast to BP_SpellMenuWidgetController 提升为变量 BPSpellMenuWidgetController
sequence
添加 监听事件 BPSpellMenuWidgetController assign ability info delegate
根据回调的技能信息设置技能图标 break AuraAbilituInfo
通过 技能标签AbilityTag识别技能 matches tag AbilityTag branch
通过 状态标签StatusTag获取技能状态 StatusTag 提升为变量 Status branch
icon 提升为变量 AbilityIcon BackgroundMaterial 提升为变量 AbilityBackground
Status matches tag matches tag-tag two-Abilities.Status.Locked 检查是否锁定技能
锁定技能 则设置背景为锁定图片 Locked_sm Image_Icon set brush from texture set brush from texture-texture-Locked_sm
Image_Background set brush from material set brush from material-material-MI_LockedBG 折叠到函数 SetGlobeLocked
branch Status matches tag matches tag-tag two-Abilities.Status.Equipped 检查是否装备技能
则设置背景为技能图片 Locked_sm Image_Icon set brush from texture set brush from texture-texture-AbilityIcon
Image_Background set brush from material set brush from material-material-AbilityBackground 折叠到函数 SetGlobeEquippedOrUnlocked
branch Status matches tag matches tag-tag two-Abilities.Status.Eligible 检查是否可用技能
则设置背景为技能图片 Locked_sm Image_Icon set brush from texture set brush from texture-texture-AbilityIcon
Image_Background set brush from material set brush from material-material-MI_LockedBG 折叠到函数 SetGlobeEligible
branch Status matches tag matches tag-tag two-Abilities.Status.Unlocked 检查是否解锁技能
or boolean
后续使用装备技能节点
当打开一个属性菜单控件或技能菜单控件后不关闭控件,直接结束游戏,将导致此错误。
这是由于玩家控制器的销毁早于控件销毁导致。
打开 WBP_Overlay 图表: 控制器之后添加 工具-Is Valid 验证 这仅在菜单已关闭的情况下执行。否则导致错误。
技能升级时,需要等级符合条件。
初始技能不包括全部的符合升级条件的技能。
Source/Aura/Public/AbilitySystem/Data/AbilityInfo.h
class UGameplayAbility;
// 技能信息数据数据结构
USTRUCT(BlueprintType)
struct FAuraAbilityInfo
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag AbilityTag = FGameplayTag();
// 冷却标签
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag CooldownTag = FGameplayTag();
// 输入操作标签
// 不应公开给蓝图,应在代码中设置,通过技能获取,可以运行时改变
UPROPERTY(BlueprintReadOnly)
FGameplayTag InputTag = FGameplayTag();
// 技能状态标签
UPROPERTY(BlueprintReadOnly)
FGameplayTag StatusTag = FGameplayTag();
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<const UTexture2D> Icon = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<const UMaterialInterface> BackgroundMaterial = nullptr;
// 技能升级的等级条件
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
int32 LevelRequirement = 1;
// 技能本身
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TSubclassOf<UGameplayAbility> Ability;
};
Source/Aura/Public/Game/AuraGameModeBase.h
class UAbilityInfo;
public:
// 技能信息资产
UPROPERTY(EditDefaultsOnly, Category = "Ability Info")
TObjectPtr<UAbilityInfo> AbilityInfo;
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
class UAbilityInfo;
public:
// 获取技能信息资产
UFUNCTION(BlueprintCallable, Category="AuraAbilitySystemLibrary|CharacterClassDefaults")
static UAbilityInfo* GetAbilityInfo(const UObject* WorldContextObject);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
UCharacterClassInfo* UAuraAbilitySystemLibrary::GetCharacterClassInfo(const UObject* WorldContextObject)
{
const AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if (AuraGameMode == nullptr) return nullptr;
return AuraGameMode->CharacterClassInfo;
}
UAbilityInfo* UAuraAbilitySystemLibrary::GetAbilityInfo(const UObject* WorldContextObject)
{
const AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if (AuraGameMode == nullptr) return nullptr;
return AuraGameMode->AbilityInfo;
}
火球术 LevelRequirement-1
Ability-GA_FireBolt
Ability Info-DA_AbilityInfo
Source/Aura/Public/AuraGameplayTags.h
public:
FGameplayTag Abilities_Lightning_Electrocute;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
GameplayTags.Abilities_Lightning_Electrocute = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Lightning.Electrocute"),
FString("Electrocute Ability Tag")
);
}
Content/Blueprints/AbilitySystem/Aura/Abilities/Lightning/GA_Electrocute.uasset
图表: event activateAbility print string delay end ability
类默认设置 Ability tag-Abilities.Lightning.Electrocute
AbilityTag-Abilities.Lightning.Electrocute CooldownTag- InputTag- StatusTag- Icon-Shock BackgroundMaterial-MI_ShockSkillBG LevelRequirement-2 Ability-GA_Electrocute
设计器: 第二列下1: AbilityTag-Abilities.Lightning.Electrocute
一旦玩家等级到达2级,雷电技能应该显示为可用状态。可以解锁。
技能系统组件可以访问所有激活的技能和赋予的技能。
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
FGameplayAbilitySpec* GetSpecFromAbilityTag(const FGameplayTag& AbilityTag);
void UpdateAbilityStatuses(int32 Level);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
#include "AbilitySystem/Data/AbilityInfo.h"
FGameplayAbilitySpec* UAuraAbilitySystemComponent::GetSpecFromAbilityTag(const FGameplayTag& AbilityTag)
{
// 锁定技能列表,防止在循环技能列表中更改技能
FScopedAbilityListLock ActiveScopeLoc(*this);
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
for (FGameplayTag Tag : AbilitySpec.Ability.Get()->AbilityTags)
{
if (Tag.MatchesTag(AbilityTag))
{
//返回指针
return &AbilitySpec;
}
}
}
return nullptr;
}
void UAuraAbilitySystemComponent::UpdateAbilityStatuses(int32 Level)
{
UAbilityInfo* AbilityInfo = UAuraAbilitySystemLibrary::GetAbilityInfo(GetAvatarActor());
for (const FAuraAbilityInfo& Info : AbilityInfo->AbilityInformation)
{
if (!Info.AbilityTag.IsValid()) continue;
if (Level < Info.LevelRequirement) continue;
// 如果该技能尚不存在与技能系统,则赋予该技能,设置为可用状态
if (GetSpecFromAbilityTag(Info.AbilityTag) == nullptr)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(Info.Ability, 1);
AbilitySpec.DynamicAbilityTags.AddTag(FAuraGameplayTags::Get().Abilities_Status_Eligible);
GiveAbility(AbilitySpec);
// 标记技能规格
// 强制系统立即复制该技能规格,不需要等到下一次更新
// 之后可以广播到控件了
MarkAbilitySpecDirty(AbilitySpec);
}
}
}
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::AddToPlayerLevel_Implementation(int32 InPlayerLevel)
{
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
check(AuraPlayerState);
AuraPlayerState->AddToLevel(InPlayerLevel);
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(GetAbilitySystemComponent()))
{
AuraASC->UpdateAbilityStatuses(AuraPlayerState->GetPlayerLevel());
}
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
// 声明技能状态变更
DECLARE_MULTICAST_DELEGATE_TwoParams(FAbilityStatusChanged, const FGameplayTag& /*AbilityTag*/,
const FGameplayTag& /*StatusTag*/);
public:
// 技能状态变更委托
FAbilityStatusChanged AbilityStatusChanged;
protected:
// client 使其成为RPC ,如果没有client则只在服务端运行。有client 使其在服务端执行,然后复制到拥有权限的客户端
// Reliable 即使丢包也能保证复制到达客户端
// RPC 的实现必须使用约定 ClientUpdateAbilityStatus_Implementation
UFUNCTION(Client, Reliable)
void ClientUpdateAbilityStatus(const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::UpdateAbilityStatuses(int32 Level)
{
UAbilityInfo* AbilityInfo = UAuraAbilitySystemLibrary::GetAbilityInfo(GetAvatarActor());
for (const FAuraAbilityInfo& Info : AbilityInfo->AbilityInformation)
{
if (!Info.AbilityTag.IsValid()) continue;
if (Level < Info.LevelRequirement) continue;
// 如果该技能尚不存在与技能系统,则赋予该技能,设置为可用状态
if (GetSpecFromAbilityTag(Info.AbilityTag) == nullptr)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(Info.Ability, 1);
AbilitySpec.DynamicAbilityTags.AddTag(FAuraGameplayTags::Get().Abilities_Status_Eligible);
GiveAbility(AbilitySpec);
// 标记技能规格
// 强制系统立即复制该技能规格,不需要等到下一次更新
// 之后可以广播到控件了
MarkAbilitySpecDirty(AbilitySpec);
ClientUpdateAbilityStatus(Info.AbilityTag,FAuraGameplayTags::Get().Abilities_Status_Eligible);
}
}
}
// RPC
void UAuraAbilitySystemComponent::ClientUpdateAbilityStatus_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag)
{
AbilityStatusChanged.Broadcast(AbilityTag, StatusTag);
}
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
#include "AbilitySystem/AuraAbilitySystemComponent.h"
#include "AbilitySystem/Data/AbilityInfo.h"
void USpellMenuWidgetController::BindCallbacksToDependencies()
{
GetAuraASC()->AbilityStatusChanged.AddLambda([this](const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag)
{
if (AbilityInfo)
{
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
Info.StatusTag = StatusTag;
AbilityInfoDelegate.Broadcast(Info);
}
});
}
现在,当玩家升级到2级,雷电技能显示为灰色可用状态
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
public:
UPROPERTY(BlueprintAssignable)
FOnPlayerStatChangedSignature SpellPointsChanged;
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
#include "Player/AuraPlayerState.h"
void USpellMenuWidgetController::BroadcastInitialValues()
{
BroadcastAbilityInfo();
SpellPointsChanged.Broadcast(GetAuraPS()->GetSpellPoints());
}
void USpellMenuWidgetController::BindCallbacksToDependencies()
{
GetAuraASC()->AbilityStatusChanged.AddLambda([this](const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag)
{
if (AbilityInfo)
{
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
Info.StatusTag = StatusTag;
AbilityInfoDelegate.Broadcast(Info);
}
});
GetAuraPS()->OnSpellPointsChangedDelegate.AddLambda([this](int32 SpellPoints)
{
SpellPointsChanged.Broadcast(SpellPoints);
});
}
打开 WBP_SpellMenu 设计器: 技能点 WBP Framed Value 重命名为 FramedValue_SpellPoints
图表: 在广播初始值之前监听。
BPSpellMenuWidgetController assign SpellPointsChanged
FramedValue_SpellPoints get TextBlock_Value SetText(Text)
折叠到函数 SpellPointsChanged
图表: 添加函数 Select
Image_Selection set render opacity select animation play animation play sound 2D play sound 2D-sound-SFX_UI_ButtonClick_02
主图表: on clicked (Button_Ring) Select
添加函数 Deselect Image_Selection set render opacity
OnSpellGlobeSelected 添加参数 SelectedGlobe 类型 WBP_SpellGlobe_Button
点击技能按钮时调用 OnSpellGlobeSelected
OnSpellGlobeSelected-SelectedGlobe -self
打开 WBP_OffensiveSpellTree
图表: event contruct WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 至 WBP_SpellGlobe_Button_8
assign OnSpellGlobeSelected
每个按钮都执行 Deselect 事件返回的按钮执行 Select
打开 WBP_PassiveSpellTree 图表:
WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 WBP_SpellGlobe_Button_2
assign OnSpellGlobeSelected
每个按钮都执行 Deselect 事件返回的按钮执行 Select
图表:
OnOffensiveSpellGlobeSelected 每次点击攻击技能按钮最后都调用
WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 至 WBP_SpellGlobe_Button_8 Deselect
图表:
OnPassiveSpellGlobeSelected 每次点击攻击技能按钮最后都调用
WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 WBP_SpellGlobe_Button_2 Deselect
图表:
WBP_OffensiveSpellTree assign OnOffensiveSpellGlobeSelected
WBP_PassiveSpellTree DeselectAll
WBP_PassiveSpellTree assign OnPassiveSpellGlobeSelected
WBP_OffensiveSpellTree DeselectAll
设计器: Image_Selection-渲染-渲染不透明度-1
技能点为0时,禁用使用技能点按钮和装备技能按钮。 WBP_SpellMenu 打开技能菜单时,禁用使用技能点按钮和装备技能按钮 选择技能后才可以使用技能点按钮和装备技能按钮
类似空指针
Source/Aura/Public/AuraGameplayTags.h
public:
// 为技能树中为设置过技能标签的按钮定义一个空标签,表示无技能,
// 类似空指针
FGameplayTag Abilities_None;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
GameplayTags.Abilities_None = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.None"),
FString("No Ability - like the nullptr for Ability Tags")
);
}
选择技能后才可以使用技能点按钮和装备技能按钮
设计器: WBP Wide Button 技能点按钮控件重命名为 Button_SpellPoints
WBP Wide Button 装备技能按钮控件重命名为 Button_Equip
图表: 预购键时,禁用2个按钮 event pre construct
Button_SpellPoints set is enabled
Button_Equip set is enabled
为此需要一个蓝图可调用函数供选择技能使用。
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
#include "AuraGameplayTags.h"
#include "GameplayTagContainer.h"
// 定义技能树中技能按钮选择委托
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSpellGlobeSelectedSignature, bool, bSpendPointsButtonEnabled, bool,
bEquipButtonEnabled);
public:
// 技能树中技能按钮选择委托
UPROPERTY(BlueprintAssignable)
FSpellGlobeSelectedSignature SpellGlobeSelectedDelegate;
// 选择技能树中的一个攻击技能时,控件通知控件控制器,发送选择的技能的技能标签。
// 为此需要一个蓝图可调用函数供选择技能使用。
UFUNCTION(BlueprintCallable)
void SpellGlobeSelected(const FGameplayTag& AbilityTag);
private:
// 不会改变当前类的任何值,设置为静态函数
static void ShouldEnableButtons(const FGameplayTag& AbilityStatus, int32 SpellPoints,
bool& bShouldEnableSpellPointsButton, bool& bShouldEnableEquipButton);
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
void USpellMenuWidgetController::SpellGlobeSelected(const FGameplayTag& AbilityTag)
{
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
const int32 SpellPoints = GetAuraPS()->GetSpellPoints();
FGameplayTag AbilityStatus;
const bool bTagValid = AbilityTag.IsValid();
const bool bTagNone = AbilityTag.MatchesTag(GameplayTags.Abilities_None);
const FGameplayAbilitySpec* AbilitySpec = GetAuraASC()->GetSpecFromAbilityTag(AbilityTag);
const bool bSpecValid = AbilitySpec != nullptr;
if (!bTagValid || bTagNone || !bSpecValid)
{
AbilityStatus = GameplayTags.Abilities_Status_Locked;
}
else
{
AbilityStatus = GetAuraASC()->GetStatusFromSpec(*AbilitySpec);
}
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(AbilityStatus, SpellPoints, bEnableSpendPoints, bEnableEquip);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip);
}
void USpellMenuWidgetController::ShouldEnableButtons(const FGameplayTag& AbilityStatus, int32 SpellPoints, bool& bShouldEnableSpellPointsButton, bool& bShouldEnableEquipButton)
{
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
bShouldEnableSpellPointsButton = false;
bShouldEnableEquipButton = false;
if (AbilityStatus.MatchesTagExact(GameplayTags.Abilities_Status_Equipped))
{
bShouldEnableEquipButton = true;
if (SpellPoints > 0)
{
bShouldEnableSpellPointsButton = true;
}
}
else if (AbilityStatus.MatchesTagExact(GameplayTags.Abilities_Status_Eligible))
{
if (SpellPoints > 0)
{
bShouldEnableSpellPointsButton = true;
}
}
else if (AbilityStatus.MatchesTagExact(GameplayTags.Abilities_Status_Unlocked))
{
bShouldEnableEquipButton = true;
if (SpellPoints > 0)
{
bShouldEnableSpellPointsButton = true;
}
}
}
Source/Aura/Public/Player/AuraPlayerState.h
private:
UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_SpellPoints)
int32 SpellPoints = 0;
图表: BPSpellMenuWidgetController SpellGlobeSelected 传入自身的技能标签 AbilityTag
从广播中接收两个按钮的状态 图表: BPSpellMenuWidgetController assign SpellGlobeSelectedDelegate
Button_SpellPoints set is enabled
Button_Equip set is enabled 折叠到函数 SetButtonsEnabled
重命名输入参数 SpendPointsEnabled EquipEnabled
SetButtonsEnabled
打开 WBP_PassiveSpellTree 图表: Event Widget Controller Set WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 WBP_SpellGlobe_Button_2 get widget controller
折叠到函数 SetWidgetControllers
处理边缘情况,开启技能菜单时,选中一个技能,然后攻击敌人升级,获得技能点数 此时使用技能点按钮和装备技能按钮也需要实时更新状态
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
// 技能树中选中的技能的结构
struct FSelectedAbility
{
FGameplayTag Ability = FGameplayTag();
FGameplayTag Status = FGameplayTag();
};
private:
// 选中的技能信息 技能标签和技能状态
FSelectedAbility SelectedAbility = {
FAuraGameplayTags::Get().Abilities_None, FAuraGameplayTags::Get().Abilities_Status_Locked
};
//选中的技能
int32 CurrentSpellPoints = 0;
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
void USpellMenuWidgetController::BindCallbacksToDependencies()
{
GetAuraASC()->AbilityStatusChanged.AddLambda([this](const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag)
{
// 如果技能状态改变的技能是当前已选中的技能,为选中的技能按钮手动触发选择事件
if (SelectedAbility.Ability.MatchesTagExact(AbilityTag))
{
SelectedAbility.Status = StatusTag;
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(StatusTag, CurrentSpellPoints, bEnableSpendPoints, bEnableEquip);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip);
}
if (AbilityInfo)
{
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
Info.StatusTag = StatusTag;
AbilityInfoDelegate.Broadcast(Info);
}
});
GetAuraPS()->OnSpellPointsChangedDelegate.AddLambda([this](int32 SpellPoints)
{
SpellPointsChanged.Broadcast(SpellPoints);
CurrentSpellPoints = SpellPoints;
// 每当技能点改变时,手动触发已选中技能按钮的选择事件
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(SelectedAbility.Status, CurrentSpellPoints, bEnableSpendPoints, bEnableEquip);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip);
});
}
void USpellMenuWidgetController::SpellGlobeSelected(const FGameplayTag& AbilityTag)
{
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
const int32 SpellPoints = GetAuraPS()->GetSpellPoints();
FGameplayTag AbilityStatus;
const bool bTagValid = AbilityTag.IsValid();
const bool bTagNone = AbilityTag.MatchesTag(GameplayTags.Abilities_None);
const FGameplayAbilitySpec* AbilitySpec = GetAuraASC()->GetSpecFromAbilityTag(AbilityTag);
const bool bSpecValid = AbilitySpec != nullptr;
if (!bTagValid || bTagNone || !bSpecValid)
{
AbilityStatus = GameplayTags.Abilities_Status_Locked;
}
else
{
AbilityStatus = GetAuraASC()->GetStatusFromSpec(*AbilitySpec);
}
// 更新已选择的技能信息
SelectedAbility.Ability = AbilityTag;
SelectedAbility.Status = AbilityStatus;
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(AbilityStatus, SpellPoints, bEnableSpendPoints, bEnableEquip);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip);
}
解锁,升级技能
更新技能状态变更委托的参数 最终会广播技能信息到控件
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
// 声明技能状态变更
DECLARE_MULTICAST_DELEGATE_ThreeParams(FAbilityStatusChanged, const FGameplayTag& /*AbilityTag*/,
const FGameplayTag& /*StatusTag*/, int32 /*AbilityLevel*/);
public:
// 服务端RPC 在 技能系统组件执行消耗技能点,仅在服务端执行
UFUNCTION(Server, Reliable)
void ServerSpendSpellPoint(const FGameplayTag& AbilityTag);
protected:
// client 使其成为RPC ,如果没有client则只在服务端运行。有client 使其在服务端执行,然后复制到拥有权限的客户端
// Reliable 即使丢包也能保证复制到达客户端
// RPC 的实现必须使用约定 ClientUpdateAbilityStatus_Implementation
UFUNCTION(Client, Reliable)
void ClientUpdateAbilityStatus(const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag, int32 AbilityLevel);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::UpdateAbilityStatuses(int32 Level)
{
UAbilityInfo* AbilityInfo = UAuraAbilitySystemLibrary::GetAbilityInfo(GetAvatarActor());
for (const FAuraAbilityInfo& Info : AbilityInfo->AbilityInformation)
{
if (!Info.AbilityTag.IsValid()) continue;
if (Level < Info.LevelRequirement) continue;
// 如果该技能尚不存在与技能系统,则赋予该技能,设置为可用状态
if (GetSpecFromAbilityTag(Info.AbilityTag) == nullptr)
{
// 空技能规格表示这是第一次使用解锁技能,技能等级应该为 1
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(Info.Ability, 1);
AbilitySpec.DynamicAbilityTags.AddTag(FAuraGameplayTags::Get().Abilities_Status_Eligible);
GiveAbility(AbilitySpec);
// 标记技能规格
// 强制系统立即复制该技能规格,不需要等到下一次更新
// 之后可以广播到控件了
MarkAbilitySpecDirty(AbilitySpec);
ClientUpdateAbilityStatus(Info.AbilityTag,FAuraGameplayTags::Get().Abilities_Status_Eligible, 1);
}
}
}
void UAuraAbilitySystemComponent::ServerSpendSpellPoint_Implementation(const FGameplayTag& AbilityTag)
{
if (FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
{
if (GetAvatarActor()->Implements<UPlayerInterface>())
{
IPlayerInterface::Execute_AddToSpellPoints(GetAvatarActor(), -1);
}
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
FGameplayTag Status = GetStatusFromSpec(*AbilitySpec);
if (Status.MatchesTagExact(GameplayTags.Abilities_Status_Eligible))
{
AbilitySpec->DynamicAbilityTags.RemoveTag(GameplayTags.Abilities_Status_Eligible);
AbilitySpec->DynamicAbilityTags.AddTag(GameplayTags.Abilities_Status_Unlocked);
Status = GameplayTags.Abilities_Status_Unlocked;
}
else if (Status.MatchesTagExact(GameplayTags.Abilities_Status_Equipped) || Status.MatchesTagExact(
GameplayTags.Abilities_Status_Unlocked))
{
AbilitySpec->Level += 1;
}
ClientUpdateAbilityStatus(AbilityTag, Status, AbilitySpec->Level);
MarkAbilitySpecDirty(*AbilitySpec);
}
}
// RPC
void UAuraAbilitySystemComponent::ClientUpdateAbilityStatus_Implementation(
const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag, int32 AbilityLevel)
{
AbilityStatusChanged.Broadcast(AbilityTag, StatusTag, AbilityLevel);
}
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
public:
// 消耗技能点数按钮点击函数
UFUNCTION(BlueprintCallable)
void SpendPointButtonPressed();
AbilityStatusChanged 参数更新
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
void USpellMenuWidgetController::BindCallbacksToDependencies()
{
GetAuraASC()->AbilityStatusChanged.AddLambda(
[this](const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag, int32 NewLevel)
{
// 如果技能状态改变的技能是当前已选中的技能,为选中的技能按钮手动触发选择事件
if (SelectedAbility.Ability.MatchesTagExact(AbilityTag))
{
SelectedAbility.Status = StatusTag;
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(StatusTag, CurrentSpellPoints, bEnableSpendPoints, bEnableEquip);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip);
}
if (AbilityInfo)
{
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
Info.StatusTag = StatusTag;
AbilityInfoDelegate.Broadcast(Info);
}
});
GetAuraPS()->OnSpellPointsChangedDelegate.AddLambda([this](int32 SpellPoints)
{
SpellPointsChanged.Broadcast(SpellPoints);
CurrentSpellPoints = SpellPoints;
// 每当技能点改变时,手动触发已选中技能按钮的选择事件
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(SelectedAbility.Status, CurrentSpellPoints, bEnableSpendPoints, bEnableEquip);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip);
});
}
void USpellMenuWidgetController::SpendPointButtonPressed()
{
if (GetAuraASC())
{
GetAuraASC()->ServerSpendSpellPoint(SelectedAbility.Ability);
}
}
要在控件控制器设置之后执行该事件绑定
打开 WBP_SpellMenu 图表: Button_SpellPoints get button assign on clicked
BPSpellMenuWidgetController SpendPointButtonPressed
整个节点上移
现在可以解锁和升级技能
显示选定技能的当前等级和下一等级描述
设计器:
添加 Rich text block 多格式文本块 控件: RichText_Description
添加 Rich text block 多格式文本块 控件: RichText_NextLevelDescription
右键-其他-数据表格-RichTextStyleRow DT_RichTextStyle
Content/Blueprints/UI/Data/DT_RichTextStyle.uasset
添加行 Default 可以设置样式属性
添加行 Damage
设计器: RichText_Description-外观-文本样式集-DT_RichTextStyle
内容-文本-
Source/Aura/Public/AbilitySystem/Abilities/AuraGameplayAbility.h
public:
virtual FString GetDescription(int32 Level);
virtual FString GetNextLevelDescription(int32 Level);
static FString GetLockedDescription(int32 Level);
Source/Aura/Private/AbilitySystem/Abilities/AuraGameplayAbility.cpp
#include "AbilitySystem/Abilities/AuraGameplayAbility.h"
FString UAuraGameplayAbility::GetDescription(int32 Level)
{
// L宽字符
return FString::Printf(
TEXT("<Default>%s, </><Level>%d</>"),
L"Default Ability Name - LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum LoremIpsum",
Level);
}
FString UAuraGameplayAbility::GetNextLevelDescription(int32 Level)
{
return FString::Printf(TEXT("<Default>Next Level: </><Level>%d</> \n<Default>Causes much more damage. </>"), Level);
}
FString UAuraGameplayAbility::GetLockedDescription(int32 Level)
{
return FString::Printf(TEXT("<Default>Spell Locked Until Level: %d</>"), Level);
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
bool GetDescriptionsByAbilityTag(const FGameplayTag& AbilityTag, FString& OutDescription,
FString& OutNextLevelDescription);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
bool UAuraAbilitySystemComponent::GetDescriptionsByAbilityTag(const FGameplayTag& AbilityTag, FString& OutDescription,
FString& OutNextLevelDescription)
{
// 激活的技能
if (const FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
{
if (UAuraGameplayAbility* AuraAbility = Cast<UAuraGameplayAbility>(AbilitySpec->Ability))
{
OutDescription = AuraAbility->GetDescription(AbilitySpec->Level);
OutNextLevelDescription = AuraAbility->GetNextLevelDescription(AbilitySpec->Level + 1);
return true;
}
}
// 未激活
const UAbilityInfo* AbilityInfo = UAuraAbilitySystemLibrary::GetAbilityInfo(GetAvatarActor());
OutDescription = UAuraGameplayAbility::GetLockedDescription(
AbilityInfo->FindAbilityInfoForTag(AbilityTag).LevelRequirement);
OutNextLevelDescription = FString();
return false;
}
更新 能按钮选择委托,广播技能描述
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
// 定义技能树中技能按钮选择委托
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FSpellGlobeSelectedSignature, bool, bSpendPointsButtonEnabled, bool,
bEquipButtonEnabled, FString, DescriptionString, FString,
NextLevelDescriptionString);
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
void USpellMenuWidgetController::BindCallbacksToDependencies()
{
GetAuraASC()->AbilityStatusChanged.AddLambda(
[this](const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag, int32 NewLevel)
{
// 如果技能状态改变的技能是当前已选中的技能,为选中的技能按钮手动触发选择事件
if (SelectedAbility.Ability.MatchesTagExact(AbilityTag))
{
SelectedAbility.Status = StatusTag;
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(StatusTag, CurrentSpellPoints, bEnableSpendPoints, bEnableEquip);
FString Description;
FString NextLevelDescription;
GetAuraASC()->GetDescriptionsByAbilityTag(AbilityTag, Description, NextLevelDescription);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip, Description,
NextLevelDescription);
}
if (AbilityInfo)
{
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
Info.StatusTag = StatusTag;
AbilityInfoDelegate.Broadcast(Info);
}
});
GetAuraPS()->OnSpellPointsChangedDelegate.AddLambda([this](int32 SpellPoints)
{
SpellPointsChanged.Broadcast(SpellPoints);
CurrentSpellPoints = SpellPoints;
// 每当技能点改变时,手动触发已选中技能按钮的选择事件
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(SelectedAbility.Status, CurrentSpellPoints, bEnableSpendPoints, bEnableEquip);
FString Description;
FString NextLevelDescription;
GetAuraASC()->GetDescriptionsByAbilityTag(SelectedAbility.Ability, Description, NextLevelDescription);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip, Description, NextLevelDescription);
});
}
void USpellMenuWidgetController::SpellGlobeSelected(const FGameplayTag& AbilityTag)
{
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
const int32 SpellPoints = GetAuraPS()->GetSpellPoints();
FGameplayTag AbilityStatus;
const bool bTagValid = AbilityTag.IsValid();
const bool bTagNone = AbilityTag.MatchesTag(GameplayTags.Abilities_None);
const FGameplayAbilitySpec* AbilitySpec = GetAuraASC()->GetSpecFromAbilityTag(AbilityTag);
const bool bSpecValid = AbilitySpec != nullptr;
if (!bTagValid || bTagNone || !bSpecValid)
{
AbilityStatus = GameplayTags.Abilities_Status_Locked;
}
else
{
AbilityStatus = GetAuraASC()->GetStatusFromSpec(*AbilitySpec);
}
// 更新已选择的技能信息
SelectedAbility.Ability = AbilityTag;
SelectedAbility.Status = AbilityStatus;
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(AbilityStatus, SpellPoints, bEnableSpendPoints, bEnableEquip);
FString Description;
FString NextLevelDescription;
GetAuraASC()->GetDescriptionsByAbilityTag(AbilityTag, Description, NextLevelDescription);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip, Description, NextLevelDescription);
}
设计器: RichText_Description-细节-换行-自动包裹文本-启用 使文本块自动换行
RichText_NextLevelDescription-细节-换行-自动包裹文本-启用
图表: RichText_Description set text
RichText_NextLevelDescription set text
Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h
public:
virtual FString GetDescription(int32 Level) override;
virtual FString GetNextLevelDescription(int32 Level) override;
protected:
// 发射的投射物上限
UPROPERTY(EditDefaultsOnly)
int32 NumProjectiles = 5;
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
FString UAuraProjectileSpell::GetDescription(int32 Level)
{
const int32 Damage = DamageTypes[FAuraGameplayTags::Get().Damage_Fire].GetValueAtLevel(Level);
if (Level == 1)
{
return FString::Printf(TEXT("<Title>FIRE BOLT</>\n\n<Default>Launches a bolt of fire, exploding on impact and dealing: </><Damage>%d</><Default> fire damage with a chance to burn</>\n\n<Small>Level: </><Level>%d</>"), Damage, Level);
}
else
{
return FString::Printf(TEXT("<Title>FIRE BOLT</>\n\n<Default>Launches %d bolts of fire, exploding on impact and dealing: </><Damage>%d</><Default> fire damage with a chance to burn</>\n\n<Small>Level: </><Level>%d</>"), FMath::Min(Level, NumProjectiles), Damage, Level);
}
}
FString UAuraProjectileSpell::GetNextLevelDescription(int32 Level)
{
const int32 Damage = DamageTypes[FAuraGameplayTags::Get().Damage_Fire].GetValueAtLevel(Level);
return FString::Printf(TEXT("<Title>NEXT LEVEL: </>\n\n<Default>Launches %d bolts of fire, exploding on impact and dealing: </><Damage>%d</><Default> fire damage with a chance to burn</>\n\n<Small>Level: </><Level>%d</>"), FMath::Min(Level, NumProjectiles), Damage, Level);
}
将描述移动到抛射物技能子类
Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h
删除 GetDescription,GetNextLevelDescription
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
删除 GetDescription,GetNextLevelDescription
Source/Aura/Public/AbilitySystem/Abilities/AuraFireBolt.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraProjectileSpell.h"
#include "AuraFireBolt.generated.h"
UCLASS()
class AURA_API UAuraFireBolt : public UAuraProjectileSpell
{
GENERATED_BODY()
public:
virtual FString GetDescription(int32 Level) override;
virtual FString GetNextLevelDescription(int32 Level) override;
};
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBolt.cpp
#include "AbilitySystem/Abilities/AuraFireBolt.h"
#include "Aura/Public/AuraGameplayTags.h"
FString UAuraFireBolt::GetDescription(int32 Level)
{
const int32 Damage = DamageTypes[FAuraGameplayTags::Get().Damage_Fire].GetValueAtLevel(Level);
if (Level == 1)
{
return FString::Printf(
TEXT(
"<Title>FIRE BOLT</>\n\n<Default>Launches a bolt of fire, exploding on impact and dealing: </><Damage>%d</><Default> fire damage with a chance to burn</>\n\n<Small>Level: </><Level>%d</>"),
Damage, Level);
}
else
{
return FString::Printf(
TEXT(
"<Title>FIRE BOLT</>\n\n<Default>Launches %d bolts of fire, exploding on impact and dealing: </><Damage>%d</><Default> fire damage with a chance to burn</>\n\n<Small>Level: </><Level>%d</>"),
FMath::Min(Level, NumProjectiles), Damage, Level);
}
}
FString UAuraFireBolt::GetNextLevelDescription(int32 Level)
{
const int32 Damage = DamageTypes[FAuraGameplayTags::Get().Damage_Fire].GetValueAtLevel(Level);
return FString::Printf(
TEXT(
"<Title>NEXT LEVEL: </>\n\n<Default>Launches %d bolts of fire, exploding on impact and dealing: </><Damage>%d</><Default> fire damage with a chance to burn</>\n\n<Small>Level: </><Level>%d</>"),
FMath::Min(Level, NumProjectiles), Damage, Level);
}
GA_FireBolt-细节-类设置-类选项-父类-AuraFireBolt
Source/Aura/Public/AbilitySystem/Abilities/AuraGameplayAbility.h
protected:
float GetManaCost(float InLevel = 1.f) const;
float GetCooldown(float InLevel = 1.f) const;
Source/Aura/Private/AbilitySystem/Abilities/AuraGameplayAbility.cpp
#include "AbilitySystem/AuraAttributeSet.h"
float UAuraGameplayAbility::GetManaCost(float InLevel) const
{
float ManaCost = 0.f;
if (const UGameplayEffect* CostEffect = GetCostGameplayEffect())
{
for (FGameplayModifierInfo Mod : CostEffect->Modifiers)
{
if (Mod.Attribute == UAuraAttributeSet::GetManaAttribute())
{
// GetStaticMagnitudeIfPossible 仅在硬编码修改器的值或使用曲线值才有效
Mod.ModifierMagnitude.GetStaticMagnitudeIfPossible(InLevel, ManaCost);
break;
}
}
}
return ManaCost;
}
float UAuraGameplayAbility::GetCooldown(float InLevel) const
{
float Cooldown = 0.f;
if (const UGameplayEffect* CooldownEffect = GetCooldownGameplayEffect())
{
// GetStaticMagnitudeIfPossible 仅在硬编码修改器的值或使用曲线值才有效
CooldownEffect->DurationMagnitude.GetStaticMagnitudeIfPossible(InLevel, Cooldown);
}
return Cooldown;
}
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
protected:
float GetDamageByDamageType(float InLevel, const FGameplayTag& DamageType);
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
float UAuraDamageGameplayAbility::GetDamageByDamageType(float InLevel, const FGameplayTag& DamageType)
{
checkf(DamageTypes.Contains(DamageType), TEXT("GameplayAbilit [%s] does not contain DamageType [%s]"),
*GetNameSafe(this), *DamageType.ToString());
return DamageTypes[DamageType].GetValueAtLevel(InLevel);
}
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBolt.cpp
#include "AbilitySystem/Abilities/AuraFireBolt.h"
#include "Aura/Public/AuraGameplayTags.h"
FString UAuraFireBolt::GetDescription(int32 Level)
{
const int32 Damage = GetDamageByDamageType(Level, FAuraGameplayTags::Get().Damage_Fire);
const float ManaCost = FMath::Abs(GetManaCost(Level));
const float Cooldown = GetCooldown(Level);
if (Level == 1)
{
return FString::Printf(TEXT(
// Title 多双引号用法,其间的换行空格将被忽略
"<Title>FIRE BOLT</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
"<Default>Launches a bolt of fire, "
"exploding on impact and dealing: </>"
// Damage
"<Damage>%d</><Default> fire damage with"
" a chance to burn</>"),
// Values
Level,
ManaCost,
Cooldown,
Damage);
}
else
{
return FString::Printf(TEXT(
// Title
"<Title>FIRE BOLT</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
// Number of FireBolts
"<Default>Launches %d bolts of fire, "
"exploding on impact and dealing: </>"
// Damage
"<Damage>%d</><Default> fire damage with"
" a chance to burn</>"),
// Values
Level,
ManaCost,
Cooldown,
FMath::Min(Level, NumProjectiles),
Damage);
}
}
FString UAuraFireBolt::GetNextLevelDescription(int32 Level)
{
const int32 Damage = GetDamageByDamageType(Level, FAuraGameplayTags::Get().Damage_Fire);
const float ManaCost = FMath::Abs(GetManaCost(Level));
const float Cooldown = GetCooldown(Level);
return FString::Printf(TEXT(
// Title
"<Title>NEXT LEVEL: </>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
// Number of FireBolts
"<Default>Launches %d bolts of fire, "
"exploding on impact and dealing: </>"
// Damage
"<Damage>%d</><Default> fire damage with"
" a chance to burn</>"),
// Values
Level,
ManaCost,
Cooldown,
FMath::Min(Level, NumProjectiles),
Damage);
}
设计器: 添加图像到描述滚动下方 水平填充,垂直填充 全透明
图像-行为-可视性-非可命中测试(仅自身) 可见但无法进行命中测试(无法与指针交互),且不影响子项上(如有)的命中测试。 使被图像遮挡的滚动框可被滚动。
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
public:
// 自行取消选择
UFUNCTION(BlueprintCallable)
void GlobeDeselect();
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
void USpellMenuWidgetController::GlobeDeselect()
{
SelectedAbility.Ability = FAuraGameplayTags::Get().Abilities_None;
SelectedAbility.Status = FAuraGameplayTags::Get().Abilities_Status_Locked;
SpellGlobeSelectedDelegate.Broadcast(false, false, FString(), FString());
}
图表: 添加变量 Selected 布尔
Select 函数中: Selected
Deselect 函数中: Selected
主图表: 按钮点击事件中检查 Selected branch 未选择时执行后续事件
已选择时,执行控件控制器的 GlobeDeselect Deselect BPSpellMenuWidgetController GlobeDeselect
播放音效 play sound 2D-SFX_UI_Cancel_01
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
bool UAuraAbilitySystemComponent::GetDescriptionsByAbilityTag(const FGameplayTag& AbilityTag, FString& OutDescription,
FString& OutNextLevelDescription)
{
// 激活的技能
if (const FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
{
if (UAuraGameplayAbility* AuraAbility = Cast<UAuraGameplayAbility>(AbilitySpec->Ability))
{
OutDescription = AuraAbility->GetDescription(AbilitySpec->Level);
OutNextLevelDescription = AuraAbility->GetNextLevelDescription(AbilitySpec->Level + 1);
return true;
}
}
// 未激活
const UAbilityInfo* AbilityInfo = UAuraAbilitySystemLibrary::GetAbilityInfo(GetAvatarActor());
// 不可用
if (!AbilityTag.IsValid() || AbilityTag.MatchesTagExact(FAuraGameplayTags::Get().Abilities_None))
{
OutDescription = FString();
}
// 可用未解锁
else
{
OutDescription = UAuraGameplayAbility::GetLockedDescription(AbilityInfo->FindAbilityInfoForTag(AbilityTag).LevelRequirement);
}
OutNextLevelDescription = FString();
return false;
}
设计器: 添加 Overlay 用于在装备栏覆盖控件
添加 图像 :Image_OffensiveSelecttionBox 默认-OffensiveSelectionBox
添加 图像 :Image_PassiveSelecttionBox 默认-PassiveSelectionBox
添加 Image_OffensiveSelecttionBox 轨道 添加变换, 添加渲染不透明度
时间轴:0 缩放x,y-1.05,1.05 渲染不透明度-1
时间轴:1 缩放x,y-1,1 渲染不透明度-0
添加 Image_OffensiveSelecttionBox 轨道 添加渲染不透明度
时间轴:0 渲染不透明度-1
时间轴:0.75 渲染不透明度-0
添加 Image_PassiveSelecttionBox 轨道 添加变换, 添加渲染不透明度
时间轴:0 缩放x,y-1.05,1.05 渲染不透明度-1
时间轴:1 缩放x,y-1,1 渲染不透明度-0
添加 Image_PassiveSelecttionBox 轨道 添加渲染不透明度
时间轴:0 渲染不透明度-1
时间轴:0.75 渲染不透明度-0
测试循环播放动画 OffensiveSelecttionAnimation
event construct get OffensiveSelecttionAnimation play animation play animation-num loops to play-0
get PassiveSelecttionAnimation play animation play animation-num loops to play-0
删除节点
Source/Aura/Public/AbilitySystem/Data/AbilityInfo.h
// 技能信息数据数据结构
USTRUCT(BlueprintType)
struct FAuraAbilityInfo
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag AbilityTag = FGameplayTag();
// 冷却标签
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag CooldownTag = FGameplayTag();
// 技能类型标签,用以区分主动,被动技能
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag AbilityType = FGameplayTag();
// 输入操作标签
// 不应公开给蓝图,应在代码中设置,通过技能获取,可以运行时改变
UPROPERTY(BlueprintReadOnly)
FGameplayTag InputTag = FGameplayTag();
// 技能状态标签
UPROPERTY(BlueprintReadOnly)
FGameplayTag StatusTag = FGameplayTag();
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<const UTexture2D> Icon = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<const UMaterialInterface> BackgroundMaterial = nullptr;
// 技能升级的等级条件
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
int32 LevelRequirement = 1;
// 技能本身
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TSubclassOf<UGameplayAbility> Ability;
};
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWaitForEquipSelectionSignature, const FGameplayTag&, AbilityType);
public:
UPROPERTY(BlueprintAssignable)
FWaitForEquipSelectionSignature WaitForEquipDelegate;
UPROPERTY(BlueprintAssignable)
FWaitForEquipSelectionSignature StopWaitingForEquipDelegate;
UFUNCTION(BlueprintCallable)
void EquipButtonPressed();
private:
bool bWaitingForEquipSelection = false;
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
void USpellMenuWidgetController::SpellGlobeSelected(const FGameplayTag& AbilityTag)
{
if (bWaitingForEquipSelection)
{
const FGameplayTag SelectedAbilityType = AbilityInfo->FindAbilityInfoForTag(AbilityTag).AbilityType;
StopWaitingForEquipDelegate.Broadcast(SelectedAbilityType);
bWaitingForEquipSelection = false;
}
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
const int32 SpellPoints = GetAuraPS()->GetSpellPoints();
FGameplayTag AbilityStatus;
const bool bTagValid = AbilityTag.IsValid();
const bool bTagNone = AbilityTag.MatchesTag(GameplayTags.Abilities_None);
const FGameplayAbilitySpec* AbilitySpec = GetAuraASC()->GetSpecFromAbilityTag(AbilityTag);
const bool bSpecValid = AbilitySpec != nullptr;
if (!bTagValid || bTagNone || !bSpecValid)
{
AbilityStatus = GameplayTags.Abilities_Status_Locked;
}
else
{
AbilityStatus = GetAuraASC()->GetStatusFromSpec(*AbilitySpec);
}
// 更新已选择的技能信息
SelectedAbility.Ability = AbilityTag;
SelectedAbility.Status = AbilityStatus;
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(AbilityStatus, SpellPoints, bEnableSpendPoints, bEnableEquip);
FString Description;
FString NextLevelDescription;
GetAuraASC()->GetDescriptionsByAbilityTag(AbilityTag, Description, NextLevelDescription);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip, Description, NextLevelDescription);
}
void USpellMenuWidgetController::GlobeDeselect()
{
if (bWaitingForEquipSelection)
{
const FGameplayTag SelectedAbilityType = AbilityInfo->FindAbilityInfoForTag(SelectedAbility.Ability).AbilityType;
StopWaitingForEquipDelegate.Broadcast(SelectedAbilityType);
bWaitingForEquipSelection = false;
}
SelectedAbility.Ability = FAuraGameplayTags::Get().Abilities_None;
SelectedAbility.Status = FAuraGameplayTags::Get().Abilities_Status_Locked;
SpellGlobeSelectedDelegate.Broadcast(false, false, FString(), FString());
}
void USpellMenuWidgetController::EquipButtonPressed()
{
const FGameplayTag AbilityType = AbilityInfo->FindAbilityInfoForTag(SelectedAbility.Ability).AbilityType;
WaitForEquipDelegate.Broadcast(AbilityType);
bWaitingForEquipSelection = true;
}
AbilityType-Abilities.Type.Offensive
图表: 技能描述节点折叠到函数 SetDescriptions
选中技能后,控件监听 WaitForEquipDelegate 等待装备技能事件 BPSpellMenuWidgetController assign WaitForEquipDelegate
输入1-AbilityType gameplay标签类型 matches tag 检查是否主动类型 提升为本地变量 IsOffensive
禁用装备按钮 set button enabled
branch IsOffensive 如果是主动技能,循环播放 OffensiveSelecttionAnimation
WBP_EquippedSpellRow get OffensiveSelecttionAnimation play animation
如果是被动技能,循环播放 PassiveSelecttionAnimation WBP_EquippedSpellRow get PassiveSelecttionAnimation play animation
Button_Equip get button assign on clicked BPSpellMenuWidgetController EquipButtonPressed
现在,选中一个技能后,点击装备,装备会显示动画。 提示应该要选择装备的输入之一。
Image_OffensiveSelecttionBox,Image_PassiveSelecttionBox 渲染-渲染不透明度-0
图像-行为-可视性-非可命中测试(仅自身) 防止遮挡装备栏的点击事件
图表:
BPSpellMenuWidgetController assign StopWaitingForEquipDelegate
输入1-AbilityType gameplay标签类型 matches tag 检查是否主动类型 提升为本地变量 IsOffensive
WBP_EquippedSpellRow stop all animation branch IsOffensive
WBP_EquippedSpellRow 如果是主动技能,循环播放 HideOffensiveBox get HideOffensiveBox play animation play animation-num loops to play-1 隐藏动画只播放一次
如果是被动技能,循环播放 HidePassiveBox WBP_EquippedSpellRow get HidePassiveBox play animation play animation-num loops to play-1 隐藏动画只播放一次
StopWaitingForEquip
技能树种选择技能,按下装备技能按钮时 检查技能树中选中的技能是否已装备。 如果已装备该技能,存储该技能对应的插槽,即输入标签到 SelectedSlot。
否则,该技能未装备,也就没有输入标签。
创建一个游戏标签变量存储选择的装备栏位置. 按下装备按钮时,清除所选装备栏的旧技能,设置为新技能
声明技能装备委托
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
// 声明技能装备委托
DECLARE_MULTICAST_DELEGATE_FourParams(FAbilityEquipped, const FGameplayTag& /*AbilityTag*/,
const FGameplayTag& /*Status*/, const FGameplayTag& /*Slot*/,
const FGameplayTag& /*PrevSlot*/);
public:
// 技能装备委托
FAbilityEquipped AbilityEquipped;
FGameplayTag GetStatusFromAbilityTag(const FGameplayTag& AbilityTag);
FGameplayTag GetInputTagFromAbilityTag(const FGameplayTag& AbilityTag);
UFUNCTION(Server, Reliable)
void ServerEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Slot);
// 此时定义中未加 UFUNCTION(Client, Reliable) ,实际上不是客户端RPC,需要之后加上修复
void ClientEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot,
const FGameplayTag& PreviousSlot);
void ClearSlot(FGameplayAbilitySpec* Spec);
void ClearAbilitiesOfSlot(const FGameplayTag& Slot);
static bool AbilityHasSlot(FGameplayAbilitySpec* Spec, const FGameplayTag& Slot);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
FGameplayTag UAuraAbilitySystemComponent::GetStatusFromAbilityTag(const FGameplayTag& AbilityTag)
{
if (const FGameplayAbilitySpec* Spec = GetSpecFromAbilityTag(AbilityTag))
{
return GetStatusFromSpec(*Spec);
}
return FGameplayTag();
}
FGameplayTag UAuraAbilitySystemComponent::GetInputTagFromAbilityTag(const FGameplayTag& AbilityTag)
{
if (const FGameplayAbilitySpec* Spec = GetSpecFromAbilityTag(AbilityTag))
{
return GetInputTagFromSpec(*Spec);
}
return FGameplayTag();
}
void UAuraAbilitySystemComponent::ServerEquipAbility_Implementation(const FGameplayTag& AbilityTag,
const FGameplayTag& Slot)
{
if (FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
const FGameplayTag& PrevSlot = GetInputTagFromSpec(*AbilitySpec);
const FGameplayTag& Status = GetStatusFromSpec(*AbilitySpec);
// 已装备或以解锁的技能才可以装备
const bool bStatusValid = Status == GameplayTags.Abilities_Status_Equipped || Status == GameplayTags.
Abilities_Status_Unlocked;
if (bStatusValid)
{
// 将此InputTag(插槽)从具有它的任何技能中删除。
// Remove this InputTag (slot) from any Ability that has it.
ClearAbilitiesOfSlot(Slot);
// 清除此技能的插槽,以防万一,它已装备到另一个插槽
// Clear this ability's slot, just in case, it's a different slot
ClearSlot(AbilitySpec);
// Now, assign this ability to this slot 现在,将此技能分配给新选择的插槽
AbilitySpec->DynamicAbilityTags.AddTag(Slot);
if (Status.MatchesTagExact(GameplayTags.Abilities_Status_Unlocked))
{
AbilitySpec->DynamicAbilityTags.RemoveTag(GameplayTags.Abilities_Status_Unlocked);
AbilitySpec->DynamicAbilityTags.AddTag(GameplayTags.Abilities_Status_Equipped);
}
MarkAbilitySpecDirty(*AbilitySpec);
}
// 此时在服务端执行
// 需要调用客户端RPC,将服务端的信息复制到客户端
// 但此时定义中未加 UFUNCTION(Client, Reliable) ,实际上不是客户端RPC,需要之后加上修复
ClientEquipAbility(AbilityTag, GameplayTags.Abilities_Status_Equipped, Slot, PrevSlot);
}
}
void UAuraAbilitySystemComponent::ClientEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PreviousSlot)
{
AbilityEquipped.Broadcast(AbilityTag, Status, Slot, PreviousSlot);
}
void UAuraAbilitySystemComponent::ClearSlot(FGameplayAbilitySpec* Spec)
{
const FGameplayTag Slot = GetInputTagFromSpec(*Spec);
Spec->DynamicAbilityTags.RemoveTag(Slot);
MarkAbilitySpecDirty(*Spec);
}
void UAuraAbilitySystemComponent::ClearAbilitiesOfSlot(const FGameplayTag& Slot)
{
FScopedAbilityListLock ActiveScopeLock(*this);
for (FGameplayAbilitySpec& Spec : GetActivatableAbilities())
{
if (AbilityHasSlot(&Spec, Slot))
{
ClearSlot(&Spec);
}
}
}
bool UAuraAbilitySystemComponent::AbilityHasSlot(FGameplayAbilitySpec* Spec, const FGameplayTag& Slot)
{
for (FGameplayTag Tag : Spec->DynamicAbilityTags)
{
if (Tag.MatchesTagExact(Slot))
{
return true;
}
}
return false;
}
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
public:
// 选择装备栏事件回调
// 装备栏关联输入标签
// 需要参数 输入标签,技能类型
UFUNCTION(BlueprintCallable)
void SpellRowGlobePressed(const FGameplayTag& SlotTag, const FGameplayTag& AbilityType);
void OnAbilityEquipped(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot,
const FGameplayTag& PreviousSlot);
private:
// 存储从技能树中选择的技能的插槽,即技能输入标签
FGameplayTag SelectedSlot;
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
void USpellMenuWidgetController::BindCallbacksToDependencies()
{
GetAuraASC()->AbilityStatusChanged.AddLambda(
[this](const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag, int32 NewLevel)
{
// 如果技能状态改变的技能是当前已选中的技能,为选中的技能按钮手动触发选择事件
if (SelectedAbility.Ability.MatchesTagExact(AbilityTag))
{
SelectedAbility.Status = StatusTag;
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(StatusTag, CurrentSpellPoints, bEnableSpendPoints, bEnableEquip);
FString Description;
FString NextLevelDescription;
GetAuraASC()->GetDescriptionsByAbilityTag(AbilityTag, Description, NextLevelDescription);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip, Description,
NextLevelDescription);
}
if (AbilityInfo)
{
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
Info.StatusTag = StatusTag;
AbilityInfoDelegate.Broadcast(Info);
}
});
GetAuraASC()->AbilityEquipped.AddUObject(this, &USpellMenuWidgetController::OnAbilityEquipped);
GetAuraPS()->OnSpellPointsChangedDelegate.AddLambda([this](int32 SpellPoints)
{
SpellPointsChanged.Broadcast(SpellPoints);
CurrentSpellPoints = SpellPoints;
// 每当技能点改变时,手动触发已选中技能按钮的选择事件
bool bEnableSpendPoints = false;
bool bEnableEquip = false;
ShouldEnableButtons(SelectedAbility.Status, CurrentSpellPoints, bEnableSpendPoints, bEnableEquip);
FString Description;
FString NextLevelDescription;
GetAuraASC()->GetDescriptionsByAbilityTag(SelectedAbility.Ability, Description, NextLevelDescription);
SpellGlobeSelectedDelegate.Broadcast(bEnableSpendPoints, bEnableEquip, Description, NextLevelDescription);
});
}
void USpellMenuWidgetController::EquipButtonPressed()
{
const FGameplayTag AbilityType = AbilityInfo->FindAbilityInfoForTag(SelectedAbility.Ability).AbilityType;
WaitForEquipDelegate.Broadcast(AbilityType);
bWaitingForEquipSelection = true;
// 技能树中选中的技能类型
const FGameplayTag SelectedStatus = GetAuraASC()->GetStatusFromAbilityTag(SelectedAbility.Ability);
// 如果选中的是已装备的技能,存储选中技能类型的插槽,即输入标签
if (SelectedStatus.MatchesTagExact(FAuraGameplayTags::Get().Abilities_Status_Equipped))
{
SelectedSlot = GetAuraASC()->GetInputTagFromAbilityTag(SelectedAbility.Ability);
}
}
void USpellMenuWidgetController::SpellRowGlobePressed(const FGameplayTag& SlotTag, const FGameplayTag& AbilityType)
{
if (!bWaitingForEquipSelection) return;
// 根据插槽的技能类型检查所选技能。
//(不要在被动位置装备攻击性技能,反之亦然)
// Check selected ability against the slot's ability type.
// (don't equip an offensive spell in a passive slot and vice versa)
const FGameplayTag& SelectedAbilityType = AbilityInfo->FindAbilityInfoForTag(SelectedAbility.Ability).AbilityType;
// 技能树中选择的技能要和装备栏中的技能 类型一致,主动或被动
if (!SelectedAbilityType.MatchesTagExact(AbilityType)) return;
// 重要行为,必须使用服务器RPC在服务器端执行装备技能到装备栏功能
// 将选择的技能装备到新选择的插槽中
GetAuraASC()->ServerEquipAbility(SelectedAbility.Ability, SlotTag);
}
void USpellMenuWidgetController::OnAbilityEquipped(const FGameplayTag& AbilityTag, const FGameplayTag& Status,
const FGameplayTag& Slot, const FGameplayTag& PreviousSlot)
{
bWaitingForEquipSelection = false;
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
// 清空旧插槽 广播空标签技能
FAuraAbilityInfo LastSlotInfo;
LastSlotInfo.StatusTag = GameplayTags.Abilities_Status_Unlocked;
LastSlotInfo.InputTag = PreviousSlot;
LastSlotInfo.AbilityTag = GameplayTags.Abilities_None;
// 如果PreviousSlot是有效插槽,则广播空信息。仅当装备已装备的技能时
// Broadcast empty info if PreviousSlot is a valid slot. Only if equipping an already-equipped spell
AbilityInfoDelegate.Broadcast(LastSlotInfo);
// 填充新插槽
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
Info.StatusTag = Status;
Info.InputTag = Slot;
AbilityInfoDelegate.Broadcast(Info);
StopWaitingForEquipDelegate.Broadcast(AbilityInfo->FindAbilityInfoForTag(AbilityTag).AbilityType);
}
图表:
添加变量 AbilityType
gameplay标签类型
公开
设计器: 为每个按钮指定技能类型标签 可多选批量设置
Globe_LMB-细节-AbilityType-Abilities.Type.Offensive Globe_RMB-细节-AbilityType-Abilities.Type.Offensive Globe_1-细节-AbilityType-Abilities.Type.Offensive Globe_2-细节-AbilityType-Abilities.Type.Offensive Globe_3-细节-AbilityType-Abilities.Type.Offensive Globe_4-细节-AbilityType-Abilities.Type.Offensive
Globe_Passive_1-细节-AbilityType-Abilities.Type.Passive Globe_Passive_2-细节-AbilityType-Abilities.Type.Passive
打开 WBP_EquippedRow_Button
图表: Button_Ring assign on clicked BPSpellMenuWidgetController SpellRowGlobePressed InputTag AbilityType
matches tag branch ClearGlobe
否则,设置了标签的按钮,在更换装备栏插槽时,背景会空白。
打开 WBP_OffensiveSpellTree WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 至 WBP_SpellGlobe_Button_8 细节- AbilityTag-留空 但火球术,雷电术按钮除外,需要设置对应的技能。
否则,设置了标签的按钮,在更换装备栏插槽时,背景会空白。
打开 WBP_PassiveSpellTree
WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 至 WBP_SpellGlobe_Button_2 细节- AbilityTag-留空
否则,设置了标签的按钮,在更换装备栏插槽时,背景会空白。
ReceiveAbilityInfo 函数图表: 检查当前标签之后再清理
Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h
protected:
// 技能装备事件
void OnAbilityEquipped(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot,
const FGameplayTag& PreviousSlot) const;
Source/Aura/Private/UI/WidgetController/OverlayWidgetController.cpp
#include "AuraGameplayTags.h"
void UOverlayWidgetController::BindCallbacksToDependencies()
{
// 为玩家状态的经验值变更委托绑定回调
GetAuraPS()->OnXPChangedDelegate.AddUObject(this, &UOverlayWidgetController::OnXPChanged);
// 为等级委托绑定回调
GetAuraPS()->OnLevelChangedDelegate.AddLambda(
[this](int32 NewLevel)
{
OnPlayerLevelChangedDelegate.Broadcast(NewLevel);
}
);
// GetGameplayAttributeValueChangeDelegate() 返回游戏属性变更多播委托,非动态。
// 所以不能使用 AddDynamic
// 只能使用 AddUObject 将回调绑定到多播委托
// 当技能系统的属性集的Health属性变化时,将调用函数 &UOverlayWidgetController::HealthChanged
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(GetAuraAS()->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(GetAuraAS()->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(GetAuraAS()->GetManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnManaChanged.Broadcast(Data.NewValue);
}
);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(GetAuraAS()->GetMaxManaAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxManaChanged.Broadcast(Data.NewValue);
}
);
// 资产标签响应函数
if (GetAuraASC())
{
// 为技能装备绑定事件回调
GetAuraASC()->AbilityEquipped.AddUObject(this, &UOverlayWidgetController::OnAbilityEquipped);
// 如果技能系统组件初始化了初始技能,开始在控制器中初始化初始技能,广播给控件
if (GetAuraASC()->bStartupAbilitiesGiven)
{
BroadcastAbilityInfo();
}
// 否则 监听技能系统组件的赋予技能委托
else
{
GetAuraASC()->AbilitiesGivenDelegate.AddUObject(this, &UOverlayWidgetController::BroadcastAbilityInfo);
}
// 响应技能系统组件的委托时获取 传入的资产标签容器参数 AssetTags
// 传入this参数,可使用当前类中的属性和方法
GetAuraASC()->EffectAssetTags.AddLambda(
[this](const FGameplayTagContainer& AssetTags)
{
for (const FGameplayTag& Tag : AssetTags)
{
// 查找标签是否包含 Message 字符,是表示消息游戏标签
// For example, say that Tag = Message.HealthPotion
// "Message.HealthPotion".MatchesTag("Message") will return True, "Message".MatchesTag("Message.HealthPotion") will return False
FGameplayTag MessageTag = FGameplayTag::RequestGameplayTag(FName("Message"));
if (Tag.MatchesTag(MessageTag))
{
// 通过行名称/标签名称查找对应的文本消息等数据。
const FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
// 将数据表的一行数据广播
MessageWidgetRowDelegate.Broadcast(*Row);
//之后在控件蓝图时间中绑定覆盖该事件以接受该行数据
}
}
}
);
}
}
void UOverlayWidgetController::OnAbilityEquipped(const FGameplayTag& AbilityTag, const FGameplayTag& Status,
const FGameplayTag& Slot, const FGameplayTag& PreviousSlot) const
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
FAuraAbilityInfo LastSlotInfo;
LastSlotInfo.StatusTag = GameplayTags.Abilities_Status_Unlocked;
LastSlotInfo.InputTag = PreviousSlot;
LastSlotInfo.AbilityTag = GameplayTags.Abilities_None;
// Broadcast empty info if PreviousSlot is a valid slot. Only if equipping an already-equipped spell
AbilityInfoDelegate.Broadcast(LastSlotInfo);
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
Info.StatusTag = Status;
Info.InputTag = Slot;
AbilityInfoDelegate.Broadcast(Info);
}
否则,设置了标签的按钮,在更换装备栏插槽时,背景会空白。
ReceiveAbilityInfo 函数图表: 检查接收的技能信息的 AbilityTag 是否为 Ability.None 如果是,则执行清理 branch matches tag ClearGlobe
清理后将冷却标签置空 set CooldownTag
一旦装备了技能,则将该技能在技能树中取消选择。
Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h
// 技能图表重新分配
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpellGlobeReassignedSignature, const FGameplayTag&, AbilityTag);
public:
UPROPERTY(BlueprintAssignable)
FSpellGlobeReassignedSignature SpellGlobeReassignedDelegate;
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
void USpellMenuWidgetController::OnAbilityEquipped(const FGameplayTag& AbilityTag, const FGameplayTag& Status,
const FGameplayTag& Slot, const FGameplayTag& PreviousSlot)
{
bWaitingForEquipSelection = false;
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
// 清空旧插槽 广播空标签技能
FAuraAbilityInfo LastSlotInfo;
LastSlotInfo.StatusTag = GameplayTags.Abilities_Status_Unlocked;
LastSlotInfo.InputTag = PreviousSlot;
LastSlotInfo.AbilityTag = GameplayTags.Abilities_None;
// 如果PreviousSlot是有效插槽,则广播空信息。仅当装备已装备的技能时
// Broadcast empty info if PreviousSlot is a valid slot. Only if equipping an already-equipped spell
AbilityInfoDelegate.Broadcast(LastSlotInfo);
// 填充新插槽
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
Info.StatusTag = Status;
Info.InputTag = Slot;
AbilityInfoDelegate.Broadcast(Info);
StopWaitingForEquipDelegate.Broadcast(AbilityInfo->FindAbilityInfoForTag(AbilityTag).AbilityType);
SpellGlobeReassignedDelegate.Broadcast(AbilityTag);
GlobeDeselect();
}
打开 WBP_SpellGlobe_Button 图表: BPSpellMenuWidgetController assign SpellGlobeReassignedDelegate
添加函数 OnSpellGlobeReassigned 输入 AbilityTag 类型 gameplay标签
matches tag branch AbilityTag
如果标签一致,则清除选中状态。 Image_Selection set render opacity
play sound 2D-SFX_UI_Unlock_SelectSkill Selected 设为false
主图表: OnSpellGlobeReassigned
修复 重新分类技能时的取消状态音效不断累加的错误
图表: event destruct
解绑绑定技能菜单的重新分配委托 BPSpellMenuWidgetController unbind all events from SpellGlobeReassignedDelegate
解绑绑定技能信息委托 BPSpellMenuWidgetController unbind all events from AbilityInfoDelegate
图表 event destruct BPOverlayWidgetController unbind all events from onPlayerLevelChangedDelegate
图表 event destruct BPOverlayWidgetController unbind all events from AbilityInfoDelegate
图表 event destruct BPSpellMenuWidgetController BPSpellMenuWidgetController 转换为有效get unbind all events from AbilityInfoDelegate
图表 event destruct BPSpellMenuWidgetController unbind all events from SpellGlobeSelectedDelegate
BPSpellMenuWidgetController unbind all events from WaitForEquipDelegate
BPSpellMenuWidgetController unbind all events from StopWaitingForEquipDelegate
图表 event destruct AttributeMenuWidgetController unbind all events from AttributePointsChangedDelegate
不同属性的技能,造成伤害时,在一段时间内对目标持续造成伤害的减益效果。
DamageTypes 替换为 DamageType 之后的循环伤害类型代码替换为直接使用单一类型
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
protected:
// 包含多种伤害类型
// 每一组键值对对应一种伤害类型。
// 并且可扩展浮动值-例如曲线表 包含不同等级下的伤害值。
// 使技能可具有多种伤害类型,水属性伤害,火属性伤害
// UPROPERTY(EditDefaultsOnly, Category = "Damage")
// TMap<FGameplayTag, FScalableFloat> DamageTypes;
UPROPERTY(EditDefaultsOnly, Category = "Damage")
FGameplayTag DamageType;
UPROPERTY(EditDefaultsOnly, Category = "Damage")
FScalableFloat Damage;
删除 GetDamageByDamageType
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
void UAuraDamageGameplayAbility::CauseDamage(AActor* TargetActor)
{
FGameplayEffectSpecHandle DamageSpecHandle = MakeOutgoingGameplayEffectSpec(DamageEffectClass, 1.f);
// for (TTuple<FGameplayTag, FScalableFloat> Pair : DamageTypes)
// {
// // 为伤害技能标签分配可扩展伤害值
// const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(DamageSpecHandle, Pair.Key, ScaledDamage);
// }
const float ScaledDamage = Damage.GetValueAtLevel(GetAbilityLevel());
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(DamageSpecHandle, DamageType, ScaledDamage);
// 将该伤害效果赋予actor
GetAbilitySystemComponentFromActorInfo()->ApplyGameplayEffectSpecToTarget(
*DamageSpecHandle.Data.Get(), UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor));
}
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag,
bool bOverridePitch, float PitchOverride)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家指定的插槽的位置,例如武器,左右手,尾巴尖等插槽
// 参数1:实现该接口的 actor :GetAvatarActorFromActorInfo()
// 参数2:蒙太奇标签
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
SocketTag);
// 设置投射物旋转 方向 直接瞄准敌人
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
// Rotation.Pitch = 0.f; // 由于服务器与客户端的火球运行时间差异 不应将俯仰角归0,如果投射物生成位置高于敌人,投射物将高于敌人运行
if (bOverridePitch)
{
Rotation.Pitch = PitchOverride;
}
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(
GetAvatarActorFromActorInfo());
// 为游戏情景添加技能,源对象,投射物,技能命中结果。
FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
EffectContextHandle.SetAbility(this);
EffectContextHandle.AddSourceObject(Projectile);
TArray<TWeakObjectPtr<AActor>> Actors;
Actors.Add(Projectile);
EffectContextHandle.AddActors(Actors);
FHitResult HitResult;
HitResult.Location = ProjectileTargetLocation;
EffectContextHandle.AddHitResult(HitResult);
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(
DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
// 魔法投射物技能作为伤害型技能,可能拥有多种类型的伤害技能
// for (auto& Pair : DamageTypes)
// {
// // 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// // 只需要在应用时能够从游戏效果中访问该键值对
// const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// // 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
// UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
// }
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
const float ScaledDamage = Damage.GetValueAtLevel(GetAbilityLevel());
// 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, DamageType, ScaledDamage);
Projectile->DamageEffectSpecHandle = SpecHandle;
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBolt.cpp
#include "AbilitySystem/Abilities/AuraFireBolt.h"
#include "Aura/Public/AuraGameplayTags.h"
FString UAuraFireBolt::GetDescription(int32 Level)
{
// const int32 Damage = GetDamageByDamageType(Level, FAuraGameplayTags::Get().Damage_Fire);
const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
const float ManaCost = FMath::Abs(GetManaCost(Level));
const float Cooldown = GetCooldown(Level);
if (Level == 1)
{
return FString::Printf(TEXT(
// Title 多双引号用法,其间的换行空格将被忽略
"<Title>FIRE BOLT</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
"<Default>Launches a bolt of fire, "
"exploding on impact and dealing: </>"
// Damage
"<Damage>%d</><Default> fire damage with"
" a chance to burn</>"),
// Values
Level,
ManaCost,
Cooldown,
ScaledDamage);
}
else
{
return FString::Printf(TEXT(
// Title
"<Title>FIRE BOLT</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
// Number of FireBolts
"<Default>Launches %d bolts of fire, "
"exploding on impact and dealing: </>"
// Damage
"<Damage>%d</><Default> fire damage with"
" a chance to burn</>"),
// Values
Level,
ManaCost,
Cooldown,
FMath::Min(Level, NumProjectiles),
ScaledDamage);
}
}
FString UAuraFireBolt::GetNextLevelDescription(int32 Level)
{
//const int32 Damage = GetDamageByDamageType(Level, FAuraGameplayTags::Get().Damage_Fire);
const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
const float ManaCost = FMath::Abs(GetManaCost(Level));
const float Cooldown = GetCooldown(Level);
return FString::Printf(TEXT(
// Title
"<Title>NEXT LEVEL: </>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
// Number of FireBolts
"<Default>Launches %d bolts of fire, "
"exploding on impact and dealing: </>"
// Damage
"<Damage>%d</><Default> fire damage with"
" a chance to burn</>"),
// Values
Level,
ManaCost,
Cooldown,
FMath::Min(Level, NumProjectiles),
ScaledDamage);
}
类默认值: DamageType-Damage.Fire Damage-1, CT_Damage, Abilities.FireBolt
类默认值: DamageType-Damage.Fire Damage-1, CT_Damage, Abilities.FireBolt
类默认值: DamageType-Damage.Physical Damage-1, CT_Damage, Abilities.Melee
类默认值: DamageType-Damage.Physical Damage-1, CT_Damage, Abilities.Ranged
伤害类型,伤害抗性,伤害减益效果 3者对应
Source/Aura/Public/AuraGameplayTags.h
public:
// 减益效果
// 每种伤害类型都有自己的减益效果
// 火属性 燃烧效果
FGameplayTag Debuff_Burn;
// 雷电光属性 眩晕效果
FGameplayTag Debuff_Stun;
// 秘属性 秘减益
FGameplayTag Debuff_Arcane;
// 物理属性 物理减益
FGameplayTag Debuff_Physical;
// 伤害-减益效果组,为每种伤害类型技能标签映射一个减益效果类型属性标签
TMap<FGameplayTag, FGameplayTag> DamageTypesToDebuffs;
Source/Aura/Private/AuraGameplayTags.cpp
/*
* Debuffs
*/
GameplayTags.Debuff_Arcane = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Debuff.Arcane"),
FString("Debuff for Arcane damage")
);
GameplayTags.Debuff_Burn = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Debuff.Burn"),
FString("Debuff for Fire damage")
);
GameplayTags.Debuff_Physical = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Debuff.Physical"),
FString("Debuff for Physical damage")
);
GameplayTags.Debuff_Stun = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Debuff.Stun"),
FString("Debuff for Lightning damage")
);
/*
* Map of Damage Types to Debuffs
*/
GameplayTags.DamageTypesToDebuffs.Add(GameplayTags.Damage_Arcane, GameplayTags.Debuff_Arcane);
GameplayTags.DamageTypesToDebuffs.Add(GameplayTags.Damage_Lightning, GameplayTags.Debuff_Stun);
GameplayTags.DamageTypesToDebuffs.Add(GameplayTags.Damage_Physical, GameplayTags.Debuff_Physical);
GameplayTags.DamageTypesToDebuffs.Add(GameplayTags.Damage_Fire, GameplayTags.Debuff_Burn);
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
protected:
//减益效果参数
// 减益效果触发几率
UPROPERTY(EditDefaultsOnly, Category = "Damage")
float DebuffChance = 20.f;
// 减益效果伤害值
UPROPERTY(EditDefaultsOnly, Category = "Damage")
float DebuffDamage = 5.f;
// 减益效果 频率,例如 每1秒施加一次伤害
UPROPERTY(EditDefaultsOnly, Category = "Damage")
float DebuffFrequency = 1.f;
// 减益效果 持续时间
UPROPERTY(EditDefaultsOnly, Category = "Damage")
float DebuffDuration = 5.f;
Source/Aura/Public/AuraGameplayTags.h
public:
// 减益效果参数标签
// 减益效果触发几率
FGameplayTag Debuff_Chance;
// 减益效果 伤害值
FGameplayTag Debuff_Damage;
// 减益效果 持续时间
FGameplayTag Debuff_Duration;
// 减益效果 频率,例如 每1秒施加一次伤害
FGameplayTag Debuff_Frequency;
Source/Aura/Private/AuraGameplayTags.cpp
GameplayTags.Debuff_Chance = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Debuff.Chance"),
FString("Debuff Chance")
);
GameplayTags.Debuff_Damage = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Debuff.Damage"),
FString("Debuff Damage")
);
GameplayTags.Debuff_Duration = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Debuff.Duration"),
FString("Debuff Duration")
);
GameplayTags.Debuff_Frequency = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Debuff.Frequency"),
FString("Debuff Frequency")
);
Source/Aura/Public/AuraAbilityTypes.h
class UGameplayEffect;
// 伤害效果参数结构类型
USTRUCT(BlueprintType)
struct FDamageEffectParams
{
GENERATED_BODY()
FDamageEffectParams(){}
UPROPERTY()
TObjectPtr<UObject> WorldContextObject = nullptr;
UPROPERTY()
TSubclassOf<UGameplayEffect> DamageGameplayEffectClass = nullptr;
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> SourceAbilitySystemComponent;
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> TargetAbilitySystemComponent;
UPROPERTY()
float BaseDamage = 0.f;
UPROPERTY()
float AbilityLevel = 1.f;
UPROPERTY()
FGameplayTag DamageType = FGameplayTag();
UPROPERTY()
float DebuffChance = 0.f;
UPROPERTY()
float DebuffDamage = 0.f;
UPROPERTY()
float DebuffDuration = 0.f;
UPROPERTY()
float DebuffFrequency = 0.f;
};
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
#include "AuraAbilityTypes.h"
public:
FDamageEffectParams MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor = nullptr) const;
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
FDamageEffectParams UAuraDamageGameplayAbility::MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor) const
{
FDamageEffectParams Params;
Params.WorldContextObject = GetAvatarActorFromActorInfo();
Params.DamageGameplayEffectClass = DamageEffectClass;
Params.SourceAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
Params.TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
Params.BaseDamage = Damage.GetValueAtLevel(GetAbilityLevel());
Params.AbilityLevel = GetAbilityLevel();
Params.DamageType = DamageType;
Params.DebuffChance = DebuffChance;
Params.DebuffDamage = DebuffDamage;
Params.DebuffDuration = DebuffDuration;
Params.DebuffFrequency = DebuffFrequency;
return Params;
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|DamageEffect")
static FGameplayEffectContextHandle ApplyDamageEffect(const FDamageEffectParams& DamageEffectParams);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
#include "AbilitySystemBlueprintLibrary.h"
#include "AuraGameplayTags.h"
FGameplayEffectContextHandle UAuraAbilitySystemLibrary::ApplyDamageEffect(const FDamageEffectParams& DamageEffectParams)
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
const AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();
FGameplayEffectContextHandle EffectContexthandle = DamageEffectParams.SourceAbilitySystemComponent->
MakeEffectContext();
EffectContexthandle.AddSourceObject(SourceAvatarActor);
const FGameplayEffectSpecHandle SpecHandle = DamageEffectParams.SourceAbilitySystemComponent->MakeOutgoingSpec(
DamageEffectParams.DamageGameplayEffectClass, DamageEffectParams.AbilityLevel, EffectContexthandle);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, DamageEffectParams.DamageType,
DamageEffectParams.BaseDamage);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Chance,
DamageEffectParams.DebuffChance);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Damage,
DamageEffectParams.DebuffDamage);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Duration,
DamageEffectParams.DebuffDuration);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Frequency,
DamageEffectParams.DebuffFrequency);
DamageEffectParams.TargetAbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data);
return EffectContexthandle;
}
Source/Aura/Public/Actor/AuraProjectile.h
删除FGameplayEffectSpecHandle DamageEffectSpecHandle;
#include "AuraAbilityTypes.h"
public:
// 使投射物携带游戏效果参数
// 在生成时公开
UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = true))
FDamageEffectParams DamageEffectParams;
protected:
void OnHit();
Source/Aura/Private/Actor/AuraProjectile.cpp
void AAuraProjectile::OnHit()
{
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
// 此时投射物可能已销毁 循环音效也将一同销毁
if (LoopingSoundComponent) LoopingSoundComponent->Stop();
bHit = true;
}
void AAuraProjectile::Destroyed()
{
if (!bHit && !HasAuthority()) OnHit();
Super::Destroyed();
}
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();
if (SourceAvatarActor == OtherActor) return;
// 技能抛射物不能伤害友军
if (!UAuraAbilitySystemLibrary::IsNotFriend(SourceAvatarActor, OtherActor)) return;
if (!bHit) OnHit();
if (HasAuthority())
{
// 只在服务器上应用伤害效果即可
// 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端
// 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
DamageEffectParams.TargetAbilitySystemComponent = TargetASC;
UAuraAbilitySystemLibrary::ApplyDamageEffect(DamageEffectParams);
}
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
else bHit = true;
}
Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp
删除以下代码,
使用`Projectile->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();``替代以下代码
//TODO: Give the Projectile a Gameplay Effect Spec for causing Damage.
// 为投射物增加伤害效果
// 给投射物一个造成伤害的游戏效果规格
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(
GetAvatarActorFromActorInfo());
// 为游戏情景添加技能,源对象,投射物,技能命中结果。
FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
EffectContextHandle.SetAbility(this);
EffectContextHandle.AddSourceObject(Projectile);
TArray<TWeakObjectPtr<AActor>> Actors;
Actors.Add(Projectile);
EffectContextHandle.AddActors(Actors);
FHitResult HitResult;
HitResult.Location = ProjectileTargetLocation;
EffectContextHandle.AddHitResult(HitResult);
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(
DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
// GetAbilityLevel 获取当前技能等级
// 魔法投射物技能作为伤害型技能,可能拥有多种类型的伤害技能
// for (auto& Pair : DamageTypes)
// {
// // 现在这个游戏效果规格句柄携带着一个键值对,伤害游戏标签:值
// // 只需要在应用时能够从游戏效果中访问该键值对
// const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
// // 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
// UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
// }
// 从 技能中Damage属性设置的曲线表格中获取当前技能等级的伤害值
const float ScaledDamage = Damage.GetValueAtLevel(GetAbilityLevel());
// 后续可由 Spec.GetSetByCallerMagnitude(DamageTypeTag) 取出该伤害值
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, DamageType, ScaledDamage);
Projectile->DamageEffectSpecHandle = SpecHandle;
新的优化的代码
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag,
bool bOverridePitch, float PitchOverride)
{
// 只在服务端生成投射物,服务器处理投射物的移动,位置等
// 需要投射物可以网络复制,客户端可以看到复制版本
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
// 技能激活时生成投射物需要变换,位置信息时,不应依赖玩家,应依赖接口
// 获取玩家指定的插槽的位置,例如武器,左右手,尾巴尖等插槽
// 参数1:实现该接口的 actor :GetAvatarActorFromActorInfo()
// 参数2:蒙太奇标签
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
SocketTag);
// 设置投射物旋转 方向 直接瞄准敌人
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
// Rotation.Pitch = 0.f; // 由于服务器与客户端的火球运行时间差异 不应将俯仰角归0,如果投射物生成位置高于敌人,投射物将高于敌人运行
if (bOverridePitch)
{
Rotation.Pitch = PitchOverride;
}
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
//TODO: Set the Projectile Rotation
SpawnTransform.SetRotation(Rotation.Quaternion());
// 生成投射物,并为投射物设置技能效果规格等属性
// 使投射物可对其他actor施加技能效果
// 参数1:投射类
// 参数2:投射物变换位置,武器插槽处
// 参数3:投射物的所有者 可以是玩家
// 参数4:煽动者 可以是玩家
// 参数5:碰撞处理方法,例如 无论碰撞,重叠,总是生成
// 延迟生成,此时没有真正生成
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
// 在重叠时才设置目标
Projectile->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
// 完成生成投射物
Projectile->FinishSpawning(SpawnTransform);
}
Source/Aura/Public/AbilitySystem/ExecCalc/ExecCalc_Damage.h
public:
void DetermineDebuff(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
const FGameplayEffectSpec& Spec,
FAggregatorEvaluateParameters EvaluationParameters,
const TMap<FGameplayTag, FGameplayEffectAttributeCaptureDefinition>& InTagsToDefs) const;
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
删除 捕获定义映与属性标签映射
TMap<FGameplayTag, FGameplayEffectAttributeCaptureDefinition> TagsToCaptureDefs;
// 没有 F前缀,不会公开给蓝图和反射系统
// C++原始内部结构
struct AuraDamageStatics
{
// 属性捕获定义 捕获属性ArmorDef
// 创建游戏效果属性和属性捕获定义
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
DECLARE_ATTRIBUTE_CAPTUREDEF(ArmorPenetration);
// 定义格挡几率
DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitResistance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitDamage);
// 抗性属性
DECLARE_ATTRIBUTE_CAPTUREDEF(FireResistance);
DECLARE_ATTRIBUTE_CAPTUREDEF(LightningResistance);
DECLARE_ATTRIBUTE_CAPTUREDEF(ArcaneResistance);
DECLARE_ATTRIBUTE_CAPTUREDEF(PhysicalResistance);
// 构造函数
AuraDamageStatics()
{
// 创建并定义属性捕获定义 Armor
// 参数3:当前正在捕获Armor属性,进行伤害计算,目标受伤,需要目标的护甲Armor,非来源的护甲
// 参数4:是否快照
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, Armor, Target, false);
// 目标的格挡几率属性
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, BlockChance, Target, false);
// 来源的,攻击者的护甲穿透
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, ArmorPenetration, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitChance, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitResistance, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitDamage, Source, false);
// 目标的抗性
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, FireResistance, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, LightningResistance, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, ArcaneResistance, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, PhysicalResistance, Target, false);
}
};
void UExecCalc_Damage::DetermineDebuff(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
const FGameplayEffectSpec& Spec,
FAggregatorEvaluateParameters EvaluationParameters,
const TMap<FGameplayTag, FGameplayEffectAttributeCaptureDefinition>&
InTagsToDefs) const
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
for (TTuple<FGameplayTag, FGameplayTag> Pair : GameplayTags.DamageTypesToDebuffs)
{
const FGameplayTag& DamageType = Pair.Key;
const FGameplayTag& DebuffType = Pair.Value;
const float TypeDamage = Spec.GetSetByCallerMagnitude(DamageType, false, -1.f);
if (TypeDamage > -.5f) // .5 padding for floating point [im]precision
{
// Determine if there was a successful debuff
const float SourceDebuffChance = Spec.GetSetByCallerMagnitude(GameplayTags.Debuff_Chance, false, -1.f);
float TargetDebuffResistance = 0.f;
const FGameplayTag& ResistanceTag = GameplayTags.DamageTypesToResistances[DamageType];
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(InTagsToDefs[ResistanceTag],
EvaluationParameters, TargetDebuffResistance);
TargetDebuffResistance = FMath::Max<float>(TargetDebuffResistance, 0.f);
const float EffectiveDebuffChance = SourceDebuffChance * (100 - TargetDebuffResistance) / 100.f;
const bool bDebuff = FMath::RandRange(1, 100) < EffectiveDebuffChance;
if (bDebuff)
{
//TODO: What do we do?
}
}
}
}
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
TMap<FGameplayTag, FGameplayEffectAttributeCaptureDefinition> TagsToCaptureDefs;
const FAuraGameplayTags& Tags = FAuraGameplayTags::Get();
// 使用局部变量捕获def,延迟添加,否则减益效果DetermineDebuff捕获不到
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_Armor, DamageStatics().ArmorDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_BlockChance, DamageStatics().BlockChanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_ArmorPenetration, DamageStatics().ArmorPenetrationDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitChance, DamageStatics().CriticalHitChanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitResistance, DamageStatics().CriticalHitResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitDamage, DamageStatics().CriticalHitDamageDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, DamageStatics().ArcaneResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Fire, DamageStatics().FireResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Lightning, DamageStatics().LightningResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Physical, DamageStatics().PhysicalResistanceDef);
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
//ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
//ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
int32 SourcePlayerLevel = 1;
if (SourceAvatar->Implements<UCombatInterface>())
{
SourcePlayerLevel = ICombatInterface::Execute_GetPlayerLevel(SourceAvatar);
}
int32 TargetPlayerLevel = 1;
if (TargetAvatar->Implements<UCombatInterface>())
{
TargetPlayerLevel = ICombatInterface::Execute_GetPlayerLevel(TargetAvatar);
}
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// Debuff
DetermineDebuff(ExecutionParams, Spec, EvaluationParameters, TagsToCaptureDefs);
// 根据调用者大小计算伤害值 累加
// DamageTypesToResistances 伤害型标签-抗性属性 组包含所有伤害类型的标签 和与伤害关联的 抗性属性标签
// DamageTypesToResistances 标签只存在于伤害型技能中
// Get Damage Set by Caller Magnitude
float Damage = 0.f;
for (const TTuple<FGameplayTag, FGameplayTag>& Pair : FAuraGameplayTags::Get().DamageTypesToResistances)
{
// 游戏标签对应的的值由 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude 设置过【】
// Pair.Key 为伤害类型技能标签 Pair.Value 为抗性属性标签
// 根据标签 后续可以取出不同的抗性属性 Pair.Value,减少对应类型伤害的值 例如火抗 将 火属性伤害值减少一些
// 取出伤害类型技能的值
// const float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
// 伤害型技能标签
const FGameplayTag DamageTypeTag = Pair.Key;
// 抗性属性标签
const FGameplayTag ResistanceTag = Pair.Value;
checkf(TagsToCaptureDefs.Contains(ResistanceTag),
TEXT("TagsToCaptureDefs doesn't contain Tag: [%s] in ExecCalc_Damage"), *ResistanceTag.ToString());
// 通过属性标签,找到相关联的捕获属性定义,当前只需要抗性捕获定义
// 定义在 TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, ArcaneResistanceDef);
const FGameplayEffectAttributeCaptureDefinition CaptureDef = TagsToCaptureDefs[ResistanceTag];
// 参数2 :未找到相关属性时是否警告
float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key, false);
// 计算捕获的目标的属性 通过 Resistance 传出
float Resistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(CaptureDef, EvaluationParameters, Resistance);
// 抗性最大抵消100%的伤害
Resistance = FMath::Clamp(Resistance, 0.f, 100.f);
// 每一点抗性抵消1%的伤害
DamageTypeValue *= (100.f - Resistance) / 100.f;
Damage += DamageTypeValue;
}
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
//
FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
//为自定义效果情景设置是否格挡
UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourcePlayerLevel);
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetPlayerLevel);
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef,
EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef,
EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef,
EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(TargetPlayerLevel);
// Critical Hit Resistance reduces Critical Hit Chance by a certain percentage
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance *
CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// 为自定义效果情景设置是否暴击
UAuraAbilitySystemLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
// Double damage plus a bonus if critical hit
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
细节-Gameplay Effect-Modifiers--Attribute-AuraAttributeSet.FireResistance Modifiers--Modifier Op-Override Modifiers--Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifiers--Modifier Magnitude-Scalable Float Magnitude-20
为效果情景,属性集 添加减益效果信息 减益效果需要网络序列化
Source/Aura/Public/AuraAbilityTypes.h
struct FAuraGameplayEffectContext : public FGameplayEffectContext
public:
bool IsSuccessfulDebuff() const { return bIsSuccessfulDebuff; }
float GetDebuffDamage() const { return DebuffDamage; }
float GetDebuffDuration() const { return DebuffDuration; }
float GetDebuffFrequency() const { return DebuffFrequency; }
TSharedPtr<FGameplayTag> GetDamageType() const { return DamageType; }
void SetIsSuccessfulDebuff(bool bInIsDebuff) { bIsSuccessfulDebuff = bInIsDebuff; }
void SetDebuffDamage(float InDamage) { DebuffDamage = InDamage; }
void SetDebuffDuration(float InDuration) { DebuffDuration = InDuration; }
void SetDebuffFrequency(float InFrequency) { DebuffFrequency = InFrequency; }
protected:
UPROPERTY()
bool bIsSuccessfulDebuff = false;
UPROPERTY()
float DebuffDamage = 0.f;
UPROPERTY()
float DebuffDuration = 0.f;
UPROPERTY()
float DebuffFrequency = 0.f;
TSharedPtr<FGameplayTag> DamageType;
Source/Aura/Private/AuraAbilityTypes.cpp
#include "AuraAbilityTypes.h"
bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
uint32 RepBits = 0;
// 存储
if (Ar.IsSaving())
{
if (bReplicateInstigator && Instigator.IsValid())
{
RepBits |= 1 << 0;
}
if (bReplicateEffectCauser && EffectCauser.IsValid() )
{
RepBits |= 1 << 1;
}
if (AbilityCDO.IsValid())
{
RepBits |= 1 << 2;
}
if (bReplicateSourceObject && SourceObject.IsValid())
{
RepBits |= 1 << 3;
}
if (Actors.Num() > 0)
{
RepBits |= 1 << 4;
}
if (HitResult.IsValid())
{
RepBits |= 1 << 5;
}
if (bHasWorldOrigin)
{
RepBits |= 1 << 6;
}
if (bIsBlockedHit)
{
// 如果格挡,翻转第7位-1
RepBits |= 1 << 7;
}
if (bIsCriticalHit)
{
// 如果暴击,翻转第8位-1
RepBits |= 1 << 8;
}
if (bIsSuccessfulDebuff)
{
RepBits |= 1 << 9;
}
if (DebuffDamage > 0.f)
{
RepBits |= 1 << 10;
}
if (DebuffDuration > 0.f)
{
RepBits |= 1 << 11;
}
if (DebuffFrequency > 0.f)
{
RepBits |= 1 << 12;
}
if (DamageType.IsValid())
{
RepBits |= 1 << 13;
}
}
//序列化前13位
Ar.SerializeBits(&RepBits, 13);
if (RepBits & (1 << 0))
{
Ar << Instigator;
}
if (RepBits & (1 << 1))
{
Ar << EffectCauser;
}
if (RepBits & (1 << 2))
{
Ar << AbilityCDO;
}
if (RepBits & (1 << 3))
{
Ar << SourceObject;
}
if (RepBits & (1 << 4))
{
SafeNetSerializeTArray_Default<31>(Ar, Actors);
}
if (RepBits & (1 << 5))
{
if (Ar.IsLoading())
{
if (!HitResult.IsValid())
{
HitResult = TSharedPtr<FHitResult>(new FHitResult());
}
}
HitResult->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 6))
{
Ar << WorldOrigin;
bHasWorldOrigin = true;
}
else
{
bHasWorldOrigin = false;
}
if (RepBits & (1 << 7))
{
Ar << bIsBlockedHit;
}
if (RepBits & (1 << 8))
{
Ar << bIsCriticalHit;
}
if (RepBits & (1 << 9))
{
Ar << bIsSuccessfulDebuff;
}
if (RepBits & (1 << 10))
{
Ar << DebuffDamage;
}
if (RepBits & (1 << 11))
{
Ar << DebuffDuration;
}
if (RepBits & (1 << 12))
{
Ar << DebuffFrequency;
}
if (RepBits & (1 << 13))
{
if (Ar.IsLoading())
{
if (!DamageType.IsValid())
{
DamageType = TSharedPtr<FGameplayTag>(new FGameplayTag());
}
}
DamageType->NetSerialize(Ar, Map, bOutSuccess);
}
if (Ar.IsLoading())
{
AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
}
bOutSuccess = true;
return true;
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static bool IsSuccessfulDebuff(const FGameplayEffectContextHandle& EffectContextHandle);
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static float GetDebuffDamage(const FGameplayEffectContextHandle& EffectContextHandle);
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static float GetDebuffDuration(const FGameplayEffectContextHandle& EffectContextHandle);
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static float GetDebuffFrequency(const FGameplayEffectContextHandle& EffectContextHandle);
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static FGameplayTag GetDamageType(const FGameplayEffectContextHandle& EffectContextHandle);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
bool UAuraAbilitySystemLibrary::IsSuccessfulDebuff(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->IsSuccessfulDebuff();
}
return false;
}
float UAuraAbilitySystemLibrary::GetDebuffDamage(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->GetDebuffDamage();
}
return 0.f;
}
float UAuraAbilitySystemLibrary::GetDebuffDuration(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->GetDebuffDuration();
}
return 0.f;
}
float UAuraAbilitySystemLibrary::GetDebuffFrequency(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->GetDebuffFrequency();
}
return 0.f;
}
FGameplayTag UAuraAbilitySystemLibrary::GetDamageType(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
if (AuraEffectContext->GetDamageType().IsValid())
{
return *AuraEffectContext->GetDamageType();
}
}
return FGameplayTag();
}
Source/Aura/Public/AuraAbilityTypes.h
struct FAuraGameplayEffectContext : public FGameplayEffectContext
public:
void SetDamageType(TSharedPtr<FGameplayTag> InDamageType) { DamageType = InDamageType; }
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetIsSuccessfulDebuff(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInSuccessfulDebuff);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetDebuffDamage(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, float InDamage);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetDebuffDuration(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, float InDuration);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetDebuffFrequency(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, float InFrequency);
static void SetDamageType(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, const FGameplayTag& InDamageType);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
void UAuraAbilitySystemLibrary::SetIsSuccessfulDebuff(FGameplayEffectContextHandle& EffectContextHandle,
bool bInSuccessfulDebuff)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetIsSuccessfulDebuff(bInSuccessfulDebuff);
}
}
void UAuraAbilitySystemLibrary::SetDebuffDamage(FGameplayEffectContextHandle& EffectContextHandle, float InDamage)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetDebuffDamage(InDamage);
}
}
void UAuraAbilitySystemLibrary::SetDebuffDuration(FGameplayEffectContextHandle& EffectContextHandle, float InDuration)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetDebuffDuration(InDuration);
}
}
void UAuraAbilitySystemLibrary::SetDebuffFrequency(FGameplayEffectContextHandle& EffectContextHandle, float InFrequency)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetDebuffFrequency(InFrequency);
}
}
void UAuraAbilitySystemLibrary::SetDamageType(FGameplayEffectContextHandle& EffectContextHandle,
const FGameplayTag& InDamageType)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
const TSharedPtr<FGameplayTag> DamageType = MakeShared<FGameplayTag>(InDamageType);
AuraEffectContext->SetDamageType(DamageType);
}
}
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
void UExecCalc_Damage::DetermineDebuff(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
const FGameplayEffectSpec& Spec,
FAggregatorEvaluateParameters EvaluationParameters,
const TMap<FGameplayTag, FGameplayEffectAttributeCaptureDefinition>&
InTagsToDefs) const
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
for (TTuple<FGameplayTag, FGameplayTag> Pair : GameplayTags.DamageTypesToDebuffs)
{
const FGameplayTag& DamageType = Pair.Key;
const FGameplayTag& DebuffType = Pair.Value;
const float TypeDamage = Spec.GetSetByCallerMagnitude(DamageType, false, -1.f);
if (TypeDamage > -.5f) // .5 padding for floating point [im]precision
{
// Determine if there was a successful debuff
const float SourceDebuffChance = Spec.GetSetByCallerMagnitude(GameplayTags.Debuff_Chance, false, -1.f);
float TargetDebuffResistance = 0.f;
const FGameplayTag& ResistanceTag = GameplayTags.DamageTypesToResistances[DamageType];
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(InTagsToDefs[ResistanceTag],
EvaluationParameters, TargetDebuffResistance);
TargetDebuffResistance = FMath::Max<float>(TargetDebuffResistance, 0.f);
const float EffectiveDebuffChance = SourceDebuffChance * (100 - TargetDebuffResistance) / 100.f;
const bool bDebuff = FMath::RandRange(1, 100) < EffectiveDebuffChance;
if (bDebuff)
{
FGameplayEffectContextHandle ContextHandle = Spec.GetContext();
UAuraAbilitySystemLibrary::SetIsSuccessfulDebuff(ContextHandle, true);
UAuraAbilitySystemLibrary::SetDamageType(ContextHandle, DamageType);
const float DebuffDamage = Spec.GetSetByCallerMagnitude(GameplayTags.Debuff_Damage, false, -1.f);
const float DebuffDuration = Spec.GetSetByCallerMagnitude(GameplayTags.Debuff_Duration, false, -1.f);
const float DebuffFrequency = Spec.GetSetByCallerMagnitude(GameplayTags.Debuff_Frequency, false, -1.f);
UAuraAbilitySystemLibrary::SetDebuffDamage(ContextHandle, DebuffDamage);
UAuraAbilitySystemLibrary::SetDebuffDuration(ContextHandle, DebuffDuration);
UAuraAbilitySystemLibrary::SetDebuffFrequency(ContextHandle, DebuffFrequency);
}
}
}
}
Source/Aura/Public/AbilitySystem/AuraAttributeSet.h
private:
void HandleIncomingDamage(const FEffectProperties& Props);
void HandleIncomingXP(const FEffectProperties& Props);
void Debuff(const FEffectProperties& Props);
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
// 仅在服务器端执行
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
FEffectProperties Props;
SetEffectProperties(Data, Props);
// 获取发生改变的属性-Health
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
//UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
//UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
// 正确夹紧属性
// 这发生在游戏效果应用之后
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
HandleIncomingDamage(Props);
}
if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
{
HandleIncomingXP(Props);
}
}
void UAuraAttributeSet::HandleIncomingDamage(const FEffectProperties& Props)
{
// IncomingDamage 属性只在服务器执行获取,不会复制到客户端
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
CombatInterface->Die();
}
// 死亡后发送XP事件
SendXPEvent(Props);
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
// 是否暴击,格挡,显示提示文本
const bool bBlock = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
ShowFloatingText(Props, LocalIncomingDamage, bBlock, bCriticalHit);
if (UAuraAbilitySystemLibrary::IsSuccessfulDebuff(Props.EffectContextHandle))
{
Debuff(Props);
}
}
}
void UAuraAttributeSet::Debuff(const FEffectProperties& Props)
{
}
void UAuraAttributeSet::HandleIncomingXP(const FEffectProperties& Props)
{
// 本地存储XP元属性副本
const float LocalIncomingXP = GetIncomingXP();
// 原XP元属性归零
SetIncomingXP(0.f);
// 为伤害来源添加经验
// Source Character is the owner, since GA_ListenForEvents applies GE_EventBasedEffect, adding to IncomingXP
if (Props.SourceCharacter->Implements<UPlayerInterface>() && Props.SourceCharacter->Implements<UCombatInterface>())
{
const int32 CurrentLevel = ICombatInterface::Execute_GetPlayerLevel(Props.SourceCharacter);
const int32 CurrentXP = IPlayerInterface::Execute_GetXP(Props.SourceCharacter);
const int32 NewLevel = IPlayerInterface::Execute_FindLevelForXP(
Props.SourceCharacter, CurrentXP + LocalIncomingXP);
const int32 NumLevelUps = NewLevel - CurrentLevel;
if (NumLevelUps > 0)
{
const int32 AttributePointsReward = IPlayerInterface::Execute_GetAttributePointsReward(
Props.SourceCharacter, CurrentLevel);
const int32 SpellPointsReward = IPlayerInterface::Execute_GetSpellPointsReward(
Props.SourceCharacter, CurrentLevel);
// 游戏后处理效果完成后,等级才会实际增加,此时获取的最大健康值依然不是最新的值
IPlayerInterface::Execute_AddToPlayerLevel(Props.SourceCharacter, NumLevelUps);
IPlayerInterface::Execute_AddToAttributePoints(Props.SourceCharacter, AttributePointsReward);
IPlayerInterface::Execute_AddToSpellPoints(Props.SourceCharacter, SpellPointsReward);
// 升级后,设置可以设置健康值,魔力值为最大值
bTopOffHealth = true;
bTopOffMana = true;
IPlayerInterface::Execute_LevelUp(Props.SourceCharacter);
}
IPlayerInterface::Execute_AddToXP(Props.SourceCharacter, LocalIncomingXP);
}
}
C++动态创建的游戏效果不会复制,仅在服务端执行。 需要正确设置动态游戏效果。
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
#include "AuraAbilityTypes.h"
#include "GameplayEffectComponents/TargetTagsGameplayEffectComponent.h"
// 仅在服务器端执行
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
FEffectProperties Props;
SetEffectProperties(Data, Props);
if (Props.TargetCharacter->Implements<UCombatInterface>() &&
ICombatInterface::Execute_IsDead(Props.TargetCharacter)) return;
// 获取发生改变的属性-Health
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 改变后的健康值
//UE_LOG(LogTemp,Warning,TEXT("Health: %f"),GetHealth());
// 健康值的改变程度
//UE_LOG(LogTemp,Warning,TEXT("Health 的改变大小: %f"),Data.EvaluatedData.Magnitude);
// 正确夹紧属性
// 这发生在游戏效果应用之后
// SetHealth 是真正的修改属性原值 然后由服务端复制到客户端
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
HandleIncomingDamage(Props);
}
if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
{
HandleIncomingXP(Props);
}
}
void UAuraAttributeSet::Debuff(const FEffectProperties& Props)
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
FGameplayEffectContextHandle EffectContext = Props.SourceASC->MakeEffectContext();
EffectContext.AddSourceObject(Props.SourceAvatarActor);
const FGameplayTag DamageType = UAuraAbilitySystemLibrary::GetDamageType(Props.EffectContextHandle);
const float DebuffDamage = UAuraAbilitySystemLibrary::GetDebuffDamage(Props.EffectContextHandle);
const float DebuffDuration = UAuraAbilitySystemLibrary::GetDebuffDuration(Props.EffectContextHandle);
const float DebuffFrequency = UAuraAbilitySystemLibrary::GetDebuffFrequency(Props.EffectContextHandle);
FString DebuffName = FString::Printf(TEXT("DynamicDebuff_%s"), *DamageType.ToString());
// 动态创建游戏效果
UGameplayEffect* Effect = NewObject<UGameplayEffect>(GetTransientPackage(), FName(DebuffName));
Effect->DurationPolicy = EGameplayEffectDurationType::HasDuration;
Effect->Period = DebuffFrequency;
Effect->DurationMagnitude = FScalableFloat(DebuffDuration);
// 为游戏效果授予游戏标签
// Effect->InheritableOwnedTagsContainer.AddTag(GameplayTags.DamageTypesToDebuffs[DamageType]); 弃用
FInheritedTagContainer TagContainer = FInheritedTagContainer();
// we create and add the component to the gameplay effect
UTargetTagsGameplayEffectComponent& TargetTagsComponent = Effect->AddComponent<UTargetTagsGameplayEffectComponent>();
TagContainer.Added.AddTag(GameplayTags.DamageTypesToDebuffs[DamageType]);
TargetTagsComponent.SetAndApplyTargetTagChanges(TagContainer);
// 按源聚合
Effect->StackingType = EGameplayEffectStackingType::AggregateBySource;
Effect->StackLimitCount = 1;
//修改器
const int32 Index = Effect->Modifiers.Num();
Effect->Modifiers.Add(FGameplayModifierInfo());
FGameplayModifierInfo& ModifierInfo = Effect->Modifiers[Index];
ModifierInfo.ModifierMagnitude = FScalableFloat(DebuffDamage);
ModifierInfo.ModifierOp = EGameplayModOp::Additive;
ModifierInfo.Attribute = UAuraAttributeSet::GetIncomingDamageAttribute();
if (FGameplayEffectSpec* MutableSpec = new FGameplayEffectSpec(Effect, EffectContext, 1.f))
{
FAuraGameplayEffectContext* AuraContext = static_cast<FAuraGameplayEffectContext*>(MutableSpec->GetContext().Get());
TSharedPtr<FGameplayTag> DebuffDamageType = MakeShareable(new FGameplayTag(DamageType));
AuraContext->SetDamageType(DebuffDamageType);
Props.TargetASC->ApplyGameplayEffectSpecToSelf(*MutableSpec);
// 现在,没有为效果情景句柄设置 减益可用标志,UAuraAbilitySystemLibrary::IsSuccessfulDebuff
// 所以下一次执行 HandleIncomingDamage 时,不会再次应用减益效果,不会导致无限循环的减益效果
}
}
现在,火球术技能有几率触发火属性减益效果伤害,每1秒造成一次伤害,持续一段时间。
目标获得减益标签时激活粒子,失去标签时停用粒子。
如果减益粒子组件能获取当前粒子组件的拥有者的技能系统组件, 如果此时粒子组件的拥有者还没有技能系统组件, 那就立即创建一个委托,在粒子组件的拥有者获取到技能系统组件时广播该委托, 减益粒子组件不应依赖角色,应依赖战斗接口。
Source/Aura/Public/Interaction/CombatInterface.h
class UAbilitySystemComponent;
// 声明技能系统组件注册委托,用于技能系统组件有效时获取通知
DECLARE_MULTICAST_DELEGATE_OneParam(FOnASCRegistered, UAbilitySystemComponent*)
public:
// 返回委托
virtual FOnASCRegistered GetOnASCRegisteredDelegate() = 0;
DebuffNiagaraComponent
Source/Aura/Public/AbilitySystem/Debuff/DebuffNiagaraComponent.h
#pragma once
#include "CoreMinimal.h"
#include "NiagaraComponent.h"
#include "GameplayTagContainer.h"
#include "DebuffNiagaraComponent.generated.h"
UCLASS()
class AURA_API UDebuffNiagaraComponent : public UNiagaraComponent
{
GENERATED_BODY()
public:
UDebuffNiagaraComponent();
// 通过游戏标签识别该粒子系统组件
UPROPERTY(VisibleAnywhere)
FGameplayTag DebuffTag;
protected:
virtual void BeginPlay() override;
// 减益标签变更回调 绑定到减益目标技能系统组件
void DebuffTagChanged(const FGameplayTag CallbackTag, int32 NewCount);
};
Source/Aura/Private/AbilitySystem/Debuff/DebuffNiagaraComponent.cpp
#include "AbilitySystem/Debuff/DebuffNiagaraComponent.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
#include "Interaction/CombatInterface.h"
UDebuffNiagaraComponent::UDebuffNiagaraComponent()
{
bAutoActivate = false;
}
void UDebuffNiagaraComponent::BeginPlay()
{
Super::BeginPlay();
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetOwner());
// 如果能获取当前粒子组件的拥有者的技能系统组件
if (UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetOwner()))
{
ASC->RegisterGameplayTagEvent(DebuffTag, EGameplayTagEventType::NewOrRemoved).AddUObject(
this, &UDebuffNiagaraComponent::DebuffTagChanged);
}
// 如果此时粒子组件的拥有者还没有技能系统组件
// 那就立即创建一个监听技能系统组件注册的委托,在粒子组件的拥有者获取到技能系统组件时绑定标签变更事件到技能系统组件
else if (CombatInterface)
{
CombatInterface->GetOnASCRegisteredDelegate().AddWeakLambda(this, [this](UAbilitySystemComponent* InASC)
{
InASC->RegisterGameplayTagEvent(DebuffTag, EGameplayTagEventType::NewOrRemoved).AddUObject(
this, &UDebuffNiagaraComponent::DebuffTagChanged);
});
}
}
void UDebuffNiagaraComponent::DebuffTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
if (NewCount > 0)
{
Activate();
}
else
{
Deactivate();
}
}
Source/Aura/Public/Character/AuraCharacterBase.h
class UDebuffNiagaraComponent;
public:
virtual FOnASCRegistered GetOnASCRegisteredDelegate() override;
FOnASCRegistered OnAscRegistered;
protected:
UPROPERTY(VisibleAnywhere)
TObjectPtr<UDebuffNiagaraComponent> BurnDebuffComponent;
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "AbilitySystem/Debuff/DebuffNiagaraComponent.h"
AAuraCharacterBase::AAuraCharacterBase()
{
PrimaryActorTick.bCanEverTick = false;
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
BurnDebuffComponent = CreateDefaultSubobject<UDebuffNiagaraComponent>("BurnDebuffComponent");
BurnDebuffComponent->SetupAttachment(GetRootComponent());
BurnDebuffComponent->DebuffTag = GameplayTags.Debuff_Burn;
// 防止相机与角色碰撞导致相机视角放大
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
// 设置胶囊体不生成重叠事件,防止多次触发重叠事件,因为网格体组件已设置了重叠事件
GetCapsuleComponent()->SetGenerateOverlapEvents(false);
GetMesh()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
GetMesh()->SetCollisionResponseToChannel(ECC_Projectile, ECR_Overlap);
GetMesh()->SetGenerateOverlapEvents(true);
//初始化武器
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>("weapon");
//将武器附加到骨架插槽
Weapon->SetupAttachment(GetMesh(),FName("WeaponHandSocket"));
//武器不应有任何碰撞
Weapon->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
// 服务器,客户端执行
void AAuraCharacterBase::MulticastHandleDeath_Implementation()
{
UGameplayStatics::PlaySoundAtLocation(this, DeathSound, GetActorLocation(), GetActorRotation());
// 启用模拟物理
Weapon->SetSimulatePhysics(true);
// 启用重力确保武器掉落
Weapon->SetEnableGravity(true);
// 启用碰撞,无查询,无重叠 【武器默认无碰撞】
Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 将网格体布娃娃化
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetEnableGravity(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 对世界静态类型阻止,网格体可以阻止其他物体
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
// 交胶囊体禁用碰撞 不会再阻挡其他物体通过
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Dissolve();
bDead = true;
BurnDebuffComponent->Deactivate();
}
FOnASCRegistered AAuraCharacterBase::GetOnASCRegisteredDelegate()
{
return OnAscRegistered;
}
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::InitAbilityActorInfo()
{
// 获取玩家状态
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
if (AuraPlayerState == nullptr)return;
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(1, 1.F, FColor::Cyan, FString("AuraPlayerState"));
}
// check(AuraPlayerState);
// 从玩家状态获取技能系统组件
// 然后初始技能参与者信息
// owner 为 玩家状态类,avatar 为当前类即玩家角色
AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState, this);
// 为技能系统组件设置技能Actor相关信息
Cast<UAuraAbilitySystemComponent>(AuraPlayerState->GetAbilitySystemComponent())->AbilityActorInfoSet();
// 将 玩家状态上的 技能系统组件 和 属性集 拷贝到 角色类上,因为角色基类也有同样的变量需要构造
// 技能系统组件
AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
// 属性集
AttributeSet = AuraPlayerState->GetAttributeSet();
// 广播技能系统组件注册事件
OnAscRegistered.Broadcast(AbilitySystemComponent);
// 初始化并添加覆盖控件,覆盖控件控制器
// 在多人游戏,只有服务端的玩家控制器有效,
// 服务器拥有所有玩家的玩家控制器,但每个玩家只有自己的玩家控制器。
// 在控制该特定角色的客户端机器上,该玩家控制器是有效的。
// 但是该客户端计算机上非本地控制的其他角色没有有效的玩家控制器。
// 例如,在三人游戏中,如果您是客户端,则您的玩家控制器有效,
// 但在你的机器上,另外两个角色,这两个副本没有有效的玩家控制器和初始化能力演员信息。
// 在这种情况下,将调用此函数InitAbilityActorInfo,并且/或玩家控制器将是空指针。
// 在这种情况下,对于此功能或玩家控制器,在多人游戏中可以为空,
// 我们只想在它不为空时继续执行。
// 所以这种情况使用if检查【为空是合理的,只要不继续执行】。不使程序崩溃。
// 否则使用check断言,程序崩溃。【游戏前置条件不能继续执行】
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
{
if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(AuraPlayerController->GetHUD()))
{
AuraHUD->InitOverlay(AuraPlayerController, AuraPlayerState, AbilitySystemComponent, AttributeSet);
}
}
// 此时技能系统组件已经初始化
// 初始化主属性
// 一般只在服务端初始化属性,因为属性设置了网络复制
// 此处会在服务端与客户端初始化属性,也可以,这无需等待从服务端复制
InitializeDefaultAttributes();
}
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::InitAbilityActorInfo()
{
// 初始技能参与者信息 服务器和客户端都在此设置
// 两者均为敌人类自身角色
AbilitySystemComponent->InitAbilityActorInfo(this, this);
Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent)->AbilityActorInfoSet();
// 为敌人基类临时添加初始化属性功能 仅作学习用
if (HasAuthority())
{
InitializeDefaultAttributes();
// 内部用到了游戏模式
}
// 广播技能系统组件注册事件
OnAscRegistered.Broadcast(AbilitySystemComponent);
}
Source/Aura/Public/Interaction/CombatInterface.h
// 声明死亡委托
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDeath,AActor*,DeadActor);
public:
// 返回死亡委托
virtual FOnDeath GetOnDeathDelegate()=0;
Source/Aura/Public/Character/AuraCharacterBase.h
public:
virtual FOnDeath GetOnDeathDelegate() override;
FOnDeath OnDeath;
Source/Aura/Private/Character/AuraCharacterBase.cpp
FOnDeath AAuraCharacterBase::GetOnDeathDelegate()
{
return OnDeath;
}
Source/Aura/Public/AbilitySystem/Debuff/DebuffNiagaraComponent.h
UFUNCTION()
void OnOwnerDeath(AActor* DeadActor);
Source/Aura/Private/AbilitySystem/Debuff/DebuffNiagaraComponent.cpp
void UDebuffNiagaraComponent::BeginPlay()
{
Super::BeginPlay();
ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetOwner());
// 如果能获取当前粒子组件的拥有者的技能系统组件
if (UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetOwner()))
{
ASC->RegisterGameplayTagEvent(DebuffTag, EGameplayTagEventType::NewOrRemoved).AddUObject(
this, &UDebuffNiagaraComponent::DebuffTagChanged);
}
// 如果此时粒子组件的拥有者还没有技能系统组件
// 那就立即创建一个监听技能系统组件注册的委托,在粒子组件的拥有者获取到技能系统组件时绑定标签变更事件到技能系统组件
else if (CombatInterface)
{
// AddWeakLambda 保持对委托的引用,但不增加引用计数,使其可以被垃圾回收,就像没有引用委托
// 参数1为用户对象
CombatInterface->GetOnASCRegisteredDelegate().AddWeakLambda(this, [this](UAbilitySystemComponent* InASC)
{
InASC->RegisterGameplayTagEvent(DebuffTag, EGameplayTagEventType::NewOrRemoved).AddUObject(
this, &UDebuffNiagaraComponent::DebuffTagChanged);
});
}
if (CombatInterface)
{
CombatInterface->GetOnDeathDelegate().AddDynamic(this, &UDebuffNiagaraComponent::OnOwnerDeath);
}
}
void UDebuffNiagaraComponent::OnOwnerDeath(AActor* DeadActor)
{
Deactivate();
}
BurnDebuffComponent 组件-Niagara-Niagara系统资产-NS_Fire
BurnDebuffComponent 组件-Niagara-Niagara系统资产-NS_Fire
BurnDebuffComponent 组件-激活-自动启用-启用 仅用于调整 BurnDebuffComponent 位置,测试 调整后需关闭。
类设置-类选项-父类-AuraDamageGameplayAbility
现在,可以在 类默认值设置减益效果
该值只与伤害型技能相关 角色死亡时受到的物理冲击值,使角色被弹开。
Source/Aura/Public/AuraAbilityTypes.h
// 伤害效果参数结构类型
USTRUCT(BlueprintType)
struct FDamageEffectParams
{
GENERATED_BODY()
FDamageEffectParams(){}
UPROPERTY()
TObjectPtr<UObject> WorldContextObject = nullptr;
UPROPERTY()
TSubclassOf<UGameplayEffect> DamageGameplayEffectClass = nullptr;
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> SourceAbilitySystemComponent;
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> TargetAbilitySystemComponent;
UPROPERTY()
float BaseDamage = 0.f;
UPROPERTY()
float AbilityLevel = 1.f;
UPROPERTY()
FGameplayTag DamageType = FGameplayTag();
UPROPERTY()
float DebuffChance = 0.f;
UPROPERTY()
float DebuffDamage = 0.f;
UPROPERTY()
float DebuffDuration = 0.f;
UPROPERTY()
float DebuffFrequency = 0.f;
// 死亡物理效果的力值
UPROPERTY()
float DeathImpulseMagnitude = 0.f;
};
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
protected:
// 死亡物理效果的力值
UPROPERTY(EditDefaultsOnly, Category = "Damage")
float DeathImpulseMagnitude = 60.f;
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
FDamageEffectParams UAuraDamageGameplayAbility::MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor) const
{
FDamageEffectParams Params;
Params.WorldContextObject = GetAvatarActorFromActorInfo();
Params.DamageGameplayEffectClass = DamageEffectClass;
Params.SourceAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
Params.TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
Params.BaseDamage = Damage.GetValueAtLevel(GetAbilityLevel());
Params.AbilityLevel = GetAbilityLevel();
Params.DamageType = DamageType;
Params.DebuffChance = DebuffChance;
Params.DebuffDamage = DebuffDamage;
Params.DebuffDuration = DebuffDuration;
Params.DebuffFrequency = DebuffFrequency;
Params.DeathImpulseMagnitude = DeathImpulseMagnitude;
return Params;
}
角色死亡时,技能投射物在应用伤害效果到目标时,生成一个向量,将角色弹开。
Source/Aura/Public/AuraAbilityTypes.h
// 伤害效果参数结构类型
USTRUCT(BlueprintType)
struct FDamageEffectParams
{
GENERATED_BODY()
FDamageEffectParams(){}
UPROPERTY()
TObjectPtr<UObject> WorldContextObject = nullptr;
UPROPERTY()
TSubclassOf<UGameplayEffect> DamageGameplayEffectClass = nullptr;
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> SourceAbilitySystemComponent;
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> TargetAbilitySystemComponent;
UPROPERTY()
float BaseDamage = 0.f;
UPROPERTY()
float AbilityLevel = 1.f;
UPROPERTY()
FGameplayTag DamageType = FGameplayTag();
UPROPERTY()
float DebuffChance = 0.f;
UPROPERTY()
float DebuffDamage = 0.f;
UPROPERTY()
float DebuffDuration = 0.f;
UPROPERTY()
float DebuffFrequency = 0.f;
// 死亡物理效果的力值
UPROPERTY()
float DeathImpulseMagnitude = 0.f;
// 死亡冲击向量
UPROPERTY()
FVector DeathImpulse = FVector::ZeroVector;
};
struct FAuraGameplayEffectContext : public FGameplayEffectContext
public:
FVector GetDeathImpulse() const { return DeathImpulse; }
void SetDeathImpulse(const FVector& InImpulse) { DeathImpulse = InImpulse; }
protected:
UPROPERTY()
FVector DeathImpulse = FVector::ZeroVector;
Source/Aura/Private/AuraAbilityTypes.cpp
#include "AuraAbilityTypes.h"
bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
uint32 RepBits = 0;
// 存储
if (Ar.IsSaving())
{
if (bReplicateInstigator && Instigator.IsValid())
{
RepBits |= 1 << 0;
}
if (bReplicateEffectCauser && EffectCauser.IsValid() )
{
RepBits |= 1 << 1;
}
if (AbilityCDO.IsValid())
{
RepBits |= 1 << 2;
}
if (bReplicateSourceObject && SourceObject.IsValid())
{
RepBits |= 1 << 3;
}
if (Actors.Num() > 0)
{
RepBits |= 1 << 4;
}
if (HitResult.IsValid())
{
RepBits |= 1 << 5;
}
if (bHasWorldOrigin)
{
RepBits |= 1 << 6;
}
if (bIsBlockedHit)
{
// 如果格挡,翻转第7位-1
RepBits |= 1 << 7;
}
if (bIsCriticalHit)
{
// 如果暴击,翻转第8位-1
RepBits |= 1 << 8;
}
if (bIsSuccessfulDebuff)
{
RepBits |= 1 << 9;
}
if (DebuffDamage > 0.f)
{
RepBits |= 1 << 10;
}
if (DebuffDuration > 0.f)
{
RepBits |= 1 << 11;
}
if (DebuffFrequency > 0.f)
{
RepBits |= 1 << 12;
}
if (DamageType.IsValid())
{
RepBits |= 1 << 13;
}
if (!DeathImpulse.IsZero())
{
RepBits |= 1 << 14;
}
}
//序列化前14位
Ar.SerializeBits(&RepBits, 14);
if (RepBits & (1 << 0))
{
Ar << Instigator;
}
if (RepBits & (1 << 1))
{
Ar << EffectCauser;
}
if (RepBits & (1 << 2))
{
Ar << AbilityCDO;
}
if (RepBits & (1 << 3))
{
Ar << SourceObject;
}
if (RepBits & (1 << 4))
{
SafeNetSerializeTArray_Default<31>(Ar, Actors);
}
if (RepBits & (1 << 5))
{
if (Ar.IsLoading())
{
if (!HitResult.IsValid())
{
HitResult = TSharedPtr<FHitResult>(new FHitResult());
}
}
HitResult->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 6))
{
Ar << WorldOrigin;
bHasWorldOrigin = true;
}
else
{
bHasWorldOrigin = false;
}
if (RepBits & (1 << 7))
{
Ar << bIsBlockedHit;
}
if (RepBits & (1 << 8))
{
Ar << bIsCriticalHit;
}
if (RepBits & (1 << 9))
{
Ar << bIsSuccessfulDebuff;
}
if (RepBits & (1 << 10))
{
Ar << DebuffDamage;
}
if (RepBits & (1 << 11))
{
Ar << DebuffDuration;
}
if (RepBits & (1 << 12))
{
Ar << DebuffFrequency;
}
if (RepBits & (1 << 13))
{
if (Ar.IsLoading())
{
if (!DamageType.IsValid())
{
DamageType = TSharedPtr<FGameplayTag>(new FGameplayTag());
}
}
DamageType->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 14))
{
DeathImpulse.NetSerialize(Ar, Map, bOutSuccess);
}
if (Ar.IsLoading())
{
AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
}
bOutSuccess = true;
return true;
}
Source/Aura/Private/Actor/AuraProjectile.cpp
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();
if (SourceAvatarActor == OtherActor) return;
// 技能抛射物不能伤害友军
if (!UAuraAbilitySystemLibrary::IsNotFriend(SourceAvatarActor, OtherActor)) return;
if (!bHit) OnHit();
if (HasAuthority())
{
// 只在服务器上应用伤害效果即可
// 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端
// 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
// 死亡冲击向量
const FVector DeathImpulse = GetActorForwardVector() * DamageEffectParams.DeathImpulseMagnitude;
DamageEffectParams.DeathImpulse = DeathImpulse;
DamageEffectParams.TargetAbilitySystemComponent = TargetASC;
UAuraAbilitySystemLibrary::ApplyDamageEffect(DamageEffectParams);
}
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
else bHit = true;
}
应用伤害效果后,设置设置死亡冲击向量
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static FVector GetDeathImpulse(const FGameplayEffectContextHandle& EffectContextHandle);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetDamageType(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, const FGameplayTag& InDamageType);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetDeathImpulse(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, const FVector& InImpulse);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
FVector UAuraAbilitySystemLibrary::GetDeathImpulse(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->GetDeathImpulse();
}
return FVector::ZeroVector;
}
void UAuraAbilitySystemLibrary::SetDeathImpulse(FGameplayEffectContextHandle& EffectContextHandle,
const FVector& InImpulse)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetDeathImpulse(InImpulse);
}
}
FGameplayEffectContextHandle UAuraAbilitySystemLibrary::ApplyDamageEffect(const FDamageEffectParams& DamageEffectParams)
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
const AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();
FGameplayEffectContextHandle EffectContexthandle = DamageEffectParams.SourceAbilitySystemComponent->
MakeEffectContext();
EffectContexthandle.AddSourceObject(SourceAvatarActor);
SetDeathImpulse(EffectContexthandle, DamageEffectParams.DeathImpulse);
const FGameplayEffectSpecHandle SpecHandle = DamageEffectParams.SourceAbilitySystemComponent->MakeOutgoingSpec(
DamageEffectParams.DamageGameplayEffectClass, DamageEffectParams.AbilityLevel, EffectContexthandle);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, DamageEffectParams.DamageType,
DamageEffectParams.BaseDamage);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Chance,
DamageEffectParams.DebuffChance);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Damage,
DamageEffectParams.DebuffDamage);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Duration,
DamageEffectParams.DebuffDuration);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Frequency,
DamageEffectParams.DebuffFrequency);
DamageEffectParams.TargetAbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data);
return EffectContexthandle;
}
属性集中,受到致命伤害,使角色朝死亡冲击方向飞去。 属性集不应依赖角色。应依赖战斗接口。
Source/Aura/Public/Interaction/CombatInterface.h
public:
virtual void Die(const FVector& DeathImpulse) = 0;
Source/Aura/Public/Character/AuraCharacterBase.h
public:
// 只在服务器端执行
virtual void Die(const FVector& DeathImpulse) override;
// 处理角色死亡时所有客户端发生的事情
// NetMulticast - 多播RPC
// Reliable -死亡必须可靠复制
// 实现- MulticastHandleDeath_Implementation
UFUNCTION(NetMulticast, Reliable)
virtual void MulticastHandleDeath(const FVector& DeathImpulse);
Source/Aura/Private/Character/AuraCharacterBase.cpp
// 只在服务器端执行
void AAuraCharacterBase::Die(const FVector& DeathImpulse)
{
// 分离武器 如果在服务器分离,则不必在客户端分离
Weapon->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepWorld, true));
// 调用多播
MulticastHandleDeath(DeathImpulse);
}
// 服务器,客户端执行
void AAuraCharacterBase::MulticastHandleDeath_Implementation(const FVector& DeathImpulse)
{
UGameplayStatics::PlaySoundAtLocation(this, DeathSound, GetActorLocation(), GetActorRotation());
// 启用模拟物理
Weapon->SetSimulatePhysics(true);
// 启用重力确保武器掉落
Weapon->SetEnableGravity(true);
// 启用碰撞,无查询,无重叠 【武器默认无碰撞】
Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
Weapon->AddImpulse(DeathImpulse * 0.1f, NAME_None, true);
// 将网格体布娃娃化
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetEnableGravity(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 对世界静态类型阻止,网格体可以阻止其他物体
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
// 参数3 true表示冲击不考虑质量
GetMesh()->AddImpulse(DeathImpulse, NAME_None, true);
// 交胶囊体禁用碰撞 不会再阻挡其他物体通过
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Dissolve();
bDead = true;
BurnDebuffComponent->Deactivate();
}
Source/Aura/Public/Character/AuraEnemy.h
public:
virtual void Die(const FVector& DeathImpulse) override;
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::Die(const FVector& DeathImpulse)
{
// 设置寿命
SetLifeSpan(LifeSpan);
// 设置黑板键Dead
if (AuraAIController) AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("Dead"), true);
Super::Die(DeathImpulse);
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::HandleIncomingDamage(const FEffectProperties& Props)
{
// IncomingDamage 属性只在服务器执行获取,不会复制到客户端
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
FVector Impulse = UAuraAbilitySystemLibrary::GetDeathImpulse(Props.EffectContextHandle);
CombatInterface->Die(UAuraAbilitySystemLibrary::GetDeathImpulse(Props.EffectContextHandle));
}
// 死亡后发送XP事件
SendXPEvent(Props);
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
// 是否暴击,格挡,显示提示文本
const bool bBlock = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
ShowFloatingText(Props, LocalIncomingDamage, bBlock, bCriticalHit);
if (UAuraAbilitySystemLibrary::IsSuccessfulDebuff(Props.EffectContextHandle))
{
Debuff(Props);
}
}
}
GA_FireBolt-Death Impulse Magnitude-10000
Source/Aura/Private/AbilitySystem/Debuff/DebuffNiagaraComponent.cpp
void UDebuffNiagaraComponent::DebuffTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
const bool bOwnerValid = IsValid(GetOwner());
const bool bOwnerAlive = GetOwner()->Implements<UCombatInterface>() && !ICombatInterface::Execute_IsDead(GetOwner());
if (NewCount > 0 && bOwnerValid && bOwnerAlive)
{
Activate();
}
else
{
Deactivate();
}
}
GA_HitReact-类默认值-标签-Activation Blocked Tags-Debuff.Burn
Source/Aura/Public/AuraAbilityTypes.h
// 伤害效果参数结构类型
USTRUCT(BlueprintType)
struct FDamageEffectParams
{
GENERATED_BODY()
FDamageEffectParams(){}
UPROPERTY(BlueprintReadWrite)
TObjectPtr<UObject> WorldContextObject = nullptr;
UPROPERTY(BlueprintReadWrite)
TSubclassOf<UGameplayEffect> DamageGameplayEffectClass = nullptr;
UPROPERTY(BlueprintReadWrite)
TObjectPtr<UAbilitySystemComponent> SourceAbilitySystemComponent;
UPROPERTY(BlueprintReadWrite)
TObjectPtr<UAbilitySystemComponent> TargetAbilitySystemComponent;
UPROPERTY(BlueprintReadWrite)
float BaseDamage = 0.f;
UPROPERTY(BlueprintReadWrite)
float AbilityLevel = 1.f;
UPROPERTY(BlueprintReadWrite)
FGameplayTag DamageType = FGameplayTag();
UPROPERTY(BlueprintReadWrite)
float DebuffChance = 0.f;
UPROPERTY(BlueprintReadWrite)
float DebuffDamage = 0.f;
UPROPERTY(BlueprintReadWrite)
float DebuffDuration = 0.f;
UPROPERTY(BlueprintReadWrite)
float DebuffFrequency = 0.f;
// 死亡物理效果的力值
UPROPERTY(BlueprintReadWrite)
float DeathImpulseMagnitude = 0.f;
// 死亡冲击向量
UPROPERTY(BlueprintReadWrite)
FVector DeathImpulse = FVector::ZeroVector;
// 击退力度
UPROPERTY(BlueprintReadWrite)
float KnockbackForceMagnitude = 0.f;
// 击退几率
UPROPERTY(BlueprintReadWrite)
float KnockbackChance = 0.f;
// 击退力
UPROPERTY(BlueprintReadWrite)
FVector KnockbackForce = FVector::ZeroVector;
};
struct FAuraGameplayEffectContext : public FGameplayEffectContext
public:
FVector GetKnockbackForce() const { return KnockbackForce; }
void SetKnockbackForce(const FVector& InForce) { KnockbackForce = InForce; }
protected:
UPROPERTY()
FVector KnockbackForce = FVector::ZeroVector;
Source/Aura/Private/AuraAbilityTypes.cpp
#include "AuraAbilityTypes.h"
bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
uint32 RepBits = 0;
// 存储
if (Ar.IsSaving())
{
if (bReplicateInstigator && Instigator.IsValid())
{
RepBits |= 1 << 0;
}
if (bReplicateEffectCauser && EffectCauser.IsValid() )
{
RepBits |= 1 << 1;
}
if (AbilityCDO.IsValid())
{
RepBits |= 1 << 2;
}
if (bReplicateSourceObject && SourceObject.IsValid())
{
RepBits |= 1 << 3;
}
if (Actors.Num() > 0)
{
RepBits |= 1 << 4;
}
if (HitResult.IsValid())
{
RepBits |= 1 << 5;
}
if (bHasWorldOrigin)
{
RepBits |= 1 << 6;
}
if (bIsBlockedHit)
{
// 如果格挡,翻转第7位-1
RepBits |= 1 << 7;
}
if (bIsCriticalHit)
{
// 如果暴击,翻转第8位-1
RepBits |= 1 << 8;
}
if (bIsSuccessfulDebuff)
{
RepBits |= 1 << 9;
}
if (DebuffDamage > 0.f)
{
RepBits |= 1 << 10;
}
if (DebuffDuration > 0.f)
{
RepBits |= 1 << 11;
}
if (DebuffFrequency > 0.f)
{
RepBits |= 1 << 12;
}
if (DamageType.IsValid())
{
RepBits |= 1 << 13;
}
if (!DeathImpulse.IsZero())
{
RepBits |= 1 << 14;
}
if (!KnockbackForce.IsZero())
{
RepBits |= 1 << 15;
}
}
//序列化前15位
Ar.SerializeBits(&RepBits, 15);
if (RepBits & (1 << 0))
{
Ar << Instigator;
}
if (RepBits & (1 << 1))
{
Ar << EffectCauser;
}
if (RepBits & (1 << 2))
{
Ar << AbilityCDO;
}
if (RepBits & (1 << 3))
{
Ar << SourceObject;
}
if (RepBits & (1 << 4))
{
SafeNetSerializeTArray_Default<31>(Ar, Actors);
}
if (RepBits & (1 << 5))
{
if (Ar.IsLoading())
{
if (!HitResult.IsValid())
{
HitResult = TSharedPtr<FHitResult>(new FHitResult());
}
}
HitResult->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 6))
{
Ar << WorldOrigin;
bHasWorldOrigin = true;
}
else
{
bHasWorldOrigin = false;
}
if (RepBits & (1 << 7))
{
Ar << bIsBlockedHit;
}
if (RepBits & (1 << 8))
{
Ar << bIsCriticalHit;
}
if (RepBits & (1 << 9))
{
Ar << bIsSuccessfulDebuff;
}
if (RepBits & (1 << 10))
{
Ar << DebuffDamage;
}
if (RepBits & (1 << 11))
{
Ar << DebuffDuration;
}
if (RepBits & (1 << 12))
{
Ar << DebuffFrequency;
}
if (RepBits & (1 << 13))
{
if (Ar.IsLoading())
{
if (!DamageType.IsValid())
{
DamageType = TSharedPtr<FGameplayTag>(new FGameplayTag());
}
}
DamageType->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 14))
{
DeathImpulse.NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 15))
{
KnockbackForce.NetSerialize(Ar, Map, bOutSuccess);
}
if (Ar.IsLoading())
{
AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
}
bOutSuccess = true;
return true;
}
应用伤害效果时也设置击退参数
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static FVector GetKnockbackForce(const FGameplayEffectContextHandle& EffectContextHandle);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetKnockbackForce(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, const FVector& InForce);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
FVector UAuraAbilitySystemLibrary::GetKnockbackForce(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->GetKnockbackForce();
}
return FVector::ZeroVector;
}
void UAuraAbilitySystemLibrary::SetKnockbackForce(FGameplayEffectContextHandle& EffectContextHandle,
const FVector& InForce)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetKnockbackForce(InForce);
}
}
FGameplayEffectContextHandle UAuraAbilitySystemLibrary::ApplyDamageEffect(const FDamageEffectParams& DamageEffectParams)
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
const AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();
FGameplayEffectContextHandle EffectContexthandle = DamageEffectParams.SourceAbilitySystemComponent->
MakeEffectContext();
EffectContexthandle.AddSourceObject(SourceAvatarActor);
SetDeathImpulse(EffectContexthandle, DamageEffectParams.DeathImpulse);
SetKnockbackForce(EffectContexthandle, DamageEffectParams.KnockbackForce);
const FGameplayEffectSpecHandle SpecHandle = DamageEffectParams.SourceAbilitySystemComponent->MakeOutgoingSpec(
DamageEffectParams.DamageGameplayEffectClass, DamageEffectParams.AbilityLevel, EffectContexthandle);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, DamageEffectParams.DamageType,
DamageEffectParams.BaseDamage);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Chance,
DamageEffectParams.DebuffChance);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Damage,
DamageEffectParams.DebuffDamage);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Duration,
DamageEffectParams.DebuffDuration);
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Debuff_Frequency,
DamageEffectParams.DebuffFrequency);
DamageEffectParams.TargetAbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data);
return EffectContexthandle;
}
Source/Aura/Private/Actor/AuraProjectile.cpp
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();
if (SourceAvatarActor == OtherActor) return;
// 技能抛射物不能伤害友军
if (!UAuraAbilitySystemLibrary::IsNotFriend(SourceAvatarActor, OtherActor)) return;
if (!bHit) OnHit();
if (HasAuthority())
{
// 只在服务器上应用伤害效果即可
// 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端
// 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
// 死亡冲击向量
const FVector DeathImpulse = GetActorForwardVector() * DamageEffectParams.DeathImpulseMagnitude;
DamageEffectParams.DeathImpulse = DeathImpulse;
// 击退
const bool bKnockback = FMath::RandRange(1, 100) < DamageEffectParams.KnockbackChance;
if (bKnockback)
{
FRotator Rotation = GetActorRotation();
// 向上旋转45度
Rotation.Pitch = 45.f;
const FVector KnockbackDirection = Rotation.Vector();
const FVector KnockbackForce = KnockbackDirection * DamageEffectParams.KnockbackForceMagnitude;
DamageEffectParams.KnockbackForce = KnockbackForce;
}
DamageEffectParams.TargetAbilitySystemComponent = TargetASC;
UAuraAbilitySystemLibrary::ApplyDamageEffect(DamageEffectParams);
}
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
else bHit = true;
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::HandleIncomingDamage(const FEffectProperties& Props)
{
// IncomingDamage 属性只在服务器执行获取,不会复制到客户端
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
FVector Impulse = UAuraAbilitySystemLibrary::GetDeathImpulse(Props.EffectContextHandle);
CombatInterface->Die(UAuraAbilitySystemLibrary::GetDeathImpulse(Props.EffectContextHandle));
}
// 死亡后发送XP事件
SendXPEvent(Props);
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
// 击退
const FVector& KnockbackForce = UAuraAbilitySystemLibrary::GetKnockbackForce(Props.EffectContextHandle);
if (!KnockbackForce.IsNearlyZero(1.f))
{
Props.TargetCharacter->LaunchCharacter(KnockbackForce, true, true);
}
}
// 是否暴击,格挡,显示提示文本
const bool bBlock = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
ShowFloatingText(Props, LocalIncomingDamage, bBlock, bCriticalHit);
if (UAuraAbilitySystemLibrary::IsSuccessfulDebuff(Props.EffectContextHandle))
{
Debuff(Props);
}
}
}
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
public:
UFUNCTION(BlueprintPure)
FDamageEffectParams MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor = nullptr) const;
protected:
// 击退力度
UPROPERTY(EditDefaultsOnly, Category = "Damage")
float KnockbackForceMagnitude = 1000.f;
// 击退几率
UPROPERTY(EditDefaultsOnly, Category = "Damage")
float KnockbackChance = 0.f;
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
FDamageEffectParams UAuraDamageGameplayAbility::MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor) const
{
FDamageEffectParams Params;
Params.WorldContextObject = GetAvatarActorFromActorInfo();
Params.DamageGameplayEffectClass = DamageEffectClass;
Params.SourceAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
Params.TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
Params.BaseDamage = Damage.GetValueAtLevel(GetAbilityLevel());
Params.AbilityLevel = GetAbilityLevel();
Params.DamageType = DamageType;
Params.DebuffChance = DebuffChance;
Params.DebuffDamage = DebuffDamage;
Params.DebuffDuration = DebuffDuration;
Params.DebuffFrequency = DebuffFrequency;
Params.DeathImpulseMagnitude = DeathImpulseMagnitude;
Params.KnockbackForceMagnitude = KnockbackForceMagnitude;
Params.KnockbackChance = KnockbackChance;
if (IsValid(TargetActor))
{
FRotator Rotation = (TargetActor->GetActorLocation() - GetAvatarActorFromActorInfo()->GetActorLocation()).Rotation();
// 向上旋转45度
Rotation.Pitch = 45.f;
const FVector ToTarget = Rotation.Vector();
Params.DeathImpulse = ToTarget * DeathImpulseMagnitude;
Params.KnockbackForce = ToTarget * KnockbackForceMagnitude;
}
return Params;
}
事件图表: Make Damage Effect Params from Class Defaults
apply damage effect
远程伤害已经应用了伤害效果
GA_FireBolt-类默认值-KnockbackChance -100 击退几率 100% 用于测试
打开 ABP_Aura 事件图表: sequence
character movement Is Falling提升为 IsFalling
如果在空中,播放空中动画 InAir 右键-添加状态别名 ToInAir
ToInAir-IdleWalkRun-启用 ToInAir-Idle-启用
ToInAir 到 InAir 转换规则: IsFalling
右键-添加状态别名 ToInAir InAir 到 Idle 转换规则: IsFalling not boolean
添加-体积-阻挡体积 防止在地图边界
Source/Aura/Public/AbilitySystem/Abilities/AuraFireBolt.h
public:
UFUNCTION(BlueprintCallable)
void SpawnProjectiles(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag, bool bOverridePitch,
float PitchOverride, AActor* HomingTarget);
protected:
// 生成抛射物的扇形角度
UPROPERTY(EditDefaultsOnly, Category = "FireBolt")
float ProjectileSpread = 90.f;
// 抛射物最大生成量
UPROPERTY(EditDefaultsOnly, Category = "FireBolt")
int32 MaxNumProjectiles = 5;
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBolt.cpp
#include "Kismet/KismetSystemLibrary.h"
void UAuraFireBolt::SpawnProjectiles(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag,
bool bOverridePitch, float PitchOverride, AActor* HomingTarget)
{
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
SocketTag);
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
if (bOverridePitch) Rotation.Pitch = PitchOverride;
const FVector Forward = Rotation.Vector();
const FVector LeftOfSpread = Forward.RotateAngleAxis(-ProjectileSpread / 2.f, FVector::UpVector);
const FVector RightOfSpread = Forward.RotateAngleAxis(ProjectileSpread / 2.f, FVector::UpVector);
//NumProjectiles = FMath::Min(MaxNumProjectiles, GetAbilityLevel());
if (NumProjectiles > 1)
{
const float DeltaSpread = ProjectileSpread / (NumProjectiles - 1);
for (int32 i = 0; i < NumProjectiles; i++)
{
const FVector Direction = LeftOfSpread.RotateAngleAxis(DeltaSpread * i, FVector::UpVector);
const FVector Start = SocketLocation + FVector(0, 0, 5);
UKismetSystemLibrary::DrawDebugArrow(
GetAvatarActorFromActorInfo(),
Start,
Start + Direction * 75.f,
1,
FLinearColor::Red,
120,
1);
}
}
else
{
// Single projectile
const FVector Start = SocketLocation + FVector(0, 0, 5);
UKismetSystemLibrary::DrawDebugArrow(
GetAvatarActorFromActorInfo(),
Start,
Start + Forward * 75.f,
1,
FLinearColor::Red,
120,
1);
}
UKismetSystemLibrary::DrawDebugArrow(GetAvatarActorFromActorInfo(), SocketLocation,
SocketLocation + Forward * 100.f, 1, FLinearColor::White, 120, 1);
UKismetSystemLibrary::DrawDebugArrow(GetAvatarActorFromActorInfo(), SocketLocation,
SocketLocation + LeftOfSpread * 100.f, 1, FLinearColor::Gray, 120, 1);
UKismetSystemLibrary::DrawDebugArrow(GetAvatarActorFromActorInfo(), SocketLocation,
SocketLocation + RightOfSpread * 100.f, 1, FLinearColor::Gray, 120, 1);
}
事件图表: 断开原 SpawnProjectile
添加 SpawnProjectiles
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
// 等距旋转 可用于生成抛射物
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayMechanics")
static TArray<FRotator> EvenlySpacedRotators(const FVector& Forward, const FVector& Axis, float Spread, int32 NumRotators);
// 等距旋转 向量版本
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayMechanics")
static TArray<FVector> EvenlyRotatedVectors(const FVector& Forward, const FVector& Axis, float Spread, int32 NumVectors);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
TArray<FRotator> UAuraAbilitySystemLibrary::EvenlySpacedRotators(const FVector& Forward, const FVector& Axis,
float Spread, int32 NumRotators)
{
TArray<FRotator> Rotators;
const FVector LeftOfSpread = Forward.RotateAngleAxis(-Spread / 2.f, Axis);
if (NumRotators > 1)
{
const float DeltaSpread = Spread / (NumRotators - 1);
for (int32 i = 0; i < NumRotators; i++)
{
const FVector Direction = LeftOfSpread.RotateAngleAxis(DeltaSpread * i, FVector::UpVector);
Rotators.Add(Direction.Rotation());
}
}
else
{
Rotators.Add(Forward.Rotation());
}
return Rotators;
}
TArray<FVector> UAuraAbilitySystemLibrary::EvenlyRotatedVectors(const FVector& Forward, const FVector& Axis,
float Spread, int32 NumVectors)
{
TArray<FVector> Vectors;
const FVector LeftOfSpread = Forward.RotateAngleAxis(-Spread / 2.f, Axis);
if (NumVectors > 1)
{
const float DeltaSpread = Spread / (NumVectors - 1);
for (int32 i = 0; i < NumVectors; i++)
{
const FVector Direction = LeftOfSpread.RotateAngleAxis(DeltaSpread * i, FVector::UpVector);
Vectors.Add(Direction);
}
}
else
{
Vectors.Add(Forward);
}
return Vectors;
}
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBolt.cpp
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
void UAuraFireBolt::SpawnProjectiles(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag,
bool bOverridePitch, float PitchOverride, AActor* HomingTarget)
{
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
SocketTag);
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
if (bOverridePitch) Rotation.Pitch = PitchOverride;
const FVector Forward = Rotation.Vector();
TArray<FVector> Directions= UAuraAbilitySystemLibrary::EvenlyRotatedVectors(Forward,FVector::UpVector,ProjectileSpread,NumProjectiles);
TArray<FRotator> Rotations=UAuraAbilitySystemLibrary::EvenlySpacedRotators(Forward,FVector::UpVector,ProjectileSpread,NumProjectiles);
for (FVector& Direction : Directions)
{
UKismetSystemLibrary::DrawDebugArrow(
GetAvatarActorFromActorInfo(),
SocketLocation,
SocketLocation + Direction * 75.f,
1,
FLinearColor::Red,
120,
1);
}
for (FRotator& Rot : Rotations)
{
FVector Start = SocketLocation + FVector(0, 0, 5);
UKismetSystemLibrary::DrawDebugArrow(
GetAvatarActorFromActorInfo(),
Start,
Start + Rot.Vector() * 75.f,
1,
FLinearColor::Red,
120,
1);
}
}
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBolt.cpp
#include "Actor/AuraProjectile.h"
void UAuraFireBolt::SpawnProjectiles(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag,
bool bOverridePitch, float PitchOverride, AActor* HomingTarget)
{
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
SocketTag);
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
if (bOverridePitch) Rotation.Pitch = PitchOverride;
const FVector Forward = Rotation.Vector();
TArray<FRotator> Rotations=UAuraAbilitySystemLibrary::EvenlySpacedRotators(Forward,FVector::UpVector,ProjectileSpread,NumProjectiles);
for (const FRotator& Rot : Rotations)
{
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
SpawnTransform.SetRotation(Rot.Quaternion());
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
Projectile->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
Projectile->FinishSpawning(SpawnTransform);
}
}
BP_FireBolt-类默认设置-抛射物-发射物重力范围-1 初始速度-750 最大速度-750
现在火球会在发射后坠落到地面。
事件图表: 生成时设置俯仰角 override SpawnProjectiles-override pitch-启用 SpawnProjectiles-pitch override-65
事件图表: 抛射物重叠结果-hit actor 提升为变量 MouseHitActor MouseHitActor 作为 抛射物追踪目标 Homing Target
HomingTargetComponent 为弱引用指针 如果鼠标点击的actor不是敌人, 则创建新的场景组件作为追踪actor。 不能使用鼠标的点击目标,例如地板,中心点不在鼠标点击处。
由于 ProjectileMovement->HomingTargetComponent 是弱引用,其上创建的场景组件在抛射物销毁时不会垃圾回收 所以需要为附加到抛射物专用组件 Projectile->HomingTargetSceneComponent 上
Source/Aura/Public/Actor/AuraProjectile.h
public:
// 点击非敌人时使用 场景组件作为追踪目标
// 用于垃圾回收
UPROPERTY()
TObjectPtr<USceneComponent> HomingTargetSceneComponent;
Source/Aura/Public/AbilitySystem/Abilities/AuraFireBolt.h
protected:
// 飞向追踪目标的最小速度
UPROPERTY(EditDefaultsOnly, Category = "FireBolt")
float HomingAccelerationMin = 1600.f;
UPROPERTY(EditDefaultsOnly, Category = "FireBolt")
float HomingAccelerationMax = 3200.f;
// 是否发射追踪物
UPROPERTY(EditDefaultsOnly, Category = "FireBolt")
bool bLaunchHomingProjectiles = true;
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBolt.cpp
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
#include "Actor/AuraProjectile.h"
#include "GameFramework/ProjectileMovementComponent.h"
void UAuraFireBolt::SpawnProjectiles(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag,
bool bOverridePitch, float PitchOverride, AActor* HomingTarget)
{
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
if (!bIsServer) return;
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocation(
GetAvatarActorFromActorInfo(),
SocketTag);
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation();
if (bOverridePitch) Rotation.Pitch = PitchOverride;
const FVector Forward = Rotation.Vector();
const int32 EffectiveNumProjectiles = FMath::Min(NumProjectiles, GetAbilityLevel());
TArray<FRotator> Rotations = UAuraAbilitySystemLibrary::EvenlySpacedRotators(
Forward, FVector::UpVector, ProjectileSpread, EffectiveNumProjectiles);
for (const FRotator& Rot : Rotations)
{
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
SpawnTransform.SetRotation(Rot.Quaternion());
AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetOwningActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
Projectile->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
if (HomingTarget && HomingTarget->Implements<UCombatInterface>())
{
// HomingTargetComponent 为弱引用指针
Projectile->ProjectileMovement->HomingTargetComponent = HomingTarget->GetRootComponent();
}
else
{
// 创建新的场景组件作为追踪actor
// 不能使用鼠标的点击目标,例如地板,中心点不在鼠标点击处
// 由于 ProjectileMovement->HomingTargetComponent 是弱引用,其上创建的场景组件在抛射物销毁时不会垃圾回收
// 所以需要为附加到抛射物专用组件 Projectile->HomingTargetSceneComponent 上
Projectile->HomingTargetSceneComponent = NewObject<USceneComponent>(USceneComponent::StaticClass());
Projectile->HomingTargetSceneComponent->SetWorldLocation(ProjectileTargetLocation);
Projectile->ProjectileMovement->HomingTargetComponent = Projectile->HomingTargetSceneComponent;
}
Projectile->ProjectileMovement->HomingAccelerationMagnitude = FMath::FRandRange(
HomingAccelerationMin, HomingAccelerationMax);
Projectile->ProjectileMovement->bIsHomingProjectile = bLaunchHomingProjectiles;
Projectile->FinishSpawning(SpawnTransform);
}
}
现在,如果鼠标选择敌人,则火球会朝飞行。 指向地面,则飞向选中地点。撞击地面,地面需要与火球有重叠事件。生成重叠。
俯仰角太高会无法击中近距离敌人
Source/Aura/Public/Player/AuraPlayerController.h
class UNiagaraSystem;
private:
// 指示目标点的粒子特效
UPROPERTY(EditDefaultsOnly)
TObjectPtr<UNiagaraSystem> ClickNiagaraSystem;
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "NiagaraFunctionLibrary.h"
void AAuraPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)
{
// 如果释放的不是鼠标左键,而是其他键,则激活释放对应键的技能
// 此时一定不是自动奔跑或鼠标释放左键技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())GetASC()->AbilityInputTagReleased(InputTag);
return;
}
// 如果释放的是鼠标左键,且跟踪到敌人,则执行释放鼠标左键的技能
if (GetASC()) GetASC()->AbilityInputTagReleased(InputTag);
// 如果是鼠标左键释放,并且鼠标没有跟踪到敌人目标,并且没有按下shift ,则自动奔跑至目的地
if (!bTargeting && !bShiftKeyDown)
{
const APawn* ControlledPawn = GetPawn();
// 如果鼠标跟随时间小于短按阙值,表示是短按
if (FollowTime <= ShortPressThreshold && ControlledPawn)
{
// 通过受控pawn位置和目的地位置,同步查找位置路径,生成导航路径
if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(
this, ControlledPawn->GetActorLocation(), CachedDestination))
{
//清除样条线的点
Spline->ClearSplinePoints();
// 循环导航路径的点
for (const FVector& PointLoc : NavPath->PathPoints)
{
//向样条曲线添加点
Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
//DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Green, false, 5.f);
}
// 修复玩家自动寻路错误
if (NavPath->PathPoints.Num() > 0)
{
// 减去目的地路径导航点的最后一个点,防止目标点为障碍物中心时,玩家永远无法到达而不停奔跑
CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
// 正在自动奔跑为真
bAutoRunning = true;
}
}
// 指示目标点的粒子特效
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ClickNiagaraSystem, CachedDestination);
}
//自动奔跑时重置跟随时间
FollowTime = 0.f;
// 自动奔跑时没有跟踪敌人
bTargeting = false;
}
}
类默认值-Click Niagara System-FX_Cursor
打开 BP_AuraCharacter 事件图表: 添加变量 InShockLoop 布尔类型 公开
右键长按不断施放雷电技能,并循环播放雷电技能动画
打开 ABP_Aura
Cast_Shock_Loop 动画序列
BP_Aura_Character get InShockLoop 提升为变量 InShockLoop
根据 InShockLoop 转换 idle 和 Cast_Shock_Loop动画序列
idle 到 Cast_Shock_Loop 规则: InShockLoop
Cast_Shock_Loop 到 idle 规则: 非 InShockLoop not boolean
Source/Aura/Public/Interaction/CombatInterface.h
public:
// 设置是否播放雷击动画
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void SetInShockLoop(bool bInLoop);
startup abilities- GA_Electrocute
长按才能持续触发雷电技能
配置技能初始输入操作标签 细节-输入-startup input tag-InputTag.RMB 右键
事件图表: 清空测试节点
event activateAbility 测试释放按键触发技能 print string wait input release print string end ability 此时释放右键不能打印雷电技能
InvokeReplicatedEvent 向服务器发送 技能通用复制中的按下事件数据, 告知服务器,正在按下键 参数3:预测键:使用了技能首次激活/上次激活 的原始的预测键 这之后 wait input release 等待按键释放事件有效才会有效
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
void AbilityInputTagPressed(const FGameplayTag& InputTag);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::AbilityInputTagPressed(const FGameplayTag& InputTag)
{
if (!InputTag.IsValid()) return;
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
if (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag))
{
AbilitySpecInputPressed(AbilitySpec);
if (AbilitySpec.IsActive())
{
// InvokeReplicatedEvent 向服务器发送 技能通用复制中的按下事件数据,
// 告知服务器,正在按下键
// 参数3:预测键:使用了技能首次激活/上次激活 的原始的预测键
// 这之后 wait input release 等待按键释放事件有效才会有效
InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, AbilitySpec.Handle,
AbilitySpec.ActivationInfo.GetActivationPredictionKey());
}
}
}
}
void UAuraAbilitySystemComponent::AbilityInputTagReleased(const FGameplayTag& InputTag)
{
if (!InputTag.IsValid()) return;
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
if (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag) && AbilitySpec.IsActive())
{
//通知技能规格,已经释放输入按键
AbilitySpecInputReleased(AbilitySpec);
InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputReleased, AbilitySpec.Handle,
AbilitySpec.ActivationInfo.GetActivationPredictionKey());
}
}
}
Source/Aura/Private/Player/AuraPlayerController.cpp
// 按下时触发 根据标签识别按键
void AAuraPlayerController::AbilityInputTagPressed(FGameplayTag InputTag)
{
// 鼠标左键为特殊按键,
// 可以激活技能,ThisActor 表示敌人,如果鼠标跟踪到敌人,则激活技能
// 也可以使角色自动奔跑。如果鼠标没有跟踪到敌人,并且是短按
if (InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
// 鼠标是否跟踪到敌人
bTargeting = ThisActor ? true : false;
// 按下鼠标左键瞬间,还无法确定是否短按,不应自动奔跑。需要等待鼠标左键释放时确定短按长按
bAutoRunning = false;
}
if (GetASC()) GetASC()->AbilityInputTagPressed(InputTag);
}
现在等待释放事件有效。 按下鼠标右键时,雷电技能激活。 长按不释放鼠标右键。 知道释放鼠标右键才会触发 wait input release 事件,开始之后的雷电技能逻辑。
事件图表: 释放10秒后再结束技能。 delay
监听按下操作 wait input press wait input press-test already pressed-启用 将监听第一次下【用于激活技能的右键按下,否则不会监听第一次按下】 print string 第一次按下右键后,触发按下操作。只会触发一次按下监听。 然后激活技能,松开后执行技能逻辑。 10秒内只会触发一次按下监听。
由于控制器设置了右键按下开关标志,右键按下事件不会每一帧重复触发。
这是雷击技能的核心。 右键按下,雷击开始,直到松开右键,雷击才会结束,最多持续10秒.
防止内存泄漏。
Source/Aura/Private/Actor/AuraProjectile.cpp
void AAuraProjectile::OnHit()
{
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
// 此时投射物可能已销毁 循环音效也将一同销毁
if (LoopingSoundComponent)
{
LoopingSoundComponent->Stop();
LoopingSoundComponent->DestroyComponent();
}
bHit = true;
}
void AAuraProjectile::Destroyed()
{
if (LoopingSoundComponent)
{
LoopingSoundComponent->Stop();
LoopingSoundComponent->DestroyComponent();
}
if (!bHit && !HasAuthority()) OnHit();
Super::Destroyed();
}
GA_FireBolt HomingAccelerationMin - 6500
HomingAccelerationMax -7800
朝向归航目标的加速度大小。总体速度大小仍将受到MaxSpeed的限制
为雷电技能创建父类技能C++,替换当前使用的父类 AuraDamageGameplayAbility 伤害型技能。 包含雷电技能相关的密集型计算函数。
AuraBeamSpell
激活技能时,获取鼠标选中的目标数据。
Source/Aura/Public/AbilitySystem/Abilities/AuraBeamSpell.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraDamageGameplayAbility.h"
#include "AuraBeamSpell.generated.h"
UCLASS()
class AURA_API UAuraBeamSpell : public UAuraDamageGameplayAbility
{
GENERATED_BODY()
public:
// 激活技能时获取鼠标选中目标的信息并存储
UFUNCTION(BlueprintCallable)
void StoreMouseDataInfo(const FHitResult& HitResult);
// 存储控制器,之后将使用控制器控制光标在光束释放时隐藏,释放完毕显示。
UFUNCTION(BlueprintCallable)
void StoreOwnerPlayerController();
protected:
UPROPERTY(BlueprintReadWrite, Category = "Beam")
FVector MouseHitLocation;
UPROPERTY(BlueprintReadWrite, Category = "Beam")
TObjectPtr<AActor> MouseHitActor;
UPROPERTY(BlueprintReadWrite, Category = "Beam")
TObjectPtr<APlayerController> OwnerPlayerController;
};
Source/Aura/Private/AbilitySystem/Abilities/AuraBeamSpell.cpp
#include "AbilitySystem/Abilities/AuraBeamSpell.h"
void UAuraBeamSpell::StoreMouseDataInfo(const FHitResult& HitResult)
{
// 鼠标指针有有效的阻挡时才存储命中目标信息
if (HitResult.bBlockingHit)
{
MouseHitLocation = HitResult.ImpactPoint;
MouseHitActor = HitResult.GetActor();
}
else
{
CancelAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true);
}
}
void UAuraBeamSpell::StoreOwnerPlayerController()
{
if (CurrentActorInfo)
{
OwnerPlayerController = CurrentActorInfo->PlayerController.Get();
}
}
类设置-父类- AuraBeamSpell
事件图表: 删除测试节点: 保留 wait input release
激活技能时获取鼠标选中目标 TargetDataUnderMouse
从 TargetDataUnderMouse-data handle 获取鼠标命中结果 get hit result from target data 拆分命中结果 break hit result 【不再需要】 调用C++ StoreMouseDataInfo 存储 TargetDataUnderMouse-get hit result from target data 获取 撞击点,命中actor等信息,存储到C++. 用于设置光束生成位置和结束位置,设置光束伤害的目标actor
使用控制器控制光标在光束释放时隐藏,释放完毕显示。
调用C++ StoreOwnerPlayerController 存储玩家控制器【从自身actor获取】。
sequence
首先,通过玩家控制器隐藏光标 获取c++的 OwnerPlayerController set show mouse cursor
然后 wait input release - on release 等待按键释放再次显示光标 获取c++的 OwnerPlayerController set show mouse cursor 结束技能 end ability
释放雷击技能后,自身播放雷击施法蒙太奇动画, 为目标的赋予触电蒙太奇标签。目标播放触电蒙太奇动画 目标所属的职业可以有自己专属的触电蒙太奇动画。
添加变量 Montage_Electrocute 类型 动画蒙太奇-对象引用
项目设置-gameplay tags-管理gameplay标签
Content/Assets/Characters/Aura/Animations/Abilities/AM_Cast_Electrocute.uasset
打开AM_Cast_Electrocute 混合选项-混入-混合时间-0.1 混合选项-混出-混合时间-0.1 防止动画衔接抖动
动画-比率范围-0.5 放慢动画
默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人施法动作的开始攻击 - 到施法动作攻击结束
选择 MotionWarping 通知-Root Motion Modifier-Warp Target Name-FacingTarget FacingTarget 将用在敌人扭曲运动事件的 add or update warp target from location-warp target name
选择 MotionWarping 通知-Root Motion Modifier-Warp Translation-不启用 只有扭曲旋转
Root Motion Modifier-Rotation Type-Facing 旋转以面对该目标
决定施法攻击的时机
右键-添加通知-AN_MontageEvent 【自定义的通知】 在法杖尖位置 选择 AN_MontageEvent -动画通知-event tag-Event.Montage.Electrocute 用以在游戏中监听
标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇上的插槽位置,生成重叠虚拟球。 播放此蒙太奇可触发通知事件,带有事件标签 Event.Montage.Electrocute 后续通过标签监听此事件。
攻击音效
添加通知-播放音效- sfx_Swipe
Montage_Electrocute-默认值-AM_Cast_Electrocute
事件图表:
直接调用C++ 扭曲动画 Update Facing Target(message) 带邮件标志
get avatar actor from actor info MouseHitLocation
play montage and wait play montage and wait-stop when ability ends-取消 Montage_Electrocute
事件图表: 直接调用C++ 扭曲动画 Update Facing Target(message)带邮件标志 替换原 Update Facing Target 删除 cast to combatInterface
事件图表: get avatar actor from actor info 检查是否实现战斗接口 does implement interface-interface-combat interface branch 未实现则结束技能 end ability 折叠到函数 EnforceImplementsCombatInterface
事件图表: 监听动画蓝图设置释放雷击术循环变量事件 Event Set in shock loop set InShockLoop
事件图表: 开始播放攻击动画时立即设置变量 不许考虑动画混合时间问题 set InShockLoop-true get avatar actor from actor info
松开键时设置 set InShockLoop 为false get avatar actor from actor info
这样,在长按右键时,ABP动画蓝图将持续播放释放雷电术动画 AM_Cast_Electrocute,松开键时不再播放。
InShockLoop 存储在 BP_AuraCharacter 上,在技能与动画蓝图中传递。
选中 cast_shock_loop 到 idle 的规则 细节-混合设置-时长-0.1 时间越小,过渡越快,收起法杖动作越快
StoreOwnerPlayerController() 改为 StoreOwnerVariables()
Source/Aura/Public/AbilitySystem/Abilities/AuraBeamSpell.h
public:
UFUNCTION(BlueprintCallable)
void StoreOwnerVariables();
protected:
UPROPERTY(BlueprintReadWrite, Category = "Beam")
TObjectPtr<ACharacter> OwnerCharacter;
Source/Aura/Private/AbilitySystem/Abilities/AuraBeamSpell.cpp
#include "GameFramework/Character.h"
void UAuraBeamSpell::StoreOwnerVariables()
{
if (CurrentActorInfo)
{
OwnerPlayerController = CurrentActorInfo->PlayerController.Get();
OwnerCharacter = Cast<ACharacter>(CurrentActorInfo->AvatarActor);
}
}
施法时,获取自身移动组件,停止移动。
事件图表: 替换 StoreOwnerPlayerController() 改为 StoreOwnerVariables()
施法时,获取自身,再获取移动组件,停止移动。 get OwnerCharacter get Character Movement DIsable Movement
结束技能之前,启用移动 get OwnerCharacter get Character Movement set Movement mode-new Movement mode-Walking 行走
这样,长按雷电技能键时,无法移动,松开键即可移动。
如果在扭曲运动之前禁用移动,则扭曲运动失效。 应该在监听到施法动画蒙太奇发送的自定义事件之后禁用移动。 wait gameplay event wait gameplay event-event tag-Event.Montage.Electrocute wait gameplay event-onl;y trigger once-启用 只触发一次
PrepareToEndAbility
InShockLoop
UpdateFacingTarget
GA_Electrocute 修复 按住右键,同时可以点击左键触发其他技能的错误 技能拥有多种标签,actor的技能系统组件拥有这些标签,可用于激活技能,阻止技能 玩家控制器可以检查技能系统组件上的这些标签
Source/Aura/Public/AuraGameplayTags.h
public:
// 禁用按键事件,光标追踪的相关标签
FGameplayTag Player_Block_InputPressed;
FGameplayTag Player_Block_InputHeld;
FGameplayTag Player_Block_InputReleased;
FGameplayTag Player_Block_CursorTrace;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
/*
* Player Tags
*/
GameplayTags.Player_Block_CursorTrace = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Player.Block.CursorTrace"),
FString("Block tracing under the cursor")
);
GameplayTags.Player_Block_InputHeld = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Player.Block.InputHeld"),
FString("Block Input Held callback for input")
);
GameplayTags.Player_Block_InputPressed = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Player.Block.InputPressed"),
FString("Block Input Pressed callback for input")
);
GameplayTags.Player_Block_InputReleased = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Player.Block.InputReleased"),
FString("Block Input Released callback for input")
);
}
Player.Block.CursorTrace 标签使actor无法被选择,高亮 Player.Block.InputHeld 标签使actor的控制器无法按住键,也就不会触发相关输入操作对应的技能。 Player.Block.InputPressed 标签使actor的控制器无法左键单击,使actor无法左键单击移动 Player.Block.InputReleased 标签使actor的控制器无法右键单击
Source/Aura/Private/Player/AuraPlayerController.cpp
void AAuraPlayerController::CursorTrace()
{
// Player.Block.CursorTrace 标签使actor无法被选择,高亮
if (GetASC() && GetASC()->HasMatchingGameplayTag(FAuraGameplayTags::Get().Player_Block_CursorTrace))
{
if (LastActor) LastActor->UnHighlightActor();
if (ThisActor) ThisActor->UnHighlightActor();
LastActor = nullptr;
ThisActor = nullptr;
return;
}
// 光标命中的结果 使用 ECC_Visibility 通道进行跟踪 ,简单碰撞跟踪
GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
// 检查跟踪结果
if (!CursorHit.bBlockingHit) return;
LastActor = ThisActor;
ThisActor = Cast<IEnemyInterface>(CursorHit.GetActor());
if (LastActor != ThisActor)
{
if (LastActor) LastActor->UnHighlightActor();
if (ThisActor) ThisActor->HighlightActor();
}
}
// 按下时触发 根据标签识别按键
void AAuraPlayerController::AbilityInputTagPressed(FGameplayTag InputTag)
{
if (GetASC() && GetASC()->HasMatchingGameplayTag(FAuraGameplayTags::Get().Player_Block_InputPressed))
{
return;
}
// 鼠标左键为特殊按键,
// 可以激活技能,ThisActor 表示敌人,如果鼠标跟踪到敌人,则激活技能
// 也可以使角色自动奔跑。如果鼠标没有跟踪到敌人,并且是短按
if (InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
// 鼠标是否跟踪到敌人
bTargeting = ThisActor ? true : false;
// 按下鼠标左键瞬间,还无法确定是否短按,不应自动奔跑。需要等待鼠标左键释放时确定短按长按
bAutoRunning = false;
}
if (GetASC()) GetASC()->AbilityInputTagPressed(InputTag);
}
void AAuraPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)
{
if (GetASC() && GetASC()->HasMatchingGameplayTag(FAuraGameplayTags::Get().Player_Block_InputReleased))
{
return;
}
// 如果释放的不是鼠标左键,而是其他键,则激活释放对应键的技能
// 此时一定不是自动奔跑或鼠标释放左键技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())GetASC()->AbilityInputTagReleased(InputTag);
return;
}
// 如果释放的是鼠标左键,且跟踪到敌人,则执行释放鼠标左键的技能
if (GetASC()) GetASC()->AbilityInputTagReleased(InputTag);
// 如果是鼠标左键释放,并且鼠标没有跟踪到敌人目标,并且没有按下shift ,则自动奔跑至目的地
if (!bTargeting && !bShiftKeyDown)
{
const APawn* ControlledPawn = GetPawn();
// 如果鼠标跟随时间小于短按阙值,表示是短按
if (FollowTime <= ShortPressThreshold && ControlledPawn)
{
// 通过受控pawn位置和目的地位置,同步查找位置路径,生成导航路径
if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(
this, ControlledPawn->GetActorLocation(), CachedDestination))
{
//清除样条线的点
Spline->ClearSplinePoints();
// 循环导航路径的点
for (const FVector& PointLoc : NavPath->PathPoints)
{
//向样条曲线添加点
Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
//DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Green, false, 5.f);
}
// 修复玩家自动寻路错误
if (NavPath->PathPoints.Num() > 0)
{
// 减去目的地路径导航点的最后一个点,防止目标点为障碍物中心时,玩家永远无法到达而不停奔跑
CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
// 正在自动奔跑为真
bAutoRunning = true;
}
}
// 指示目标点的粒子特效
if (GetASC() && !GetASC()->HasMatchingGameplayTag(FAuraGameplayTags::Get().Player_Block_InputPressed))
{
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ClickNiagaraSystem, CachedDestination);
}
}
//自动奔跑时重置跟随时间
FollowTime = 0.f;
// 自动奔跑时没有跟踪敌人
bTargeting = false;
}
}
void AAuraPlayerController::AbilityInputTagHeld(FGameplayTag InputTag)
{
if (GetASC() && GetASC()->HasMatchingGameplayTag(FAuraGameplayTags::Get().Player_Block_InputHeld))
{
return;
}
// 如果按住的不是鼠标左键,而是其他键,则激活按住对应键的技能
// 此时一定不是自动奔跑或鼠标左键单击技能
if (!InputTag.MatchesTagExact(FAuraGameplayTags::Get().InputTag_LMB))
{
if (GetASC())GetASC()->AbilityInputTagHeld(InputTag);
return;
}
// 如果是鼠标左键按住,并且鼠标跟踪到敌人目标,则激活按住左键技能
// 或者是鼠标悬浮+shift,并且鼠标跟踪到敌人目标,则激活按住左键技能
if (bTargeting || bShiftKeyDown)
{
if (GetASC())GetASC()->AbilityInputTagHeld(InputTag);
}
// 如果是鼠标左键按住,并且鼠标没有跟踪到敌人目标,则执行移动
else
{
// 开始累计按住的鼠标跟随时间,在鼠标释放时用以判断短按或长按
FollowTime += GetWorld()->GetDeltaSeconds();
// 鼠标按住时获取移动目的地
// 跟踪通道
// false 不跟踪复杂碰撞
if (CursorHit.bBlockingHit) CachedDestination = CursorHit.ImpactPoint;
// 如果有可控制pawn
if (APawn* ControlledPawn = GetPawn())
{
// 移动的方向 :从受控pawn 到 目的地 的方向向量,归一化
const FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
// 向该方向移动 每帧执行
ControlledPawn->AddMovementInput(WorldDirection);
}
}
}
void AAuraPlayerController::Move(const FInputActionValue& InputActionValue)
{
if (GetASC() && GetASC()->HasMatchingGameplayTag(FAuraGameplayTags::Get().Player_Block_InputPressed))
{
return;
}
//获取输入操作的二维轴向量
const FVector2D InputAxisVector = InputActionValue.Get<FVector2D>();
//使用X 轴和 Y 轴
const FRotator Rotation = GetControlRotation();
const FRotator YawRotation(0.f, Rotation.Yaw, 0.f);
// 前进的方向
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
// 前进的右方向
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// 使用if是因为 Move 可能会在每一帧中被调用,因此在此之前调用它可能有点为时过早,所以不使用check
if (APawn* ControlledPawn = GetPawn<APawn>())
{
//WS绑定在输入操作Y轴
ControlledPawn->AddMovementInput(ForwardDirection, InputAxisVector.Y);
//AD绑定在输入操作X轴
ControlledPawn->AddMovementInput(RightDirection, InputAxisVector.X);
}
}
Ability Tags 这个技能有这些标签 Cancel Abilities with Tag 执行此技能时,具有这些标签的技能会取消 Block Abilities with Tag 具有这些标签的技能在此技能处于作用中时被封锁,不能执行。 Activation Owned Tags 当此技能处于活动状态时,要套用至启动拥有者的标签。如果在Abiltysystem Global中启用了Replicate ActivationOwnedTag,则会复制这些标签。 此技能激活时,技能拥有者将被授予此标签。 但在技能结束或取消时,这些标签被删除
Activation Required Tags 只有当激活的参与者/组件具有所有这些标签时才能激活此技能 Activation Blocked Tags 如果激活的参与者/组件具有以下标签中的任何一个,此技能将被阻止 Source Required Tags 只有当源参与者/组件具有所有这些标签时才能激活此技能 Source Blocked Tags 如果源参与者/组件具有以下任何一个标签,则阻止此技能 Target Required Tags 只有当目标参与者/组件具有所有这些标签时才能激活此技能 Target Blocked Tags 如果目标参与者/组件具有以下标签中的任何一个,则阻止此技能
Activation Owned Tags- Player.Block.CursorTrace Player.Block.InputHeld Player.Block.InputPressed
在激活雷电技能时,阻止鼠标追踪,左键单击,左键按住事件。
技能中在等待释放按键,所以不阻止 Player.Block.InputReleased
此时,右键长按,激活雷电技能。雷电技能的拥有者玩家被赋予标签 Player.Block.CursorTrace, Player.Block.InputHeld, Player.Block.InputPressed,
玩家控制器在使用光标追踪时,检测出技能系统组件包含标签 Player.Block.CursorTrace,所以光标追踪事件直接返回,不再执行。
单击时,检测出 Player.Block.InputPressed ,不再触发任意的按下输入操作,例如左键,右键,数字键,不会触发执行相应的技能例如火球术技能。 长按时,检测出 Player.Block.InputHeld,不再触发长按键输入操作,不会触发执行相应的技能。例如其他按键的长按技能。
雷电技能结束时,这些标签将从技能拥有者玩家删除。 不影响玩家右键长按再次触发雷电技能。
通过标签识别执行 游戏性Cue显示 游戏性Cue显示标签 必须与"GameplayCue"开头的游戏性标记相关联,且GameplayCue必须是顶层标签。
项目设置中添加标签 GameplayCue.ShockBurst
静态游戏性Cue显示 不会生成实例
Content/Blueprints/AbilitySystem/GameplayCueNotifies/GC_ShockBurst.uasset
通过 GameplayCue.MeleeImpact 标签识别执行激活 游戏性Cue显示
类默认值-gameplay cue-gameplay cue tag-GameplayCue.MeleeImpact
事件图表: 指定执行cue发生什么 重载函数 on execute 同时调用父函数,类似 super get actor location play sound at location play sound at location-Sound-sfx_Shock
事件图表: 激活技能后就激活 游戏性Cue显示 execute gameplayCue on owner execute gameplayCue on owner-gameplay cue tag-GameplayCue.MeleeImpact
现在长按右键将播放雷击音效, 服务端和客户端都可以显示硬编码的粒子和音效。 这些效果都可以复制到客户端。
但此时会有警告: LogAbilitySystem: Warning: No GameplayCueNotifyPaths were specified in DefaultGame.ini under [/Script/GameplayAbilities.AbilitySystemGlobals]. Falling back to using all of /Game/. This may be slow on large projects. Consider specifying which paths are to be searched.
LogAbilitySystem:警告:在DefaultGame.ini中[/Script/GameplayAbilitys.AbilitySystem.Globals]下未指定GameplayCueNotifyPaths。返回使用所有/Game/。对于大型项目来说,这可能是缓慢的。请考虑指定要搜索的路径。
Content/Blueprints/AbilitySystem/GameplayCueNotifies/GC_ShockBurst.uasset
Content/Blueprints/AbilitySystem/GameplayCueNotifies/GC_MeleeImpact.uasset
项目设置中指定 游戏性Cue显示 位置
Config/DefaultGame.ini
在 [/Script/GameplayAbilities.AbilitySystemGlobals] 下,
可以添加多个目录位置
+GameplayCueNotifyPaths=/Game/Blueprints/AbilitySystem/GameplayCueNotifies
Game 表示内容文件夹
完整:
[/Script/EngineSettings.GeneralProjectSettings]
ProjectID=03DEC7104419EED03885698C4D9C8D6A
[/Script/GameplayAbilities.AbilitySystemGlobals]
+AbilitySystemGlobalsClassName="/Script/Aura.AuraAbilitySystemGlobals"
+GameplayCueNotifyPaths=/Game/Blueprints/AbilitySystem/GameplayCueNotifies
设置 RPC发送数量限制为10个每次网络更新:不应超过10.
Config/DefaultEngine.ini
[ConsoleVariables]
net.MaxRPCPerNetUpdate=10
所以, 游戏性Cue显示 只能用于必须用的地方,除此之外不应使用。以节省RPC。 根据带宽和游戏设置,每秒可以接收60-100次网络更新。
雷电技能中,将法杖骨骼网格体传输给游戏性Cue显示。
游戏性Cue显示 生成雷电粒子系统,附加到 法杖插槽名上。
spawn system attached 用于将粒子系统附加到法杖的插槽上
这是实例化的游戏性Cue显示。
Content/Blueprints/AbilitySystem/GameplayCueNotifies/GC_ShockLoop.uasset
这种游戏性Cue显示可以添加用于显示的组件。 是通知Actor。可以实现其他接口。
配置标签 类默认值-gameplay cue-gameplay cue tag-GameplayCue.ShockLoop
设置删除时自动销毁: Cleanup-Auto Destroy on Remove-启用
事件图表: on active 在首次激活具有持续时间的GameplayCue时调用,只有在客户端见证激活时才会调用 while active 当具有持续时间的GameplayCue第一次被视为活动时调用,即使它实际上并没有被应用(正在加入,等等)
例如: 瞬时爆炸效果,在新玩家加入游戏时,新玩家不应该在看到瞬时爆炸,这已成为过去。使用 on active
持续雷击效果,在新玩家加入游戏时,应该也可以看到该效果,使用 while active
所以雷击术使用 while active
重载函数 while active
get object name print string
重载函数 on remove print string
打开 GA_Electrocute 事件图表:
收到自定义蒙太奇动画通知时使用 GameplayCue:GC_ShockLoop add GameplayCue on actor (looping) add GameplayCue on actor (looping)-gameplay cue tag-GameplayCue.ShockLoop ability-get avatar actor from actor info
make Gameplay Cue Parameters
此时GameplayCue: GC_ShockLoop 出激活状态,如果不删除,之后的技能执行不能再次触发cue音效.
技能结束时删除 GameplayCue:GC_ShockLoop
PrepareToEndAbility 函数中:
remove GameplayCue from owner remove GameplayCue from owner-gameplay cue tag-GameplayCue.ShockLoop 【可以省略,会自行找到】
Source/Aura/Public/Interaction/CombatInterface.h
public:
// 获取武器
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
USkeletalMeshComponent* GetWeapon();
Source/Aura/Public/Character/AuraCharacterBase.h
public:
virtual USkeletalMeshComponent* GetWeapon_Implementation() override;
Source/Aura/Private/Character/AuraCharacterBase.cpp
USkeletalMeshComponent* AAuraCharacterBase::GetWeapon_Implementation()
{
return Weapon;
}
Beam Start,BeaM End 的蓝色 用户标志 : 一个只读值,可根据系统进行初始化并通过蓝图或c++在关卡中进行外部修改。
事件图表: GetWeapon ability-get avatar actor from actor info
将鼠标位置传入 Mouse Hit Location
while active 函数: break gameplay cue parameters break gameplay cue parameters-target attach component break gameplay cue parameters-Location 提升为变量 MouseHitLocation
spawn system attached-attach point name-TipSocket spawn system attached-auto destroy-启用 spawn system attached-system template-NS_ElectricBeam spawn system attached提升为变量 BeamSystem
为 NS_ElectricBeam 指定粒子末端 Set Niagara Variable (Vector3) Set Niagara Variable (Vector3)-in variable name-Beam End [粒子系统中指定的末端位置参数名] MouseHitLocation 输入给 粒子参数 Set Niagara Variable (Vector3)-in variable
BeamSystem-转换为有效get 如果有效则销毁 destroy component
右键按住时,客户端施法动画可以正常复制。 但服务端的施法动画只会在客户端上显示一次周期,因为玩家角色蓝图的 InShockLoop没有正常复制到客户端。
打开 BP_AuraCharacter 事件图表: 左侧选择 InShockLoop 变量-细节-变量-复制-Replicated
现在 InShockLoop 现实了两个白色球标志表示会网络复制
事件图表:
break gameplay cue parameters--target attach component 提升为变量 -TargetAttachComponent 传入的武器骨骼网格组件
spawn sound attached 可以将生成的音效保持住,直到销毁 spawn sound attached -sound-sfx_ShockLoop spawn sound attached -stop when attached to destroyed-启用 提升为变量 ShockLoopSound
将保持的音效附加到武器网格上 TargetAttachComponent
ShockLoopSound 转换为有效get
淡出音量0.3秒后再销毁 fade out
现在,长按释放技能时会持续播放音效,最后淡出音效。
默认墙体不会阻挡可视化通道,无法正确用雷击粒子末端位置参数。
项目设置-引擎-碰撞-object channels 新建对象通道 Target 默认响应 Block
占用第二个自定义通道
Source/Aura/Aura.h
#define ECC_Target ECollisionChannel::ECC_GameTraceChannel2
不再使用 PC->GetHitResultUnderCursor(ECC_Visibility, false, CursorHit); 可见性通道 使用 PC->GetHitResultUnderCursor(ECC_Target, false, CursorHit);
Source/Aura/Private/AbilitySystem/AbilityTasks/TargetDataUnderMouse.cpp
#include "Aura/Aura.h"
void UTargetDataUnderMouse::SendMouseCursorData()
{
// 预测窗口
FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent.Get());
// 技能任务拥有他们所属技能
APlayerController* PC = Ability->GetCurrentActorInfo()->PlayerController.Get();
FHitResult CursorHit;
// 光标只是追踪 ECC_Target 通道
// 仅跟踪简单的碰撞并传递光标点击数据到 CursorHit
PC->GetHitResultUnderCursor(ECC_Target, false, CursorHit);
FGameplayAbilityTargetDataHandle DataHandle;
FGameplayAbilityTargetData_SingleTargetHit* Data = new FGameplayAbilityTargetData_SingleTargetHit();
// 目标数据对象
Data->HitResult = CursorHit;
DataHandle.Add(Data);
// 将光标跟踪位置 DataHandle 这个目标数据发送到服务器,
// 服务器接受的类型之一 FGameplayAbilityTargetDataHandle
// 参数1:技能规格句柄
// 参数2:预测键。 技能启动相关。 GetActivationPredictionKey:技能最初被激活的时间
// 参数3:目标数据句柄,打包了目标数据
// 参数4:游戏标签
// 参数5:当前预测键
AbilitySystemComponent->ServerSetReplicatedTargetData(
GetAbilitySpecHandle(),
GetActivationPredictionKey(),
DataHandle,
FGameplayTag(),
AbilitySystemComponent->ScopedPredictionKey);
// 发送到服务端后,如果可以,在本地也广播发送
if (ShouldBroadcastAbilityTaskDelegates())
{
// 一旦激活此技能,就会广播该委托,这意味着有效的数据执行引脚将被执行。
// 但它不一定是立刻执行,这是一个异步任务。
// 根据某些技能任务的游戏机制,我们可能希望稍后广播这些委托
// 但对于当前情况,我们立即进行广播。
ValidData.Broadcast(DataHandle);
}
}
现在 雷击术可以被所有物体阻挡,正确长生末端位置。不会穿透墙体。
可以是鼠标追踪穿透空气墙,追踪到空气墙下方的物体。
BP_AuraCharacter Box组件-细节-碰撞预设-Target-忽略
打开 BP_FadeActor 事件图表:
FadeFinished 函数事件图表: 将可视性通道替换为 Target 通道 删除 BlockVisibility 判断分支
显示简单碰撞 细节-碰撞-碰撞预设-碰撞复杂度- 将复杂碰撞用作简单碰撞 由于该静态网格体由多个网格组成,将会触发多次重叠事件。
只创建复杂形状(每个多边形)。将复杂形状用于所有场景查询和碰撞测试。只可用于静态形状的模拟中(即可被碰撞,但无法通过力或速度来移动。)
应该设置为: 细节-碰撞-碰撞预设-碰撞复杂度- 项目默认 使用项目物理设置(DefaultShapeComplexity)
碰撞-添加盒体简化碰撞
这样,将只有一个碰撞盒体,只会触发1次重叠事件。
这样在视线被阻挡时,墙体不会闪烁。
Source/Aura/Public/AbilitySystem/Abilities/AuraBeamSpell.h
public:
// 使雷电击中第一个目标,如果鼠标选中目标与法杖中间有其他目标,则击中雷电经过的第一个目标
// BeamTargetLocation 一般为鼠标指向的目标
UFUNCTION(BlueprintCallable)
void TraceFirstTarget(const FVector& BeamTargetLocation);
Source/Aura/Private/AbilitySystem/Abilities/AuraBeamSpell.cpp
EDrawDebugTrace::ForDuration 表示显示调试 EDrawDebugTrace::None 表示不显示调试
#include "Kismet/KismetSystemLibrary.h"
void UAuraBeamSpell::TraceFirstTarget(const FVector& BeamTargetLocation)
{
check(OwnerCharacter);
if (OwnerCharacter->Implements<UCombatInterface>())
{
if (USkeletalMeshComponent* Weapon = ICombatInterface::Execute_GetWeapon(OwnerCharacter))
{
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(OwnerCharacter);
FHitResult HitResult;
const FVector SocketLocation = Weapon->GetSocketLocation(FName("TipSocket"));
// 不是非常精确的球体轨迹追踪,追踪选择目标与武器之间是否有其他actor
UKismetSystemLibrary::SphereTraceSingle(
OwnerCharacter,
SocketLocation,
BeamTargetLocation,
10.f,
TraceTypeQuery1,
false,
ActorsToIgnore,
EDrawDebugTrace::ForDuration,//EDrawDebugTrace::None,
HitResult,
true);
// 重置鼠标选择的位置和目标
if (HitResult.bBlockingHit)
{
MouseHitLocation = HitResult.ImpactPoint;
MouseHitActor = HitResult.GetActor();
}
}
}
}
事件图表: 为武器生成雷电粒子 折叠到函数 SpawnElectricBeam
SpawnElectricBeam 函数事件图表: 首先调用C++技能跟踪鼠标选择的目标与武器之间的其他目标的第一个
TraceFirstTarget get MouseHitLocation
关闭C++中的调试信息。 现在雷击将攻击雷击技能经过的第一个敌人。
为技能经过的第一个敌人 添加CUE make Gameplay Cue Parameters get MouseHitLocation get MouseHitActor get OwnerCharacter get weapon 提升为变量 FirstTargetCueParameters
检查击中的目标是否实现战斗接口 防止击中建筑物,击中敌人才会添加CUE get MouseHitActor does implement interface-interface-combat interface
branch
add GameplayCue on actor (looping)的target参数提升为变量 CueTarget
击中敌人,则将cue的目标设置为击中的当前敌人,表示为第一个敌人目标添加cue get MouseHitActor set CueTarget
击中非敌人,则为往前角色添加cue ability-get avatar actor from actor info set CueTarget
FirstTargetCueParameters
while active 事件:
传入参数的源对象 提升为变量 SourceObject
生成粒子后检查SourceObject get SourceObject does implement interface-interface-combat interface
branch
未实现战斗接口,例如地板,则在鼠标点击位置即地板处生成粒子 get BeamSystem
如果实现了接口,则在目标出生成粒子 get SourceObject cast to actor get actor location
Set Niagara Variable (Vector3) Set Niagara Variable (Vector3)-in variable name-Beam End [粒子系统中指定的末端位置参数名] get actor location 输入给 粒子参数 Set Niagara Variable (Vector3)-in variable
无论是实现战斗接口,都添加音效cue
does implement interface 结果提升为变量 FirstTargetImplementInterface
结束时删除cue 根据 FirstTargetImplementInterface branch
如果true,表示实现了战斗接口,从 get MouseHitActor 删除 remove GameplayCue on actor(looping)-gameplay cue tag-GameplayCue.ShockLoop FirstTargetCueParameters
如果false,表示未实现战斗接口,从 自身 删除 get avatar actor from actor info remove GameplayCue on actor(looping)-gameplay cue tag-GameplayCue.ShockLoop FirstTargetCueParameters
这是因为,如果没有击中敌人,则不会将雷电传播到未来的目标。 例如击中地板不会继续传播雷电到其他目标。
击中第一个敌人后,继续将雷电传播到第一个敌人中心半径内的其他敌人 需要为其他敌人添加cue
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
// 第一个目标为中心的半径内的最近的几个目标
// 返回的数组距离原点从近到远
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayMechanics")
static void GetClosestTargets(int32 MaxTargets, const TArray<AActor*>& Actors, TArray<AActor*>& OutClosestTargets,
const FVector& Origin);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
void UAuraAbilitySystemLibrary::GetClosestTargets(int32 MaxTargets, const TArray<AActor*>& Actors,
TArray<AActor*>& OutClosestTargets, const FVector& Origin)
{
if (Actors.Num() <= MaxTargets)
{
OutClosestTargets = Actors;
return;
}
TArray<AActor*> ActorsToCheck = Actors;
int32 NumTargetsFound = 0;
while (NumTargetsFound < MaxTargets)
{
if (ActorsToCheck.Num() == 0) break;
double ClosestDistance = TNumericLimits<double>::Max();//一个极大值
AActor* ClosestActor;
for (AActor* PotentialTarget : ActorsToCheck)
{
// 检查潜在目标到原点的距离
// 找到距离原点最近的目标
const double Distance = (PotentialTarget->GetActorLocation() - Origin).Length();
if (Distance < ClosestDistance)
{
ClosestDistance = Distance;
ClosestActor = PotentialTarget;
}
}
// 将最近的目标添加到集合,继续下一轮查找
ActorsToCheck.Remove(ClosestActor);
OutClosestTargets.AddUnique(ClosestActor);
++NumTargetsFound;
}
}
不存储自身和第一个敌人目标
Source/Aura/Public/AbilitySystem/Abilities/AuraBeamSpell.h
public:
// 存储其他敌人
UFUNCTION(BlueprintCallable)
void StoreAdditionalTargets(TArray<AActor*>& OutAdditionalTargets);
protected:
// 雷击目标上限
UPROPERTY(EditDefaultsOnly, Category = "Beam")
int32 MaxNumShockTargets = 5;
Source/Aura/Private/AbilitySystem/Abilities/AuraBeamSpell.cpp
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
void UAuraBeamSpell::StoreAdditionalTargets(TArray<AActor*>& OutAdditionalTargets)
{
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(GetAvatarActorFromActorInfo());
ActorsToIgnore.Add(MouseHitActor);
TArray<AActor*> OverlappingActors;
UAuraAbilitySystemLibrary::GetLivePlayersWithinRadius(
GetAvatarActorFromActorInfo(),
OverlappingActors,
ActorsToIgnore,
850.f,
MouseHitActor->GetActorLocation());
//int32 NumAdditionalTargets = FMath::Min(GetAbilityLevel() - 1, MaxNumShockTargets);
int32 NumAdditionTargets = 5;
// 返回 以第一个目标敌人为中心的半径内的指定数量敌人目标,且由近到远排序
UAuraAbilitySystemLibrary::GetClosestTargets(NumAdditionTargets, OverlappingActors, OutAdditionalTargets,
MouseHitActor->GetActorLocation());
}
移除第一个目标的cue后,获取其他目标,仅做测试 StoreAdditionalTargets
循环为每个其他目标创建调试球
for each loop draw debug sphere get actor location
绘制限制半径 draw debug sphere get MouseHitLocation
移动 StoreAdditionalTargets 到函数 SpawnElectricBeam 中
为第一个目标添加cue后,开始循环为其他目标添加cue
输入参数 AdditionalTarget 类型: Actor 对象引用
AdditionalTarget 提升为局部变量 TargetActor make Gameplay Cue Parameters
映射目标和目标参数 添加变量 AdditionalActorsToCueParameters 类型:映射型 Actor 对象引用 键类型为 Actor 对象引用 值类型为 Gameplay Cue Parameters
get AdditionalActorsToCueParameters add 键:TargetActor 值:make Gameplay Cue Parameters
TargetActor为参数源对象 get actor location 为位置
target attach component 之前为武器,现在为当前目标的上一个目标敌人根组件 MouseHitActor get root component
为每个目标添加cue TargetActor add GameplayCue on actor (looping)
add GameplayCue on actor (looping)-gameplay cue tag-GameplayCue.ShockLoop
StoreAdditionalTargets 的返回值提升为变量 AdditionalTargets 用于删除cue, 因为蓝图无法使用foe each loop 循环映射类型
结束技能shi时,通过 AdditionalActorsToCueParameters 删除每个cue 仅在实现战斗接口的分支执行删除
循环 AdditionalTargets for each loop
从 AdditionalActorsToCueParameters 查找每一个 目标获取目标参数 find branch
删除目标的cue remove GameplayCue on actor(looping) remove GameplayCue on actor(looping)-gameplay cue tag-GameplayCue.ShockLoop
折叠到函数 RemoveShockLoopCueFromAdditionalTarget 输入参数重命名 AdditionalTarget AdditionalTarget 提升为局部变量 Target Target Target
Add Shock Loop Cue to Additional Target
现在,雷击术将击中5个敌人
击中地板,地板未实现战斗接口,之后不应该该继续查找附近的敌人和存储附近敌人 StoreAdditionalTargets StoreAdditionalTargets 之前应该检查 击中的第一个目标是否实现战斗接口。
FirstTargetImplementInterface branch
1,1 40,20 自动平滑
1,1.5,1.75,2,2.5,3,3.75,4.5,5.75,7
Content/Blueprints/AbilitySystem/Aura/Abilities/Lightning/GE_Cost_Electrocute.uasset
持续时间-Duration Policy-Instant 即时
GameplayEffect -Modifiers: attribute-AuraAttributeSet.Mana 表示释放该火球术技能时,消耗 AuraAttributeSet.Mana 魔力属性的值
Modifier Op-Add
Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifier Magnitude-Scalable Float Magnitude- -1,CT_Cost ,Lighting.Electrocute
每次释放雷击术,消耗对应魔力点。
项目设置 -标签管理器-Cooldown.Lighting.Electrocute
Content/Blueprints/AbilitySystem/Aura/Abilities/Lightning/GE_Cooldown_Electrocute.uasset
提交技能时,可以将技能冷却效果 GE_Cooldown_Electrocute 效果应用到技能上。
Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Target Tags Gameplay Effect Component
-Add Tags-Cooldown.Lighting.Electrocute
当把GE_Cooldown_Electrocute设置为雷击术技能的技能冷却效果时, 游戏效果 GE_Cooldown_Electrocute 将为目标授予Cooldown.Lighting.Electrocute 冷却 效果标签。
细节-持续时间-Duration Policy-Has Duration 有持续时间 细节-持续时间-Duration Magnitude-Magnitude calculation Type-scalable float 细节-持续时间-Duration Magnitude-Scalable Float Magnitude-1 【该效果持续1秒,然后自行消失】
打开 GA_Electrocute
类默认值-细节-damage effect class-GE_Damage
damage type-Damage.Lighting
damage-1,CT_Damage,Abilities.Electrocute
debuff chance-0
Instancing Policy-Instanced Per Actor
Cooldowns- Cooldown Gameplay Effect Class-GE_Cooldown_Electrocute
costs-cost gameplay effect class-GE_Cost_Electrocute
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
public:
UFUNCTION(BlueprintPure)
float GetDamageAtLevel() const;
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
float UAuraDamageGameplayAbility::GetDamageAtLevel() const
{
return Damage.GetValueAtLevel(GetAbilityLevel());
}
事件图表: 每秒消耗10次魔力
添加变量 DamageDeltaTime 浮点类型 默认值0.1
使用定时器每0.1秒执行一次 自定义事件 set timer by event set timer by event-looping-true 循环执行事件 custom event:DamageAndCost DamageDeltaTime
set timer by event 返回值提升为变量 DamageAndCostTimer 用于清除定时器
自定义事件中提交技能成本 commitAbilityCost 检测应用消耗是否成功 branch
false 表示魔力不足导致应用失败,则清除计时器,结束技能 DamageAndCostTimer Clear and Invalidate Timer by Handle PrepareToEndAbility end ability
true 则表示消耗魔力,开始应用伤害 此时长按右键,将持续消耗魔力,每0.1秒消耗一次即1点魔力 【1级时】
首先对鼠标选中的actor造成伤害 Get Ability System Component from Actor Info make effect context Make Outgoing spec
get ability level get damage effect class
不再每次消耗魔力时施加减益效果,而是在松开按键结束技能时施加。
因为当前技能设置了 damage type 为 Damage.Lighting, 且指定了 damage 的曲线表格 所以 自己在技能系统组件规格上使用 Damage.Lighting 曲线和技能等级设置伤害修改器的具体值 assign tag set by caller magnitude assign tag set by caller magnitude-data tag-Damage.Lighting
具体伤害值 通过调用 GetDamageAtLevel 获取 GetDamageAtLevel 的值赋予 assign tag set by caller magnitude-magnitude
将伤害技能效果规格应用到鼠标选中的目标的技能系统组件上 Mouse Hit Actor Get Ability System Component ApplyGameplayEffectSpecToself
折叠到函数 ApplyDamageSingleTarget 输入参数重命名为 Actor
再将伤害循环应用到其他目标上 for each loop AdditionalTargets ApplyDamageSingleTarget
如果消耗魔力成功,则可以调用 上面蓝图的 ApplyDamage 对所有目标造成伤害
如果鼠标选中目标死亡,则结束技能 添加变量 TargetDead 布尔类型
ApplyDamage 之前检查鼠标选中目标是否死亡,没有死亡才可以应用伤害 TargetDead branch
死亡则结束技能
清除计时器,结束技能等折叠到函数 ClearTimerAndEndAbility DamageAndCostTimer Clear and Invalidate Timer by Handle PrepareToEndAbility end ability ClearTimerAndEndAbility
输入参数 Actor 提升为局部变量 DamageTarget
DamageTarget
现在ji将对所有目标造成伤害
Health Bar 组件-细节-用户界面-空间-屏幕 才可以使空间始终面向屏幕,始终可见。 场景选项,表示正面可见。
在目标死亡后结束光束粒子。 在主要目标或最后一个目标死亡后,结束雷电技能。
原 DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDeath,AActor,DeadActor); 修改为 DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDeathSignature, AActor, DeadActor);
原 virtual FOnDeath GetOnDeathDelegate()=0; 修改为 virtual FOnDeathSignature& GetOnDeathDelegate() = 0;
Source/Aura/Public/Interaction/CombatInterface.h
// 声明actor死亡委托
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDeathSignature, AActor*, DeadActor);
public:
// FOnDeathSignature& 引用符号表示实现此接口的实际Actor引用,非拷贝
virtual FOnDeathSignature& GetOnDeathDelegate() = 0;
原 virtual FOnDeath GetOnDeathDelegate() override; 修改为 virtual FOnDeathSignature& GetOnDeathDelegate() override;
原 FOnDeath OnDeath; 修改为 FOnDeathSignature OnDeathDelegate;
相应的实现也替换为新版
Source/Aura/Public/Character/AuraCharacterBase.h
public:
virtual FOnDeathSignature& GetOnDeathDelegate() override;
FOnDeathSignature OnDeathDelegate;
Source/Aura/Private/Character/AuraCharacterBase.cpp
FOnDeathSignature& AAuraCharacterBase::GetOnDeathDelegate()
{
return OnDeathDelegate;
}
// 服务器,客户端执行
void AAuraCharacterBase::MulticastHandleDeath_Implementation(const FVector& DeathImpulse)
{
UGameplayStatics::PlaySoundAtLocation(this, DeathSound, GetActorLocation(), GetActorRotation());
// 启用模拟物理
Weapon->SetSimulatePhysics(true);
// 启用重力确保武器掉落
Weapon->SetEnableGravity(true);
// 启用碰撞,无查询,无重叠 【武器默认无碰撞】
Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
Weapon->AddImpulse(DeathImpulse * 0.1f, NAME_None, true);
// 将网格体布娃娃化
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetEnableGravity(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 对世界静态类型阻止,网格体可以阻止其他物体
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
// 参数3 true表示冲击不考虑质量
GetMesh()->AddImpulse(DeathImpulse, NAME_None, true);
// 交胶囊体禁用碰撞 不会再阻挡其他物体通过
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Dissolve();
bDead = true;
BurnDebuffComponent->Deactivate();
OnDeathDelegate.Broadcast(this);
}
Source/Aura/Public/AbilitySystem/Abilities/AuraBeamSpell.h
public:
// 主目标死亡回调
UFUNCTION(BlueprintImplementableEvent)
void PrimaryTargetDied(AActor* DeadActor);
// 主目标之外的其他目标死亡
UFUNCTION(BlueprintImplementableEvent)
void AdditionalTargetDied(AActor* DeadActor);
Source/Aura/Private/AbilitySystem/Abilities/AuraBeamSpell.cpp
void UAuraBeamSpell::TraceFirstTarget(const FVector& BeamTargetLocation)
{
check(OwnerCharacter);
if (OwnerCharacter->Implements<UCombatInterface>())
{
if (USkeletalMeshComponent* Weapon = ICombatInterface::Execute_GetWeapon(OwnerCharacter))
{
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(OwnerCharacter);
FHitResult HitResult;
const FVector SocketLocation = Weapon->GetSocketLocation(FName("TipSocket"));
// 不是非常精确的球体轨迹追踪,追踪选择目标与武器之间是否有其他actor
UKismetSystemLibrary::SphereTraceSingle(
OwnerCharacter,
SocketLocation,
BeamTargetLocation,
10.f,
TraceTypeQuery1,
false,
ActorsToIgnore,
EDrawDebugTrace::None,
HitResult,
true);
// 重置鼠标选择的位置和目标
if (HitResult.bBlockingHit)
{
MouseHitLocation = HitResult.ImpactPoint;
MouseHitActor = HitResult.GetActor();
}
}
}
if (ICombatInterface* CombatInterface = Cast<ICombatInterface>(MouseHitActor))
{
if (!CombatInterface->GetOnDeathDelegate().IsAlreadyBound(this, &UAuraBeamSpell::PrimaryTargetDied))
{
CombatInterface->GetOnDeathDelegate().AddDynamic(this, &UAuraBeamSpell::PrimaryTargetDied);
}
}
}
void UAuraBeamSpell::StoreAdditionalTargets(TArray<AActor*>& OutAdditionalTargets)
{
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(GetAvatarActorFromActorInfo());
ActorsToIgnore.Add(MouseHitActor);
TArray<AActor*> OverlappingActors;
UAuraAbilitySystemLibrary::GetLivePlayersWithinRadius(
GetAvatarActorFromActorInfo(),
OverlappingActors,
ActorsToIgnore,
850.f,
MouseHitActor->GetActorLocation());
int32 NumAdditionalTargets = FMath::Min(GetAbilityLevel() - 1, MaxNumShockTargets);
//int32 NumAdditionTargets = 5;
// 返回 以第一个目标敌人为中心的半径内的指定数量敌人目标,且由近到远排序
//UAuraAbilitySystemLibrary::GetClosestTargets(NumAdditionTargets, OverlappingActors, OutAdditionalTargets,
//MouseHitActor->GetActorLocation());
UAuraAbilitySystemLibrary::GetClosestTargets(
NumAdditionalTargets,
OverlappingActors,
OutAdditionalTargets,
MouseHitActor->GetActorLocation());
for (AActor* Target : OutAdditionalTargets)
{
if (ICombatInterface* CombatInterface = Cast<ICombatInterface>(Target))
{
if (!CombatInterface->GetOnDeathDelegate().IsAlreadyBound(this, &UAuraBeamSpell::AdditionalTargetDied))
{
CombatInterface->GetOnDeathDelegate().AddDynamic(this, &UAuraBeamSpell::AdditionalTargetDied);
}
}
}
}
事件图表:
实现 PrimaryTargetDied event PrimaryTargetDied 删除主目标的游戏性cue显示 remove GameplayCue on actor(looping) FirstTargetImplementInterface remove GameplayCue on actor(looping)-gameplay cue tag-GameplayCue.ShockLoop
提交技能冷却 CommitAbilityCooldown 显示光标 设置玩家可以移动 清除计时器以结束伤害,且结束技能 ClearTimerAndEndAbility
实现 AdditionalTargetDied event AdditionalTargetDied 从其他目标队列中循环删除cue RemoveShockLoopCueFromAdditionalTarget
从其他目标队列中循环删除目标 AdditionalTargets remove item
优化目标死亡逻辑 消耗魔力成功后,仅应用伤害
提交冷却优化,移动到 ClearTimerAndEndAbility 函数中
提交冷却 CommitAbilityCooldown
技能结束前,清空存储数组中的其他目标,否则,第一个目标死亡后,再对地板施法其他目标也会受伤害。因为此时其他目标仍存储在数组中。
清空其他目标数组 AdditionalTargets clear
清空对象参数映射 AdditionalActorsToCueParameters clear
设置鼠标目标为空 set Mouse Hit Actor
设置第一目标为false FirstTargetImplementInterface
对敌人应用伤害前,检查其是否存活,检查其技能系统组件是否有效 工具-is valid
将技能系统组件提升为局部变量 ASC 工具-is valid
检查伤害目标的技能系统组件 工具-is valid
松开按键结束技能时,清除伤害计时器
一旦施放雷电技能,设置至少释放技能0.5秒。保证可以触发雷电粒子效果。 添加变量 MinSpellTime 浮点类型 默认值 0.5 等待按键松开事件中 检查按下的时间 time held
松开时设置 wait inout release-time held 提升为变量 检查其是否小于 MinSpellTime time held < branch
time held delay
如果不小于,直接清除计时器,结束技能
结束技能前清空 time held set time held
Cooldown tag-Cooldown.Lighting.Electrocute 控件将响应该冷却标签,显示冷却倒计时。 现在每次松开右键或技能被结束后,将显示冷却倒计时。
修复 命中敌人的额火球,在敌人死亡之后到达,火球不会消失。
在tick中修复。 但必须将tick值设置的较低,否则会消耗大量性能。不可使用。
事件图表: actor tick-tick 间隔(秒)-0 默认 这表示最大tick,每帧都执行。一般为每秒60-120次tick.现代计算机为120次tick. 此tick函数将被执行的频率(秒)。如小于或等于0,则其将每帧tick
event tick get actor location get actor location 提升为变量 LocationThisFrame draw debug sphere
tick 0 将绘制一大堆调试球
actor tick-tick 间隔(秒)-0.1 表示每隔0.1秒执行一次tick,每秒执行10次tick. actor tick-tick 间隔(秒)-0.2 表示每隔0.2秒执行一次tick,每秒执行5次tick.
添加变量 MinDistancePerFrame 浮点类型 默认10 表示火球每帧之间最小距离
get actor location 提升为变量 LocationLastFrame
将 LocationThisFrame 赋值给 LocationLastFrame LocationThisFrame
vector length <= branch
如果小于等则 播放impact dound 设置的 爆炸音效 spawn sound at location-sound-sfx_FireBolt_Impact 位置在 LocationLastFrame
且播放粒子 spawn system at location spawn system at location-system template-NS_FireExplosion1 NS_FireExplosion 位置在 LocationLastFrame
最后销毁actor destroy actor
现在,如果在敌人死亡之后命中,则火球悬停一会后爆炸。 actor tick-tick 间隔(秒)-0.2
折叠到函数 MakeGoKaboomIfNoMove
这是一种高性能解决方案。
雷击技能的debuff 是敌人无法行动。
标签-Block Abilities with Tag-Abilities 表示施放当前技能火球术时,其他的带Abilities标签的技能都会被阻止施放。 火球术施放完成后,才能施放其他技能。 可以阻止 Abilities.Fire 和 Abilities.Lighting 标签
属性集会根据debuff标签为目标应用游戏效果。
PrepareToEndAbility 事件图表: 为鼠标目标即第一个目标应用效果 get mouse hit actor Make Damage Effect Params from Class Defaults break Damage Effect Params apply damage effect
可以在眩晕时造成伤害【可选】 Make Damage Effect Params from Class Defaults-break Damage Effect Params 【可选】 Make Damage Effect Params 【可选】
为其他目标都应用效果 Make Damage Effect Params from Class Defaults apply damage effect
现在技能上的 debuff 各个属性在技能结束时都会应用到目标 几率,频率,击退
Knockback Force Magnitude-0 表示不再击退
眩晕时步行速度为0
Source/Aura/Public/Character/AuraCharacterBase.h
public:
// 服务端设置是否眩晕
UPROPERTY(ReplicatedUsing, BlueprintReadOnly)
bool bIsStunned = false;
protected:
// 对接收到的眩晕标签作出响应
virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount);
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat")
float BaseWalkSpeed = 600.f;
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "GameFramework/CharacterMovementComponent.h"
void AAuraCharacterBase::StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
bIsStunned = NewCount > 0;
GetCharacterMovement()->MaxWalkSpeed = bIsStunned ? 0.f : BaseWalkSpeed;
}
该逻辑不应放在角色基类,因为需要考虑调用super的时机,使代码变得脆弱。
Source/Aura/Private/Character/AuraCharacter.cpp
#include "AuraGameplayTags.h"
#include "AbilitySystem/Debuff/DebuffNiagaraComponent.h"
void AAuraCharacter::InitAbilityActorInfo()
{
// 获取玩家状态
AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
if (AuraPlayerState == nullptr)return;
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(1, 1.F, FColor::Cyan, FString("AuraPlayerState"));
}
// check(AuraPlayerState);
// 从玩家状态获取技能系统组件
// 然后初始技能参与者信息
// owner 为 玩家状态类,avatar 为当前类即玩家角色
AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState, this);
// 为技能系统组件设置技能Actor相关信息
Cast<UAuraAbilitySystemComponent>(AuraPlayerState->GetAbilitySystemComponent())->AbilityActorInfoSet();
// 将 玩家状态上的 技能系统组件 和 属性集 拷贝到 角色类上,因为角色基类也有同样的变量需要构造
// 技能系统组件
AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
// 属性集
AttributeSet = AuraPlayerState->GetAttributeSet();
// 广播技能系统组件注册事件
OnAscRegistered.Broadcast(AbilitySystemComponent);
// 技能系统组件可用时,监听眩晕标签
AbilitySystemComponent->RegisterGameplayTagEvent(FAuraGameplayTags::Get().Debuff_Stun,
EGameplayTagEventType::NewOrRemoved).AddUObject(
this, &AAuraCharacter::StunTagChanged);
// 初始化并添加覆盖控件,覆盖控件控制器
// 在多人游戏,只有服务端的玩家控制器有效,
// 服务器拥有所有玩家的玩家控制器,但每个玩家只有自己的玩家控制器。
// 在控制该特定角色的客户端机器上,该玩家控制器是有效的。
// 但是该客户端计算机上非本地控制的其他角色没有有效的玩家控制器。
// 例如,在三人游戏中,如果您是客户端,则您的玩家控制器有效,
// 但在你的机器上,另外两个角色,这两个副本没有有效的玩家控制器和初始化能力演员信息。
// 在这种情况下,将调用此函数InitAbilityActorInfo,并且/或玩家控制器将是空指针。
// 在这种情况下,对于此功能或玩家控制器,在多人游戏中可以为空,
// 我们只想在它不为空时继续执行。
// 所以这种情况使用if检查【为空是合理的,只要不继续执行】。不使程序崩溃。
// 否则使用check断言,程序崩溃。【游戏前置条件不能继续执行】
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
{
if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(AuraPlayerController->GetHUD()))
{
AuraHUD->InitOverlay(AuraPlayerController, AuraPlayerState, AbilitySystemComponent, AttributeSet);
}
}
// 此时技能系统组件已经初始化
// 初始化主属性
// 一般只在服务端初始化属性,因为属性设置了网络复制
// 此处会在服务端与客户端初始化属性,也可以,这无需等待从服务端复制
InitializeDefaultAttributes();
}
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::InitAbilityActorInfo()
{
// 初始技能参与者信息 服务器和客户端都在此设置
// 两者均为敌人类自身角色
AbilitySystemComponent->InitAbilityActorInfo(this, this);
Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent)->AbilityActorInfoSet();
// 技能系统组件可用时,监听眩晕标签
AbilitySystemComponent->RegisterGameplayTagEvent(FAuraGameplayTags::Get().Debuff_Stun,
EGameplayTagEventType::NewOrRemoved).AddUObject(
this, &AAuraEnemy::StunTagChanged);
// 为敌人基类临时添加初始化属性功能 仅作学习用
if (HasAuthority())
{
InitializeDefaultAttributes();
// 内部用到了游戏模式
}
// 广播技能系统组件注册事件
OnAscRegistered.Broadcast(AbilitySystemComponent);
}
头文件删除 BaseWalkSpeed 定义,因为已在基类定义
Source/Aura/Private/Character/AuraEnemy.cpp
AAuraEnemy::AAuraEnemy()
{
// 设置敌人基类的网格体组件的碰撞预设为 custom,检测响应-Visibility-阻挡,
// 使光标跟踪生效,因为光标跟踪Visibility通道。
// GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
// 构造敌人类的技能系统组件
AbilitySystemComponent = CreateDefaultSubobject<UAuraAbilitySystemComponent>("AbilitySystemComponent");
// 设置为网络复制
AbilitySystemComponent->SetIsReplicated(true);
// 设置复制模式 游戏效果不重复。游戏提示和游戏标签复制到所有客户端。
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
// 使用控制器所需的旋转
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
bUseControllerRotationYaw = false;
GetCharacterMovement()->bUseControllerDesiredRotation = true;
// 构造敌人类的属性集
AttributeSet = CreateDefaultSubobject<UAuraAttributeSet>("AttributeSet");
// 构造健康条控件
HealthBar = CreateDefaultSubobject<UWidgetComponent>("HealthBar");
// 将健康条控件附加到根组件
HealthBar->SetupAttachment(GetRootComponent());
BaseWalkSpeed = 250.f;
}
它的重写版本存在于每个有复制标记的属性的Actor子类中(override版本)
作用:将需要复制的Properties(UPROPERTY中标记为Replicated或replicatedUsing=)真正添加进复制列表。(而UPROPERTY中的标记仅用于反射)。在每个有自定义replicated property的Actor子类中都要override此方法添加自定义属性。必须要调用父类同名函数,不然会丢失父类的注册信息。
Source/Aura/Public/Character/AuraCharacterBase.h
public:
// 将需要复制的Properties(UPROPERTY中标记为Replicated或replicatedUsing=)真正添加进复制列表
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const;
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "Net/UnrealNetwork.h"
void AAuraCharacterBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 添加进复制列表
DOREPLIFETIME(AAuraCharacterBase, bIsStunned);
// DOREPLIFETIME(AAuraCharacterBase, bIsBurned);
}
事件图表:
从敌人类获取是否被眩晕,判断是否被眩晕 原 BP AuraEnemy 重命名为 AuraEnemy
event blueprint update animation sequence AuraEnemy 转换为有效get IsStunned IsStunned 提升为变量 Stunned
添加状态 Stunned
IdleWalkRun 到 Stunned 转换规则 Stunned 为true
Stunned到 IdleWalkRun 转换规则 Stunned 为false not boolean
添加 序列播放器 sequence player
之后,基于 此 ABP_Enemy 的动画蓝图都需要设置 序列播放器 sequence player
资产覆盖编辑器-ABP_Enemy-AnimGraph-Main States-Stunned -序列播放器-Slingshot_Stun_Loop 【动画序列】
资产覆盖编辑器-ABP_Enemy-AnimGraph-Main States-Stunned -序列播放器-Stun_Loop 【动画序列】
资产覆盖编辑器-ABP_Enemy-AnimGraph-Main States-Stunned -序列播放器-Shaman_Stun 【动画序列】
资产覆盖编辑器-ABP_Enemy-AnimGraph-Main States-Stunned -序列播放器-Stun 【动画序列】
资产覆盖编辑器-ABP_Enemy-AnimGraph-Main States-Stunned -序列播放器-Demon_Stun 【动画序列】
event blueprint update animation 从 BP_Aura_Character 获取是否眩晕变量 IsStunned IsStunned 提升为变量 Stunned
右侧资产处 选择 动画序列 Stun 拖入成为状态Stun
设置为循环动画 选择 序列播放器 Stun-细节-设置-循环动画-启用
Stunned到 Idle 转换规则 Stunned 为false
添加状态别名 ToStun 包含stun之外的所有状态
ToStun 到 Stunned 转换规则 Stunned 为 true
现在敌人受到雷击debuff时,先播放受击动画,再播放眩晕动画。
IdleWalkRun 到 Stunned 转换规则-细节-混合设置-时长-增大到 0.4
找到 AM_HitReact_GoblinSpear 的 动画引用 HitReact_Spear 动画序列 复制 HitReact_Spear 为 HitReact_Spear_cut
时间轴移动到大约中间部分 移除后面的帧
根运动-启用根运动-启用
拖入 HitReact_Spear_cut 到 defaultGroup.defaultSlot 删除 HitReact_Spear
新建关键帧 Stunned 布尔类型 表示是否眩晕,用于判断
所有任务之前的选择器中 添加 blackboard 装饰器 配置与我是否没有激活受击技能装饰器相同
表示:未眩晕时,才会继续往下执行。
Source/Aura/Public/Character/AuraEnemy.h
protected:
// 眩晕标签回调
virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount) override;
Source/Aura/Private/Character/AuraEnemy.cpp
void AAuraEnemy::StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
Super::StunTagChanged(CallbackTag, NewCount);
if (AuraAIController && AuraAIController->GetBlackboardComponent())
{
AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("Stunned"), bIsStunned);
}
}
所有任务之前的选择器中 添加 blackboard 装饰器 配置与我是否没有激活受击技能装饰器相同
damage type-Damage.Lighting [原 Damage.Physical] debuff chance-100 这使 战士warrior职业击中玩家时,玩家眩晕5秒。 此时玩家不可移动,但却可以旋转方向,施放技能,需要修复。
将附带光属性减益效果的雷电技能的减益效果触发几率 Debuff_Chance 设为100,将频率 DebuffFrequency 设为1,持续时间DebuffDuration 设为 5,则表示受到雷击的目标在5秒内,每隔1秒触发一次减益效果,该效果导致眩晕cue。
类默认值-标签-activation blocked tags-Debuff.Stun
类默认值-标签-activation blocked tags-Debuff.Stun
现在玩家被击晕时,将不能施放火球术和雷击术技能。
在玩家控制器的按键等输入操作中检查是否有阻止标签 ,该功能已拥有。
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::Debuff(const FEffectProperties& Props)
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
FGameplayEffectContextHandle EffectContext = Props.SourceASC->MakeEffectContext();
EffectContext.AddSourceObject(Props.SourceAvatarActor);
const FGameplayTag DamageType = UAuraAbilitySystemLibrary::GetDamageType(Props.EffectContextHandle);
const float DebuffDamage = UAuraAbilitySystemLibrary::GetDebuffDamage(Props.EffectContextHandle);
const float DebuffDuration = UAuraAbilitySystemLibrary::GetDebuffDuration(Props.EffectContextHandle);
const float DebuffFrequency = UAuraAbilitySystemLibrary::GetDebuffFrequency(Props.EffectContextHandle);
FString DebuffName = FString::Printf(TEXT("DynamicDebuff_%s"), *DamageType.ToString());
// 动态创建游戏效果
UGameplayEffect* Effect = NewObject<UGameplayEffect>(GetTransientPackage(), FName(DebuffName));
Effect->DurationPolicy = EGameplayEffectDurationType::HasDuration;
Effect->Period = DebuffFrequency;
Effect->DurationMagnitude = FScalableFloat(DebuffDuration);
// 为游戏效果授予游戏标签
// Effect->InheritableOwnedTagsContainer.AddTag(GameplayTags.DamageTypesToDebuffs[DamageType]); 弃用
FInheritedTagContainer TagContainer = FInheritedTagContainer();
// we create and add the component to the gameplay effect
UTargetTagsGameplayEffectComponent& TargetTagsComponent = Effect->AddComponent<UTargetTagsGameplayEffectComponent>();
TagContainer.Added.AddTag(GameplayTags.DamageTypesToDebuffs[DamageType]);
TargetTagsComponent.SetAndApplyTargetTagChanges(TagContainer);
// 检查是否有减益标签
const FGameplayTag DebuffTag = GameplayTags.DamageTypesToDebuffs[DamageType];
// 授予减益标签
TagContainer.Added.AddTag(DebuffTag);
// 如果有眩晕减益标签,则添加 阻住actor的鼠标选择和按键操作标签
if (DebuffTag.MatchesTagExact(GameplayTags.Debuff_Stun))
{
TagContainer.Added.AddTag(GameplayTags.Player_Block_CursorTrace);
TagContainer.Added.AddTag(GameplayTags.Player_Block_InputHeld);
TagContainer.Added.AddTag(GameplayTags.Player_Block_InputPressed);
TagContainer.Added.AddTag(GameplayTags.Player_Block_InputReleased);
}
TargetTagsComponent.SetAndApplyTargetTagChanges(TagContainer);
// 按源聚合
Effect->StackingType = EGameplayEffectStackingType::AggregateBySource;
Effect->StackLimitCount = 1;
//修改器
const int32 Index = Effect->Modifiers.Num();
Effect->Modifiers.Add(FGameplayModifierInfo());
FGameplayModifierInfo& ModifierInfo = Effect->Modifiers[Index];
ModifierInfo.ModifierMagnitude = FScalableFloat(DebuffDamage);
ModifierInfo.ModifierOp = EGameplayModOp::Additive;
ModifierInfo.Attribute = UAuraAttributeSet::GetIncomingDamageAttribute();
if (FGameplayEffectSpec* MutableSpec = new FGameplayEffectSpec(Effect, EffectContext, 1.f))
{
FAuraGameplayEffectContext* AuraContext = static_cast<FAuraGameplayEffectContext*>(MutableSpec->GetContext().Get());
TSharedPtr<FGameplayTag> DebuffDamageType = MakeShareable(new FGameplayTag(DamageType));
AuraContext->SetDamageType(DebuffDamageType);
Props.TargetASC->ApplyGameplayEffectSpecToSelf(*MutableSpec);
// 现在,没有为效果情景句柄设置 减益可用标志,UAuraAbilitySystemLibrary::IsSuccessfulDebuff
// 所以下一次执行 HandleIncomingDamage 时,不会再次应用减益效果,不会导致无限循环的减益效果
}
}
但这仅限服务端,客户端无效。 因为在C++创建的动态标签是无法复制的。所以这些阻止标签不会复制到客户端。
Source/Aura/Public/Character/AuraCharacterBase.h
public:
// 服务端设置是否眩晕 客户端使用 OnRep_Stunned 获取通知
UPROPERTY(ReplicatedUsing=OnRep_Stunned, BlueprintReadOnly)
bool bIsStunned = false;
// 客户端接收是否眩晕
UFUNCTION()
virtual void OnRep_Stunned();
Source/Aura/Private/Character/AuraCharacterBase.cpp
void AAuraCharacterBase::OnRep_Stunned()
{
}
Source/Aura/Public/Character/AuraCharacter.h
public:
virtual void OnRep_Stunned() override;
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::OnRep_Stunned()
{
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent))
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
FGameplayTagContainer BlockedTags;
BlockedTags.AddTag(GameplayTags.Player_Block_CursorTrace);
BlockedTags.AddTag(GameplayTags.Player_Block_InputHeld);
BlockedTags.AddTag(GameplayTags.Player_Block_InputPressed);
BlockedTags.AddTag(GameplayTags.Player_Block_InputReleased);
if (bIsStunned)
{
AuraASC->AddLooseGameplayTags(BlockedTags);
}
else
{
AuraASC->RemoveLooseGameplayTags(BlockedTags);
}
}
}
事件图表:
最小施法事件发生在清除计时器之前,这太早了。
检查是否是服务器端.只有服务器端才有权限为最小施法左延时,清除定时器,结束技能 HasAuthority
防止此时客户端光标不显示 如果不是在服务器,则执行 PrepareToEndAbility
只有服务器才有权限应用伤害效果 HasAuthority
如果不是服务器,则绕过应用伤害效果继续执行 HasAuthority
Source/Aura/Private/Actor/AuraProjectile.cpp
void AAuraProjectile::BeginPlay()
{
Super::BeginPlay();
SetLifeSpan(LifeSpan);
// 运动也需要复制
SetReplicateMovement(true);
Sphere->OnComponentBeginOverlap.AddDynamic(this, &AAuraProjectile::OnSphereOverlap);
LoopingSoundComponent = UGameplayStatics::SpawnSoundAttached(LoopingSound, GetRootComponent());
}
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
if (DamageEffectParams.SourceAbilitySystemComponent == nullptr) return;
AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();
if (SourceAvatarActor == OtherActor) return;
// 技能抛射物不能伤害友军
if (!UAuraAbilitySystemLibrary::IsNotFriend(SourceAvatarActor, OtherActor)) return;
if (!bHit) OnHit();
if (HasAuthority())
{
// 只在服务器上应用伤害效果即可
// 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端
// 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
// 死亡冲击向量
const FVector DeathImpulse = GetActorForwardVector() * DamageEffectParams.DeathImpulseMagnitude;
DamageEffectParams.DeathImpulse = DeathImpulse;
// 击退
const bool bKnockback = FMath::RandRange(1, 100) < DamageEffectParams.KnockbackChance;
if (bKnockback)
{
FRotator Rotation = GetActorRotation();
// 向上旋转45度
Rotation.Pitch = 45.f;
const FVector KnockbackDirection = Rotation.Vector();
const FVector KnockbackForce = KnockbackDirection * DamageEffectParams.KnockbackForceMagnitude;
DamageEffectParams.KnockbackForce = KnockbackForce;
}
DamageEffectParams.TargetAbilitySystemComponent = TargetASC;
UAuraAbilitySystemLibrary::ApplyDamageEffect(DamageEffectParams);
}
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
else bHit = true;
}
细节-复制-复制运动-启用
现在客户端可以正常复制火球
Source/Aura/Public/Character/AuraCharacterBase.h
protected:
UPROPERTY(VisibleAnywhere)
TObjectPtr<UDebuffNiagaraComponent> StunDebuffComponent;
Source/Aura/Private/Character/AuraCharacterBase.cpp
AAuraCharacterBase::AAuraCharacterBase()
{
PrimaryActorTick.bCanEverTick = false;
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
BurnDebuffComponent = CreateDefaultSubobject<UDebuffNiagaraComponent>("BurnDebuffComponent");
BurnDebuffComponent->SetupAttachment(GetRootComponent());
BurnDebuffComponent->DebuffTag = GameplayTags.Debuff_Burn;
StunDebuffComponent = CreateDefaultSubobject<UDebuffNiagaraComponent>("StunDebuffComponent");
StunDebuffComponent->SetupAttachment(GetRootComponent());
StunDebuffComponent->DebuffTag = GameplayTags.Debuff_Stun;
// 防止相机与角色碰撞导致相机视角放大
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
// 设置胶囊体不生成重叠事件,防止多次触发重叠事件,因为网格体组件已设置了重叠事件
GetCapsuleComponent()->SetGenerateOverlapEvents(false);
GetMesh()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
GetMesh()->SetCollisionResponseToChannel(ECC_Projectile, ECR_Overlap);
GetMesh()->SetGenerateOverlapEvents(true);
//初始化武器
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>("weapon");
//将武器附加到骨架插槽
Weapon->SetupAttachment(GetMesh(),FName("WeaponHandSocket"));
//武器不应有任何碰撞
Weapon->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
Source/Aura/Public/Interaction/CombatInterface.h
public:
// 返回委托
virtual FOnASCRegistered& GetOnASCRegisteredDelegate() = 0;
Source/Aura/Public/Character/AuraCharacterBase.h
public:
virtual FOnASCRegistered& GetOnASCRegisteredDelegate() override;
Source/Aura/Private/Character/AuraCharacterBase.cpp
FOnASCRegistered& AAuraCharacterBase::GetOnASCRegisteredDelegate()
{
return OnAscRegistered;
}
Source/Aura/Private/Character/AuraCharacterBase.cpp
// 服务器,客户端执行
void AAuraCharacterBase::MulticastHandleDeath_Implementation(const FVector& DeathImpulse)
{
UGameplayStatics::PlaySoundAtLocation(this, DeathSound, GetActorLocation(), GetActorRotation());
// 启用模拟物理
Weapon->SetSimulatePhysics(true);
// 启用重力确保武器掉落
Weapon->SetEnableGravity(true);
// 启用碰撞,无查询,无重叠 【武器默认无碰撞】
Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
Weapon->AddImpulse(DeathImpulse * 0.1f, NAME_None, true);
// 将网格体布娃娃化
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetEnableGravity(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// 对世界静态类型阻止,网格体可以阻止其他物体
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
// 参数3 true表示冲击不考虑质量
GetMesh()->AddImpulse(DeathImpulse, NAME_None, true);
// 交胶囊体禁用碰撞 不会再阻挡其他物体通过
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Dissolve();
bDead = true;
BurnDebuffComponent->Deactivate();
StunDebuffComponent->Deactivate();
OnDeathDelegate.Broadcast(this);
}
niagara 系统资产-NS_Stars
调整眩晕粒子组件高度到头部
激活-自动启用 用以测试效果
默认眩晕减益标签未复制到客户端
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::OnRep_Stunned()
{
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent))
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
FGameplayTagContainer BlockedTags;
BlockedTags.AddTag(GameplayTags.Player_Block_CursorTrace);
BlockedTags.AddTag(GameplayTags.Player_Block_InputHeld);
BlockedTags.AddTag(GameplayTags.Player_Block_InputPressed);
BlockedTags.AddTag(GameplayTags.Player_Block_InputReleased);
if (bIsStunned)
{
AuraASC->AddLooseGameplayTags(BlockedTags);
StunDebuffComponent->Activate();
}
else
{
AuraASC->RemoveLooseGameplayTags(BlockedTags);
StunDebuffComponent->Deactivate();
}
}
}
Source/Aura/Public/Character/AuraCharacterBase.h
public:
UPROPERTY(ReplicatedUsing=OnRep_Burned, BlueprintReadOnly)
bool bIsBurned = false;
UFUNCTION()
virtual void OnRep_Burned();
Source/Aura/Private/Character/AuraCharacterBase.cpp
void AAuraCharacterBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 添加进复制列表
DOREPLIFETIME(AAuraCharacterBase, bIsStunned);
DOREPLIFETIME(AAuraCharacterBase, bIsBurned);
}
void AAuraCharacterBase::OnRep_Burned()
{
}
角色类实现 OnRep_Burned
Source/Aura/Public/Character/AuraCharacter.h
public:
virtual void OnRep_Burned() override;
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::OnRep_Burned()
{
if (bIsBurned)
{
BurnDebuffComponent->Activate();
}
else
{
BurnDebuffComponent->Deactivate();
}
}
目标被雷击术命中时根据是否被雷击 播放雷击命中动画,非普通命中动画
Source/Aura/Public/Interaction/CombatInterface.h
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
bool IsBeingShocked() const;
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
void SetIsBeingShocked(bool bInShock);
Source/Aura/Public/Character/AuraCharacterBase.h
public:
virtual void SetIsBeingShocked_Implementation(bool bInShock) override;
virtual bool IsBeingShocked_Implementation() const override;
// 是否被雷击术击中
UPROPERTY(Replicated, BlueprintReadOnly)
bool bIsBeingShocked = false;
Source/Aura/Private/Character/AuraCharacterBase.cpp
void AAuraCharacterBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 添加进复制列表
DOREPLIFETIME(AAuraCharacterBase, bIsStunned);
DOREPLIFETIME(AAuraCharacterBase, bIsBurned);
DOREPLIFETIME(AAuraCharacterBase, bIsBeingShocked);
}
void AAuraCharacterBase::SetIsBeingShocked_Implementation(bool bInShock)
{
bIsBeingShocked = bInShock;
}
bool AAuraCharacterBase::IsBeingShocked_Implementation() const
{
return bIsBeingShocked;
}
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::HandleIncomingDamage(const FEffectProperties& Props)
{
// IncomingDamage 属性只在服务器执行获取,不会复制到客户端
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 如果健康值到0,则是致命伤害
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if (CombatInterface)
{
FVector Impulse = UAuraAbilitySystemLibrary::GetDeathImpulse(Props.EffectContextHandle);
CombatInterface->Die(UAuraAbilitySystemLibrary::GetDeathImpulse(Props.EffectContextHandle));
}
// 死亡后发送XP事件
SendXPEvent(Props);
}
else
// 通过技能标签激活技能 更通用
// 效果目标受击且未死亡时,通过命中响应标签 激活 效果目标命中响应技能
// 不依赖玩家或敌人
{
// 如果未被雷击术击中,才应用命中响应效果
if (Props.TargetCharacter->Implements<UCombatInterface>() && !ICombatInterface::Execute_IsBeingShocked(
Props.TargetCharacter))
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 将从客户端预测性激活
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
// 击退
const FVector& KnockbackForce = UAuraAbilitySystemLibrary::GetKnockbackForce(Props.EffectContextHandle);
if (!KnockbackForce.IsNearlyZero(1.f))
{
Props.TargetCharacter->LaunchCharacter(KnockbackForce, true, true);
}
}
// 是否暴击,格挡,显示提示文本
const bool bBlock = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
ShowFloatingText(Props, LocalIncomingDamage, bBlock, bCriticalHit);
if (UAuraAbilitySystemLibrary::IsSuccessfulDebuff(Props.EffectContextHandle))
{
Debuff(Props);
}
}
}
获取ABPEnemy 的 IsBeingShocked 提升为变量 IsBeingShocked
添加状态 BeingShocked
BeingShocked 中添加序列播放器 sequence player
IdleWalkRun 到 BeingShocked 规则 IsBeingShocked
BeingShocked 到 IdleWalkRun 规则 IsBeingShocked not boolean
序列播放器-DemonShockLoop
ShockLoop
ShockLoop
ShockLoop
Shaman_ShockLoop
生成cue后,为第一个鼠标选择的目标cue target设置 IsBeingShocked cue target set IsBeingShocked-in loop-true
其他目标循环设置 set IsBeingShocked-in loop-true
删除cue设置为false 第一个目标 set IsBeingShocked-in loop-false
这是复制变量,不需要检查是否在服务器
其他目标循环设置 set IsBeingShocked-in loop-false
Source/Aura/Public/AuraGameplayTags.h
public:
// 被动技能标签
FGameplayTag Abilities_Passive_HaloOfProtection;
FGameplayTag Abilities_Passive_LifeSiphon;
FGameplayTag Abilities_Passive_ManaSiphon;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
/*
* Passive Spells
*/
GameplayTags.Abilities_Passive_LifeSiphon = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Passive.LifeSiphon"),
FString("Life Siphon")
);
GameplayTags.Abilities_Passive_ManaSiphon = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Passive.ManaSiphon"),
FString("Mana Siphon")
);
GameplayTags.Abilities_Passive_HaloOfProtection = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Passive.HaloOfProtection"),
FString("Halo Of Protection")
);
}
用以运行时被动技能栏动态更改被动技能
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
// 声明结束被动技能委托
DECLARE_MULTICAST_DELEGATE_OneParam(FDeactivatePassiveAbility, const FGameplayTag& /*AbilityTag*/);
public:
// 结束被动技能委托
FDeactivatePassiveAbility DeactivatePassiveAbility;
Source/Aura/Public/AbilitySystem/Abilities/AuraPassiveAbility.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraGameplayAbility.h"
#include "AuraPassiveAbility.generated.h"
UCLASS()
class AURA_API UAuraPassiveAbility : public UAuraGameplayAbility
{
GENERATED_BODY()
public:
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
void ReceiveDeactivate(const FGameplayTag& AbilityTag);
};
Source/Aura/Private/AbilitySystem/Abilities/AuraPassiveAbility.cpp
#include "AbilitySystem/Abilities/AuraPassiveAbility.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystem/AuraAbilitySystemComponent.h"
void UAuraPassiveAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(
UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo())))
{
// 被动技能激活后,为被动技能结束委托注册回调
AuraASC->DeactivatePassiveAbility.AddUObject(this, &UAuraPassiveAbility::ReceiveDeactivate);
}
}
void UAuraPassiveAbility::ReceiveDeactivate(const FGameplayTag& AbilityTag)
{
if (AbilityTags.HasTagExact(AbilityTag))
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
}
}
Content/Blueprints/AbilitySystem/Aura/Abilities/PassiveSpells/GA_HaloOfProtection.uasset
通过 Abilities.Passive.HaloOfProtection 标签识别该被动技能
标签-AbilityTag-Abilities.Passive.HaloOfProtection
Content/Blueprints/AbilitySystem/Aura/Abilities/PassiveSpells/GA_LifeSiphon.uasset
通过 Abilities.Passive.LifeSiphon 标签识别该被动技能
标签-AbilityTag-Abilities.Passive.LifeSiphon
Content/Blueprints/AbilitySystem/Aura/Abilities/PassiveSpells/GA_ManaSiphon.uasset
通过 Abilities.Passive.ManaSiphon 标签识别该被动技能
标签-AbilityTag-Abilities.Passive.ManaSiphon
控件需要直到监听哪些信息用于被动技能信息显示
设计器: 重命名按钮 WBP_SpellGlobe_Button为 Button_HaloOfProtection Button_LifeSiphon Button_ManaSiphon
事件图表: 需要为按钮设置技能标签 event construct
Button_HaloOfProtection set ability tag-Abilities.Passive.HaloOfProtection
Button_LifeSiphon set ability tag-Abilities.Passive.LifeSiphon
Button_ManaSiphon set ability tag-Abilities.Passive.ManaSiphon
现在可以响应技能信息广播
2级时被动可用,可以消耗技能点解锁。
为被动按钮设置控件控制器 SpellGlobe_Passive_1 SpellGlobe_Passive_2
SpellGlobe_Passive_1 SpellGlobe_Passive_2
现在可以装备解锁的被动技能
需要修复 一次性升级2级,技能点只奖励1点的错误。
现在是为当前增加的等级计算技能点和属性点,应该为增加的每个等级计算技能点和属性点
Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp
void UAuraAttributeSet::HandleIncomingXP(const FEffectProperties& Props)
{
// 本地存储XP元属性副本
const float LocalIncomingXP = GetIncomingXP();
// 原XP元属性归零
SetIncomingXP(0.f);
// 为伤害来源添加经验
// Source Character is the owner, since GA_ListenForEvents applies GE_EventBasedEffect, adding to IncomingXP
if (Props.SourceCharacter->Implements<UPlayerInterface>() && Props.SourceCharacter->Implements<UCombatInterface>())
{
const int32 CurrentLevel = ICombatInterface::Execute_GetPlayerLevel(Props.SourceCharacter);
const int32 CurrentXP = IPlayerInterface::Execute_GetXP(Props.SourceCharacter);
const int32 NewLevel = IPlayerInterface::Execute_FindLevelForXP(
Props.SourceCharacter, CurrentXP + LocalIncomingXP);
const int32 NumLevelUps = NewLevel - CurrentLevel;
if (NumLevelUps > 0)
{
// 游戏后处理效果完成后,等级才会实际增加,此时获取的最大健康值依然不是最新的值
IPlayerInterface::Execute_AddToPlayerLevel(Props.SourceCharacter, NumLevelUps);
int32 AttributePointsReward = 0;
int32 SpellPointsReward = 0;
for (int32 i = 0; i < NumLevelUps; ++i)
{
SpellPointsReward += IPlayerInterface::Execute_GetSpellPointsReward(Props.SourceCharacter, CurrentLevel + i);
AttributePointsReward += IPlayerInterface::Execute_GetAttributePointsReward(Props.SourceCharacter, CurrentLevel + i);
}
IPlayerInterface::Execute_AddToAttributePoints(Props.SourceCharacter, AttributePointsReward);
IPlayerInterface::Execute_AddToSpellPoints(Props.SourceCharacter, SpellPointsReward);
// 升级后,设置可以设置健康值,魔力值为最大值
bTopOffHealth = true;
bTopOffMana = true;
IPlayerInterface::Execute_LevelUp(Props.SourceCharacter);
}
IPlayerInterface::Execute_AddToXP(Props.SourceCharacter, LocalIncomingXP);
}
}
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
// 服务端使用的客户端RPC,将信息复制到客户端
// 服务端调用时,在客户端执行
UFUNCTION(Client, Reliable)
void ClientEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot,
const FGameplayTag& PreviousSlot);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::ClientEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Status,
const FGameplayTag& Slot, const FGameplayTag& PreviousSlot)
{
AbilityEquipped.Broadcast(AbilityTag, Status, Slot, PreviousSlot);
}
被动技能不是通过输入操作激活,而是在装备时激活,在取消装备时取消激活。
类默认值-高级-net execution policy-server initiated 在服务器初始化,在客户端也执行。 这是因为被动技能的特殊性。
事件图表: event activateAbility print string
event onEndAbility print string
GetInputTagFromAbilityTag 函数替换为 GetSlotFromAbilityTag 因为被动技能没有输入操作标签 插槽标签更通用 清空插槽改为静态函数
循环操作多个技能时都需要锁
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
public:
FGameplayTag GetSlotFromAbilityTag(const FGameplayTag& AbilityTag);
bool SlotIsEmpty(const FGameplayTag& Slot);
// const FGameplayAbilitySpec& Spec 表示使用原始的技能规格,而非拷贝一个副本
static bool AbilityHasSlot(const FGameplayAbilitySpec& Spec, const FGameplayTag& Slot);
static bool AbilityHasAnySlot(const FGameplayAbilitySpec& Spec);
FGameplayAbilitySpec* GetSpecWithSlot(const FGameplayTag& Slot);
bool IsPassiveAbility(const FGameplayAbilitySpec& Spec) const;
static void AssignSlotToAbility(FGameplayAbilitySpec& Spec, const FGameplayTag& Slot);
static void ClearSlot(FGameplayAbilitySpec* Spec);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
FGameplayTag UAuraAbilitySystemComponent::GetSlotFromAbilityTag(const FGameplayTag& AbilityTag)
{
if (const FGameplayAbilitySpec* Spec = GetSpecFromAbilityTag(AbilityTag))
{
return GetInputTagFromSpec(*Spec);
}
return FGameplayTag();
}
bool UAuraAbilitySystemComponent::SlotIsEmpty(const FGameplayTag& Slot)
{
FScopedAbilityListLock ActiveScopeLoc(*this);
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
if (AbilityHasSlot(AbilitySpec, Slot))
{
return false;
}
}
return true;
}
bool UAuraAbilitySystemComponent::AbilityHasSlot(const FGameplayAbilitySpec& Spec, const FGameplayTag& Slot)
{
return Spec.DynamicAbilityTags.HasTagExact(Slot);
}
bool UAuraAbilitySystemComponent::AbilityHasAnySlot(const FGameplayAbilitySpec& Spec)
{
return Spec.DynamicAbilityTags.HasTag(FGameplayTag::RequestGameplayTag(FName("InputTag")));
}
FGameplayAbilitySpec* UAuraAbilitySystemComponent::GetSpecWithSlot(const FGameplayTag& Slot)
{
FScopedAbilityListLock ActiveScopeLock(*this);
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
if (AbilitySpec.DynamicAbilityTags.HasTagExact(Slot))
{
return &AbilitySpec;
}
}
return nullptr;
}
bool UAuraAbilitySystemComponent::IsPassiveAbility(const FGameplayAbilitySpec& Spec) const
{
const UAbilityInfo* AbilityInfo = UAuraAbilitySystemLibrary::GetAbilityInfo(GetAvatarActor());
const FGameplayTag AbilityTag = GetAbilityTagFromSpec(Spec);
const FAuraAbilityInfo& Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
const FGameplayTag AbilityType = Info.AbilityType;
return AbilityType.MatchesTagExact(FAuraGameplayTags::Get().Abilities_Type_Passive);
}
void UAuraAbilitySystemComponent::AssignSlotToAbility(FGameplayAbilitySpec& Spec, const FGameplayTag& Slot)
{
ClearSlot(&Spec);
Spec.DynamicAbilityTags.AddTag(Slot);
}
void UAuraAbilitySystemComponent::ClearSlot(FGameplayAbilitySpec* Spec)
{
const FGameplayTag Slot = GetInputTagFromSpec(*Spec);
Spec->DynamicAbilityTags.RemoveTag(Slot);
// MarkAbilitySpecDirty(*Spec); 不需要强制复制了,因为其他地方已经复制
}
void UAuraAbilitySystemComponent::ServerEquipAbility_Implementation(const FGameplayTag& AbilityTag,
const FGameplayTag& Slot)
{
if (FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
const FGameplayTag& PrevSlot = GetInputTagFromSpec(*AbilitySpec);
const FGameplayTag& Status = GetStatusFromSpec(*AbilitySpec);
// 已装备或以解锁的技能才可以装备
const bool bStatusValid = Status == GameplayTags.Abilities_Status_Equipped || Status == GameplayTags.Abilities_Status_Unlocked;
if (bStatusValid)
{
// 处理被动技能的激活/停用
// Handle activation/deactivation for passive abilities
// 如果这个插槽中已经有一个技能了。停用并清除其插槽。
if (!SlotIsEmpty(Slot)) // There is an ability in this slot already. Deactivate and clear its slot.
{
FGameplayAbilitySpec* SpecWithSlot = GetSpecWithSlot(Slot);
if (SpecWithSlot)
{
// is that ability the same as this ability? If so, we can return early.
if (AbilityTag.MatchesTagExact(GetAbilityTagFromSpec(*SpecWithSlot)))
{
// ClientEquipAbility 是为了使提示框消失
ClientEquipAbility(AbilityTag, GameplayTags.Abilities_Status_Equipped, Slot, PrevSlot);
return;
}
if (IsPassiveAbility(*SpecWithSlot))
{
DeactivatePassiveAbility.Broadcast(GetAbilityTagFromSpec(*SpecWithSlot));
}
// 清除此技能的插槽,以防万一,它已装备到另一个插槽
ClearSlot(SpecWithSlot);
}
}
if (!AbilityHasAnySlot(*AbilitySpec)) // Ability doesn't yet have a slot (it's not active)
{
if (IsPassiveAbility(*AbilitySpec))
{
TryActivateAbility(AbilitySpec->Handle);
}
}
AssignSlotToAbility(*AbilitySpec, Slot);
MarkAbilitySpecDirty(*AbilitySpec);
}
// 此时在服务端执行
// 需要调用客户端RPC,将服务端的信息复制到客户端
ClientEquipAbility(AbilityTag, GameplayTags.Abilities_Status_Equipped, Slot, PrevSlot);
}
}
void UAuraAbilitySystemComponent::AbilityInputTagPressed(const FGameplayTag& InputTag)
{
if (!InputTag.IsValid()) return;
FScopedAbilityListLock ActiveScopeLoc(*this);
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
if (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag))
{
AbilitySpecInputPressed(AbilitySpec);
if (AbilitySpec.IsActive())
{
// InvokeReplicatedEvent 向服务器发送 技能通用复制中的按下事件数据,
// 告知服务器,正在按下键
// 参数3:预测键:使用了技能首次激活/上次激活 的原始的预测键
// 这之后 wait input release 等待按键释放事件有效才会有效
InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, AbilitySpec.Handle,
AbilitySpec.ActivationInfo.GetActivationPredictionKey());
}
}
}
}
void UAuraAbilitySystemComponent::AbilityInputTagHeld(const FGameplayTag& InputTag)
{
if (!InputTag.IsValid()) return;
FScopedAbilityListLock ActiveScopeLoc(*this);
// 根据技能标签激活技能
// 如果技能已激活,则不再之后每一帧继续执行激活
// 根据输入标签检查是否有可激活的技能
// GetActivatableAbilities() 获取可激活的技能,会返回一系列游戏技能规格
// 可激活的技能意味着我们拥有可以激活的技能。
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
// 检查输入标签,激活任何具有输入标签的技能
// 动态技能标签是一个游戏标签容器
if (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag))
{
//通知技能规格,已经按下输入
AbilitySpecInputPressed(AbilitySpec);
if (!AbilitySpec.IsActive())
{
// 尝试激活技能
TryActivateAbility(AbilitySpec.Handle);
}
}
}
}
void UAuraAbilitySystemComponent::AbilityInputTagReleased(const FGameplayTag& InputTag)
{
if (!InputTag.IsValid()) return;
FScopedAbilityListLock ActiveScopeLoc(*this);
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
if (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag) && AbilitySpec.IsActive())
{
//通知技能规格,已经释放输入按键
AbilitySpecInputReleased(AbilitySpec);
InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputReleased, AbilitySpec.Handle,
AbilitySpec.ActivationInfo.GetActivationPredictionKey());
}
}
}
Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp
void USpellMenuWidgetController::EquipButtonPressed()
{
const FGameplayTag AbilityType = AbilityInfo->FindAbilityInfoForTag(SelectedAbility.Ability).AbilityType;
WaitForEquipDelegate.Broadcast(AbilityType);
bWaitingForEquipSelection = true;
// 技能树中选中的技能类型
const FGameplayTag SelectedStatus = GetAuraASC()->GetStatusFromAbilityTag(SelectedAbility.Ability);
// 如果选中的是已装备的技能,存储选中技能类型的插槽,即输入标签
if (SelectedStatus.MatchesTagExact(FAuraGameplayTags::Get().Abilities_Status_Equipped))
{
SelectedSlot = GetAuraASC()->GetSlotFromAbilityTag(SelectedAbility.Ability);
}
}
各种减益,被动粒子组件,监听对应的游戏标签变动事件。
Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h
// 声明被动技能激活委托
DECLARE_MULTICAST_DELEGATE_TwoParams(FActivatePassiveEffect, const FGameplayTag& /*AbilityTag*/, bool /*bActivate*/);
public:
FActivatePassiveEffect ActivatePassiveEffect;
UFUNCTION(NetMulticast, Unreliable)
void MulticastActivatePassiveEffect(const FGameplayTag& AbilityTag, bool bActivate);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp
void UAuraAbilitySystemComponent::MulticastActivatePassiveEffect_Implementation(const FGameplayTag& AbilityTag, bool bActivate)
{
ActivatePassiveEffect.Broadcast(AbilityTag, bActivate);
}
void UAuraAbilitySystemComponent::ServerEquipAbility_Implementation(const FGameplayTag& AbilityTag,
const FGameplayTag& Slot)
{
if (FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
{
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
const FGameplayTag& PrevSlot = GetInputTagFromSpec(*AbilitySpec);
const FGameplayTag& Status = GetStatusFromSpec(*AbilitySpec);
// 已装备或以解锁的技能才可以装备
const bool bStatusValid = Status == GameplayTags.Abilities_Status_Equipped || Status == GameplayTags.Abilities_Status_Unlocked;
if (bStatusValid)
{
// 处理被动技能的激活/停用
// Handle activation/deactivation for passive abilities
// 如果这个插槽中已经有一个技能了。停用并清除其插槽。
if (!SlotIsEmpty(Slot)) // There is an ability in this slot already. Deactivate and clear its slot.
{
FGameplayAbilitySpec* SpecWithSlot = GetSpecWithSlot(Slot);
if (SpecWithSlot)
{
// is that ability the same as this ability? If so, we can return early.
if (AbilityTag.MatchesTagExact(GetAbilityTagFromSpec(*SpecWithSlot)))
{
// ClientEquipAbility 是为了使提示框消失
ClientEquipAbility(AbilityTag, GameplayTags.Abilities_Status_Equipped, Slot, PrevSlot);
return;
}
if (IsPassiveAbility(*SpecWithSlot))
{
MulticastActivatePassiveEffect(GetAbilityTagFromSpec(*SpecWithSlot), false);
DeactivatePassiveAbility.Broadcast(GetAbilityTagFromSpec(*SpecWithSlot));
}
// 清除此技能的插槽,以防万一,它已装备到另一个插槽
ClearSlot(SpecWithSlot);
}
}
if (!AbilityHasAnySlot(*AbilitySpec)) // Ability doesn't yet have a slot (it's not active)
{
if (IsPassiveAbility(*AbilitySpec))
{
TryActivateAbility(AbilitySpec->Handle);
MulticastActivatePassiveEffect(AbilityTag, true);
}
}
AssignSlotToAbility(*AbilitySpec, Slot);
MarkAbilitySpecDirty(*AbilitySpec);
}
// 此时在服务端执行
// 需要调用客户端RPC,将服务端的信息复制到客户端
ClientEquipAbility(AbilityTag, GameplayTags.Abilities_Status_Equipped, Slot, PrevSlot);
}
}
Source/Aura/Public/AbilitySystem/Passive/PassiveNiagaraComponent.h
#pragma once
#include "CoreMinimal.h"
#include "NiagaraComponent.h"
#include "GameplayTagContainer.h"
#include "PassiveNiagaraComponent.generated.h"
UCLASS()
class AURA_API UPassiveNiagaraComponent : public UNiagaraComponent
{
GENERATED_BODY()
public:
UPassiveNiagaraComponent();
UPROPERTY(EditDefaultsOnly)
FGameplayTag PassiveSpellTag;
protected:
virtual void BeginPlay() override;
void OnPassiveActivate(const FGameplayTag& AbilityTag, bool bActivate);
};
Source/Aura/Private/AbilitySystem/Passive/PassiveNiagaraComponent.cpp
#include "AbilitySystem/Passive/PassiveNiagaraComponent.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystem/AuraAbilitySystemComponent.h"
#include "Interaction/CombatInterface.h"
UPassiveNiagaraComponent::UPassiveNiagaraComponent()
{
bAutoActivate = false;
}
void UPassiveNiagaraComponent::BeginPlay()
{
Super::BeginPlay();
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetOwner())))
{
AuraASC->ActivatePassiveEffect.AddUObject(this, &UPassiveNiagaraComponent::OnPassiveActivate);
}
else if (ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetOwner()))
{
CombatInterface->GetOnASCRegisteredDelegate().AddLambda([this](UAbilitySystemComponent* ASC)
{
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetOwner())))
{
AuraASC->ActivatePassiveEffect.AddUObject(this, &UPassiveNiagaraComponent::OnPassiveActivate);
}
});
}
}
void UPassiveNiagaraComponent::OnPassiveActivate(const FGameplayTag& AbilityTag, bool bActivate)
{
if (AbilityTag.MatchesTagExact(PassiveSpellTag))
{
if (bActivate && !IsActive())
{
Activate();
}
else
{
Deactivate();
}
}
}
Source/Aura/Public/Character/AuraCharacterBase.h
class UPassiveNiagaraComponent;
public:
virtual void Tick(float DeltaTime) override;
private:
UPROPERTY(VisibleAnywhere)
TObjectPtr<UPassiveNiagaraComponent> HaloOfProtectionNiagaraComponent;
UPROPERTY(VisibleAnywhere)
TObjectPtr<UPassiveNiagaraComponent> LifeSiphonNiagaraComponent;
UPROPERTY(VisibleAnywhere)
TObjectPtr<UPassiveNiagaraComponent> ManaSiphonNiagaraComponent;
UPROPERTY(VisibleAnywhere)
TObjectPtr<USceneComponent> EffectAttachComponent;
Source/Aura/Private/Character/AuraCharacterBase.cpp
#include "AbilitySystem/Passive/PassiveNiagaraComponent.h"
AAuraCharacterBase::AAuraCharacterBase()
{
PrimaryActorTick.bCanEverTick = true;
const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
BurnDebuffComponent = CreateDefaultSubobject<UDebuffNiagaraComponent>("BurnDebuffComponent");
BurnDebuffComponent->SetupAttachment(GetRootComponent());
BurnDebuffComponent->DebuffTag = GameplayTags.Debuff_Burn;
StunDebuffComponent = CreateDefaultSubobject<UDebuffNiagaraComponent>("StunDebuffComponent");
StunDebuffComponent->SetupAttachment(GetRootComponent());
StunDebuffComponent->DebuffTag = GameplayTags.Debuff_Stun;
// 防止相机与角色碰撞导致相机视角放大
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
// 设置胶囊体不生成重叠事件,防止多次触发重叠事件,因为网格体组件已设置了重叠事件
GetCapsuleComponent()->SetGenerateOverlapEvents(false);
GetMesh()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
GetMesh()->SetCollisionResponseToChannel(ECC_Projectile, ECR_Overlap);
GetMesh()->SetGenerateOverlapEvents(true);
//初始化武器
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>("weapon");
//将武器附加到骨架插槽
Weapon->SetupAttachment(GetMesh(),FName("WeaponHandSocket"));
//武器不应有任何碰撞
Weapon->SetCollisionEnabled(ECollisionEnabled::NoCollision);
EffectAttachComponent = CreateDefaultSubobject<USceneComponent>("EffectAttachPoint");
EffectAttachComponent->SetupAttachment(GetRootComponent());
HaloOfProtectionNiagaraComponent = CreateDefaultSubobject<UPassiveNiagaraComponent>("HaloOfProtectionComponent");
HaloOfProtectionNiagaraComponent->SetupAttachment(EffectAttachComponent);
LifeSiphonNiagaraComponent = CreateDefaultSubobject<UPassiveNiagaraComponent>("LifeSiphonNiagaraComponent");
LifeSiphonNiagaraComponent->SetupAttachment(EffectAttachComponent);
ManaSiphonNiagaraComponent = CreateDefaultSubobject<UPassiveNiagaraComponent>("ManaSiphonNiagaraComponent");
ManaSiphonNiagaraComponent->SetupAttachment(EffectAttachComponent);
}
void AAuraCharacterBase::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 使粒子特效不会随人物旋转
EffectAttachComponent->SetWorldRotation(FRotator::ZeroRotator);
}
HaloOfProtectionNiagaraComponent 组件-细节-Niagara系统资产-NS_Halo
HaloOfProtectionNiagaraComponent 组件 设置被动技能标签 HaloOfProtectionNiagaraComponent 组件-细节-PassiveSpellTag-Abilities.Passive.HaloOfProtection
LifeSiphonNiagaraComponent 组件-细节-Niagara系统资产-NS_LifeSiphon LifeSiphonNiagaraComponent组件-细节-PassiveSpellTag-Abilities.Passive.LifeSiphon
ManaSiphonNiagaraComponent 组件-细节-Niagara系统资产-NS_ManaSiphon ManaSiphonNiagaraComponent 组件-细节-PassiveSpellTag-Abilities.Passive.ManaSiphon
现在装备被动技能会显示对应的粒子特效
魔法阵技能,将为地面贴上圆形贴画Decal
Source/Aura/Public/Actor/MagicCircle.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MagicCircle.generated.h"
UCLASS()
class AURA_API AMagicCircle : public AActor
{
GENERATED_BODY()
public:
AMagicCircle();
virtual void Tick(float DeltaTime) override;
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere)
TObjectPtr<UDecalComponent> MagicCircleDecal;
};
Source/Aura/Private/Actor/MagicCircle.cpp
#include "Actor/MagicCircle.h"
#include "Components/DecalComponent.h"
AMagicCircle::AMagicCircle()
{
PrimaryActorTick.bCanEverTick = true;
MagicCircleDecal = CreateDefaultSubobject<UDecalComponent>("MagicCircleDecal");
MagicCircleDecal->SetupAttachment(GetRootComponent());
}
void AMagicCircle::BeginPlay()
{
Super::BeginPlay();
}
void AMagicCircle::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
Content/Blueprints/AbilitySystem/Aura/Abilities/Arcane/ArcaneShards/BP_MagicCircle.uasset
MagicCircleDecal 组件-细节-贴花-贴花材质-M_MagicCircle_1
贴花在投射到物体上才能可视化。
将 BP_MagicCircle 拖入场景,放置在墙上上 这是因为该贴花面向侧面显示。不是从上到下显示。
BP_MagicCircle中 旋转 MagicCircleDecal 组件 使其X轴向上 这样可以在地面显示。
贴花立方体内的物体都会被贴上贴花材质,所需需要将贴花组件高度变小,使其尽量不贴在角色身上。
当前会贴在角色脚上
Source/Aura/Public/Player/AuraPlayerController.h
class AMagicCircle;
public:
UFUNCTION(BlueprintCallable)
void ShowMagicCircle();
UFUNCTION(BlueprintCallable)
void HideMagicCircle();
private:
UPROPERTY(EditDefaultsOnly)
TSubclassOf<AMagicCircle> MagicCircleClass;
UPROPERTY()
TObjectPtr<AMagicCircle> MagicCircle;
void UpdateMagicCircleLocation();
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "Actor/MagicCircle.h"
void AAuraPlayerController::PlayerTick(float DeltaTime)
{
Super::PlayerTick(DeltaTime);
CursorTrace();
AutoRun();
UpdateMagicCircleLocation();
}
void AAuraPlayerController::ShowMagicCircle()
{
if (!IsValid(MagicCircle))
{
MagicCircle = GetWorld()->SpawnActor<AMagicCircle>(MagicCircleClass);
}
}
void AAuraPlayerController::HideMagicCircle()
{
if (IsValid(MagicCircle))
{
MagicCircle->Destroy();
}
}
void AAuraPlayerController::UpdateMagicCircleLocation()
{
if (IsValid(MagicCircle))
{
MagicCircle->SetActorLocation(CursorHit.ImpactPoint);
}
}
Source/Aura/Public/Actor/MagicCircle.h
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<UDecalComponent> MagicCircleDecal;
Magic Circle Class-BP_MagicCircle
event beginPlay
ShowMagicCircle delay HideMagicCircle
运行后,将显示魔法阵5秒,始终跟随鼠标位置。
打开 BP_MagicCircle 事件图表: MagicCircleDecal add local rotation multiply
魔法阵依赖玩家接口,不依赖玩家。 将魔法阵的材质设为参数,可定制。
Source/Aura/Public/Actor/MagicCircle.h
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<UDecalComponent> MagicCircleDecal;
Source/Aura/Public/Player/AuraPlayerController.h
public:
UFUNCTION(BlueprintCallable)
void ShowMagicCircle(UMaterialInterface* DecalMaterial = nullptr);
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "Components/DecalComponent.h"
void AAuraPlayerController::ShowMagicCircle(UMaterialInterface* DecalMaterial)
{
if (!IsValid(MagicCircle))
{
MagicCircle = GetWorld()->SpawnActor<AMagicCircle>(MagicCircleClass);
if (DecalMaterial)
{
MagicCircle->MagicCircleDecal->SetMaterial(0, DecalMaterial);
}
}
}
Source/Aura/Public/Interaction/PlayerInterface.h
public:
// 参数为材质
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
void ShowMagicCircle(UMaterialInterface* DecalMaterial = nullptr);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
void HideMagicCircle();
Source/Aura/Public/Character/AuraCharacter.h
public:
virtual void ShowMagicCircle_Implementation(UMaterialInterface* DecalMaterial) override;
virtual void HideMagicCircle_Implementation() override;
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::ShowMagicCircle_Implementation(UMaterialInterface* DecalMaterial)
{
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
{
AuraPlayerController->ShowMagicCircle(DecalMaterial);
}
}
void AAuraCharacter::HideMagicCircle_Implementation()
{
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
{
AuraPlayerController->HideMagicCircle();
}
}
事件图表: event beginPlay self ShowMagicCircle
使用材质实例 MI_MagicCircle_1 [可选,否则使用默认材质]
删除测试节点。
Source/Aura/Public/AuraGameplayTags.h
public:
FGameplayTag Abilities_Arcane_ArcaneShards;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
GameplayTags.Abilities_Arcane_ArcaneShards = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Arcane.ArcaneShards"),
FString("Arcane Shards Ability Tag")
);
}
Content/Blueprints/AbilitySystem/Aura/Abilities/Arcane/ArcaneShards/GA_ArcaneShards.uasset
设置技能标签 为 Abilities.Arcane.ArcaneShards ability tags-Abilities.Arcane.ArcaneShards
伤害类 damage effect class-GE_Damage
damage type-Damage.Arcane
选择第三列的 WBP_SpellGlobe_Button_6 细节-ability tag-Abilities.Arcane.ArcaneShards
现在,技能树中可以使用解锁 GA_ArcaneShards 技能
Source/Aura/Private/Character/AuraCharacter.cpp
void AAuraCharacter::ShowMagicCircle_Implementation(UMaterialInterface* DecalMaterial)
{
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
{
AuraPlayerController->ShowMagicCircle(DecalMaterial);
AuraPlayerController->bShowMouseCursor = false;
}
}
void AAuraCharacter::HideMagicCircle_Implementation()
{
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
{
AuraPlayerController->HideMagicCircle();
AuraPlayerController->bShowMouseCursor = true;
}
}
事件图表: sequence ShowMagicCircle ability-get avatar actor from actor info
等待按下按键 wait input press 按下时隐藏魔法阵 ability-get avatar actor from actor info HideMagicCircle
现在,第一次按下奥术碎片技能输入键,生成魔法阵,再按一次技能输入键,魔法阵消失。
end ability
结束节能之前需要延迟一段时间,否则第二次按下会立即出发第二次技能生成魔法阵。 delay
设置抗锯齿,使魔法阵贴花移动式更加顺畅
项目设置-引擎-渲染-默认设置-抗锯齿方法: 默认为 临时超分辨率(tsr) 这是最昂贵的抗锯齿方法,也是效果最好的。 但是,这与贴花不兼容。会导致移动贴花会显示尾迹。
应改为 多重取样抗锯齿(MSAA) 不再显示尾迹。
项目设置-引擎-渲染-默认设置-动态模糊-取消 防止移动时 贴花显示出现模糊化
奥术碎片技能,释放后会显示冰刺,按等级增多。需要自动或手动计算生成位置的点。 需要考虑再非平面的生成。
Source/Aura/Public/Actor/PointCollection.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PointCollection.generated.h"
UCLASS()
class AURA_API APointCollection : public AActor
{
GENERATED_BODY()
public:
APointCollection();
// 返回生成点 的拷贝,使用通道追踪,确保点不在地面之下
// NumPoints 接近中心点开始的点数量
UFUNCTION(BlueprintPure)
TArray<USceneComponent*> GetGroundPoints(const FVector& GroundLocation, int32 NumPoints, float YawOverride = 0.f);
protected:
virtual void BeginPlay() override;
// 生成点不可变数组
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TArray<USceneComponent*> ImmutablePts;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_0;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_1;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_2;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_3;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_4;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_5;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_6;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_7;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_8;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_9;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<USceneComponent> Pt_10;
};
Source/Aura/Private/Actor/PointCollection.cpp
#include "Actor/PointCollection.h"
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
#include "Kismet/KismetMathLibrary.h"
// Sets default values
APointCollection::APointCollection()
{
PrimaryActorTick.bCanEverTick = false;
Pt_0 = CreateDefaultSubobject<USceneComponent>("Pt_0");
ImmutablePts.Add(Pt_0);
SetRootComponent(Pt_0);
Pt_1 = CreateDefaultSubobject<USceneComponent>("Pt_1");
ImmutablePts.Add(Pt_1);
Pt_1->SetupAttachment(GetRootComponent());
Pt_2 = CreateDefaultSubobject<USceneComponent>("Pt_2");
ImmutablePts.Add(Pt_2);
Pt_2->SetupAttachment(GetRootComponent());
Pt_3 = CreateDefaultSubobject<USceneComponent>("Pt_3");
ImmutablePts.Add(Pt_3);
Pt_3->SetupAttachment(GetRootComponent());
Pt_4 = CreateDefaultSubobject<USceneComponent>("Pt_4");
ImmutablePts.Add(Pt_4);
Pt_4->SetupAttachment(GetRootComponent());
Pt_5 = CreateDefaultSubobject<USceneComponent>("Pt_5");
ImmutablePts.Add(Pt_5);
Pt_5->SetupAttachment(GetRootComponent());
Pt_6 = CreateDefaultSubobject<USceneComponent>("Pt_6");
ImmutablePts.Add(Pt_6);
Pt_6->SetupAttachment(GetRootComponent());
Pt_7 = CreateDefaultSubobject<USceneComponent>("Pt_7");
ImmutablePts.Add(Pt_7);
Pt_7->SetupAttachment(GetRootComponent());
Pt_8 = CreateDefaultSubobject<USceneComponent>("Pt_8");
ImmutablePts.Add(Pt_8);
Pt_8->SetupAttachment(GetRootComponent());
Pt_9 = CreateDefaultSubobject<USceneComponent>("Pt_9");
ImmutablePts.Add(Pt_9);
Pt_9->SetupAttachment(GetRootComponent());
Pt_10 = CreateDefaultSubobject<USceneComponent>("Pt_10");
ImmutablePts.Add(Pt_10);
Pt_10->SetupAttachment(GetRootComponent());
}
TArray<USceneComponent*> APointCollection::GetGroundPoints(const FVector& GroundLocation, int32 NumPoints,
float YawOverride)
{
checkf(ImmutablePts.Num() >= NumPoints, TEXT("Attempted to access ImmutablePts out of bounds."));
// 创建点副本 不修改原数组
TArray<USceneComponent*> ArrayCopy;
for (USceneComponent* Pt : ImmutablePts)
{
if (ArrayCopy.Num() >= NumPoints) return ArrayCopy;
// 不旋转根组件点
if (Pt != Pt_0)
{
// 每个点到中心点的距离向量
FVector ToPoint = Pt->GetComponentLocation() - Pt_0->GetComponentLocation();
// 旋转距离向量
ToPoint = ToPoint.RotateAngleAxis(YawOverride, FVector::UpVector);
// 每个点使用新的点位置旋转向量
Pt->SetWorldLocation(Pt_0->GetComponentLocation() + ToPoint);
}
// 追踪线起点
const FVector RaisedLocation = FVector(Pt->GetComponentLocation().X, Pt->GetComponentLocation().Y,
Pt->GetComponentLocation().Z + 500.f);
// 追踪线终点
const FVector LoweredLocation = FVector(Pt->GetComponentLocation().X, Pt->GetComponentLocation().Y,
Pt->GetComponentLocation().Z - 500.f);
// 从高出向下追踪的命中位置
FHitResult HitResult;
TArray<AActor*> IgnoreActors;
// 获取半径内的存活玩家,填充到 IgnoreActors 数组中,将作为线条追踪要忽略的actor数组
UAuraAbilitySystemLibrary::GetLivePlayersWithinRadius(this, IgnoreActors, TArray<AActor*>(), 1500.f,
GetActorLocation());
// 追踪 BlockAll ,忽略存活玩家
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActors(IgnoreActors);
GetWorld()->LineTraceSingleByProfile(HitResult, RaisedLocation, LoweredLocation, FName("BlockAll"),
QueryParams);
// 使用追踪到新的z轴值,调整点的位置 ,即落到地面的位置
const FVector AdjustedLocation = FVector(Pt->GetComponentLocation().X, Pt->GetComponentLocation().Y,
HitResult.ImpactPoint.Z);
Pt->SetWorldLocation(AdjustedLocation);
Pt->SetWorldRotation(UKismetMathLibrary::MakeRotFromZ(HitResult.ImpactNormal));
ArrayCopy.Add(Pt);
}
return ArrayCopy;
}
void APointCollection::BeginPlay()
{
Super::BeginPlay();
}
Content/Blueprints/AbilitySystem/Aura/Abilities/Arcane/ArcaneShards/BP_PointCollection.uasset
添加 billboard 公告板组件到 Pt 1组件下用于显示 添加 billboard 公告板组件到 Pt 0组件下用于显示 公告板组件0-精灵-sprint-TargetIcon 公告板组件1-精灵-sprint- TargetIconSpawn
按顺序设置每个组件的位置 移动 pt1距离 pt0 250个单位 100,0,0
点和点的距离不小于250单位。任意方向都可以 因为奥术碎片技能效果半径约200,防止重叠。
依次为每个点添加 公告板组件,调整点位置 进入顶视图调整位置。 序号越小,距离中心点越近。
事件图表:
从鼠标位置开始生成 TargetDataUnderMouse get hit result from Target Data break hit result 获取中心点
spawn actor from class spawn actor from class-class-BP_PointCollection spawn actor from class-固定生成,忽略碰撞 因为生成点为场景组件,不用担心碰撞 spawn actor from class 输出的点 提升为变量 PointCollection
使用生成的点获取地面点 GetGroundPoints GetGroundPoints-num points-11 添加0-360随机旋转使每次生成的点位置不固定 random float in range 纯蓝图函数与for each loop一起使用时很昂贵,需要注意,
random float in range
循环地面点 for each loop get world location draw debug sphere
结束技能前销毁 PointCollection destroy actor
可以在斜坡上正确生成点
将获取的地面点提升为变量,防止每次随机循环生成新的地面点集合,导致位置布局与组件调整的设置不同 GetGroundPoints 提升为变量 GroundPoints 这样也可以使用缓存的GroundPoints,提升性能
事件图表: set timer by event set timer by event-looping-true set timer by event-time 提升为变量 ShardSpawnDeltaTime 默认值0.2 作为生成点的间隔 set timer by event 输出提升为变量 ShardSpawnTimer 用以清除定时器
添加自定义事件 custom event: SpawnShard
GetGroundPoints-num points 提升为变量 NumPoints
生成前确保 Count 小于 NumPoints branch
添加变量 Count 整数,默认值0,跟踪已生成的点数量 每次生成 Count++
GroundPoints get (a copy) Count
get world location
设置完定时器后立即隐藏魔法阵圆环
设置完定时器后立开始生成,否则会有延时 Spawn Shard
循环false时清除定时器 ShardSpawnTimer clear and invalidate timer by handle
适合一次性游戏效果的cue,例如声音,碎片。 静态,未实例化,不能保持状态。 有自己的游戏队列通知。 无法执行延时等时间操作。
项目设置-标签管理器
Content/Blueprints/AbilitySystem/GameplayCueNotifies/GC_ArcaneShards.uasset
该cue自动设置好 gameplay cue tag-GameplayCue.ArcaneShards 最后新建的标签 但在5.3中这会有bug, 必须先将此标签改为其他标签,再改回来才会生效。
print string
事件图表:
execute gameplayCueWithParams on owner execute gameplayCueWithParams on owner-gameplay cue tag-GameplayCue.ArcaneShards
make Gameplay Cue Parameters
施放技能后,每次生成1个点会执行依次 cue
类默认值-GCN effect-burst effect-burst particles 添加一组
这将在玩家处生成粒子特效,数量与生成点一样。
类默认值-GCN effect-burst effect-burst sounds 添加一组
事件图表 get world location 提升为变量 ShardSpawnLocation 否则每次都会变更导致cue与生成点位置不同
ShardSpawnLocation
现在,释放技能可以生成粒子和音效。 服务端与客户端鼠标移动时的法阵贴花不会一致。因为这是预测键的性能优化行为。 服务端与客户端生成的调试球点位置不一致。因为调试信息不是预测动作。 但是粒子位置却一致。cue粒子是预测动作。
项目设置-标签管理器-Event.Montage.ArcaneShards
Cast_ArcaneShards 启用根运动
Content/Assets/Characters/Aura/Animations/Abilities/AM_Cast_ArcaneShards.uasset
默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人施法动作的开始攻击 - 到施法动作攻击结束
选择 MotionWarping 通知-Root Motion Modifier-Warp Target Name-FacingTarget FacingTarget 将用在敌人扭曲运动事件的 add or update warp target from location-warp target name
选择 MotionWarping 通知-Root Motion Modifier-Warp Translation-不启用 只有扭曲旋转
Root Motion Modifier-Rotation Type-Facing 旋转以面对该目标
决定施法攻击的时机
右键-添加通知-AN_MontageEvent 【自定义的通知】 在法杖尖位置 选择 AN_MontageEvent -动画通知-event tag-Event.Montage.ArcaneShards 用以在游戏中监听
标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇上的插槽位置,生成重叠虚拟球。 播放此蒙太奇可触发通知事件,带有事件标签 Event.Montage.ArcaneShards 后续通过标签监听此事件。
添加通知轨道:Sounds 攻击音效
添加通知-播放音效-
事件图表: break hit result -impact point 提升为变量 MouseHitLocation MouseHitLocation MouseHitLocation
获得地面点之后播放动画蒙太奇
play montage and wait play montage and wait-stop when ability ends-取消 play montage and wait-AM_Cast_ArcaneShards
wait gameplay event 监听蒙太奇的动画事件标签 Event.Montage.ArcaneShards wait gameplay event-only trigger once-启用
sequence
Update Facing Target(message) 带邮件标志
get avatar actor from actor info MouseHitLocation
现在可以释放技能,生成粒子,音效。
距离奥数碎片技能生成的水晶越远,伤害越小。 引擎自带该静态函数。
通过两个半径来计算伤害。 内半径伤害固定。 内半径至外半径伤害开始衰减。
Source/Aura/Public/AuraAbilityTypes.h
// 伤害效果参数结构类型
USTRUCT(BlueprintType)
struct FDamageEffectParams
{
......
// 径向损伤
UPROPERTY(BlueprintReadWrite)
bool bIsRadialDamage = false;
UPROPERTY(BlueprintReadWrite)
float RadialDamageInnerRadius = 0.f;
UPROPERTY(BlueprintReadWrite)
float RadialDamageOuterRadius = 0.f;
UPROPERTY(BlueprintReadWrite)
FVector RadialDamageOrigin = FVector::ZeroVector;
}
USTRUCT(BlueprintType)
struct FAuraGameplayEffectContext : public FGameplayEffectContext
public:
bool IsRadialDamage() const { return bIsRadialDamage; }
float GetRadialDamageInnerRadius() const { return RadialDamageInnerRadius; }
float GetRadialDamageOuterRadius() const { return RadialDamageOuterRadius; }
FVector GetRadialDamageOrigin() const { return RadialDamageOrigin; }
void SetIsRadialDamage(bool bInIsRadialDamage) { bIsRadialDamage = bInIsRadialDamage; }
void SetRadialDamageInnerRadius(float InRadialDamageInnerRadius) { RadialDamageInnerRadius = InRadialDamageInnerRadius; }
void SetRadialDamageOuterRadius(float InRadialDamageOuterRadius) { RadialDamageOuterRadius = InRadialDamageOuterRadius; }
void SetRadialDamageOrigin(const FVector& InRadialDamageOrigin) { RadialDamageOrigin = InRadialDamageOrigin; }
protected:
UPROPERTY()
bool bIsRadialDamage = false;
UPROPERTY()
float RadialDamageInnerRadius = 0.f;
UPROPERTY()
float RadialDamageOuterRadius = 0.f;
UPROPERTY()
FVector RadialDamageOrigin = FVector::ZeroVector;
Source/Aura/Private/AuraAbilityTypes.cpp
#include "AuraAbilityTypes.h"
bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
uint32 RepBits = 0;
// 存储
if (Ar.IsSaving())
{
if (bReplicateInstigator && Instigator.IsValid())
{
RepBits |= 1 << 0;
}
if (bReplicateEffectCauser && EffectCauser.IsValid() )
{
RepBits |= 1 << 1;
}
if (AbilityCDO.IsValid())
{
RepBits |= 1 << 2;
}
if (bReplicateSourceObject && SourceObject.IsValid())
{
RepBits |= 1 << 3;
}
if (Actors.Num() > 0)
{
RepBits |= 1 << 4;
}
if (HitResult.IsValid())
{
RepBits |= 1 << 5;
}
if (bHasWorldOrigin)
{
RepBits |= 1 << 6;
}
if (bIsBlockedHit)
{
// 如果格挡,翻转第7位-1
RepBits |= 1 << 7;
}
if (bIsCriticalHit)
{
// 如果暴击,翻转第8位-1
RepBits |= 1 << 8;
}
if (bIsSuccessfulDebuff)
{
RepBits |= 1 << 9;
}
if (DebuffDamage > 0.f)
{
RepBits |= 1 << 10;
}
if (DebuffDuration > 0.f)
{
RepBits |= 1 << 11;
}
if (DebuffFrequency > 0.f)
{
RepBits |= 1 << 12;
}
if (DamageType.IsValid())
{
RepBits |= 1 << 13;
}
if (!DeathImpulse.IsZero())
{
RepBits |= 1 << 14;
}
if (!KnockbackForce.IsZero())
{
RepBits |= 1 << 15;
}
if (bIsRadialDamage)
{
RepBits |= 1 << 16;
if (RadialDamageInnerRadius > 0.f)
{
RepBits |= 1 << 17;
}
if (RadialDamageOuterRadius > 0.f)
{
RepBits |= 1 << 18;
}
if (!RadialDamageOrigin.IsZero())
{
RepBits |= 1 << 19;
}
}
}
//序列化前15位
Ar.SerializeBits(&RepBits, 19);
if (RepBits & (1 << 0))
{
Ar << Instigator;
}
if (RepBits & (1 << 1))
{
Ar << EffectCauser;
}
if (RepBits & (1 << 2))
{
Ar << AbilityCDO;
}
if (RepBits & (1 << 3))
{
Ar << SourceObject;
}
if (RepBits & (1 << 4))
{
SafeNetSerializeTArray_Default<31>(Ar, Actors);
}
if (RepBits & (1 << 5))
{
if (Ar.IsLoading())
{
if (!HitResult.IsValid())
{
HitResult = TSharedPtr<FHitResult>(new FHitResult());
}
}
HitResult->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 6))
{
Ar << WorldOrigin;
bHasWorldOrigin = true;
}
else
{
bHasWorldOrigin = false;
}
if (RepBits & (1 << 7))
{
Ar << bIsBlockedHit;
}
if (RepBits & (1 << 8))
{
Ar << bIsCriticalHit;
}
if (RepBits & (1 << 9))
{
Ar << bIsSuccessfulDebuff;
}
if (RepBits & (1 << 10))
{
Ar << DebuffDamage;
}
if (RepBits & (1 << 11))
{
Ar << DebuffDuration;
}
if (RepBits & (1 << 12))
{
Ar << DebuffFrequency;
}
if (RepBits & (1 << 13))
{
if (Ar.IsLoading())
{
if (!DamageType.IsValid())
{
DamageType = TSharedPtr<FGameplayTag>(new FGameplayTag());
}
}
DamageType->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 14))
{
DeathImpulse.NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 15))
{
KnockbackForce.NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 16))
{
Ar << bIsRadialDamage;
if (RepBits & (1 << 17))
{
Ar << RadialDamageInnerRadius;
}
if (RepBits & (1 << 18))
{
Ar << RadialDamageOuterRadius;
}
if (RepBits & (1 << 19))
{
RadialDamageOrigin.NetSerialize(Ar, Map, bOutSuccess);
}
}
if (Ar.IsLoading())
{
AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
}
bOutSuccess = true;
return true;
}
参数讲依据等级调整 用于在游戏效果情景中携带 如果没有径向损伤参数,则不在网络中传输径向参数
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
protected:
// 径向损伤参数
UPROPERTY(EditDefaultsOnly, Category = "Damage")
bool bIsRadialDamage = false;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Damage")
float RadialDamageInnerRadius = 0.f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Damage")
float RadialDamageOuterRadius = 0.f;
UPROPERTY(EditDefaultsOnly, Category = "Damage")
FVector RadialDamageOrigin = FVector::ZeroVector;
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
FDamageEffectParams UAuraDamageGameplayAbility::MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor) const
{
FDamageEffectParams Params;
Params.WorldContextObject = GetAvatarActorFromActorInfo();
Params.DamageGameplayEffectClass = DamageEffectClass;
Params.SourceAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
Params.TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
Params.BaseDamage = Damage.GetValueAtLevel(GetAbilityLevel());
Params.AbilityLevel = GetAbilityLevel();
Params.DamageType = DamageType;
Params.DebuffChance = DebuffChance;
Params.DebuffDamage = DebuffDamage;
Params.DebuffDuration = DebuffDuration;
Params.DebuffFrequency = DebuffFrequency;
Params.DeathImpulseMagnitude = DeathImpulseMagnitude;
Params.KnockbackForceMagnitude = KnockbackForceMagnitude;
Params.KnockbackChance = KnockbackChance;
if (IsValid(TargetActor))
{
FRotator Rotation = (TargetActor->GetActorLocation() - GetAvatarActorFromActorInfo()->GetActorLocation()).Rotation();
// 向上旋转45度
Rotation.Pitch = 45.f;
const FVector ToTarget = Rotation.Vector();
Params.DeathImpulse = ToTarget * DeathImpulseMagnitude;
Params.KnockbackForce = ToTarget * KnockbackForceMagnitude;
}
if (bIsRadialDamage)
{
Params.bIsRadialDamage = bIsRadialDamage;
Params.RadialDamageOrigin = RadialDamageOrigin;
Params.RadialDamageInnerRadius = RadialDamageInnerRadius;
Params.RadialDamageOuterRadius = RadialDamageOuterRadius;
}
return Params;
}
并在应用伤害效果时设置径向损伤参数到效果情景中
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
public:
// 径向损伤
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static bool IsRadialDamage(const FGameplayEffectContextHandle& EffectContextHandle);
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static float GetRadialDamageInnerRadius(const FGameplayEffectContextHandle& EffectContextHandle);
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static float GetRadialDamageOuterRadius(const FGameplayEffectContextHandle& EffectContextHandle);
UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static FVector GetRadialDamageOrigin(const FGameplayEffectContextHandle& EffectContextHandle);
// 径向损伤
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetIsRadialDamage(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInIsRadialDamage);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetRadialDamageInnerRadius(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, float InInnerRadius);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetRadialDamageOuterRadius(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, float InOuterRadius);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetRadialDamageOrigin(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, const FVector& InOrigin);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
bool UAuraAbilitySystemLibrary::IsRadialDamage(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->IsRadialDamage();
}
return false;
}
float UAuraAbilitySystemLibrary::GetRadialDamageInnerRadius(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->GetRadialDamageInnerRadius();
}
return 0.f;
}
float UAuraAbilitySystemLibrary::GetRadialDamageOuterRadius(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->GetRadialDamageOuterRadius();
}
return 0.f;
}
FVector UAuraAbilitySystemLibrary::GetRadialDamageOrigin(const FGameplayEffectContextHandle& EffectContextHandle)
{
if (const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
return AuraEffectContext->GetRadialDamageOrigin();
}
return FVector::ZeroVector;
}
void UAuraAbilitySystemLibrary::SetIsRadialDamage(FGameplayEffectContextHandle& EffectContextHandle,
bool bInIsRadialDamage)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetIsRadialDamage(bInIsRadialDamage);
}
}
void UAuraAbilitySystemLibrary::SetRadialDamageInnerRadius(FGameplayEffectContextHandle& EffectContextHandle,
float InInnerRadius)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetRadialDamageInnerRadius(InInnerRadius);
}
}
void UAuraAbilitySystemLibrary::SetRadialDamageOuterRadius(FGameplayEffectContextHandle& EffectContextHandle,
float InOuterRadius)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetRadialDamageOuterRadius(InOuterRadius);
}
}
void UAuraAbilitySystemLibrary::SetRadialDamageOrigin(FGameplayEffectContextHandle& EffectContextHandle,
const FVector& InOrigin)
{
if (FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetRadialDamageOrigin(InOrigin);
}
}
在效果情景句柄中使用,计算 径向损伤
Source/Aura/Public/Interaction/CombatInterface.h
DECLARE_MULTICAST_DELEGATE_OneParam(FOnDamageSignature, float /*DamageAmount*/);
public:
virtual FOnDamageSignature& GetOnDamageSignature() = 0;
受到伤害时,广播该事件,附带伤害值
Source/Aura/Public/Character/AuraCharacterBase.h
public:
virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator,AActor* DamageCauser) override;
// 获取受伤委托
virtual FOnDamageSignature& GetOnDamageSignature() override;
FOnDamageSignature OnDamageDelegate;
Source/Aura/Private/Character/AuraCharacterBase.cpp
float AAuraCharacterBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
const float DamageTaken = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
OnDamageDelegate.Broadcast(DamageTaken);
return DamageTaken;
}
FOnDamageSignature& AAuraCharacterBase::GetOnDamageSignature()
{
return OnDamageDelegate;
}
在根据各类属性计算完伤害后,开始通过 径向损伤 来进一步调整伤害值
如果有径向损伤,距离越远,伤害衰减的越多
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
#include "Camera/CameraShakeSourceActor.h"
#include "Kismet/GameplayStatics.h"
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
TMap<FGameplayTag, FGameplayEffectAttributeCaptureDefinition> TagsToCaptureDefs;
const FAuraGameplayTags& Tags = FAuraGameplayTags::Get();
// 使用局部变量捕获def,延迟添加,否则减益效果DetermineDebuff捕获不到
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_Armor, DamageStatics().ArmorDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_BlockChance, DamageStatics().BlockChanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_ArmorPenetration, DamageStatics().ArmorPenetrationDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitChance, DamageStatics().CriticalHitChanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitResistance, DamageStatics().CriticalHitResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitDamage, DamageStatics().CriticalHitDamageDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, DamageStatics().ArcaneResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Fire, DamageStatics().FireResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Lightning, DamageStatics().LightningResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Physical, DamageStatics().PhysicalResistanceDef);
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
//ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
//ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
int32 SourcePlayerLevel = 1;
if (SourceAvatar->Implements<UCombatInterface>())
{
SourcePlayerLevel = ICombatInterface::Execute_GetPlayerLevel(SourceAvatar);
}
int32 TargetPlayerLevel = 1;
if (TargetAvatar->Implements<UCombatInterface>())
{
TargetPlayerLevel = ICombatInterface::Execute_GetPlayerLevel(TargetAvatar);
}
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// Debuff
DetermineDebuff(ExecutionParams, Spec, EvaluationParameters, TagsToCaptureDefs);
// 根据调用者大小计算伤害值 累加
// DamageTypesToResistances 伤害型标签-抗性属性 组包含所有伤害类型的标签 和与伤害关联的 抗性属性标签
// DamageTypesToResistances 标签只存在于伤害型技能中
// Get Damage Set by Caller Magnitude
float Damage = 0.f;
for (const TTuple<FGameplayTag, FGameplayTag>& Pair : FAuraGameplayTags::Get().DamageTypesToResistances)
{
// 游戏标签对应的的值由 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude 设置过【】
// Pair.Key 为伤害类型技能标签 Pair.Value 为抗性属性标签
// 根据标签 后续可以取出不同的抗性属性 Pair.Value,减少对应类型伤害的值 例如火抗 将 火属性伤害值减少一些
// 取出伤害类型技能的值
// const float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
// 伤害型技能标签
const FGameplayTag DamageTypeTag = Pair.Key;
// 抗性属性标签
const FGameplayTag ResistanceTag = Pair.Value;
checkf(TagsToCaptureDefs.Contains(ResistanceTag),
TEXT("TagsToCaptureDefs doesn't contain Tag: [%s] in ExecCalc_Damage"), *ResistanceTag.ToString());
// 通过属性标签,找到相关联的捕获属性定义,当前只需要抗性捕获定义
// 定义在 TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, ArcaneResistanceDef);
const FGameplayEffectAttributeCaptureDefinition CaptureDef = TagsToCaptureDefs[ResistanceTag];
// 参数2 :未找到相关属性时是否警告
float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key, false);
// 计算捕获的目标的属性 通过 Resistance 传出
float Resistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(CaptureDef, EvaluationParameters, Resistance);
// 抗性最大抵消100%的伤害
Resistance = FMath::Clamp(Resistance, 0.f, 100.f);
// 每一点抗性抵消1%的伤害
DamageTypeValue *= (100.f - Resistance) / 100.f;
//在根据各类属性计算完伤害后,开始通过 径向损伤 来进一步调整伤害值
//如果有径向损伤,距离越远,伤害衰减的越多
if (UAuraAbilitySystemLibrary::IsRadialDamage(EffectContextHandle))
{
// 1. override TakeDamage in AuraCharacterBase. * 覆盖AuraCharacterBase中的TakeDamage*
// 2. create delegate OnDamageDelegate, broadcast damage received in TakeDamage *
// 创建代理OnDamageDelegate,在TakeDamage中广播受到伤害*
// 3. Bind lambda to OnDamageDelegate on the Victim here. * 在此处将lambda绑定到受害者的OnDamageDelegate*
// 4. Call UGameplayStatics::ApplyRadialDamageWithFalloff to cause damage (this will result in TakeDamage being called
// on the Victim, which will then broadcast OnDamageDelegate)
// 调用UGameplayStatics::ApplyRadialDamageWithFalloff造成伤害(这将导致对受害者调用TakeDamage,然后广播DamageDelegate)
// 5. In Lambda, set DamageTypeValue to the damage received from the broadcast *
// 在Lambda中,将DamageTypeValue设置为从广播中收到的伤害*
if (ICombatInterface* CombatInterface = Cast<ICombatInterface>(TargetAvatar))
{
// & 表示捕获外部变量,例如 DamageTypeValue
CombatInterface->GetOnDamageSignature().AddLambda([&](float DamageAmount)
{
DamageTypeValue = DamageAmount;
});
}
UGameplayStatics::ApplyRadialDamageWithFalloff(
TargetAvatar,
DamageTypeValue,
0.f,
UAuraAbilitySystemLibrary::GetRadialDamageOrigin(EffectContextHandle),
UAuraAbilitySystemLibrary::GetRadialDamageInnerRadius(EffectContextHandle),
UAuraAbilitySystemLibrary::GetRadialDamageOuterRadius(EffectContextHandle),
1.f,
UDamageType::StaticClass(),
TArray<AActor*>(),
SourceAvatar,
nullptr);
}
Damage += DamageTypeValue;
}
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
//为自定义效果情景设置是否格挡
UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourcePlayerLevel);
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetPlayerLevel);
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef,
EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef,
EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef,
EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(TargetPlayerLevel);
// Critical Hit Resistance reduces Critical Hit Chance by a certain percentage
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance *
CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// 为自定义效果情景设置是否暴击
UAuraAbilitySystemLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
// Double damage plus a bonus if critical hit
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
准备在奥术碎片技能中造成径向损伤
删除变量 FVector RadialDamageOrigin = FVector::ZeroVector;
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
public:
UFUNCTION(BlueprintPure)
FDamageEffectParams MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor = nullptr, FVector InRadialDamageOrigin = FVector::ZeroVector) const;
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
FDamageEffectParams UAuraDamageGameplayAbility::MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor, FVector InRadialDamageOrigin) const
{
FDamageEffectParams Params;
Params.WorldContextObject = GetAvatarActorFromActorInfo();
Params.DamageGameplayEffectClass = DamageEffectClass;
Params.SourceAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
Params.TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
Params.BaseDamage = Damage.GetValueAtLevel(GetAbilityLevel());
Params.AbilityLevel = GetAbilityLevel();
Params.DamageType = DamageType;
Params.DebuffChance = DebuffChance;
Params.DebuffDamage = DebuffDamage;
Params.DebuffDuration = DebuffDuration;
Params.DebuffFrequency = DebuffFrequency;
Params.DeathImpulseMagnitude = DeathImpulseMagnitude;
Params.KnockbackForceMagnitude = KnockbackForceMagnitude;
Params.KnockbackChance = KnockbackChance;
if (IsValid(TargetActor))
{
FRotator Rotation = (TargetActor->GetActorLocation() - GetAvatarActorFromActorInfo()->GetActorLocation()).Rotation();
// 向上旋转45度
Rotation.Pitch = 45.f;
const FVector ToTarget = Rotation.Vector();
Params.DeathImpulse = ToTarget * DeathImpulseMagnitude;
Params.KnockbackForce = ToTarget * KnockbackForceMagnitude;
}
if (bIsRadialDamage)
{
Params.bIsRadialDamage = bIsRadialDamage;
Params.RadialDamageOrigin = InRadialDamageOrigin;
Params.RadialDamageInnerRadius = RadialDamageInnerRadius;
Params.RadialDamageOuterRadius = RadialDamageOuterRadius;
}
return Params;
}
1,10 40,200 自动
damage-1,CT_Damage,Abilities.ArcaneShards 没有debuff RadialDamageInnerRadius-25 RadialDamageOuterRadius-250
事件图表:
绘制调试用的内球和外球 RadialDamageInnerRadius RadialDamageOuterRadius draw debug sphere
对外半径内的所有敌人目标造成伤害 get avatar actor from actor info 作为世界环境,也忽略自身 RadialDamageOuterRadius get live players within radius MakeDamageEffectParamsFromClassDefaults 半径内的敌人目标数组提升为变量 Overlapping Players 对每一个目标都应用伤害,循环结束后才将计数加1 for each loop apply damage effect NumPoints 默认值改为1
为伤害参数设置伤害来源 ShardSpawnLocation 现在可以对碎片周围的敌人造成径向伤害。
防止后续多次绑定无效伤害委托
Source/Aura/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cpp
float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key, false);
if (DamageTypeValue <= 0.f)
{
continue;
}
// 执行计算将如何影响任何其他属性的值
// 这属于游戏效果
// 通过设置一个自定义的计算类给游戏效果添加一个修改器
// 这个自定义计算类决定了当我们应用游戏效果时会发生什么
// 在这里可以决定如何改变各种属性,可以捕获属性,也可以获取有关游戏效果的信息,包括谁造成了该效果以及该效果的目标是谁
void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
TMap<FGameplayTag, FGameplayEffectAttributeCaptureDefinition> TagsToCaptureDefs;
const FAuraGameplayTags& Tags = FAuraGameplayTags::Get();
// 使用局部变量捕获def,延迟添加,否则减益效果DetermineDebuff捕获不到
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_Armor, DamageStatics().ArmorDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_BlockChance, DamageStatics().BlockChanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_ArmorPenetration, DamageStatics().ArmorPenetrationDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitChance, DamageStatics().CriticalHitChanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitResistance, DamageStatics().CriticalHitResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitDamage, DamageStatics().CriticalHitDamageDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, DamageStatics().ArcaneResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Fire, DamageStatics().FireResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Lightning, DamageStatics().LightningResistanceDef);
TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Physical, DamageStatics().PhysicalResistanceDef);
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
//ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
//ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
int32 SourcePlayerLevel = 1;
if (SourceAvatar->Implements<UCombatInterface>())
{
SourcePlayerLevel = ICombatInterface::Execute_GetPlayerLevel(SourceAvatar);
}
int32 TargetPlayerLevel = 1;
if (TargetAvatar->Implements<UCombatInterface>())
{
TargetPlayerLevel = ICombatInterface::Execute_GetPlayerLevel(TargetAvatar);
}
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// Debuff
DetermineDebuff(ExecutionParams, Spec, EvaluationParameters, TagsToCaptureDefs);
// 根据调用者大小计算伤害值 累加
// DamageTypesToResistances 伤害型标签-抗性属性 组包含所有伤害类型的标签 和与伤害关联的 抗性属性标签
// DamageTypesToResistances 标签只存在于伤害型技能中
// Get Damage Set by Caller Magnitude
float Damage = 0.f;
for (const TTuple<FGameplayTag, FGameplayTag>& Pair : FAuraGameplayTags::Get().DamageTypesToResistances)
{
// 游戏标签对应的的值由 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude 设置过【】
// Pair.Key 为伤害类型技能标签 Pair.Value 为抗性属性标签
// 根据标签 后续可以取出不同的抗性属性 Pair.Value,减少对应类型伤害的值 例如火抗 将 火属性伤害值减少一些
// 取出伤害类型技能的值
// const float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
// 伤害型技能标签
const FGameplayTag DamageTypeTag = Pair.Key;
// 抗性属性标签
const FGameplayTag ResistanceTag = Pair.Value;
checkf(TagsToCaptureDefs.Contains(ResistanceTag),
TEXT("TagsToCaptureDefs doesn't contain Tag: [%s] in ExecCalc_Damage"), *ResistanceTag.ToString());
// 通过属性标签,找到相关联的捕获属性定义,当前只需要抗性捕获定义
// 定义在 TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, ArcaneResistanceDef);
const FGameplayEffectAttributeCaptureDefinition CaptureDef = TagsToCaptureDefs[ResistanceTag];
// 参数2 :未找到相关属性时是否警告
float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key, false);
if (DamageTypeValue <= 0.f)
{
continue;
}
// 计算捕获的目标的属性 通过 Resistance 传出
float Resistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(CaptureDef, EvaluationParameters, Resistance);
// 抗性最大抵消100%的伤害
Resistance = FMath::Clamp(Resistance, 0.f, 100.f);
// 每一点抗性抵消1%的伤害
DamageTypeValue *= (100.f - Resistance) / 100.f;
//在根据各类属性计算完伤害后,开始通过 径向损伤 来进一步调整伤害值
//如果有径向损伤,距离越远,伤害衰减的越多
if (UAuraAbilitySystemLibrary::IsRadialDamage(EffectContextHandle))
{
// 1. override TakeDamage in AuraCharacterBase. * 覆盖AuraCharacterBase中的TakeDamage*
// 2. create delegate OnDamageDelegate, broadcast damage received in TakeDamage *
// 创建代理OnDamageDelegate,在TakeDamage中广播受到伤害*
// 3. Bind lambda to OnDamageDelegate on the Victim here. * 在此处将lambda绑定到受害者的OnDamageDelegate*
// 4. Call UGameplayStatics::ApplyRadialDamageWithFalloff to cause damage (this will result in TakeDamage being called
// on the Victim, which will then broadcast OnDamageDelegate)
// 调用UGameplayStatics::ApplyRadialDamageWithFalloff造成伤害(这将导致对受害者调用TakeDamage,然后广播DamageDelegate)
// 5. In Lambda, set DamageTypeValue to the damage received from the broadcast *
// 在Lambda中,将DamageTypeValue设置为从广播中收到的伤害*
if (ICombatInterface* CombatInterface = Cast<ICombatInterface>(TargetAvatar))
{
// & 表示捕获外部变量,例如 DamageTypeValue
CombatInterface->GetOnDamageSignature().AddLambda([&](float DamageAmount)
{
DamageTypeValue = DamageAmount;
});
}
UGameplayStatics::ApplyRadialDamageWithFalloff(
TargetAvatar,
DamageTypeValue,
0.f,
UAuraAbilitySystemLibrary::GetRadialDamageOrigin(EffectContextHandle),
UAuraAbilitySystemLibrary::GetRadialDamageInnerRadius(EffectContextHandle),
UAuraAbilitySystemLibrary::GetRadialDamageOuterRadius(EffectContextHandle),
1.f,
UDamageType::StaticClass(),
TArray<AActor*>(),
SourceAvatar,
nullptr);
}
Damage += DamageTypeValue;
}
// Capture BlockChance on Target, and determine if there was a successful Block
// If Block, halve the damage.
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters,
TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
// 如果格挡成功,则伤害值减半
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
//为自定义效果情景设置是否格挡
UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
Damage = bBlocked ? Damage / 2.f : Damage;
// 计算捕获的目标的护甲属性 通过 TargetArmor 传出
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters,
TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
// 算捕获的来源的护甲穿透属性 通过 SourceArmorPenetration 传出
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef,
EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
// 职业信息资产
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
// 伤害计算系数资产的护甲穿透曲线
// FindCurve 通过曲线名称查找曲线
// FString() 表示未找到时,空警告
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourcePlayerLevel);
// ArmorPenetration ignores a percentage of the Target's Armor.
// 护甲穿透忽略目标护甲的百分比。
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
// 伤害计算系数资产的有效护甲曲线
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetPlayerLevel);
// 护甲忽略一定百分比的伤害。
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef,
EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef,
EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef,
EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(
FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(TargetPlayerLevel);
// Critical Hit Resistance reduces Critical Hit Chance by a certain percentage
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance *
CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// 为自定义效果情景设置是否暴击
UAuraAbilitySystemLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
// Double damage plus a bonus if critical hit
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, Damage);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
ShardSpawnLocation
折叠到函数 StoreShardSpawnLocation
变量 Overlapping Players 重命名为 PlayersToDamage PlayersToDamage
折叠到函数 StorePlayersToDamage
折叠到函数 RadialDamageToPlayer
激活魔术阵时,鼠标选中目标时,不再高亮目标.
鼠标默认跟踪可见性通道。 当激活魔法阵时,跟踪其他通道
项目设置-引擎-碰撞-object channels-添加 ExcludePlayers 通道,默认block
当鼠标通道 设置为可见性通道或 ExcludePlayers 通道时,忽略鼠标。防止鼠标选中空气墙。
表示在魔法阵激活时不被鼠标选中。 直接在基类中设置,不需要为每个敌人单独设置。
BP_EnemyBase-胶囊体组件-碰撞预设-忽略 ExcludePlayers 通道
BP_EnemyBase-网格体组件-碰撞预设-忽略 ExcludePlayers 通道
BP_EnemyBase-weapon组件-碰撞预设-忽略 ExcludePlayers 通道
表示在魔法阵激活时不被鼠标选中。
BP_AuraCharacter-胶囊体组件-碰撞预设-忽略 ExcludePlayers 通道
BP_AuraCharacter-网格体组件-碰撞预设-忽略 ExcludePlayers 通道
BP_AuraCharacter-weapon组件-碰撞预设-忽略 ExcludePlayers 通道
BP_AuraCharacter-弹簧臂碰撞盒子组件 Box-碰撞预设-忽略 ExcludePlayers 通道
Source/Aura/Aura.h
// 魔法阵激活时的用于使鼠标不选中玩家和敌人的碰撞通道
#define ECC_ExcludePlayers ECollisionChannel::ECC_GameTraceChannel3
Source/Aura/Private/Player/AuraPlayerController.cpp
#include "Aura/Aura.h"
void AAuraPlayerController::CursorTrace()
{
// Player.Block.CursorTrace 标签使actor无法被选择,高亮
if (GetASC() && GetASC()->HasMatchingGameplayTag(FAuraGameplayTags::Get().Player_Block_CursorTrace))
{
if (LastActor) LastActor->UnHighlightActor();
if (ThisActor) ThisActor->UnHighlightActor();
LastActor = nullptr;
ThisActor = nullptr;
return;
}
// 光标命中的结果 使用 ECC_Visibility 通道进行跟踪 ,简单碰撞跟踪
// 如果存在魔法阵,则设置鼠标跟踪通道排除玩家和敌人,不再选中玩家和敌人
const ECollisionChannel TraceChannel = IsValid(MagicCircle) ? ECC_ExcludePlayers : ECC_Visibility;
GetHitResultUnderCursor(TraceChannel, false, CursorHit);
// 检查跟踪结果
if (!CursorHit.bBlockingHit) return;
LastActor = ThisActor;
ThisActor = Cast<IEnemyInterface>(CursorHit.GetActor());
if (LastActor != ThisActor)
{
if (LastActor) LastActor->UnHighlightActor();
if (ThisActor) ThisActor->HighlightActor();
}
}
奥术碎片生成时,BP_Goblin_Spear 敌人不应直接被垂直击飞,应该偏向水晶中心的外侧
BP_Goblin_Spear 设置: 胶囊体
网格体
weapon
溶解材质
强制删除旧版 BP_Goblin_Spear
现在,奥术碎片将敌人击飞,并始终远离玩家。
根据技能生成物的位置计算击飞方向
Source/Aura/Public/AbilitySystem/Abilities/AuraDamageGameplayAbility.h
public:
UFUNCTION(BlueprintPure)
FDamageEffectParams MakeDamageEffectParamsFromClassDefaults(
AActor* TargetActor = nullptr,
FVector InRadialDamageOrigin = FVector::ZeroVector,
bool bOverrideKnockbackDirection = false,
FVector KnockbackDirectionOverride = FVector::ZeroVector,
bool bOverrideDeathImpulse = false,
FVector DeathImpulseDirectionOverride = FVector::ZeroVector,
bool bOverridePitch = false,
float PitchOverride = 0.f) const;
Source/Aura/Private/AbilitySystem/Abilities/AuraDamageGameplayAbility.cpp
FDamageEffectParams UAuraDamageGameplayAbility::MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor,
FVector InRadialDamageOrigin, bool bOverrideKnockbackDirection, FVector KnockbackDirectionOverride,
bool bOverrideDeathImpulse, FVector DeathImpulseDirectionOverride, bool bOverridePitch, float PitchOverride) const
{
FDamageEffectParams Params;
Params.WorldContextObject = GetAvatarActorFromActorInfo();
Params.DamageGameplayEffectClass = DamageEffectClass;
Params.SourceAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
Params.TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
Params.BaseDamage = Damage.GetValueAtLevel(GetAbilityLevel());
Params.AbilityLevel = GetAbilityLevel();
Params.DamageType = DamageType;
Params.DebuffChance = DebuffChance;
Params.DebuffDamage = DebuffDamage;
Params.DebuffDuration = DebuffDuration;
Params.DebuffFrequency = DebuffFrequency;
Params.DeathImpulseMagnitude = DeathImpulseMagnitude;
Params.KnockbackForceMagnitude = KnockbackForceMagnitude;
Params.KnockbackChance = KnockbackChance;
if (IsValid(TargetActor))
{
FRotator Rotation = (TargetActor->GetActorLocation() - GetAvatarActorFromActorInfo()->GetActorLocation()).Rotation();
// 向上旋转角度
if (bOverridePitch)
{
Rotation.Pitch = PitchOverride;
}
const FVector ToTarget = Rotation.Vector();
if (!bOverrideKnockbackDirection)
{
Params.KnockbackForce = ToTarget * KnockbackForceMagnitude;
}
if (!bOverrideDeathImpulse)
{
Params.DeathImpulse = ToTarget * DeathImpulseMagnitude;
}
}
if (bOverrideKnockbackDirection)
{
KnockbackDirectionOverride.Normalize();
Params.KnockbackForce = KnockbackDirectionOverride * KnockbackForceMagnitude;
if (bOverridePitch)
{
FRotator KnockbackRotation = KnockbackDirectionOverride.Rotation();
KnockbackRotation.Pitch = PitchOverride;
Params.KnockbackForce = KnockbackRotation.Vector() * KnockbackForceMagnitude;
}
}
if (bOverrideDeathImpulse)
{
DeathImpulseDirectionOverride.Normalize();
Params.DeathImpulse = DeathImpulseDirectionOverride * DeathImpulseMagnitude;
if (bOverridePitch)
{
FRotator DeathImpulseRotation = DeathImpulseDirectionOverride.Rotation();
DeathImpulseRotation.Pitch = PitchOverride;
Params.DeathImpulse = DeathImpulseRotation.Vector() * DeathImpulseMagnitude;
}
}
if (bIsRadialDamage)
{
Params.bIsRadialDamage = bIsRadialDamage;
Params.RadialDamageOrigin = InRadialDamageOrigin;
Params.RadialDamageInnerRadius = RadialDamageInnerRadius;
Params.RadialDamageOuterRadius = RadialDamageOuterRadius;
}
return Params;
}
ShardSpawnLocation
MakeDamageEffectParamsFromClassDefaults-PitchOverride-45
现在,击飞时,敌人远离水晶中点。
目前使用了 伤害技能C++作为雷电和奥术碎片的技能基类。 应该为此创建基于伤害技能的子类技能:雷电基类C++和奥术碎片基类C++
Source/Aura/Public/AbilitySystem/Abilities/ArcaneShards.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraDamageGameplayAbility.h"
#include "ArcaneShards.generated.h"
UCLASS()
class AURA_API UArcaneShards : public UAuraDamageGameplayAbility
{
GENERATED_BODY()
public:
virtual FString GetDescription(int32 Level) override;
virtual FString GetNextLevelDescription(int32 Level) override;
// 最大奥术碎片数量
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
int32 MaxNumShards = 11;
};
Source/Aura/Private/AbilitySystem/Abilities/ArcaneShards.cpp
#include "AbilitySystem/Abilities/ArcaneShards.h"
FString UArcaneShards::GetDescription(int32 Level)
{
const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
const float ManaCost = FMath::Abs(GetManaCost(Level));
const float Cooldown = GetCooldown(Level);
if (Level == 1)
{
return FString::Printf(TEXT(
// Title
"<Title>ARCANE SHARDS</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
"<Default>Summon a shard of arcane energy, "
"causing radial arcane damage of </>"
// Damage
"<Damage>%d</><Default> at the shard origin.</>"),
// Values
Level,
ManaCost,
Cooldown,
ScaledDamage);
}
else
{
return FString::Printf(TEXT(
// Title
"<Title>ARCANE SHARDS</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
// Addition Number of Shock Targets
"<Default>Summon %d shards of arcane energy, causing radial arcane damage of </>"
// Damage
"<Damage>%d</><Default> at the shard origins.</>"),
// Values
Level,
ManaCost,
Cooldown,
FMath::Min(Level, MaxNumShards),
ScaledDamage);
}
}
FString UArcaneShards::GetNextLevelDescription(int32 Level)
{
const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
const float ManaCost = FMath::Abs(GetManaCost(Level));
const float Cooldown = GetCooldown(Level);
return FString::Printf(TEXT(
// Title
"<Title>NEXT LEVEL: </>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
// Addition Number of Shock Targets
"<Default>Summon %d shards of arcane energy, causing radial arcane damage of </>"
// Damage
"<Damage>%d</><Default> at the shard origins.</>"),
// Values
Level,
ManaCost,
Cooldown,
FMath::Min(Level, MaxNumShards),
ScaledDamage);
}
Source/Aura/Public/AbilitySystem/Abilities/Electrocute.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraBeamSpell.h"
#include "Electrocute.generated.h"
UCLASS()
class AURA_API UElectrocute : public UAuraBeamSpell
{
GENERATED_BODY()
public:
virtual FString GetDescription(int32 Level) override;
virtual FString GetNextLevelDescription(int32 Level) override;
};
Source/Aura/Private/AbilitySystem/Abilities/Electrocute.cpp
#include "AbilitySystem/Abilities/Electrocute.h"
FString UElectrocute::GetDescription(int32 Level)
{
const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
const float ManaCost = FMath::Abs(GetManaCost(Level));
const float Cooldown = GetCooldown(Level);
if (Level == 1)
{
return FString::Printf(TEXT(
// Title
"<Title>ELECTROCUTE</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
"<Default>Emits a beam of lightning, "
"connecting with the target, repeatedly causing </>"
// Damage
"<Damage>%d</><Default> lightning damage with"
" a chance to stun</>"),
// Values
Level,
ManaCost,
Cooldown,
ScaledDamage);
}
else
{
return FString::Printf(TEXT(
// Title
"<Title>ELECTROCUTE</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
// Addition Number of Shock Targets
"<Default>Emits a beam of lightning, "
"propagating to %d additional targets nearby, causing </>"
// Damage
"<Damage>%d</><Default> lightning damage with"
" a chance to stun</>"),
// Values
Level,
ManaCost,
Cooldown,
FMath::Min(Level, MaxNumShockTargets - 1),
ScaledDamage);
}
}
FString UElectrocute::GetNextLevelDescription(int32 Level)
{
const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
const float ManaCost = FMath::Abs(GetManaCost(Level));
const float Cooldown = GetCooldown(Level);
return FString::Printf(TEXT(
// Title
"<Title>NEXT LEVEL:</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
// Addition Number of Shock Targets
"<Default>Emits a beam of lightning, "
"propagating to %d additional targets nearby, causing </>"
// Damage
"<Damage>%d</><Default> lightning damage with"
" a chance to stun</>"),
// Values
Level,
ManaCost,
Cooldown,
FMath::Min(Level, MaxNumShockTargets - 1),
ScaledDamage);
}
GA_Electrocute-类设置-父类-Electrocute
GA_ArcaneShards-类设置-父类-ArcaneShards
事件图表: MaxNumShards 替换 NumPoints 删除变量 NumPoints
用以替换之前的阶梯表格
Content/Blueprints/AbilitySystem/Data/CT_SpellCost.uasset
其他-曲线表格-cubic
曲线 FireBolt 1 2.5 40, 50 自动
曲线 Electrocute
1, 1 40, 28 自动
曲线 ArcaneShards
1,10 40,85
1,CT_SpellCost,Electrocute
1,CT_SpellCost,FireBolt
Content/Blueprints/AbilitySystem/Aura/Abilities/Arcane/ArcaneShards/GE_Cost_ArcaneShards.uasset
持续时间-Duration Policy-Instant 即时
GameplayEffect -Modifiers: attribute-AuraAttributeSet.Mana 表示释放该技能时,消耗 AuraAttributeSet.Mana 魔力属性的值
Modifier Op-Add
Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifier Magnitude-Scalable Float Magnitude- 1,CT_SpellCost,ArcaneShards
每次释放奥术碎片术,消耗魔力点。
costs gameplay effect class-GE_Cost_ArcaneShards
项目设置-标签管理器-添加 Cooldown.Arcane.ArcaneShards
Content/Blueprints/AbilitySystem/Aura/Abilities/Arcane/ArcaneShards/GE_Cooldown_ArcaneShards.uasset
提交技能时,可以将技能冷却效果 GE_Cooldown_ArcaneShards 效果应用到技能上。
Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Target Tags Gameplay Effect Component
-Add Tags-Cooldown.Arcane.ArcaneShards
当把GE_Cooldown_FireBolt设置为火球术技能的技能冷却效果时, 游戏效果 GE_Cooldown_ArcaneShards 将为目标授予Cooldown.Arcane.ArcaneShards 冷却 效果标签。
细节-持续时间-Duration Policy-Has Duration 有持续时间 细节-持续时间-Duration Magnitude-Magnitude calculation Type-scalable float 细节-持续时间-Duration Magnitude-Scalable Float Magnitude-6 【该效果持续6秒,然后自行消失】
类默认值-cooldown gameplay effect class-GE_Cooldown_ArcaneShards
Cooldown Tag-Cooldown.Arcane.ArcaneShards
事件图表: 使用等级生成技能生成物数量,限制为MaxNumShards MaxNumShards min get ability level 提升为变量 NumShards
NumShards
生成水晶时使用 NumShards 替换 MaxNumShards NumShards
在生成第一个碎片时执行技能消耗 commit ability cost
激活技能时首先检查技能消耗 check ability cost 成功时才继续 branch
删除清除计时器后的延时节点delay 销毁技能生成物后提交技能冷却效果 commit ability cooldown
生成多个火球,冲向四周。
Source/Aura/Public/AbilitySystem/Abilities/AuraFireBlast.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/AuraDamageGameplayAbility.h"
#include "AuraFireBlast.generated.h"
UCLASS()
class AURA_API UAuraFireBlast : public UAuraDamageGameplayAbility
{
GENERATED_BODY()
public:
virtual FString GetDescription(int32 Level) override;
virtual FString GetNextLevelDescription(int32 Level) override;
protected:
UPROPERTY(EditDefaultsOnly, Category = "FireBlast")
int32 NumFireBalls = 12;
};
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBlast.cpp
#include "AbilitySystem/Abilities/AuraFireBlast.h"
FString UAuraFireBlast::GetDescription(int32 Level)
{
const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
const float ManaCost = FMath::Abs(GetManaCost(Level));
const float Cooldown = GetCooldown(Level);
return FString::Printf(TEXT(
// Title
"<Title>FIRE BLAST</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
// Number of Fire Balls
"<Default>Launches %d </>"
"<Default>fire balls in all directions, each coming back and </>"
"<Default>exploding upon return, causing </>"
// Damage
"<Damage>%d</><Default> radial fire damage with"
" a chance to burn</>"),
// Values
Level,
ManaCost,
Cooldown,
NumFireBalls,
ScaledDamage);
}
FString UAuraFireBlast::GetNextLevelDescription(int32 Level)
{
const int32 ScaledDamage = Damage.GetValueAtLevel(Level);
const float ManaCost = FMath::Abs(GetManaCost(Level));
const float Cooldown = GetCooldown(Level);
return FString::Printf(TEXT(
// Title
"<Title>NEXT LEVEL:</>\n\n"
// Level
"<Small>Level: </><Level>%d</>\n"
// ManaCost
"<Small>ManaCost: </><ManaCost>%.1f</>\n"
// Cooldown
"<Small>Cooldown: </><Cooldown>%.1f</>\n\n"
// Number of Fire Balls
"<Default>Launches %d </>"
"<Default>fire balls in all directions, each coming back and </>"
"<Default>exploding upon return, causing </>"
// Damage
"<Damage>%d</><Default> radial fire damage with"
" a chance to burn</>"),
// Values
Level,
ManaCost,
Cooldown,
NumFireBalls,
ScaledDamage);
}
Source/Aura/Public/AuraGameplayTags.h
// 游戏标签结构体 单例
struct FAuraGameplayTags
{
public:
FGameplayTag Abilities_Fire_FireBlast;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
GameplayTags.Abilities_Fire_FireBlast = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Fire.FireBlast"),
FString("FireBlast Ability Tag")
);
}
Content/Blueprints/AbilitySystem/Aura/Abilities/Fire/FireBlast/GA_FireBlast.uasset
配置伤害效果和技能标签
添加新技能信息
Content/Blueprints/UI/SpellMenu/WBP_OffensiveSpellTree.uasset
火焰系技能第二个图标
升级到2级可解锁技能 GA_FireBlast
1,15 40,150
自动平滑
Content/Blueprints/AbilitySystem/Aura/Abilities/Fire/FireBlast/GE_Cost_FireBlast.uasset
持续时间-Duration Policy-Instant 即时
GameplayEffect -Modifiers: attribute-AuraAttributeSet.Mana 表示释放该技能时,消耗 AuraAttributeSet.Mana 魔力属性的值
Modifier Op-Add
Modifier Magnitude-Magnitude calculation Type-Scalable Float Modifier Magnitude-Scalable Float Magnitude- 1,CT_SpellCost,FireBlast
事件图表: 提交消耗效果 event activateAbility commitAbilityCost
现在施放技能将会消耗魔力。
项目设置
Content/Blueprints/AbilitySystem/Aura/Abilities/Fire/FireBlast/GE_Cooldown_FireBlast.uasset
Gameplay Effect-Components-Target Tags(Granted to Actor)为Actor授予标签-Target Tags Gameplay Effect Component
-Add Tags- Cooldown.Fire.FireBlast
当把 GE_Cooldown_FireBlast设置为火球爆炸技能的技能冷却效果时, 游戏效果 GE_Cooldown_FireBlast 将为目标授予 Cooldown.Fire.FireBlast 冷却 效果标签。
细节-持续时间-Duration Policy-Has Duration 有持续时间 细节-持续时间-Duration Magnitude-Magnitude calculation Type-scalable float 细节-持续时间-Duration Magnitude-Scalable Float Magnitude-5【该效果持续5秒,然后自行消失】
事件图表: 提交消耗和冷却效果 commitAbility end ability
添加 Cooldown.Fire.FireBlast 冷却 效果标签
现在释放技能后,显示冷却倒计时。
GA_FireBlast 技能将生成多个火球,然后火球爆炸。
设置默认输入操作标签
现在,初始将激活 GA_FireBlast 技能
因为 GA_FireBlast 技能生成的火球 不会在命中目标时爆炸,而是生成后自动爆炸,需要重写重叠事件
Source/Aura/Public/Actor/AuraProjectile.h
protected:
// 投射物球体碰撞组件的重叠事件
UFUNCTION()
virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult);
这与火球术生成的火球不同,因为它具有额外的属性。
Source/Aura/Public/Actor/AuraFireBall.h
#pragma once
#include "CoreMinimal.h"
#include "Actor/AuraProjectile.h"
#include "AuraFireBall.generated.h"
UCLASS()
class AURA_API AAuraFireBall : public AAuraProjectile
{
GENERATED_BODY()
public:
protected:
virtual void BeginPlay() override;
virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;
};
Source/Aura/Private/Actor/AuraFireBall.cpp
#include "Actor/AuraFireBall.h"
void AAuraFireBall::BeginPlay()
{
Super::BeginPlay();
}
void AAuraFireBall::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
}
Source/Aura/Public/AbilitySystem/Abilities/AuraFireBlast.h
class AAuraFireBall;
public:
UFUNCTION(BlueprintCallable)
TArray<AAuraFireBall*> SpawnFireBalls();
private:
UPROPERTY(EditDefaultsOnly)
TSubclassOf<AAuraFireBall> FireBallClass;
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBlast.cpp
TArray<AAuraFireBall*> UAuraFireBlast::SpawnFireBalls()
{
return TArray<AAuraFireBall*>();
}
Content/Blueprints/AbilitySystem/Aura/Abilities/Fire/FireBlast/BP_FireBall.uasset
添加 Niagara 粒子系统组件重命名为 FireBallEffect
该火球不使用自带的移动组件来移动,由技能控制火球移动
所以将速度设为0
取消tick
取消自动激活
配置火球actor
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBlast.cpp
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
#include "Actor/AuraFireBall.h"
TArray<AAuraFireBall*> UAuraFireBlast::SpawnFireBalls()
{
TArray<AAuraFireBall*> FireBalls;
const FVector Forward = GetAvatarActorFromActorInfo()->GetActorForwardVector();
const FVector Location = GetAvatarActorFromActorInfo()->GetActorLocation();
TArray<FRotator> Rotators = UAuraAbilitySystemLibrary::EvenlySpacedRotators(
Forward, FVector::UpVector, 360.f, NumFireBalls);
for (const FRotator& Rotator : Rotators)
{
FTransform SpawnTransform;
SpawnTransform.SetLocation(Location);
SpawnTransform.SetRotation(Rotator.Quaternion());
AAuraFireBall* FireBall = GetWorld()->SpawnActorDeferred<AAuraFireBall>(
FireBallClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
CurrentActorInfo->PlayerController->GetPawn(),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
FireBall->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
FireBalls.Add(FireBall);
FireBall->FinishSpawning(SpawnTransform);
}
return FireBalls;
}
事件图表 SpawnFireBalls
现在施放火球爆炸技能将绕垂直地面360度均匀生成12个火球,发射到四周
通过时间线,使火球飞出去,减速,再飞回来,飞到玩家的位置,且定位到玩家的新位置。
Source/Aura/Public/Actor/AuraFireBall.h
public:
UFUNCTION(BlueprintImplementableEvent)
void StartOutgoingTimeline();
//火球开始飞出时,保存玩家actor 在飞回时使用其位置
UPROPERTY(BlueprintReadOnly)
TObjectPtr<AActor> ReturnToActor;
Source/Aura/Private/Actor/AuraFireBall.cpp
void AAuraFireBall::BeginPlay()
{
Super::BeginPlay();
StartOutgoingTimeline();
}
Source/Aura/Public/Actor/AuraProjectile.h
protected:
UFUNCTION(BlueprintCallable)
void OnHit();
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBlast.cpp
TArray<AAuraFireBall*> UAuraFireBlast::SpawnFireBalls()
{
TArray<AAuraFireBall*> FireBalls;
const FVector Forward = GetAvatarActorFromActorInfo()->GetActorForwardVector();
const FVector Location = GetAvatarActorFromActorInfo()->GetActorLocation();
TArray<FRotator> Rotators = UAuraAbilitySystemLibrary::EvenlySpacedRotators(
Forward, FVector::UpVector, 360.f, NumFireBalls);
for (const FRotator& Rotator : Rotators)
{
FTransform SpawnTransform;
SpawnTransform.SetLocation(Location);
SpawnTransform.SetRotation(Rotator.Quaternion());
AAuraFireBall* FireBall = GetWorld()->SpawnActorDeferred<AAuraFireBall>(
FireBallClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
CurrentActorInfo->PlayerController->GetPawn(),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
FireBall->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
//火球开始飞出时,保存玩家actor 在飞回时使用其位置
FireBall->ReturnToActor = GetAvatarActorFromActorInfo();
FireBalls.Add(FireBall);
FireBall->FinishSpawning(SpawnTransform);
}
return FireBalls;
}
禁用移动组件的速度,tick,自动激活,防止火球通过移动组件自动运动。
事件图表:
event StartOutgoingTimeline 控制火球在服务器上的位置
必须有权限 switch has authority
初始位置 get actor location 提升为变量 InitialLocation
初始顶点【最高位置】 Get Actor Forward Vector 前向单位向量
multiply -b引脚转换为浮点单精度类型-提升为变量 TravelDistance 表示向前飞出距离 默认值1600
飞出时间轴 add timeline :OutgoingTimeline
OutgoingTimeline时间轴: 添加浮点轨道 OutgoingTrack 飞出时间 1 秒 0,0 1,1
自动平滑
获取初始位置和飞出去目标之间的向量 InitialLocation ApexLocation lerp(Vector) alpha =0 使用了a初始位置 alpha=1 使用了b 飞出去的目标位置
lerp(Vector) alpha 使用OutgoingTimeline 时间轴 的 轨道 OutgoingTrack 的值
set actor location 位置为初始位置和飞出去的顶点位置,通过时间轴插值 使用lerp返回值作为新位置
现在施放火球爆炸技能,火球将360的方向向四周先加速后减速【时间轴控制速度】飞出1600cm然后停止。然后等待生命周期结束后销毁。
飞出后完成,开始返回时间轴 add timeline :ReturningTimeline
添加浮点轨道ReturningTrack 飞回时间 1 秒 0,0 1,1
自动平滑 先减速再加速
set actor location
lerp(Vector)
lerp(Vector)-A- ApexLocation 初始位置 火球飞出后的实时新位置 lerp(Vector)-B- ReturnToActor-get actor location 玩家位置
lerp(Vector) alpha =0 使用了a ApexLocation火球飞出后的位置 alpha=1 使用了b 玩家位置
lerp(Vector) alpha 使用ReturningTimeline 时间轴 的 轨道 ReturningTrack 的值
branch 飞回后距离玩家一定距离火球爆炸
添加浮点变量 ExplodeDistance 默认250
ReturnToActor-get actor location 玩家位置
vector length <= ExplodeDistance 作为 branch 条件
折叠到函数 isWithinExplodeDistance 设为纯函数
爆炸 on hit destroy actor
施放火球爆炸技能后,火球向四周飞出1600cm,然后飞回玩家的实时位置250cm处发生爆炸,然后销毁火球。
1,15 40,150 自动平滑
Source/Aura/Public/Actor/AuraProjectile.h
protected:
bool IsValidOverlap(AActor* OtherActor);
bool bHit = false; // 是否命中,更换为保护类型
Source/Aura/Private/Actor/AuraProjectile.cpp
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
if (!IsValidOverlap(OtherActor)) return;
if (!bHit) OnHit();
if (HasAuthority())
{
// 只在服务器上应用伤害效果即可
// 伤害效果会改变游戏属性,而游戏属性作为变量会从服务端复制到客户端
// 从投射物的重叠目标,例如敌人上获取技能系统组件,为敌人的技能系统组件应用伤害效果
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
// 死亡冲击向量
const FVector DeathImpulse = GetActorForwardVector() * DamageEffectParams.DeathImpulseMagnitude;
DamageEffectParams.DeathImpulse = DeathImpulse;
// 击退
const bool bKnockback = FMath::RandRange(1, 100) < DamageEffectParams.KnockbackChance;
if (bKnockback)
{
FRotator Rotation = GetActorRotation();
// 向上旋转45度
Rotation.Pitch = 45.f;
const FVector KnockbackDirection = Rotation.Vector();
const FVector KnockbackForce = KnockbackDirection * DamageEffectParams.KnockbackForceMagnitude;
DamageEffectParams.KnockbackForce = KnockbackForce;
}
DamageEffectParams.TargetAbilitySystemComponent = TargetASC;
UAuraAbilitySystemLibrary::ApplyDamageEffect(DamageEffectParams);
}
// 如果在服务器端,销毁自身 /销毁投射物
Destroy();
}
// 如果在客户端 设置撞击标志为真,此时可以在音效,特效播放完后销毁自身Destroyed
else bHit = true;
}
bool AAuraProjectile::IsValidOverlap(AActor* OtherActor)
{
if (DamageEffectParams.SourceAbilitySystemComponent == nullptr) return false;
AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();
if (SourceAvatarActor == OtherActor) return false;
// 技能抛射物不能伤害友军
if (!UAuraAbilitySystemLibrary::IsNotFriend(SourceAvatarActor, OtherActor)) return false;
return true;
}
Source/Aura/Private/Actor/AuraFireBall.cpp
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
void AAuraFireBall::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
if (!IsValidOverlap(OtherActor)) return;
if (HasAuthority())
{
if (UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
const FVector DeathImpulse = GetActorForwardVector() * DamageEffectParams.DeathImpulseMagnitude;
DamageEffectParams.DeathImpulse = DeathImpulse;
DamageEffectParams.TargetAbilitySystemComponent = TargetASC;
UAuraAbilitySystemLibrary::ApplyDamageEffect(DamageEffectParams);
}
}
}
现在火球爆炸技能可以造成伤害
BP_FireBall 火球返回后爆炸时再触发单独的径向伤害
Source/Aura/Public/Actor/AuraFireBall.h
public:
//火球爆炸伤害参数
UPROPERTY(BlueprintReadWrite)
FDamageEffectParams ExplosionDamageParams;
Source/Aura/Public/AbilitySystem/AuraAbilitySystemLibrary.h
#include "AbilitySystemComponent.h"
public:
/*
* Damage Effect Params 伤害效果参数
*/
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|DamageEffect")
static void SetIsRadialDamageEffectParam(UPARAM(ref) FDamageEffectParams& DamageEffectParams, bool bIsRadial, float InnerRadius, float OuterRadius, FVector Origin);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|DamageEffect")
static void SetKnockbackDirection(UPARAM(ref) FDamageEffectParams& DamageEffectParams, FVector KnockbackDirection, float Magnitude = 0.f);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|DamageEffect")
static void SetDeathImpulseDirection(UPARAM(ref) FDamageEffectParams& DamageEffectParams, FVector ImpulseDirection, float Magnitude = 0.f);
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|DamageEffect")
static void SetTargetEffectParamsASC(UPARAM(ref) FDamageEffectParams& DamageEffectParams, UAbilitySystemComponent* InASC);
Source/Aura/Private/AbilitySystem/AuraAbilitySystemLibrary.cpp
void UAuraAbilitySystemLibrary::SetIsRadialDamageEffectParam(FDamageEffectParams& DamageEffectParams, bool bIsRadial, float InnerRadius, float OuterRadius, FVector Origin)
{
DamageEffectParams.bIsRadialDamage = bIsRadial;
DamageEffectParams.RadialDamageInnerRadius = InnerRadius;
DamageEffectParams.RadialDamageOuterRadius = OuterRadius;
DamageEffectParams.RadialDamageOrigin = Origin;
}
void UAuraAbilitySystemLibrary::SetKnockbackDirection(FDamageEffectParams& DamageEffectParams, FVector KnockbackDirection, float Magnitude)
{
KnockbackDirection.Normalize();
if (Magnitude == 0.f)
{
DamageEffectParams.KnockbackForce = KnockbackDirection * DamageEffectParams.KnockbackForceMagnitude;
}
else
{
DamageEffectParams.KnockbackForce = KnockbackDirection * Magnitude;
}
}
void UAuraAbilitySystemLibrary::SetDeathImpulseDirection(FDamageEffectParams& DamageEffectParams, FVector ImpulseDirection, float Magnitude)
{
ImpulseDirection.Normalize();
if (Magnitude == 0.f)
{
DamageEffectParams.DeathImpulse = ImpulseDirection * DamageEffectParams.DeathImpulseMagnitude;
}
else
{
DamageEffectParams.DeathImpulse = ImpulseDirection * Magnitude;
}
}
void UAuraAbilitySystemLibrary::SetTargetEffectParamsASC(FDamageEffectParams& DamageEffectParams,
UAbilitySystemComponent* InASC)
{
DamageEffectParams.TargetAbilitySystemComponent = InASC;
}
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBlast.cpp
TArray<AAuraFireBall*> UAuraFireBlast::SpawnFireBalls()
{
TArray<AAuraFireBall*> FireBalls;
const FVector Forward = GetAvatarActorFromActorInfo()->GetActorForwardVector();
const FVector Location = GetAvatarActorFromActorInfo()->GetActorLocation();
TArray<FRotator> Rotators = UAuraAbilitySystemLibrary::EvenlySpacedRotators(
Forward, FVector::UpVector, 360.f, NumFireBalls);
for (const FRotator& Rotator : Rotators)
{
FTransform SpawnTransform;
SpawnTransform.SetLocation(Location);
SpawnTransform.SetRotation(Rotator.Quaternion());
AAuraFireBall* FireBall = GetWorld()->SpawnActorDeferred<AAuraFireBall>(
FireBallClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
CurrentActorInfo->PlayerController->GetPawn(),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
FireBall->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
//火球开始飞出时,保存玩家actor 在飞回时使用其位置
FireBall->ReturnToActor = GetAvatarActorFromActorInfo();
// 火球返回后爆炸时再触发单独的径向伤害
FireBall->ExplosionDamageParams = MakeDamageEffectParamsFromClassDefaults();
FireBall->SetOwner(GetAvatarActorFromActorInfo());
FireBalls.Add(FireBall);
FireBall->FinishSpawning(SpawnTransform);
}
return FireBalls;
}
事件图表: 用以测试 ExplosionDamageParams break DamageEffectParam
SetIsRadialDamageEffectParam-isRadial-true branch 检查 break DamageEffectParam-isRadialDamage print string-isRadialDamage
每个火球爆炸时显示调试信息
删除节点
命中后设置径向伤害参数 SetIsRadialDamageEffectParam 设置内外半径 50,300 原点为火球位置 get actor location
SetIsRadialDamageEffectParam-OuterRadius 使用之后的 RadialDamageOuterRadius SetIsRadialDamageEffectParam-InnerRadius 提升为变量 RadialDamageInnerRadius
获取半径内存活的玩家 Get Live Players Within Radius-提升为变量 OverlappingPlayers self Get Live Players Within Radius-Radius 参数提升为变量 RadialDamageOuterRadius 默认300 表示寻找半径内的目标
忽略玩家 get owner 数组-add self 忽略火球自身
get actor location 火球位置做为原点
OverlappingPlayers for each loop 循环为火球的每个重叠命中目标设置伤害效果参数
将火球的目标受害者的技能系统组件存储到伤害效果参数中供之后使用 Get Ability System Component ExplosionDamageParams SetTargetEffectParamsASC
火球的位置 get actor location
相减得出得到向量长度提升为变量 KnockbackDirection
改变击退方向 KnockbackDirection Make Rot from X make rotator-y-45 get forward vector
get forward vector输出至 SetKnockbackDirection -KnockbackDirection
折叠到函数 OverrideKnockbackPitch 设为纯函数
ExplosionDamageParams SetKnockbackDirection SetKnockbackDirection-Magnitude-提升为变量 KnockbackMagnitude 默认800 KnockbackDirection
ExplosionDamageParams SetDeathImpulseDirection SetDeathImpulseDirection-Magnitude-提升为变量 DeathImpulseMagnitude 默认600 KnockbackDirection
ExplosionDamageParams apply damage effect
循环完成后销毁火球
现在,火球爆炸技能,飞出后返回,返回后爆炸,如果爆炸半径内有敌人,将再次造成伤害和击退,死亡效果。
在技能冷却期间,替换技能操作栏的技能,技能图标替换后,原位置的技能倒计时动画依然在进行。 且冷却完成后,技能图标成为默认的白色纹理。
ReceiveAbilityInfo 函数图表:
检测收到的技能标签,失败时清除冷却倒计时
CooldownTimerHandle Clear and Invalidate Timer by Handle
清除倒计时文本 Text_Cooldown Set Render opacity
默认的抛射物命中目标播放的粒子和音效都是普通效果,没有使用游戏cue复制。
游戏cue通过多播每帧可复制的限制当前为10个。 但是火球爆炸为12个火球,12个游戏cue效果
使用游戏性cue但是不通过RPC来网络复制,其他客户端将使用预测来显示本客户端的粒子和音效。
这可以极大优化性能。
Source/Aura/Public/AuraGameplayTags.h
// 游戏标签结构体 单例
struct FAuraGameplayTags
{
public:
FGameplayTag GameplayCue_FireBlast;
Source/Aura/Private/AuraGameplayTags.cpp
// 初始化游戏标签
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
......
/*
* GameplayCues
*/
GameplayTags.GameplayCue_FireBlast = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("GameplayCue.FireBlast"),
FString("FireBlast GameplayCue Tag")
);
}
Source/Aura/Private/AbilitySystem/Abilities/AuraFireBlast.cpp
TArray<AAuraFireBall*> UAuraFireBlast::SpawnFireBalls()
{
TArray<AAuraFireBall*> FireBalls;
const FVector Forward = GetAvatarActorFromActorInfo()->GetActorForwardVector();
const FVector Location = GetAvatarActorFromActorInfo()->GetActorLocation();
TArray<FRotator> Rotators = UAuraAbilitySystemLibrary::EvenlySpacedRotators(
Forward, FVector::UpVector, 360.f, NumFireBalls);
for (const FRotator& Rotator : Rotators)
{
FTransform SpawnTransform;
SpawnTransform.SetLocation(Location);
SpawnTransform.SetRotation(Rotator.Quaternion());
AAuraFireBall* FireBall = GetWorld()->SpawnActorDeferred<AAuraFireBall>(
FireBallClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
CurrentActorInfo->PlayerController->GetPawn(),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
FireBall->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
//火球开始飞出时,保存玩家actor 在飞回时使用其位置
FireBall->ReturnToActor = GetAvatarActorFromActorInfo();
// 设置火球所有者 使其跟随火球,网络复制
FireBall->SetOwner(GetAvatarActorFromActorInfo());
// 火球返回后爆炸时再触发单独的径向伤害
FireBall->ExplosionDamageParams = MakeDamageEffectParamsFromClassDefaults();
FireBall->SetOwner(GetAvatarActorFromActorInfo());
FireBalls.Add(FireBall);
FireBall->FinishSpawning(SpawnTransform);
}
return FireBalls;
}
音效组件设为保护类型用以子类使用
Source/Aura/Public/Actor/AuraProjectile.h
protected:
UFUNCTION(BlueprintCallable)
virtual void OnHit();
UPROPERTY()
TObjectPtr<UAudioComponent> LoopingSoundComponent;
Source/Aura/Public/Actor/AuraFireBall.h
protected:
virtual void OnHit() override;
Source/Aura/Private/Actor/AuraFireBall.cpp
#include "Components/AudioComponent.h"
#include "GameplayCueManager.h"
#include "AuraGameplayTags.h"
void AAuraFireBall::OnHit()
{
if (GetOwner())
{
FGameplayCueParameters CueParams;
CueParams.Location = GetActorLocation();
UGameplayCueManager::ExecuteGameplayCue_NonReplicated(
GetOwner(), FAuraGameplayTags::Get().GameplayCue_FireBlast, CueParams);
}
if (LoopingSoundComponent)
{
LoopingSoundComponent->Stop();
LoopingSoundComponent->DestroyComponent();
}
bHit = true;
}
Content/Blueprints/AbilitySystem/GameplayCueNotifies/GC_FireBlast.uasset
设置标签
设置粒子和音效
1-技能的主要属性:str,int,dex等。 技能系统组件本身具由技能相关的数据。 通过游戏标签识别技能。所以通过保存技能的游戏标签来保存技能。 也需要保存技能的等级,所以需要数据结构来保存技能的相关信息。
2-玩家状态:关卡,技能点,属性点,经验值
3-玩家位置,玩家所在关卡,玩家职业,与玩家关联的其他编号,id。
1-保存到磁盘,即玩家的实际机器。适合单人游戏,更简单。不需要数据库。 但重要内容需要保存到数据库,例如购买,排行榜。
2-保存到云,数据库,需要通过网络传输数据。适合多人游戏。 可以加密敏感信息。 虽然加密可以破解,但仍具由一定程度的安全性。
现代专用服务器使用数据库保存。 专用服务器没有玩家。 玩家从其他地方连接到专用服务器。服务器连接到数据库,为玩家准备好信息。 为了防止作弊,服务器对数据拥有权限。服务器负责保存数据和加载数据后发送到客户端。 这需要通过api连接到数据库。
但记住密码功能,需要讲用户名和密码保存到本地磁盘。
但会设计以替换为保存到数据库。 只需要添加如何连接数据库和专用服务器功能即可。
保存数据从加载屏幕开始 保存数据之前,需要菜单。
打开 MainMenu 删除玩家,敌人,导航NavMeshBoundsVolume,边界BlockingVolume
但此时可以控制镜头四处移动,该问题需要稍后禁用输入操作解决。
用以决定摄像机默认观看位置
基于 Actor 创建特殊角色蓝图
Content/Blueprints/Character/Aura/BP_Aura_MainMenu.uasset
AuraMesh-骨骼网格体资产-SKM_Aura
AuraMesh-动画模式-使用动画资产 AuraMesh-要播放的动画-AuraPose 动画序列
Fireball -Niagara 系统资产-NS_Fire_4 Niagara系统 调整Niagara火到人物右手位置
Staff-骨骼网格体资产-SKM_Staff 骨骼网格体 Staff-插槽-父项套接字-WeaponHandSocket 法杖附加到左手
通过改变火球的位置。
事件图表: event tick
正选函数需要增量运行时间 Delta Seconds + Time 的值 提升为变量 Time Time 将不断累加增加时间。
正弦函数的周期为 2派,2个圆周率,3.14 * 2 每当时间运行到 6.28318时,将 Time重置为0. 防止Time 不断增加导致数值溢出
6.28318 branch set time=0
折叠为函数 UpdateTime
Time math 数学-trig三角-Sin(Radians) 正弦弧度 可以带来周期行为 print string Time 正弦值: 0 1 0 -1 0 周期变化,每个周期都会重置时间time为0
扩展 Time Time 正弦后 乘以一个振幅 Amplitude 浮点 默认10 multiply
现在正弦值将整体扩大10倍:0 10 0 -10 0
用该值设置火球的位置 Z值 event beginplay
Fireball get world location 提升为变量 FireballInitialLocation 存储火球初始位置
event tick
然后每一帧为其位置 z 增加 Time
FireballInitialLocation
add 正弦值
正弦逻辑折叠为纯函数 SinTime
相加后的值设置为火球世界位置 Fireball set world location
现在火球开始上下浮动
调整振幅 Amplitude 改变上下移动的距离。
复制 MI_MagicCircle_1 为 MI_MagicCircle_MainMenu
贴花材质-MI_MagicCircle_MainMenu
移动贴花使其可以贴在角色后的墙面
事件图表 event tick 每一帧添加局部旋转
MagicCircleDecal Add Local Rotation
添加变量 MagicCircleRotationRate 浮点 旋转速率 默认-2 获取世界时间增量改变贴花的 x Roll get world delta seconds Multiply MagicCircleRotationRate
F11 预览
Content/Blueprints/MainMenu/WBP_Title.uasset
覆层 所需
文本 主标题
文本 副标题 下对齐
不在骨骼子级,防止控件随骨骼动画移动
TitleWidget-用户界面-控件类-WBP_Title TitleWidget-用户界面-空间-屏幕 TitleWidget-用户界面-以所需大小绘制-启用 防止控件大小错误
运行时才可以看见该控件
TitleWidget-用户界面-空间-场景 使用世界坐标 角色可以阻挡控件
旋转调整 TitleWidget
TitleWidget-用户界面-几何体模式-圆柱体 控件将弯曲 TitleWidget-用户界面-圆柱体弧形角度 可以调整弯曲度
使用 BP_Aura_MainMenu actor 来添加按钮和音效
事件图表
event beginplay play sound 2d-Music_MainMenu 音波
Content/Blueprints/UI/MainMenu/WBP_MainMenu.uasset
仅作基础控件,所以使用 canvas 画布画板
canvas 画布画板
WBP Wide Button 开始 调整位置 锚点居中下
WBP Wide Button 退出 调整位置 锚点居中下
事件图表
event beginplay create widget-WBP_MainMenu add to viewport
输入模式设置为仅UI 将WBP_MainMenu设置为焦点 get player controller set input mode UI only 显示鼠标 set show mouse cursor
复制 MainMenu 关卡为 LoadMenu
LoadMenu 删除 BP_Aura_MainMenu
event construct Button_Quit get button assign on clicked Quit Game
Button_Play get button assign on clicked 打开下一个关卡去加载数据并开始新游戏 按对象引用打开关卡 open level (by object reference) 将该关卡 open level (by object reference) -level 提升为变量 LoadMenu 默认关卡值 LoadMenu
项目-地图和模式-默认地图-游戏默认地图-MainMenu
可以在三种模式之间切换的控件:空插槽,已占用插槽,创建新游戏插槽。
Content/Blueprints/UI/LoadMenu/WBP_LoadSlot_Vacant.uasset
尺寸框 所需 宽高 256 300 SizeBox_Root
覆层 Overlay_root
image Image_Background 填充-1 背景-MI_FlowingUIBG
image Image_Border 背景 Border_Large 纹理 绘制为-边界 边缘-0.5
Content/Blueprints/UI/LoadMenu/WBP_LoadSlot_EnterName.uasset
Content/Blueprints/UI/LoadMenu/WBP_LoadSlot_Taken.uasset
点击 空插槽 WBP_LoadSlot_Vacant 进入 输入名称插槽 Enter Name Load Slot
选择 已占用插槽 WBP_LoadSlot_EnterName 进入游戏
Content/Blueprints/UI/LoadMenu/WBP_LoadMenu.uasset
设计器: 控件切换器
WBP_LoadSlot_Taken WBP_LoadSlot_EnterName WBP_LoadSlot_Vacant
图表: event construct Button_Quit get button assign on clicked Quit Game open level (by object reference)-level 提升为变量 默认 MainMenu
event beginplay create widget-WBP_LoadMenu add to viewport
get player controller set input mode UI only 显示鼠标 set show mouse cursor
根据选择的插槽的不同,加载对应数据和地图..
1-截至目前,项目使用了MVC模型视图控制器架构。即控件+控件控制器+数据。 控件控制器从中获取数据。 控件控制器依赖模型。它引用了这些类【系统,技能系统组件,属性集类】
模型不依赖控件控制器。 控件控制器直接将数据广播到视图。 视图由控件组成。
2-但是虚幻新增了更强大的插件 ViewModel 模型视图。 https://russelleast.wordpress.com/2008/08/09/overview-of-the-modelview-viewmodel-mvvm-pattern-and-data-binding/ 模型:系统,技能系统组件,属性集
与MVC相似,模型视图充当模型和视图。 不同点:通常视图模型和视图是1对1的关系,两者直接绑定。【也可以多个视图对应1个视图模型】
视图模型类似控件控制器。 视图模型链接到视图,并绑定视图中的变量。 视图具由向用户显示内容的变量。 事件驱动。 视图模型作为容器,保存了视图的数据和功能。
之后将视图模型绑定到加载菜单控件。
如果控件绑定的视图模型类型为手动,表示高视图模型是在控件外部的某处创建。
每个控件至少要和视图模型上的一个通知变量绑定。否则控件的视图模型无效。
spawn sound 2d return提升为变量 MainMenuMusic后使用音频
由于没有可聚焦控件,删除 set input mode UI only 的 输入参数 in widget to focus 留空
不再使用系统的控件切换器。
使用自定义切换控件。使其与插槽对应
Content/Blueprints/UI/LoadMenu/WBP_LoadSlot_WidgetSwitcher.uasset
控件切换器 所需 WBP_LoadSlot_Vacant WBP_LoadSlot_EnterName WBP_LoadSlot_Taken
WBP_LoadSlot_WidgetSwitcher 尺寸 256*300 【因为 WBP_LoadSlot_WidgetSwitcher是所需】
每个 WBP_LoadSlot_WidgetSwitcher 都有自己的ViewModel
将使用HUD控制。 在C++中创建他的视图模型。控制控件创建时机。 在控件获取自己的视图模型时,确保视图模型已创建好。
需要视图模型。 需要用于加载屏幕的游戏模式和HUD。
Content/Blueprints/Game/BP_LoadScreenGameMode.uasset
现在游戏模式 BP_LoadScreenGameMode可以控制HUD生成。 HUD可以创建,控制控件。 HUD可以创建,控制视图模型。
这是很好的关注点分离模式。
Source/Aura/Public/UI/HUD/LoadScreenHUD.h
作为加载屏幕控件的视图模型
Source/Aura/Public/UI/ViewModel/MVVM_LoadScreen.h
为其提供功能。
Source/Aura/Public/UI/Widget/LoadScreenWidget.h
Content/Blueprints/UI/HUD/BP_LoadScreenHUD.uasset
现在进入 LoadMenu 关卡将自动加载 BP_LoadScreenHUD
现在可以在 BP_LoadScreenHUD 执行操作,创建控件,将控件添加到视图。设置变量,模型视图。关联。
在 BP_LoadScreenHUD 中创建控件。 需要控件C++类。
Content/Blueprints/UI/LoadMenu/WBP_LoadScreenWidget_Base.uasset
这将作为其他控件的基类蓝图,为所有控件提供C++中的工具
将 LoadMenu 文件下的所有控件的父类设置为 WBP_LoadScreenWidget_Base
包括: WBP_LoadMenu WBP_LoadSlot_EnterName WBP_LoadSlot_Taken WBP_LoadSlot_Vacant WBP_LoadSlot_WidgetSwitcher
Source/Aura/Public/UI/HUD/LoadScreenHUD.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "LoadScreenHUD.generated.h"
class ULoadScreenWidget;
UCLASS()
class AURA_API ALoadScreenHUD : public AHUD
{
GENERATED_BODY()
public:
// 控件类
UPROPERTY(EditDefaultsOnly)
TSubclassOf<UUserWidget> LoadScreenWidgetClass;
// 控件类实例指针
UPROPERTY(BlueprintReadOnly)
TObjectPtr<ULoadScreenWidget> LoadScreenWidget;
protected:
virtual void BeginPlay() override;
};
Source/Aura/Private/UI/HUD/LoadScreenHUD.cpp
#include "UI/HUD/LoadScreenHUD.h"
#include "Blueprint/UserWidget.h"
#include "UI/ViewModel/MVVM_LoadScreen.h"
#include "UI/Widget/LoadScreenWidget.h"
void ALoadScreenHUD::BeginPlay()
{
Super::BeginPlay();
LoadScreenWidget = CreateWidget<ULoadScreenWidget>(GetWorld(), LoadScreenWidgetClass);
LoadScreenWidget->AddToViewport();
}
现在打开关卡 LoadMenu 将显示 WBP_LoadMenu 控件
类似于控件控制器的功能,但有不同之处。
Content/Blueprints/UI/ViewModel/BP_LoadScreenViewModel.uasset
蓝图版本视图模型也可以添加变量,函数
Source/Aura/Public/UI/HUD/LoadScreenHUD.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "LoadScreenHUD.generated.h"
class UMVVM_LoadScreen;
class ULoadScreenWidget;
UCLASS()
class AURA_API ALoadScreenHUD : public AHUD
{
GENERATED_BODY()
public:
// 控件类
UPROPERTY(EditDefaultsOnly)
TSubclassOf<UUserWidget> LoadScreenWidgetClass;
// 控件类实例指针
UPROPERTY(BlueprintReadOnly)
TObjectPtr<ULoadScreenWidget> LoadScreenWidget;
//视图模型类
UPROPERTY(EditDefaultsOnly)
TSubclassOf<UMVVM_LoadScreen> LoadScreenViewModelClass;
// 视图模型实例指针
UPROPERTY(BlueprintReadOnly)
TObjectPtr<UMVVM_LoadScreen> LoadScreenViewModel;
protected:
virtual void BeginPlay() override;
};
Source/Aura/Private/UI/HUD/LoadScreenHUD.cpp
#include "UI/HUD/LoadScreenHUD.h"
#include "Blueprint/UserWidget.h"
#include "UI/ViewModel/MVVM_LoadScreen.h"
#include "UI/Widget/LoadScreenWidget.h"
void ALoadScreenHUD::BeginPlay()
{
Super::BeginPlay();
// 控件的视图模型的创建在 控件添加到视图之前进行
LoadScreenViewModel = NewObject<UMVVM_LoadScreen>(this, LoadScreenViewModelClass);
LoadScreenWidget = CreateWidget<ULoadScreenWidget>(GetWorld(), LoadScreenWidgetClass);
LoadScreenWidget->AddToViewport();
}
现在,打开 关卡 LoadMenu ,HUD构建了视图模型,然后构建了控件,添加到视图。
可以添加仅用于绑定的测试通知字段 Bind
Source/Aura/Public/UI/ViewModel/MVVM_LoadScreen.h
#pragma once
#include "CoreMinimal.h"
#include "MVVMViewModelBase.h"
#include "MVVM_LoadScreen.generated.h"
UCLASS()
class AURA_API UMVVM_LoadScreen : public UMVVMViewModelBase
{
GENERATED_BODY()
public:
void SetBind(FString NewBind);
FString GetBind() const;
private:
// 视图模型添加至少一个通知字段,用以绑定到控件 ,Bind没有任何实际业务逻辑意义
UPROPERTY(EditAnywhere, BlueprintReadWrite, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess="true"))
FString Bind;
};
Source/Aura/Private/UI/ViewModel/MVVM_LoadScreen.cpp
#include "UI/ViewModel/MVVM_LoadScreen.h"
void UMVVM_LoadScreen::SetBind(FString NewBind)
{
UE_MVVM_SET_PROPERTY_VALUE(Bind, NewBind);
}
FString UMVVM_LoadScreen::GetBind() const
{
return Bind;
}
需要像控件控制器一样,可以在控件中,通过技能系统蓝图库,直接获取控件控制器。 视图模型通过 控件-设计器-窗口-视图模型 来关联视图模型 关联视图模型后需要重启视图模型窗口使其生效。
窗口-视图绑定-添加控件 Text_Bind
手动 -创建控件后手动为控件创建视图模型。
创建实例-每个控件创建独立的同名视图模型,不与其他控件共享视图模型的数据。
全局-全局唯一
属性路径-添加一个函数名称,控件可以调用该函数来获取模型视图
WBP_LoadMenu 使用 属性路径 FindLoadScreenViewModel 函数 来获取视图模型。 控件构造成功后,控件将自动调用控件的 FindLoadScreenViewModel 函数来获取视图模型。
将用于获取视图模型的函数定义到 WBP_LoadScreenWidget_Base 基类蓝图中,非HUD。 因为 WBP_LoadScreenWidget_Base 是加载菜单地图所有控件的基类控件。 这样所有子类都可以使用其中的函数获取视图模型来绑定到自身控件。
图表: 添加函数 FindLoadScreenViewModel 获取视图模型 该函数将返回视图模型。
所以添加输出 ,类型为 BP_LoadScreenViewModel 对象引用
通过玩家控制器获取当前HUD,再转为 LoadScreenHUD,当前地图的游戏模式配置为LoadScreenHUD,所以转换会成功,当前 LoadScreenHUD 上已有视图模型指针 LoadScreenViewModel。LoadScreenViewModel 为蓝图只读属性 UPROPERTY(BlueprintReadOnly),可以通过get 获取。再转为 当前HUD配置的 BP_LoadScreenViewModel。当前 LoadScreenHUD 已配置 BP_LoadScreenViewModel。所以转换会成功。
get player controller get HUD cast to LoadScreenHUD get LoadScreenViewModel cast to BP_LoadScreenViewModel
FindLoadScreenViewModel 函数需要设为常量 输出名为 ViewModel
图表: get BP_LoadScreenViewModel 已可以使用
现在 基于 WBP_LoadScreenWidget_Base 的控件和视图模型BP_LoadScreenViewModel已经关联。
示例代码为Login模块,仅供参考 const 常量函数表示蓝图中不需要引脚,直接调用
Source/Ro2ea/Public/UI/Widget/LoginWidget.h
class UMVVM_Login;
public:
UFUNCTION(BlueprintCallable)
UMVVM_Login* FindLoginViewModel() const;
Source/Ro2ea/Private/UI/Widget/LoginWidget.cpp
#include "UI/Widget/LoginWidget.h"
#include "Kismet/GameplayStatics.h"
#include "UI/HUD/LoginHUD.h"
UMVVM_Login* ULoginWidget::FindLoginViewModel() const
{
const APlayerController* PlayController = UGameplayStatics::GetPlayerController(this, 0);
AHUD* HUD = PlayController->GetHUD();
const ALoginHUD* LoginHUD = Cast<ALoginHUD>(HUD);
UMVVM_Login* LoginViewModel = LoginHUD->LoginViewModel;
return LoginViewModel;
}
WBP_LoadMenu 控件通过窗口关联了视图模型。
WBP_LoadMenu 中的 3个插槽控件也需要关联视图模型。 每个插槽控件实例需要获取独立的视图模型。因为他们的数据互相独立。 每个插槽都可以创建,编辑,进入游戏。可以独立保存各自的游戏状态数据。
这需要创建新的视图模型基类
在创建加载屏幕的视图模型后,创建 3个插槽的视图模型。 加载屏幕的视图模型将管理3个插槽的视图模型。
Source/Aura/Public/UI/ViewModel/MVVM_LoadSlot.h
#pragma once
#include "CoreMinimal.h"
#include "MVVMViewModelBase.h"
#include "MVVM_LoadSlot.generated.h"
UCLASS()
class AURA_API UMVVM_LoadSlot : public UMVVMViewModelBase
{
GENERATED_BODY()
};
Source/Aura/Private/UI/ViewModel/MVVM_LoadSlot.cpp
#include "UI/ViewModel/MVVM_LoadSlot.h"
Source/Aura/Public/UI/ViewModel/MVVM_LoadScreen.h
#pragma once
#include "CoreMinimal.h"
#include "MVVMViewModelBase.h"
#include "MVVM_LoadScreen.generated.h"
class UMVVM_LoadSlot;
UCLASS()
class AURA_API UMVVM_LoadScreen : public UMVVMViewModelBase
{
GENERATED_BODY()
public:
void SetBind(FString NewBind);
FString GetBind() const;
// 构造,初始化3个插槽的视图模型,并添加到map
void InitializeLoadSlots();
// 插槽视图模型类
UPROPERTY(EditDefaultsOnly)
TSubclassOf<UMVVM_LoadSlot> LoadSlotViewModelClass;
//根据索引获取插槽的视图模型
UFUNCTION(BlueprintPure)
UMVVM_LoadSlot* GetLoadSlotViewModelByIndex(int32 Index) const;
private:
// 视图模型添加至少一个通知字段,用以绑定到控件 ,Bind没有任何实际业务逻辑意义
UPROPERTY(EditAnywhere, BlueprintReadWrite, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess="true"))
FString Bind;
//3个插槽的 视图模型实例指针map,用以跟踪所有插槽视图模型,防止被垃圾收集
UPROPERTY()
TMap<int32, UMVVM_LoadSlot*> LoadSlots;
//插槽0 视图模型实例指针
UPROPERTY()
TObjectPtr<UMVVM_LoadSlot> LoadSlot_0;
UPROPERTY()
TObjectPtr<UMVVM_LoadSlot> LoadSlot_1;
UPROPERTY()
TObjectPtr<UMVVM_LoadSlot> LoadSlot_2;
};
Source/Aura/Private/UI/ViewModel/MVVM_LoadScreen.cpp
#include "UI/ViewModel/MVVM_LoadScreen.h"
#include "UI/ViewModel/MVVM_LoadSlot.h"
void UMVVM_LoadScreen::SetBind(FString NewBind)
{
UE_MVVM_SET_PROPERTY_VALUE(Bind, NewBind);
}
FString UMVVM_LoadScreen::GetBind() const
{
return Bind;
}
void UMVVM_LoadScreen::InitializeLoadSlots()
{
//创建插槽0视图模型
LoadSlot_0 = NewObject<UMVVM_LoadSlot>(this, LoadSlotViewModelClass);
LoadSlots.Add(0, LoadSlot_0);//添加到map
LoadSlot_1 = NewObject<UMVVM_LoadSlot>(this, LoadSlotViewModelClass);
LoadSlots.Add(1, LoadSlot_1);
LoadSlot_2 = NewObject<UMVVM_LoadSlot>(this, LoadSlotViewModelClass);
LoadSlots.Add(2, LoadSlot_2);
}
UMVVM_LoadSlot* UMVVM_LoadScreen::GetLoadSlotViewModelByIndex(int32 Index) const
{
return LoadSlots.FindChecked(Index);
}
Source/Aura/Private/UI/HUD/LoadScreenHUD.cpp
#include "UI/HUD/LoadScreenHUD.h"
#include "Blueprint/UserWidget.h"
#include "UI/ViewModel/MVVM_LoadScreen.h"
#include "UI/Widget/LoadScreenWidget.h"
void ALoadScreenHUD::BeginPlay()
{
Super::BeginPlay();
// 控件的视图模型的创建在 控件添加到视图之前进行
LoadScreenViewModel = NewObject<UMVVM_LoadScreen>(this, LoadScreenViewModelClass);
// 加载屏幕控件作为插槽控件父级,构造完成自己的视图模型后,才可以构造插槽的视图模型
LoadScreenViewModel->InitializeLoadSlots();
LoadScreenWidget = CreateWidget<ULoadScreenWidget>(GetWorld(), LoadScreenWidgetClass);
LoadScreenWidget->AddToViewport();
}
Content/Blueprints/UI/ViewModel/BP_LoadSlotViewModel.uasset
WBP_LoadMenu 控件的视图模型 构造了3个插槽视图模型,但没有将视图模型分别设置到插槽控件中。 必须在WBP_LoadMenu 控件的视图模型构造完成后,才可以将视图模型分别设置到插槽控件中。 加载菜单控件在蓝图中通过视图模型的工具通过索引获取插槽视图模型。
Source/Aura/Public/UI/Widget/LoadScreenWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "LoadScreenWidget.generated.h"
UCLASS()
class AURA_API ULoadScreenWidget : public UUserWidget
{
GENERATED_BODY()
public:
// 蓝图实现,加载控件通过插槽索引工具获取子级插槽控件后,有插槽控件设置自身的视图模型
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void BlueprintInitializeWidget();
};
Source/Aura/Private/UI/Widget/LoadScreenWidget.cpp
#include "UI/Widget/LoadScreenWidget.h"
Source/Aura/Private/UI/HUD/LoadScreenHUD.cpp
#include "UI/HUD/LoadScreenHUD.h"
#include "Blueprint/UserWidget.h"
#include "UI/ViewModel/MVVM_LoadScreen.h"
#include "UI/Widget/LoadScreenWidget.h"
void ALoadScreenHUD::BeginPlay()
{
Super::BeginPlay();
// 控件的视图模型的创建在 控件添加到视图之前进行
LoadScreenViewModel = NewObject<UMVVM_LoadScreen>(this, LoadScreenViewModelClass);
// 加载屏幕控件作为插槽控件父级,构造完成自己的视图模型后,才可以构造插槽的视图模型
LoadScreenViewModel->InitializeLoadSlots();
LoadScreenWidget = CreateWidget<ULoadScreenWidget>(GetWorld(), LoadScreenWidgetClass);
LoadScreenWidget->AddToViewport();
// 此时可以设置插槽的视图模型到插槽自身,函数在加载菜单控件蓝图中实现
LoadScreenWidget->BlueprintInitializeWidget();
}
插槽控件可以继承 图表: SlotIndex 公开变量
接收参数 InSlot 整数,以设置自身的索引值 set SlotIndex
Source/Aura/Public/UI/ViewModel/MVVM_LoadSlot.h
#pragma once
#include "CoreMinimal.h"
#include "MVVMViewModelBase.h"
#include "MVVM_LoadSlot.generated.h"
UCLASS()
class AURA_API UMVVM_LoadSlot : public UMVVMViewModelBase
{
GENERATED_BODY()
public:
void SetBind(FString NewBind);
FString GetBind() const;
private:
// 视图模型添加至少一个通知字段,用以绑定到控件 ,Bind没有任何实际业务逻辑意义
UPROPERTY(EditAnywhere, BlueprintReadWrite, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess="true"))
FString Bind;
};
Source/Aura/Private/UI/ViewModel/MVVM_LoadSlot.cpp
#include "UI/ViewModel/MVVM_LoadSlot.h"
void UMVVM_LoadSlot::SetBind(FString NewBind)
{
UE_MVVM_SET_PROPERTY_VALUE(Bind, NewBind);
}
FString UMVVM_LoadSlot::GetBind() const
{
return Bind;
}
WBP_LoadSlot_EnterName WBP_LoadSlot_Taken WBP_LoadSlot_Vacant
绑定类型-手动 将在插槽控件的父级控件插槽切换器中,为每个插槽设置视图模型。
将视图模型 BP_LoadSlotViewModel 的 Bind 通知字段绑定到3个插槽控件 控件的 一个隐藏控件上 例如添加一个透明的文本-Text_Bind
窗口-视图绑定-添加控件 Text_Bind 3个插槽控件分别设置
注意:插槽切换器自身没有视图模型,也没有绑定视图模型。 但可以为子插槽设置视图模型。
InitializeSlot 事件图表: sequence FindLoadScreenViewModel 函数 来获取自身视图模型 通过工具函数根据子插槽索引 SlotIndex 获取子插槽的视图模型 GetLoadSlotViewModelByIndex
cast to BP_LoadSlotViewModel 获取的视图模型提升为局部变量 BPLoadSlotViewModel
分别为3个子插槽设置索引【关联视图模型】,设置
WBP_LoadSlot_Vacant
set index
set BP_LoadSlotViewModel
【由于分别为3个子插槽控件添加了手动类型的视图模型 BP_LoadSlotViewModel,控件将自动增加 set BP_LoadSlotViewModel函数,这在幕后自动完成】
之前获取的 BPLoadSlotViewModel 作为 set BP_LoadSlotViewModel 的视图模型
WBP_LoadSlot_EnterName set index-SlotIndex set BP_LoadSlotViewModel BP_LoadSlotViewModel函数,这在幕后自动完成】 BPLoadSlotViewModel 作为 set BP_LoadSlotViewModel 的视图模型
WBP_LoadSlot_Taken set index set BP_LoadSlotViewModel BP_LoadSlotViewModel函数,这在幕后自动完成】 BPLoadSlotViewModel 作为 set BP_LoadSlotViewModel 的视图模型
这边完成了对3个子插槽的分别设置视图模型。
注意,现在提到的索引,均指插槽视图模型的索引,关联到加载菜单中初始化的3个视图模型。与切换器控件的原生控件索引无关。
图表: event BlueprintInitializeWidget 首先为3个插槽切换器设置索引 WBP_LoadSlot_WidgetSwitcher_0 InitializeSlot 0 WBP_LoadSlot_WidgetSwitcher_1 InitializeSlot 1 WBP_LoadSlot_WidgetSwitcher_2 InitializeSlot 2
注意: BP_LoadScreenViewModel 视图模型类型为 属性路径 ,属性路径为函数 FindLoadScreenViewModel 控件通过 FindLoadScreenViewModel 自动找到 BP_LoadScreenViewModel。 在 LoadScreenHUD BeginPlay 中才开始构造 LoadScreenViewModel ,然后执行 BlueprintInitializeWidget。 所以在 BlueprintInitializeWidget 事件的定义中,尚未构造 BP_LoadScreenViewModel。需要运行时通过 属性路径函数 FindLoadScreenViewModel 获取已经构造的 BP_LoadScreenViewModel,这时是有效的视图模型。 所以不可以在event BlueprintInitializeWidget定义中使用 get BP_LoadScreenViewModel 获取,这时视图模型尚未构造,获取的视图模型无效。
获取自身视图模型 FindLoadScreenViewModel 获取插槽视图模型,设置给插槽控件 GetLoadSlotViewModelByIndex
需要一种切换插槽控件的切换工具以使用切换器中的3种控件:新建,编辑,开始游戏。 在C++中定义。
MVVM_LoadSlot 视图模型中定义切换器切换委托 ,委托包含应当显示的插槽控件索引参数。
Source/Aura/Public/UI/ViewModel/MVVM_LoadSlot.h
#pragma once
#include "CoreMinimal.h"
#include "MVVMViewModelBase.h"
#include "MVVM_LoadSlot.generated.h"
//定义委托 切换控件切换时,广播激活的索引
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSetWidgetSwitcherIndex, int32, WidgetSwitcherIndex);
UCLASS()
class AURA_API UMVVM_LoadSlot : public UMVVMViewModelBase
{
GENERATED_BODY()
public:
void SetBind(FString NewBind);
FString GetBind() const;
// 切换器切换委托 包含激活得切换器索引
UPROPERTY(BlueprintAssignable)
FSetWidgetSwitcherIndex SetWidgetSwitcherIndex;
// 初始化/更新 插槽信息 广播激活的切换索引
void InitializeSlot();
private:
// 视图模型添加至少一个通知字段,用以绑定到控件 ,Bind没有任何实际业务逻辑意义
UPROPERTY(EditAnywhere, BlueprintReadWrite, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess="true"))
FString Bind;
};
Source/Aura/Private/UI/ViewModel/MVVM_LoadSlot.cpp
#include "UI/ViewModel/MVVM_LoadSlot.h"
void UMVVM_LoadSlot::SetBind(FString NewBind)
{
UE_MVVM_SET_PROPERTY_VALUE(Bind, NewBind);
}
FString UMVVM_LoadSlot::GetBind() const
{
return Bind;
}
void UMVVM_LoadSlot::InitializeSlot()
{
// TODO: Check slot status based on loaded data
SetWidgetSwitcherIndex.Broadcast(1);
}
WBP_LoadSlot_EnterName WBP_LoadSlot_Taken WBP_LoadSlot_Vacant 都绑定 BP_LoadScreenViewModel 类型同样为属性路径 FindLoadScreenViewModel 因为每个插槽修需要访问和修改其他插槽的信息。
Source/Aura/Public/UI/ViewModel/MVVM_LoadScreen.h
Source/Aura/Private/UI/ViewModel/MVVM_LoadScreen.cpp
虚幻引擎5 游戏技能系统(GAS) Unreal Engine 5 Gameplay Ability System
https://docs.unrealengine.com/5.3/zh-CN/gameplay-ability-system-for-unreal-engine/