WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
46 stars 10 forks source link

虚幻引擎5 游戏技能系统(GAS) Unreal Engine 5 Gameplay Ability System #181

Open WangShuXian6 opened 10 months ago

WangShuXian6 commented 10 months ago

虚幻引擎5 游戏技能系统(GAS) Unreal Engine 5 Gameplay Ability System

https://docs.unrealengine.com/5.3/zh-CN/gameplay-ability-system-for-unreal-engine/

WangShuXian6 commented 10 months ago

2. 项目创建 Project Creation

4. The Base Character Class 角色基类

基于 角色类创建角色基类 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();

}

5. Player and Enemy Characters 玩家与敌人角色

基于 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"

6. Character Blueprint Setup 角色蓝图设置

所有角色都拥有武器。 将武器附加到角色的骨骼网格体组件骨架插槽上。 角色骨架必须拥有指定插槽。

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();

}

基于 AuraCharacter 创建蓝图 BP_AuraCharacter 玩家角色蓝图

E:/Unreal Projects 532/Aura/Content/Blueprints/Character/Aura/BP_AuraCharacter.uasset image

网格体组件-细节-网格体-骨骼网格体资产-SKM_Aura 移动角色网格体使其面向前方 image

打开 SKM_Aura ,在骨骼 hand_l 上添加插槽 WeaponHandSocket

image

为 插槽 WeaponHandSocket 添加预览资产 SKM_Staff 法杖。 image

调整插槽 WeaponHandSocket 位置

预览场景设置-预览控制器-使用特定资产 预览场景设置-动画-Cast_FireBolt image image

打开 BP_AuraCharacter

Weapon组件-细节-网格体-骨骼网格体资产-SKM_Staff image image

基于 C++ AuraEnemy 创建 蓝图类 BP_Goblin_Spear

E:/Unreal Projects 532/Aura/Content/Blueprints/Character/Goblin_Spear/BP_Goblin_Spear.uasset

打开 BP_Goblin_Spear image

网格体组件-细节-网格体-骨骼网格体资产-SKM_Goblin image

移动角色网格体使其面向前方 image

缩小胶囊体组件适应角色 胶囊体组件-细节-半高- image image

SKM_Goblin

自带插槽 Hand-LSocket ,重命名为 WeaponHandSocket image

打开 BP_Goblin_Spear

Weapon组件-细节-网格体-骨骼网格体资产-SKM_Spear image image

将玩家角色和敌人角色哥布林拖至关卡中预览

image 删除角色。

7. Animation Blueprints 动画蓝图

使用骨骼 SK_Aura 新建动画蓝图 ABP_Aura

image

打开 ABP_Aura 添加 state machine 动画-状态机 : Main States 从 Main States 拖出 slot"DefaultSlot" 插槽。 插槽用于播放蒙太奇。 插槽 输出至 output pose. image

进入 状态机 : Main States

添加状态 IdleWalkRun image

进入 状态 IdleWalkRun

从 资产管理器 拖入 混合空间1D 动画 IdleWalkRun,输出至 output animation pose 输出动画。 image image

进入 ABP_Aura 事件图表

蓝图初始化动画 event blueprint initialize animation

重载函数-蓝图初始化动画 image image

为 Try Get Pawn Owner 添加节点 cast to BP_AuraCharacter 获取角色, 提升为变量 BP_Aura_Character

然后从 BP_Aura_Character 获取 变量-角色-character movement 角色移动组件 将 character movement 组件 提升为变量 CharacterMovement 。 现在我们有了可以在蓝图更新动画中访问的角色移动组件。 BPGraphScreenshot_2024Y-01M-30D-16h-04m-24s-900_00

event blueprint update animaition

动画被更新后执行,目标是动画实例。

如果我们想要一个更复杂的动画蓝图,我们可以使用蓝图线程安全更新动画。 但这是一个简单的动画,因此我们不需要使用多线程。 但我们会使用event blueprint update animaition 来代替蓝图更新动画来制作更复杂的动画蓝图。

image

拖出角色变量 BP_Aura_Character -右键-转换为有效的get image image

拖出角色移动变量 CharacterMovement 从 CharacterMovement 获取 速度-Velocity 从 Velocity 获取 数学-向量-Vector Length XY 将 Vector Length XY 的输出提升为变量 GroundSpeed 地面速度

现在有一个地面速度变量,用地面速度驱动混合空间动画。 BPGraphScreenshot_2024Y-01M-06D-10h-59m-11s-759_00

进入 状态机 : Main States - 状态 IdleWalkRun 用地面速度驱动混合空间动画 IdleWalkRun

拖入 GroundSpeed 地面速度 变量,输出至 混合空间动画 IdleWalkRun 的 Speed 节点 BPGraphScreenshot_2024Y-01M-06D-11h-02m-35s-893_00 此时人物具由空闲动画。 image

BP_Aura_Character 使用动画蓝图 ABP_Aura

BP_Aura_Character-网格体-细节-动画-动画模式-使用动画蓝图 动画-动画类-ABP_Aura image

BP_Goblin_Spear 哥布林魔法师蓝图

将会有多个敌人。 它们不会共享相同的骨骼网格体和骨架。 因此,为了使其更加通用并防止代码重复,敌人动画蓝图将使用模板动画蓝图。

创建 动画蓝图模板 :动画-动画蓝图-模板- ABP_Enemy

image image

打开 ABP_Enemy 所有敌人的动画蓝图都有共同点。 首先需要一个状态机。

添加 state machine 动画-状态机 : Main States

从 Main States 拖出 slot"DefaultSlot" 插槽。 插槽用于播放蒙太奇。 插槽 输出至 output pose. image

进入 状态机 : Main States

添加状态 IdleWalkRun image

进入 状态 IdleWalkRun

此处不会拖入任何特定的在闲走、跑步中混合空间。

将使用混合空间播放器。 添加 动画-混合空间-Blendspace Player

这是一个通用节点。 把它连接到输出动画姿势。 因此,当我们创建从此模板派生的子动画蓝图时,我们所要做的就是选择混合空间播放器。 image

还需要一个速度变量来驱动混合空间 x 轴。

进入 ABP_Enemy 事件图表

蓝图初始化动画 event blueprint initialize animation

重载函数-蓝图初始化动画

为 Try Get Pawn Owner 添加节点 工具-cast- cast to AuraEnemy 获取敌人角色, 提升为变量 BP_Aura_Enemy

然后从 BP_Aura_Enemy 获取 变量-角色-get character movement 敌人角色移动组件 将 character movement 组件 提升为变量 CharacterMovement 。 现在我们有了可以在蓝图更新动画中访问的敌人角色移动组件。

BPGraphScreenshot_2024Y-01M-30D-17h-40m-40s-454_00

event blueprint update animaition

动画被更新后执行,目标是动画实例。

拖出敌人角色变量 BP_Aura_Enemy -右键-转换为有效的get

拖出敌人角色移动变量 CharacterMovement 从 CharacterMovement 获取 速度-Velocity 从 Velocity 获取 数学-向量-Vector Length XY 将 Vector Length XY 的输出提升为变量 GroundSpeed 地面速度 BPGraphScreenshot_2024Y-01M-06D-13h-39m-45s-723_00

现在有一个地面速度变量,用地面速度驱动混合空间动画。

进入 状态机 : Main States - 状态 IdleWalkRun 用地面速度驱动混合空间动画 IdleWalkRun

拖入 GroundSpeed 地面速度 变量,输出至 混合空间动画 IdleWalkRun 的 Speed 节点 BPGraphScreenshot_2024Y-01M-06D-13h-40m-59s-001_00

根据这个动画蓝图模板为敌人创建一个子类。

基于动画蓝图模板 ABP_Enemy 创建动画蓝图 ABP_Goblin_Spear

右键-动画-动画蓝图-模板-骨骼选择 SK_Goblin ,父类选择 ABP_Enemy 名称:ABP_Goblin_Spear E:/Unreal Projects 532/Aura/Content/Blueprints/Character/Goblin_Spear/ABP_Goblin_Spear.uasset image image

动画蓝图 ABP_Goblin_Spear 使用 混合空间 BS_GoblinSpear_IdleRun

打开 ABP_Goblin_Spear-资产覆盖编辑器-展开列表: ABP_Enemy-AnimGraph-Main States-IdleWalkRun-混合空间播放器-选择 混合空间 BS_GoblinSpear_IdleRun image 现在敌人具有了空闲动画 image

BP_Goblin_Spear 使用 动画蓝图 ABP_Goblin_Spear

BP_Goblin_Spear-网格体-细节-动画-动画模式-使用动画蓝图 动画-动画类-ABP_Goblin_Spear image

此时敌人哥布林魔法师处于空闲动画。

BP_Goblin_Slingshot 哥布林游侠【弹弓】

基于 AuraEnemy 新建 蓝图 BP_Goblin_Slingshot image E:/Unreal Projects 532/Aura/Content/Blueprints/Character/Goblin_Slingshot/BP_Goblin_Slingshot.uasset

打开 BP_Goblin_Slingshot 网格体组件-网格体-骨骼网格体组件-SKM_Goblin image 调整 网格体位置和朝向 调整胶囊体组件-半高 image

Weapon组件-细节-网格体-骨骼网格体组件-SKM_Slingshot image image

基于动画蓝图模板 ABP_Enemy 创建动画蓝图 ABP_Goblin_Slingshot

右键-动画-动画蓝图-模板-骨骼选择 SK_Goblin ,父类选择 ABP_Enemy 名称:ABP_Goblin_Slingshot image image E:/Unreal Projects 532/Aura/Content/Blueprints/Character/Goblin_Slingshot/ABP_Goblin_Slingshot.uasset

动画蓝图 ABP_Goblin_Slingshot 使用 混合空间 BS_GoblinSlingshot_IdleRun

打开 ABP_Goblin_Slingshot-资产覆盖编辑器-展开列表: ABP_Enemy-AnimGraph-Main States-IdleWalkRun-混合空间播放器-选择 混合空间 BS_GoblinSlingshot_IdleRun

image image

BP_Goblin_Slingshot 使用 动画蓝图 ABP_Goblin_Slingshot

BP_Goblin_Slingshot-网格体组件-细节-动画-动画模式-使用动画蓝图 动画-动画类-ABP_Goblin_Slingshot image

image

现在不需要进入动画蓝图并设置所有变量或类似的内容,因为我们正在使用动画蓝图模板,可以将其共享给敌人。 需要做的就是设置网格体组件并在动画蓝图更改使用的任何动画资源。

8. Enhanced Input 增强输入

移动角色。

创建控制目录:Input image

Input 下创建目录 InputActions 现在需要一个输入操作来增强输入,以便从某种设备获取输入,例如键盘.

输入操作 IA_Move

输入操作是用户可执行操作的逻辑表示,例如”跳跃”或“蹲伏”。这些是你的Gameplay代码绑定的内容,以便监听输入状态的变化。多数情况下你的游戏代码应该监听输入操作的“已触发”事件。这将允许最有可扩展性和可定制性的输入配置,因为你可以在输入映射上下文中为每个键映射添加不同的触发器。 它们在概念上等同于旧版输入系统中的“操作”和“轴”映射名称 注意:这些是每个玩家实例化(通过FInputActioninstance)

右键-输入-输入操作 新建 IA_Move image E:/Unreal Projects 532/Aura/Content/Blueprints/Input/InputActions/IA_Move.uasset image 打开 IA_Move 细节-操作-值类型-Axis2D(Vector2D) 这将允许我接受输入具有 X 和 y 两个轴的二维向量的形式, 将X视为左和右,Y视为前和后。 因为对于运动,我希望能够处理二维运动。 左右是第一维度,前后是第二维度。 image

将输入操作其链接到角色的方式将是通过输入映射上下文,我们可以在其中将键盘按键与要移动的输入操作关联起来。

输入映射情景 IMC_AuraContext

UInputMappingContext:特定输入上下文的操作映射键的集合, 可用于: 存储预定义控制器映射(允许在控制器配置变体之间切换)。TODO:构建一个允许UnputMappingContexts重定向的系统来处理该问题。 每个载具控制点映射定义 定义上下文相关的映射(如我从枪(射击操作) 切换到抓钩(放出、收、断开操作). 定义覆层映射以应用到现有控件映射之上(如:MOBA游戏中英雄特定操作映射)

在 Input 目录右键 -输入-输入映射情景 名称:IMC_AuraContext image image 打开 IMC_AuraContext 细节-映射-映射-添加一个操作映射 选择 IA_Move 数据资产(输入操作) image

将多个控制绑定添加到操作映射

点击 IA_Move 右侧加号 将多个控制绑定添加到操作映射

键盘D:向右移动

键盘A:向左移动 为了向左移动需要添加一个 negate 否定修改器 表示负数。 只启用X轴表示左右方向的移动。 作为二位映射,Z轴无效。

键盘W:向上移动 为了向上移动需要添加一个 拌合输入轴值 修改器 表示Y轴数。 现在排序为 YXZ,Y为首选。 拌合输入值的轴组件。 将1D输入映射到2D操作的Y轴上时非常有用。

键盘W:向下移动 为了向下移动需要添加一个 negate 否定修改器 和 拌合输入轴值 修改器 表示Y轴负数。

image

AuraPlayerController 玩家角色控制器

基于 PlayerController C++类新建 PlayerController C++类 在 Player 目录下 image image

Aura Player Controller 玩家角色控制器

在 AuraPlayerController玩家 角色控制器中将增强的输入操作和输入映射情景与我们的角色相关联。

添加模块 EnhancedInput

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);
}

10. Movement Input 移动输入

之后需要在蓝图中指定 输入映射情景,输入操作 数据资产。 需要项目配置使用 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);
    }
}

基于 AuraPlayerController C++ 类创建 BP_AuraPlayerController 玩家角色控制器蓝图

内容-Blueprints 下新建目录 Player 基于 AuraPlayerController C++ 类创建 BP_AuraPlayerController 玩家角色控制器蓝图 E:/Unreal Projects 532/Aura/Content/Blueprints/Player/BP_AuraPlayerController.uasset image

打开 BP_AuraPlayerController 细节-输入-aura context- IMC_AuraContext 输入映射情景/数据资产(输入映射上下文) 细节-输入-move action-IA_Move 数据资产(输入操作) image image

11. Game Mode 游戏模式

通过游戏模式将 BP_AuraPlayerController 和玩家联系在一起。

基于 游戏模式基础 GameModeBase 创建游戏模式 C++ 类 AuraGameModeBase

image image

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

基于 C++ 类 AuraGameModeBase 创建 BP_AuraGameMode 游戏模式蓝图

E:/Unreal Projects 532/Aura/Content/Blueprints/Game/BP_AuraGameMode.uasset image

打开 BP_AuraGameMode 细节-高级-类-玩家控制器类-BP_AuraPlayerController 细节-高级-类-默认 Pawn 类-BP_AuraCharacter image

进入关卡-世界场景设置-游戏模式-游戏模式重载-BP_AuraGameMode image

添加玩家出生点 基础-玩家出生点 拖至关卡任意位置即可 image image 此时可控制玩家行走 WSAD。

在 BP_AuraCharacter 蓝图中添加相机和弹簧臂

C++设置相机和弹簧臂没有性能优势。

打开 BP_AuraCharacter 选中 胶囊体组件,作为弹簧臂的父组件。 添加 SpringArm弹簧臂组件。 选中 SpringArm弹簧臂组件,作为相机父组件。 添加Camera摄像机组件.

image

调整SpringArm弹簧臂组件位置:

image

SpringArm组件-细节-摄像机设置-使用Pawn控制旋转-不启用 我不想用控制器旋转弹簧臂,对于自上而下的拍摄,相机将被固定。 image

SpringArm组件-细节-摄像机-目标臂长度-500 image

相机延迟: SpringArm组件-细节-滞后-启用摄像机延迟-启用 image

这是一个很好的小效果,让相机有点滞后。

调整摄像机

camera组件-细节-摄像机选项-使用Pawn控制旋转 -不启用 image

image

AuraCharacter C++ 设置角色移动的控制

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;
}

BP_AuraCharacter 固定相机视角

SpringArm组件-细节-摄像机设置-继承Pitch-不启用 SpringArm组件-细节-摄像机设置-继承Yaw-不启用 SpringArm组件-细节-摄像机设置-继承Roll-不启用 image

弹簧臂没有继承俯仰偏航或滚动。

优化空闲动画,在玩家停止时不要立刻执行

打开 动画蓝图 ABP_Aura-Main states 添加 状态 Idle, image

进入 状态 Idle,拖入动画序列 Idle

image

选中 Idle 动画序列-细节-设置-循环动画-启用 image 打开事件图表-添加布尔变量-ShouldMove 将 GroundSpeed 大于3 的比较值赋 ShouldMove 。 BPGraphScreenshot_2024Y-01M-06D-21h-51m-44s-501_00

进入 ABP_Aura-Main states 设置 Idle 到 IdleWalkRun 的条件:ShouldMove 为真。 直接拖入变量 ShouldMove image

设置 IdleWalkRun 到Idle 的条件:ShouldMove 为假。 直接拖入变量 ShouldMove ,拖入 Not Bool image

12. Enemy Interface 敌人接口

使用高亮效果来告诉我们哪个敌人正在被选中瞄准。 玩家控制器类 AuraPlayerController有方法可以查看鼠标光标下方的内容。

创建 敌人接口类,当将鼠标悬停在Actor上时,我们可以检查它是否实现了这个接口。 如果是的话,我们就可以在该接口上调用接口函数。 将鼠标悬停在Actor上时。,玩家控制器不需要知道会发生什么。 它所知道的是,如果参与者实现了接口,它应该调用该接口函数。 参与者可以重写该函数并以任何他想要的方式实现它。 不同的敌人类别可以具有不同的突出显示功能。 我们可以将此界面添加到桶或门上,并以不同的方式突出显示这些对象。 在这种情况下,敌人接口这个名称可能不太适合这个名称。 image

基于 Unreal接口创建 EnemyInterface C++ 接口

image image

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()
{
}

13. Highlight Enemies 高亮选中的敌人

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
            }
        }
    }
}

BP_Goblin_Spear

事件图表 添加继承自C++的变量 bHighlighted ,蓝图中会省略 b 前缀 光标下的actor位置会画出红色调试球 BPGraphScreenshot_2024Y-01M-06D-22h-55m-52s-826_00

BP_Goblin_Spear-网格体组件-细节-碰撞-碰撞预设-custom 网格体组件-细节-碰撞-碰撞响应-检测响应-Visibility-阻挡 使光标跟踪生效,因为光标跟踪Visibility通道。 BP_Goblin_Slingshot 使用同样的设置。

GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);

image

关卡拖入一个 BP_Goblin_Spear 测试 image 但对 BP_Goblin_Slingshot 无效

14. Post Process Highlight 后处理高亮

使用后期处理效果通过轮廓突出显示敌人。

BP_EnemyBase 敌人基础蓝图

基于 C++ AuraEnemy 类新建蓝图 BP_EnemyBase E:/Unreal Projects 532/Aura/Content/Blueprints/Character/BP_EnemyBase.uasset image image

所有敌人蓝图都使用 BP_EnemyBase 当作父类

如果我们在敌人基础蓝图中做了任何事情,那么所有其他敌人的蓝图都会继承该功能。

打开 BP_Goblin_Slingshot 蓝图 类设置-类选项-父类-BP_EnemyBase image

打开 BP_Goblin_Spear 蓝图 类设置-类选项-父类-BP_EnemyBase image

轮廓 后期处理

对于像轮廓这样的效果,我们需要一个后期处理体积,因为它将是一个后期处理效果。 添加 -体积-后期处理体积 到关卡 image image image

选择 PostProcessVolume-细节-后期处理体积设置-无限范围(未设定)-启用 启用:此体积覆盖整个场景 或不启用:只有其边界中的区域

image

PostProcessVolume-细节-渲染功能-后期处理材质-添加一组材质,选择-资产引用-再选择-PP_Highlight 材质 image

点亮材质:PP_Highlight

image BPGraphScreenshot_2024Y-01M-07D-00h-01m-21s-975_00

模板通道

只有当我们将项目配置为使用自定义深度模板通道时,这才有效。

编辑-项目设置-【搜索 custom depth】-引擎-渲染-后期处理-自定义深度-模板通道-已按需求启用 首次使用时创建深度缓冲,可节约内存但会导致停转。已禁用模板。 image

关闭胶囊体组件的轮廓 BP_Goblin_Spear-胶囊体组件-细节-渲染-可视-不启用 image

现在我们的后期处理体积正在使用高光,这意味着任何设置其自定义的深度模板值为 250 的 网格将突出显示。

选择关卡的任意一个敌人: 细节-【搜索 custom depth】-渲染-mesh-渲染自定义深度通道-启用 细节-【搜索 custom depth】-渲染-mesh-自定义深度模板值-50

image 此时 的人的轮廓将高亮显示 image

调节PP_Highlight轮廓粗度 image

C++ 设置 渲染自定义深度

image

删除 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-阻挡

WangShuXian6 commented 10 months ago

3. Intro to the Gameplay Ability System / Gameplay技能系统简介 /GAS

https://docs.unrealengine.com/5.3/zh-CN/gameplay-ability-system-for-unreal-engine/

1. The Gameplay Ability System / Gameplay技能系统/ GAS

image image

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技能系统插件。 image 要使用此系统的全部功能,添加"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都应具有技能系统组件。

2. The Main Parts of GAS / GAS 的主要组成部分

image

技能系统组件 Ability System Component

这是一种可以添加到 Actor 中的组件,它可以处理许多重要的事情, 例如授予技能、激活这些技能、处理通知,当某些技能被激活或效果被应用时,以及许多其他我们会做的事情。 技能系统组件是我们必须添加到角色中的东西。

属性集 Attribute Set

与任何给定对象或角色相关联的许多属性,这些属性的范围包括生命和法力,几乎是你能想到的GAS的任何其他属性。 我们将这些属性存储在属性集上,属性集具有许多功能,使我们能够将这些属性与GAS系统的其他各个部分相关联。

技能 Gameplay Ability

游戏技能系统的核心是技能。 游戏技能是我们用来封装某种事物功能的类,是角色或物体在游戏中可以做的事情。 像攻击、施法之类的东西一般都是技能,而游戏技能让我们将所有代码和功能保留在特定的游戏技能类别中。

技能任务

游戏技能运行异步任务,我们称之为技能任务。 这些允许我们执行异步代码,这意味着一旦我们启动任务,它就可以执行它的工作并立即完成,或者它的工作可能跨越一段时间并且可能去做其他事情。 技能任务就像是为游戏技能本身执行工作的工人。

游戏效果

用来更改属性值的内容,它们具有多种与属性相关的不同的功能。 我们使用游戏效果来直接改变属性,随着时间的推移改变它们,周期性地增加或减少它们,并将这些属性更改与采用其他属性参数的各种计算相关联。

游戏提示

负责处理粒子系统和声音等外观效果,并且可以处理网络复制。

游戏玩法标签

技能系统的另一个核心部分是游戏玩法标签。 游戏标签实际上并不是GAS系统所独有的。 它们存在于GAS之外,可以用于非游戏技能系统项目。 它们的层次性质使它们比变量(例如枚举、布尔值或字符串)简单。


为了将gas纳入我们的项目,我们需要一个技能系统组件和一个属性

添加技能系统组件的方式可以有所不同。 image

1-可以直接在pawn类上添加技能系统组件。 可以对属性集执行相同的操作。

2-采用与您的 pawn 相关的其他类,例如玩家状态。 并将技能系统组件和属性集添加到该类中。

将技能系统组件和属性集放在 pawn 与玩家状态上的几个原因:

image

1-假设我们已将技能、系统组件和属性集添加到 pawn Class。

假设这是一个多人游戏,此时我们的Actor死了,我们决定摧毁它的pawn 以便我们可以重生另一个。 因为那个Actor上存在技能系统组件和属性集。 当Actor被摧毁时,技能、系统组件和属性集也会被摧毁。 如果您没有采取措施保存这些类的任何相关数据,例如在保存游戏对象中或者在某个数据库中,该数据就消失了。 当你重生一个新的 pawn 时,技能系统组件和属性集都会被重新创建, 从默认值和默认功能开始。

将技能系统组件和属性集保留在Pawn上的原因可能是Pawn可能很简单。 您的游戏可能有简单的敌人,它们确实具有技能系统组件和属性集,但是这些敌人不需要玩家状态,因为它们很简单。 他们只需要拥有属性和技能,并基于人工智能执行逻辑即可。 因此直接在 pawn 上设置的技能系统组件和属性可能适合该情况。

2-假设在玩家状态上设置技能系统组件和属性。这与我们的Pawn相关。 如果我们的 pawn 死亡并且我们摧毁了它,则技能系统组件和属性集不会被摧毁,因为它们不存在于该类中。 它们存在于玩家状态中,当您销毁 pawn 时,该状态仍然存在,因此我们可以生成一个新的 pawn 并将其与我们的玩家状态相关联。然后我们的属性集中仍然有相同的值,我们的技能系统中仍然有相同的技能组件,因为它们尚未被破坏。

另一个原因是您可能不希望您的技能系统组件和属性集使角色变得混乱。 你可能希望将你的角色换成另一个角色,但你可能不想要你的技能系统组件和属性设置于任何给定角色关联。 您可能希望将其与玩家本身保持关联。

当前游戏项目要采取的方法

image

敌人角色将直接拥有他们的技能系统组件和属性集,与 Character 关联。 玩家控制的角色,我们将把我们的技能系统组件和属性与 玩家状态 关联。

image

3. The Player State 玩家状态

https://docs.unrealengine.com/5.3/en-US/API/Runtime/Engine/GameFramework/APlayerState/

创建 AuraPlayerState 玩家状态类并将其分配给Aura角色

再 C++ Player文件夹内 基于 Player State 创建 AuraPlayerState 类 E:/Unreal Projects 532/Aura/Source/Aura/Public/Player/AuraPlayerState.h image image image

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;

}

基于 AuraPlayerState C++类创建 BP_AuraPlayerState 蓝图类

E:/Unreal Projects 532/Aura/Content/Blueprints/Player/BP_AuraPlayerState.uasset image image

将 BP_AuraPlayerState 分配给 游戏模式 BP_AuraGameMode

打开 BP_AuraGameMode 细节-高级-类-玩家状态类-BP_AuraPlayerState image 此时Aura玩家加使用该状态。 技能系统组件和属性集将适用于我们的玩家控制角色的 Aura。

4. Ability System Component and Attribute Set 技能系统组件和属性集

AuraAbilitySystemComponent

基于 AbilitySystemComponent 类 新建 C++ 类 AuraAbilitySystemComponent 作为其他技能系统组件的基类 E:/Unreal Projects 532/Aura/Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h image image image

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"

AuraAttributeSet

基于 AttributeSet 类 新建 C++ 类 AuraAttributeSet E:/Unreal Projects 532/Aura/Source/Aura/Public/AbilitySystem/AuraAttributeSet.h image image image

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"

5. GAS in Multiplayer 多人游戏中的GAS

image

在多人游戏中存在一台服务器,并且在大多数情况下只有一台服务器。 该服务器是游戏的一个实例,独立于其他机器运行。 他们也在运行自己的游戏版本。 我们称这些客户为客户端Clent。 服务器与客户端不同。

在多人游戏中,服务器可以呈现多种风格。

例如,有专用服务器 Dedicated Server。

专用服务器没有人类玩家,也不会渲染到屏幕上。 只是一台运行游戏模拟的计算机,因为没有人类玩家,也没有实际的渲染到屏幕。 不要让这种情况欺骗您,让您认为服务器上没有发生任何事情, 例如玩家在射击游戏中跑来跑去互相射击,在魔法游戏中施展咒语,升级并获得经验之类的东西。 这些都是在游戏的服务器版本上可以并且确实发生的事情。 他们只是没有通过将图像渲染到屏幕来显示.。

除了专用服务器之外,还有另一种类型的服务器模型,称为监听服务器。

与专用服务器不同,因为监听服务器是人类玩家,或者至少它是一台运行游戏版本的计算机,人类玩家在其中控制Actor或角色。 我们说那个人类玩家正在主持游戏。 现在主机在监听服务器模型上有一个优势,这个优势就是主机没有延迟。 延迟是数据从客户端到服务器通过网络传输数据所需时间的结果。 在侦听服务器中,主机是服务器,服务器不必通过网络发送数据到其自身,因此主机玩家不会出现延迟。

在虚幻引擎中,我们将服务器视为游戏的权威版本。 每台机器上都在发生事情。 玩家四处奔跑,改变位置,这种情况在游戏的每个版本中都会发生。 但由于延迟,您计算机上的角色位置版本将会与你的角色在其他玩家的机器和服务器上的位置有所不同。 所以我们必须确定哪台机器是正确的版本。 我们认为服务器是游戏的正确版本,因此在服务器上我们做了重要的事情。 服务器始终被认为是游戏的权威版本。

游戏模式GameMode仅存在于服务器上。

如果您尝试访问其中一台客户端上的游戏模式,您将得到一个空指针。 这是因为游戏模式负责诸如游戏规则、生成玩家和重新启动游戏之类的事情。 这些事情只能在服务器发生。

每个玩家的玩家控制器都存在于服务器上,但它们也存在于每个客户端上。

所以服务器有每个玩家控制器的权威服务器版本,每个客户端都有自己的本地版本。 但请注意,客户端零在其机器上只有玩家控制器零。 没有玩家控制器一或玩家控制器二。 所有其他玩家的玩家控制器都不存在于客户端零上。 只有服务器拥有游戏中的所有玩家控制器并可以访问它们。

玩家状态 对于每个玩家来说,它们都存在于服务器上,但它们也都存在于每个客户端上。

客户端零具有玩家状态零的版本,以及玩家状态一和玩家状态二。 这就是玩家状态与玩家控制器类的区别。

服务器拥有游戏中的所有三个pawn ,但每个客户端也拥有游戏中的所有三个pawn 。 如果你正在玩这个游戏,你必须能够看到你的pawn 到处跑,但你也能查看客户端一控制的 pawn 和客户端二控制的 pawn。 只有您的机器拥有游戏中所有三个pawn 的副本才有意义。 所以游戏中的所有pawn 都存在于所有机器上。 玩家状态也是如此。

HUD 和屏幕上显示的所有相关小部件,每个客户端都有自己的 HUD 类,并且仅存在于该客户端上。

如果我们谈论的是专用服务器,则服务器上没有 HUD。 如果它是侦听服务器,则唯一存在的 HUD 是播放该游戏的本地玩家的 HUD。 每个客户端只有自己的 HUD 以及显示在该玩家屏幕上的任何关联小部件。 零号客户端无权访问一号客户端的 HUD。

复制

将玩家控制器类设置为需要复制 玩家状态设置为净更新频率为 100。 网络更新频率只是意味着随着服务器上的更改,每次网络更新,服务器将数据发送到客户端,以便他们可以获知该更改。

净更新频率为 100 意味着这些更改将在客户端上每秒更新 100 次, 只要服务器能够管理数据从服务器传输到客户端时的情况。 这就是 复制。

假设您的 pawn 类中有一个变量。 现在它可以是浮点数、整数或任何其他类型。 现在它存在于 pawn 类中,这意味着 pawn 的每个版本都有该变量,即客户端上 pawn 的版本,服务器上 pawn 的版本。 他们都有变量。 如果该变量被指定为复制变量,那么如果该变量的值在服务器上发生变化。 该更改的下一个网络更新将发送到所有客户端,以便客户端的该变量的版本可以更新为新值。 这种将数据发送到客户端的行为称为复制。

如果我们有一个复制变量并且它在其中一个客户端上发生更改,服务器不会更改。 复制不是双向的。 复制仅以从服务器到客户端这一种方式工作,因此如果变量被标记为已复制,则它不应在客户端上更改,因为服务器不会知道该更改,任何其他客户也不会知道。 只有更改该变量的客户端才会知道它,并且该变量现在将不与该值的服务器版本同步。 服务器是正确的版本,因此客户端上的变量不正确。

客户端如何获取到数据:

我们用键盘和鼠标或控制器进行输入。 该输入必须以某种方式到达服务器。 它以复制函数的形式进行,我们称之为 RPC 或远程过程调用。 这种复制的高级概述就是您需要了解的有关多人游戏的全部内容。 image

6. Constructing the ASC and AS 构建ASC和AS

将技能系统组件和属性集实际添加到相应的类。

1-AuraCharacterBase 角色基类存储技能系统组件和属性集的指针,但不构造。 可以被其他敌人角色类继承,在敌人角色类中构造。

2-但对于玩家控制的角色,技能系统组件和属性集将由玩家状态存储和构造。

在AuraEnemy敌人角色类中 构造敌人类的技能系统组件和属性集

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;
}

7. Replication Mode 网络复制模式

https://docs.unrealengine.com/5.3/zh-CN/networking-and-multiplayer-in-unreal-engine/ image

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");
}

8. Init Ability Actor Info 初始技能参与者信息

技能系统组件的所有者: 敌人类:明确知道是敌人类自身; 玩家状态类:玩家状态类只是构建了技能系统组件。

技能系统组件有了技能参与者信息的概念。 这样,技能系统组件就可以始终知道参与者信息,例如谁拥有该技能系统组件。 技能系统组件也理解它可能由某个参与者拥有的概念, 由某个Pawn拥有,或者它可以由另一种类型的实体(例如玩家状态)拥有。

技能系统组件有两个变量:所有者 Owner Actor和化身 Avatar Actor。

所有者 Owner Actor 是实际拥有技能系统组件的任何类。 化身 Avatar Actor是与该技能系统组件相关的世界中的代表。

image

对于敌人角色来说,这两个是相同的,因为敌人角色类构建了技能系统组件。 所以Owner Actor是敌人角色,化身 Avatar Actor也是敌人角色。这就是视觉表现。

对于玩家控制的角色来说,这有点不同,因为技能系统组件由玩家状态构建,这就是我们希望将其视为技能系统组件所有者 Owner 的类。 所有者 Owner Actor 是玩家状态。 化身 Avatar Actor 是 所使用的 Actor,即在这个世界上看到的这个角色。

在这种情况下,Owner 、Actor和化身 Avatar Actor是两个不同的Actor。 技能系统组件区分这两者。

UAbilitySystemComponent::InitAbilityActorInfo

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。

在何时何地调用 InitAbilityActorInfo

image

1-玩家控制的角色-技能系统组件 由 Pawn构造时:

服务器: PossessedBy 必须在必须为 pawn 设置控制器之后进行。 对于一个玩家控制的角色,其中技能系统组件存在于 pawn 本身上,

客户端:AcknowledgePossession 在这个函数中我们知道占有已经发生。 我们的 pawn 确实有一个有效的控制器。 所以在这种情况下,如果我们将我们的技能系统组件放在Actor上,或者在我们的例子中是角色类上。

在这种情况下,Owner 和化身 Avatar是相同的。

2-玩家控制的角色-技能系统组件 由 玩家状态构造时:

对于玩家控制的角色,其中技能系统组件存在于玩家状态上,

服务器: PossessedBy

客户端:OnRep_PlayerState 因为技能系统组件依赖于玩家状态。 所以不仅需要确保控制器已经设置,还需要确保玩家状态在此时也是有效的。 此时将有一个有效的控制器和一个有效的玩家状态。 玩家状态将在服务器上设置,并且玩家状态是复制的实体,这意味着一旦为pawn 设置了该玩家状态,该状态就会被复制触发此代表通知,该通知将被调用以响应复制的发生。此时玩家状态已在服务器上设置并在客户端上复制下来并且​​是一个有效的指针。 因此,我们将在此处初始化技能系统组件的技能参与者信息。 把所有者 Owner actor 设置为玩家状态,将化身 Avatar actor 设置为 pawn,即玩家角色。

3- AI 控制的角色-技能系统组件 由 Pawn构造时:

敌人角色类构建了技能系统组件。

服务器:BeginPlay 客户端:BeginPlay

C++ 初始技能参与者信息

image

AI 敌人

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必须是控制器。【自身】

WangShuXian6 commented 10 months ago

4. Attributes 属性集

1. Attributes 属性集

image image

当我们在owner actor 的技能系统组件旁边构建属性集时, 属性集会自动注册到技能系统组件中。 可以有多个属性集。 技能系统组件可以访问注册的任何属性集。 可以根据类别将属性分布在多个不同的属性集中。 其中每一个都必须是独立的类。 同一类不能有多个属性集。否则,尝试从技能系统组件检索它时会出现歧义。 将所有属性包含在同一属性上是完全可以接受的, 这可以使事情变得更简单,特别是如果属性集进行计算时需要了解的其他属性。 属性占用的内存可以忽略不计,因此您甚至可以在所有类之间共享单个属性集。

image

属性是与游戏中给定实体(例如角色)相关的数值。 所有属性都是浮点数。 它们存在于称为游戏属性数据的结构中。 属性存储在属性集上,属性集对其进行密切监督。 我们可以知道属性何时发生变化,并使用我们喜欢的任何功能来响应它。

可以直接在代码中设置属性值,但更改它们的首选方法是应用游戏效果。 除了游戏效果的内置功能之外,使用它们的另一个原因是游戏玩法效果使我们能够预测属性的变化。 预测意味着客户端不需要等待服务器的许可来更改值。 该值可以在客户端立即更改,并且服务器会收到更改通知。 然后,服务器可以回滚任何它认为无效的更改。 这是一个巨大的好处,因为预测可能需要大量额外的工作来自己编程。 预测可以让多人游戏体验更加流畅。

image

向服务器发送一个请求,告诉它属性值需要更改,以便服务器接收。 该请求根据开发人员设置的任意数量的标准来决定该请求是否有效, 如果该更改被认为有效,服务器会将确认发送回客户端现在可以更改属性值。 由于数据在网络上传输需要时间,因此这会导致时间上的明显延迟。 客户端需要将该值更改为从服务器收到的实际许可的更改。 通过GAS预测,游戏效果会修改客户端的属性,并且可以立即在客户端上感知到该变化。 无滞后时间。 然后,该更改将发送到服务器,服务器仍然负责验证该更改。 如果服务器认为这是有效的更改。 它可以将更改通知其他客户端。 如果服务器确定更改无效,假设客户端破解了游戏并尝试造成不合常理的损害, 那么服务器可以拒绝该更改并回滚更改,设置客户端到正确的那个的值。 所以服务器仍然是权威的,但是我们的客户端不必有延迟。 预测很复杂,将其作为整个GAS的内置功能是一个巨大的好处。 让我们专注于创建游戏机制,而不用担心实施滞后补偿。 image

因此,属性是游戏属性数据类型的对象,并且存储在属性集中。 image

属性实际上由两个值组成:基值和当前值。 基值是属性的永久值。 当前值是基础值加上游戏效果造成的任何临时修改。 从 buff 和 debuff 的角度考虑一下,您可能在一段时间内会产生增加或减少值的效果,一旦该时间结束,该修改将被撤消,并且该属性返回到其基本值。 属性的最大值与属性本身是分开的。 如果最大值可以更改(这很常见),则最大值应该是其自己的单独属性。 这样我们就可以将游戏效果分别应用于属性本身或应用于最大属性。 例如,你的健康栏的百分比可以简单地是健康除以最大生命值的分数。

2. Health and Mana 健康与魔力

添加属性到 AuraAttributeSet

属性类型为 :FGameplayAttributeData

属性应为一个复制变量。 大多数属性都将被复制。 属性被复制到所有客户端。 如果服务器上的值发生变化,那么客户端将获得更新后的值。 如果通过游戏效果改变它,这些是可预测的属性。 在客户端本地更改,服务器将收到通知,以便服务器可以更改它。 一旦服务器上发生更改,所有其他客户端都需要知道该更改。

为了使变量被复制,用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: 如果您在服务器上设置了运行状况值并且该值没有更改,则不会进行复制。

C++

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);
}

3. Attribute Accessors 属性访问器

属性访问器 依赖 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 image 这将打开技能系统的调试视图,其中包含大量信息。 可使用上下翻页键循环显示其他调试信息。

4. Effect Actor 改变属性的Actor

使用某种可以拾取的物体来影响属性。 更改属性的首选方法是通过游戏效果。 但是我们还没有学会如何创建和使用游戏效果。 因此,我们将创建一个直接更改属性的Actor以了解其局限性。

AuraEffectActor

基于 Actor 类创建C++ 类 AuraEffectActor image 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);
}

红药水 BP_HealthPotion

image 在 内容-Blueprints 新建 目录 Actor, 基于C++类 AuraEffectActor 创建蓝图 BP_HealthPotion image image 打开 BP_HealthPotion mesh组件-细节-静态网格体-静态网格体-SM_PotionBottle image image image mesh组件-细节-变换-缩放-0.2 image

Sphere组件-细节-形状-球体半径-225 image image

将 BP_HealthPotion 拖入关卡测试重叠效果, image 运行游戏,按~波浪键唤起控制台,输入命令showdebug abilitysystem 人物移动到红药水处,健康值增大了25点:125 然后红药水消失。 image

WangShuXian6 commented 10 months ago

5. RPG Game UI 游戏UI

控件组件 https://docs.unrealengine.com/5.3/zh-CN/widget-components-in-unreal-engine/

1. Game UI Architecture 游戏UI架构

屏幕上显示的对象称为小控件Widgets。 在虚幻中,我们创建小控件蓝图来提供游戏数据的可视化表示。 继承自UUserWidget。 小控件对象可以通过多种方式设法深入游戏代码,检索角色控制器、玩家状态、技能系统组件的指针和引用,属性集并直接访问所需的所有数据。 但这不是最好的方法。 image

在一个结构良好的程序中,应该分离关注点。 用户界面有三个不同的域。 image

1-View 第一个领域是数据视觉效果的展示,例如生命条和能力图标。 基本上所有的小控件,我们将这个域称为视图,因为它包含玩家玩游戏时的视图的所有内容。

2-Model 数据本身 玩家的生命值,玩家的等级,法力经验,技能已被解锁,它们的级别是什么以及这些技能被分配到哪些按钮。 所有这些数据都存在于游戏项目本身的代码库中,我们将这些数据称为模型,因为它对游戏的基本规则和结果进行建模。 这些数据最终将驱动在视图域中看到的小控件。

3-Controller 从模型获取数据到视图是我们需要考虑的任务。 Controller 处理从模型检索数据并将其广播到视图。 该类不仅可以负责数据的检索,还可以负责任何计算或算法。处理该数据所涉及的功能。 控制器将负责从模型中检索数据并将其传递给视图,以便它可以直观地描述数据。 现在我们不是在讨论引擎中的控制器或玩家控制器类。 我们讨论的是一个控制器类,用于将数据驱动到视图。 称为小控件控制器。 这意味着视图可以简单地关注数据应该如何接收来自任何由控制器制成广播的数据。

视图可能包含玩家可以与之交互的小控件,例如按钮。 当玩家单击按钮时,该操作可能会导致模型发生一些变化,例如增加属性或赋予玩家新的技能。 因此,控制器决定小控件交互,所产生的玩家操作导致模型发生变化。 控制器是视图和模型之间的中间人。

image

这种软件架构模式的某些实现与其他模式不同。 有时小控件控制器只负责从模型中检索数据并呈现它到视图。

但对于我们的项目来说,小控件控制器将承担更多的责任。 它将处理按钮按下和玩家将提供的信息并实际发送信号到模型。 因此模型可以通过模型视图控制器架构进行更改。 每个域都与其他域隔离。这使得系统高度模块化。 它可以防止我们对依赖项进行硬编码,从而使系统变得僵化。 我们的模型不应该需要关心使用哪些控制器或小控件来表示他们的数据。 控制器本身依赖于模型中的类。 控制器永远不需要知道哪些小控件正在接收向它们广播的数据。 这是依赖于控制器的小控件。 如果我们维护这些单向依赖关系,那么模型就可以更改其小控件控制器用于其他小控件控制器,无需更改模型中类中的任何代码。

控制器不需要知道系统有哪些类型的小控件,因为小控件依赖于于控制器。 控制器也可以切换其小控件,而无需更改控制器中的任何代码。 控制器可以是一个简单的对象,UAuraWidgetController 我们将赋予它从系统收集数据的技能并将其广播到小控件。 我们将创建与我们的小控件控制器类兼容的用户小控件的子类。UAuraUserWidget image image

2. Aura User Widget and Widget Controller 用户控件 和 控件控制器

用户控件基类 AuraUserWidget

基于 UserWidget 创建 C++ AuraUserWidget 类 UserWidget :一个通过WidgetBlueprint实现UI可扩展性的控件。 Source/Aura/Public/UI/Widget/AuraUserWidget.h image image

控件控制器 AuraWidgetController

基于 Object 创建 C++ AuraWidgetController Source/Aura/Public/UI/Controller/AuraWidgetController.h image image

控件有控制器变量

当控件控制器广播数据时,我们的控件将接收该数据并对其做出响应。 控件依赖控制器变量。 控件控制器不会知道它与哪些控件关联,但控件本身知道他们的控制器是什么。 控件控制器将需要从蓝图访问,因为所有的用户控件类基于该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"

3. Globe Progress Bar 全局进度条

基于 AuraUserWidget 创建 WBP_GlobeProgressBar 控件蓝图

Blueprints 下新建UI目录/ProgressBar目录 右键-用户界面-控件蓝图-AuraUserWidget :WBP_GlobeProgressBar image image image

尺寸框 SIze Box 控件

一个控件,允许使用者指定其报告拥有和所需的大小。并非所有控件报告的所需大小均为用户实际所需的大小。将其包装在一个SizeBox中后,大小框会将其强制设为一个特定大小。 *单一子项,固定大小

打开 BP_GlobeProgressBar 控件蓝图 添加 面板-尺寸框 SIze Box ,可填满屏幕

image 右上角选择-所需 image 在细节面板调整尺寸框宽高 image

如果我们希望能够在子类中覆盖它的宽高,那就为宽度和高度创建变量

左侧重命名 尺寸框为 SIzeBox_Root image image

点击右上方-图表 进入事件图表 image

Event pre construct: 更改变量时触发

仅客户端 由游戏和编辑器调用。允许使用者对其控件进行初始设置,以更好地在设计器中预览设置;因为运行时通常需要相同的设置代码,其也将会被调用。

警告: 这纯粹只用于使用本地拥有数据的装饰更新,使用者无法安全访问游戏相关的状态;如果调用不应在编辑器时间中运行的内容,编辑器可能崩溃 将蓝图代码保存到资产,如果计算时出现崩溃,可在编辑器偏好设置”的“控件设计器”设置中关闭PreConstruct计算。 目标是用户控件 Cosmetic. This event is only for cosmetic, non-gameplay actions.

添加变量

图表界面-左侧-变量 添加浮点类型变量 BoxWidth :尺寸框宽度 BoxHeight :尺寸框高度

image

编译,然后为变量设置默认值 细节-高级-默认值-BoxWidth-250 image 细节-高级-默认值-BoxHeight-250 image

可以在 Event pre construct 事件中设置变量调整尺寸框宽高

设计器中-将控件设置为变量

设计器界面-选择控件层级、 右上方-细节-SIzeBox_Root-是变量-启用 此时 尺寸框 SIzeBox_Root 控件将可当作变量使用和设置 image image

图表中 调整尺寸框 SIzeBox_Root 控件宽高

图表 界面-拖入 尺寸框 SIzeBox_Root 控件,从SIzeBox_Root输出 布局-尺寸框-Set Width Override 函数节点用以设置宽度。 image image

添加 BoxWidth 变量,输出至 Set Width Override 的 In Width Override 节点,以设置宽度的值。 image

拖入 尺寸框 SIzeBox_Root 控件,从SIzeBox_Root输出 布局-尺寸框-Set Height Override 函数节点用以设置宽度。 image

选择所有节点-右键-折叠到函数-生成新的折叠函数节点-重命名为 Update Box Size 节点将作为函数变量 image image

Event pre construct 事件更改宽高将实时影响 图表中控件的宽高

overlay 覆层 控件

控件可互相重叠在彼此之上,使用每个图层内容的简单流布局。

设计器面板-添加 面板-覆层 放置到 SIzeBox_Root 控件的子级

重命名为:Overlay_Root image image image

之后可以在 Overlay_Root 之上放置控件。

image 图像 控件

允许显示Slate笔刷、或UI中纹理/材质的图像控件 *无子项

添加 通用-图像 控件 到 Overlay_Root 子级 名称:Image_Background

image image

调整 Image_Background 填充满 Overlay_Root 作为背景图片。

细节-插槽(覆层插槽)-填充-水平对齐-水平填充 细节-插槽(覆层插槽)-填充-垂直对齐-垂直填充 image

选择 Image_Background,标记为 变量 image

图表-为 Image_Background 添加变量 BackgroundBrush 类型:Slate Brush / Slate 笔刷 用来设置图片纹理资源

image image

变量分组

图表界面-BoxWidth-细节-类别-GlobeProperties [球体属性] image

image

设置 Image_Background

添加 节点: Image_Background BackgroundBrush Set Bursh

image

为 BackgroundBrush 选取纹理图片

BackgroundBrush 节点 -细节-默认值-Background Brush-图像-GlobeRing image image

将 设置 Image_Background 节点折叠到函数 UpdateBackgroundBrush

image

Progress Bar 进度条 控件

添加 通用-Progress Bar 进度条 控件 到Overlay_Root 子级 名称:ProgressBar_Globe image

设置以填充整个 Overlay_Root image

Progress Bar -细节-样式-样式-填充图-图像-MI_HealthGlobe 细节-样式-样式-填充图-绘制为-图像 【否则铺满界面为方形】 细节-样式-样式-进度-百分比-1 外观-填充颜色和不透明度-1,1,1,1 白色 image

去掉灰色背景: 细节-样式-样式-背景图-着色-不透明度为0 即 :1,1,1,0 image

进度条方向-由下向上: 细节-进度-条填充类型-底到顶 image

设置 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 image

当前节点部分折叠到函数 UpdateGlobeImage

image image

ProgressBar_Globe 设置进度条填充

图表: 拖入变量 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 image

GlobePadding 节点输出至Make Margin的Left,Top,Right,Bottom image

折叠到函数 UpdateGlobePadding image

设计器: 细节-插槽--填充-10 使金属圈边缘显示 image image

以上只是配置控件外观,非运行时设置。

添加炫光图片前景

设计器: 添加Image图片控件到Overlay_Root 子级 名称:Image_Glass image 填充整个Overlay_Root区域 设为变量 image

图表: 添加 Slate 笔刷/Slate Brush 类型变量:GlassBrush 类别:GlobeProperties GlassBrush -细节-默认值-Glass Brush -图像-MI_EmptyGlobe image

添加节点: Image_Glass 从 Image_Glass 拖出 外观-Set Brush 函数 Glass Brush 变量节点

折叠到函数 UpdateGlassBrush

设计器: Image_Glass 控件的 外观-笔刷-图像属性已设置为MI_EmptyGlobe image image

细节-插槽-填充-10

设置 ProgressBar_Globe 控件-插槽-填充-10

image

设置炫光图片的填充

图表: 添加节点: 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

BPGraphScreenshot_2024Y-01M-08D-14h-27m-57s-137_00

现在可以基于 WBP_GlobeProgressBar 制作控件蓝图,然后设置各类属性,各属性可公开。

4. Health Globe 健康值显示球体

基于 WBP_GlobeProgressBar 新建控件蓝图 WBP_HealthGlobe

右键-用户界面-控件蓝图 选择 WBP_GlobeProgressBar image image

打开 WBP_HealthGlobe 图表-显示继承的变量 image image

变量 ProgressBarFillImage-细节-默认值-Progress Bar Fill Image-图像-MI_HealthGlobe image image

WBP_Overlay 覆盖层控件蓝图

UI 目录下新建Overlay 目录 基于 AuraUserWidget 新建控件蓝图 WBP_Overlay image WBP_Overlay 是屏幕上的整体控件蓝图,包含多个控件。

打开 WBP_Overlay 添加控件 canvas 面板-画布画板。 image image image

尺寸框与覆层效率更高。 画布画板比较消耗性能。 所以只会为整个应用程序提供一个整体覆盖画布画板控件。

添加控件 用户创建-WBP_HealthGlobe image image image WBP_HealthGlobe-细节-插槽-锚点-中间底部 使WBP_HealthGlobe的锚点 对齐到画布画板的中间底部 image image WBP_HealthGlobe 相对 锚点缩放。

把 WBP_Overlay 控件添加到游戏关卡视图上

打开关卡蓝图 image image 事件图表-添加节点 用户界面-create widget class 选择 WBP_Overlay image image

从 create widget 的 输出节点拖出 add to viewport image image 这会把 WBP_Overlay 控件添加到游戏视图上

运行游戏,可看到健康进度球。与视图大小一起缩放。 image

基于 WBP_GlobeProgressBar 新建控件蓝图 WBP_ManaGlobe 魔力进度球

打开 WBP_HealthGlobe 图表-显示继承的变量 变量 ProgressBarFillImage-细节-默认值-Progress Bar Fill Image-图像-MI_ManaGlobe image image

将 WBP_ManaGlobe 添加到 WBP_Overlay

打开 WBP_Overlay 添加控件 WBP_ManaGlobe 到 画布画板直接子级 image

WBP_ManaGlobe-细节-插槽-锚点-中间底部 WBP_ManaGlobe-细节-插槽-尺寸X,尺寸Y-250

image

将 WBP_ManaGlobe ,WBP_HealthGlobe 的位置Y设置一样的值 image image

5. Aura HUD

不使用关卡蓝图添加 控件到视图。 删除 关卡蓝图事件图表的控件节点。 通过GameMode中设置HUD来添加控件到视图。

基于 HUD 创建 C++ AuraHUD 类

image image

蓝图中设置 OverlayWidgetClass,然后C++将其添加到视口。 image

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();

}

基于 AuraHUD 类新建 蓝图 BP_AuraHUD

在 Blueprints/UI 目录下新建目录 HUD image image

打开 BP_AuraHUD 细节-Aura HUD-Overlay Widget Class-WBP_Overlay image

BP_AuraGameMode 设置 使用 BP_AuraHUD

打开 BP_AuraGameMode 细节-类-HUD类-BP_AuraHUD image

运行游戏 image

6. Overlay Widget Controller 覆盖层控件控制器

控件依赖控件控制器 为了方便设置玩家控制器,玩家状态,技能系统组件,属性集,创建包含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;
}

基于 C++ AuraWidgetController 创建类 OverlayWidgetController 覆盖控件控制器

image

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"

在 AuraHUD中构造控件和控件控制器

游戏中只有一个 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();
}

在 初始化技能组件Actor信息的时候初始化覆盖控件 InitOverlay

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.cppAAuraPlayerController::BeginPlay() 只有本地玩家能获得有效的子系统 Subsystem, 应该使用if 而非 check. 所以修改 其中部分代码为

    // check(Subsystem);
    if (Subsystem)
    {
        //添加输入映射情景,可以同时有多个输入映射情景
        //当前只添加一个输入映射情景,优先级为0.
        Subsystem->AddMappingContext(AuraContext, 0);
    }

BP_AuraHUD 设置 控件控制器

打开 BP_AuraHUD 蓝图 细节-Aura HUD-Overlay Widget Controller Class-OverlayWidgetController OverlayWidgetController 为C++类,临时设置,之后将设置为蓝图类。 image

运行游戏 ,覆盖控件可以显示 image

现在,可以在控件 WBP_Overlay 中访问控件控制器

打开 控件 WBP_Overlay 图表: 添加事件:Event Widget Controller Set 添加节点: Widget Controller 从 Widget Controller 拖出节点 Get Object Name [访问控件控制器实例的名称] Get Object Name 输出到 Print String 的 In String 节点 image 运行游戏将显示控件控制器实例名称,这在控件控制器被设置时触发。 image

删除该测试节点。

现在,控件控制器可以访问 玩家控制器,玩家状态,技能系统组件,属性集。

优化 变量类型 TObjectPtr 方便调试

Source/Aura/Public/Player/AuraPlayerController.h

    // 帧更新之前光标跟踪的Actor
    TObjectPtr<IEnemyInterface> LastActor;

    // 光标跟踪的当前Actor
    TObjectPtr<IEnemyInterface> ThisActor;

7. Broadcasting Initial Values 广播初始值

委托 在C++对象上引用和执行成员函数的数据类型

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, ...) DECLAREDELEGATEParams(DelegateName, Param1Type, Param2Type, ...)
Function() DECLARE_DELEGATE_RetVal(RetValType, DelegateName)
Function(Param1) DECLARE_DELEGATE_RetVal_OneParam(RetValType, DelegateName, Param1Type)
Function(Param1, Param2) DECLARE_DELEGATE_RetVal_TwoParams(RetValType, DelegateName, Param1Type, Param2Type)
Function(Param1, Param2, ...) DECLARE_DELEGATERetValParams(RetValType, DelegateName, Param1Type, Param2Type, ...)

委托函数支持与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...

委托签名声明可存在于全局范围内、命名空间内、甚至类声明内。此类声明可能不在于函数体内。

组播委托/多播委托/Multi-cast Delegates]

https://docs.unrealengine.com/5.3/zh-CN/multicast-delegates-in-unreal-engine/ 可以绑定到多个函数并一次性同时执行它们的委托。

多播委托拥有大部分与单播委托相同的功能。它们只拥有对对象的弱引用,可以与结构体一起使用,可以四处轻松复制等等。 就像常规委托一样,多播委托可以远程加载/保存和触发;但多播委托函数不能使用返回值。它们最适合用来 四处轻松传递一组委托。

动态委托/Dynamic Delegates

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_Overlay

设计器: 将控件 WBP_HealthGlobe ,WBP_ManaGlobe 设置为变量。 image image

图表: 添加事件: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。 image

BP_OverlayWidgetController

基于C++ OverlayWidgetController ,在 Blueprints/UI/WIdgetController 目录下新建 蓝图类 BP_OverlayWidgetController image

打开 BP_OverlayWidgetController

BP_AuraHUD 使用 BP_OverlayWidgetController

打开 BP_AuraHUD 细节-Aura HUD-Overlay Widget Controller Class-BP_OverlayWidgetController image

打开 WBP_HealthGlobe

图表: 添加事件: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

image

设计器的 控件由父类 WBP_GlobeProgressBar 控制,在 WBP_HealthGlobe 上无法更改百分比进度。

打开 WBP_GlobeProgressBar 为 WBP_HealthGlobe 提供设置进度百分比事件 SetProgressBarPercent

图表: 左侧添加函数:SetProgressBarPercent image 选择 函数节点 SetProgressBarPercent 细节-输入-添加一个浮点输入 Percent image 拖入变量 ProgressBar_Globe 从 Progress Bar Globe 拖出 Set Percent 将 SetProgressBarPercent 的 Percent 节点输入至 Set Percent 的 In Percent 节点

image

WBP_HealthGlobe 调用 WBP_GlobeProgressBar 的 SetProgressBarPercent

使用广播接受的值设置百分比

添加节点: Set Progress Bar Percent 函数节点 Health 变量 MaxHealth 变量 Safe Divide [相除的两个数如果有0则返回0] 从事件拖出 流程控制-Sequence

将 cast to BP_OverlayWidgetController 的输出提升为变量 BPOverlayWidgetController

每当设置 Health,MaxHealth时,都会重新设置百分比。 BPGraphScreenshot_2024Y-01M-08D-22h-34m-35s-990_00

运行游戏,健康值百分比为100.

image

8. Listening for Attribute Changes 监听属性更改

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);
}

技能系统组件不依赖控件控制器。 控件控制器不依赖控件。

9. Callbacks for Mana Changes 魔力变更回调

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_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]

BPGraphScreenshot_2024Y-01M-08D-23h-50m-53s-926_00

拾取红药水时,健康增加25点,魔力减少25点。

WangShuXian6 commented 10 months ago

6. Gameplay Effects / Gameplay效果

https://docs.unrealengine.com/5.3/zh-CN/gameplay-effects-for-the-gameplay-ability-system-in-unreal-engine/

Gameplay技能系统会利用 Gameplay效果 更改Gameplay技能所针对Actor的属性。Gameplay效果包含你可以应用到Actor属性的函数库。这些效果可以是即时效果,比如施加伤害,也可以是持续效果,比如毒杀,在一定的时间内对角色造成伤害。

你可以使用Gameplay技能效果进行增益和减益。

根据你的游戏设计,让你的角色变得更强或更弱。

Gameplay效果属于资产,因此在运行时不可变。

也有例外情况,例如,当在运行时创建Gameplay效果,但未在创建和配置时修改数据的情况。

Gameplay效果规格 是Gameplay效果的运行时版本。

它们是Gameplay效果的实例数据封装器(Gameplay效果是一种资产)。 蓝图功能本身与Gameplay效果规格相关,而不是与Gameplay效果相关,这体现在技能系统蓝图库中。

Gameplay效果规格会添加到Actor的技能系统组件。

这适用于持续效果,并且可以设置为在失效前具有有限的生命周期,然后删除效果,撤销对目标Actor Gameplay属性 的更改。

Gameplay效果生命周期

Gameplay效果的 时长(Duration) 可设置为 即时(Instant) 、 无限(Infinite) 或 有持续时间(Has Duration) 。 具有持续时间的Gameplay效果将添加到 激活Gameplay效果容器(Active Gameplay Effects Container) 。激活Gameplay效果容器是技能系统组件的一部分。

image 即时的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效果如何呈现的 Gameplay效果组件 (GEComponents)。 Gameplay效果可以:

更改对其应用Gameplay效果的Actor的Gameplay标签,或根据条件删除其他激活的Gameplay效果。

你可以创建自己的游戏才有的Gameplay效果组件,组件有助于扩展Gameplay效果的可用性。 Gameplay效果组件的实现者必须仔细阅读Gameplay效果流程,并注册全部所需的回调以实现所需效果,而非提供适用于所有所需功能的更大型API。这样会将Gameplay效果组件的实现限制到原生代码。

GEComponents存在于Gameplay效果中,Gameplay效果是一种仅限数据的蓝图资产。因此,和Gameplay效果一样,所有应用实例仅存在一个GEComponent。

image 下表包含完整的可用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属性(Gameplay Attribute) 包含Actor当前状态的测量值,当前状态可通过单浮点值描述,如:

生命值

物理力量

移速

魔抗

等等。属性在属性集中声明为FGameplayAttribute类型的UProperties,属性集包含各种属性并监督所有对属性进行修改的尝试。

属性和属性集必须以原生代码创建,无法在蓝图中创建。

创建属性集

遵照以下步骤创建属性集

从UAttributeSet继承类,然后添加标记为UPROPERTY的Gameplay属性数据成员。例如,属性集仅包含类似如下的"生命值"属性:

创建属性集后,你必须通过技能系统组件注册属性集。你可以将属性集添加为技能系统组件拥有Actor的子对象,或将其传递给技能系统组件的GetOrCreateAttributeSubobject函数。

编程效果和属性交互

可以重载属性集的函数有多个,这些函数可用于处理Gameplay效果尝试修改属性时属性的响应方式。例如,示例USimpleAttributeSet中的"生命值"属性可以存储浮点值,该值可以通过Gameplay技能系统访问或更改。目前,当生命值降至零后,实际上什么也不会发生,也没有什么会阻止其下降至零以下。

要使"生命值"属性以你想要的方式呈现,属性集本身可通过重载多个虚拟函数来介入,虚拟函数负责处理对其属性的修改尝试。

以下函数通常由属性集重载:

函数名称 用途
PreAttributeChange / PreAttributeBaseChange 这些函数在即将修改属性之前调用。函数旨在实施关于属性值的规则,例如,"生命值必须介于0和最大生命值"之间,并且不得对属性更改触发游戏内响应。
PreGameplayEffectExecute 在即将修改属性值之前,此函数可以拒绝或更改拟定修改。
PostGameplayEffectExecute 在修改属性值后,此函数可立即对更改做出响应。这通常包括限制属性的最终值或触发对新值的游戏内响应,例如当"生命值"属性降至零时死亡。

1. Gameplay Effects / Gameplay效果

游戏效果是 UGameplayEffect 类型 的对象。 我们使用游戏效果来改变属性和游戏标签。 游戏效果仅是数据。我们不给它们添加逻辑。

游戏效果通过修改器和执行来改变属性。 image image

2. Effect Actor Improved /效果 Actor 改进

效果 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 设置红药水外观

打开 BP_HealthPotion image

添加 StaticMesh 静态网格体组件:PotionMesh 到 根组件 SceneRoot

image PotionMesh-细节-静态网格体-静态网格体-SM_PotionBottle PotionMesh-细节-变换-缩放-0.2 不缩放根组件。 image

PotionMesh-细节-碰撞-碰撞预设-NoCollision 防止网格体与其他物体碰撞。

添加 Sphere Collision 球体碰撞:Sphere 到 根组件 SceneRoot

image image

Sphere-细节-碰撞-碰撞预设-使用默认 OverlapAllDynamic image

事件图表

选择组件 Sphere 添加事件:On Component Begin Overlap (Sphere)

AuraEffectActor C++ 应用效果

只要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());
}

3. Instant Gameplay Effects 即时游戏效果

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

基于 GameplayEffect 类新建蓝图 GE_PotionHeal 游戏效果 治疗

image image

打开 游戏效果 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 image

当我们应用此效果时,我们将获取生命值,为其添加 25 的值,效果将立即应用。

在 BP_HealthPotion 蓝图类中通过重叠事件,使用 ApplyEffectToTarget 对玩家应用即使游戏效果

打开 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

image

选择 Instant Gameplay Effect Class 节点 细节-默认值-Instant Gameplay Effect Class-GE_PotionHeal 游戏效果 image

与红药水重叠的玩家被应用游戏效果后销毁红药水自身。 玩家健康值增加25点。

魔力药水

image

基于 GameplayEffect 类新建蓝图 GE_PotionMana 游戏效果 魔力回复

打开 GE_PotionMana image

基于 AuraEffectActor 新建蓝图 BP_ManaPotion 魔力药剂

打开 BP_ManaPotion 细节-Instant Gameplay Effect Class-GE_PotionMana 游戏效果 image

基于 SM_PotionBottle 静态网格体 制作魔力药水瓶 SM_ManaPotionBottle

SM_ManaPotionBottle 使用材质实例 MI_BlueLiquid image

添加 StaticMesh 静态网格体组件:PotionMesh 到 根组件 SceneRoot

PotionMesh-细节-静态网格体-静态网格体-SM_ManaPotionBottle PotionMesh-细节-变换-缩放-0.2 不缩放根组件。

PotionMesh-细节-碰撞-碰撞预设-NoCollision 【可以省略这个设置】 防止网格体与其他物体碰撞。

image image

添加 BoxCollision 盒体碰撞:Box 到 根组件 SceneRoot

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

image 与魔力药水重叠的玩家被应用游戏效果后销毁红魔力药水自身。 玩家魔力值增加30点。

4. Duration Gameplay Effects 持续时间游戏效果/有持续时间

持续时间结束后效果自动移除或撤销。

Source/Aura/Public/Actor/AuraEffectActor.h

protected:
    // 持续时间游戏效果 在蓝图中指定
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
    TSubclassOf<UGameplayEffect> DurationGameplayEffectClass;

基于 GameplayEffect 类新建蓝图 GE_CrystalHeal 持续时间游戏效果 持续治疗水晶

image

打开 GE_CrystalHeal

细节-持续时间-Duration Policy-Has Duration 有持续时间 细节-持续时间-Duration Magnitude-Magnitude calculation Type-scalable float 细节-持续时间-Duration Magnitude-Scalable Float Magnitude-2 【该效果持续2秒,然后自行消失】 image

细节-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] image

基于 AuraEffectActor 新建蓝图 BP_HealthCrystal

image

打开 BP_HealthCrystal 细节-Duration Gameplay Effect Class-GE_CrystalHeal 游戏效果 image

添加 StaticMesh 静态网格体组件:CrystalMesh 到 根组件 SceneRoot

CrystalMesh-细节-静态网格体-静态网格体-SM_HealthCrystal CrystalMesh-细节-变换-缩放-0.2 不缩放根组件。

CrystalMesh-细节-碰撞-碰撞预设-NoCollision 【可以省略这个设置】 防止网格体与其他物体碰撞。 image

添加 Capsule Collision 胶囊体体碰撞:Capsule 到 根组件 SceneRoot

Capsule-细节-形状-胶囊体半高-24 Capsule-细节-形状-半径-14 Capsule-细节-碰撞-碰撞预设-使用默认 OverlapAllDynamic image

进入顶视图移动 静态网格体

事件图表

选择组件 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

image 与健康水晶重叠的玩家被应用游戏效果2秒后销毁健康水晶自身。 玩家最大健康值增加100至200,持续2秒。 2秒后玩家最大健康值恢复为原来的100. image image image 仅为演示持续时间效果。

5. Periodic Gameplay Effects 周期性游戏效果

周期性效果在每个周期执行。 定期执行某种更改。

image

游戏属性具由 BaseValue 基础值 和 Current Value 当前值。

即时效果似乎是永久性的,这是因为即时效果会影响具有永久变化的基值。 当我们应用即时效果时,我们不会将该更改应用于当前值,因为我们以后不要打算撤消它。

这与持续时间效果和无限效果的工作方式不同。 持续时间效果和无限效果会修改Current Value 当前值。如果效果被移除,则可以撤消该值。

基于持续时间的效果会自行消失。 设置持续时间两秒,两秒后效果就会被删除,这意味着2秒后撤消对属性的更改。 无限效果的行为类似。唯一的区别是它不会自行移除。如果您想删除它们,则必须手动执行此操作。

持续时间和无限效果可以转换为周期性效果,这意味着它们执行定期对属性进行某种更改,只要该效果当前有效。 周期性的游戏效果是一种持续时间效果和无限效果的特殊类型, 但是周期性的变化执行时被视为即时游戏效果,并且永久更改基本值。不可以撤消。

如果设置为周期性的持续时间效果或无限游戏效果被删除,所有改变都会生效。 每个周期发生的改变都是永久性的。当效果被移除时,它们不会被撤销。

可以将任何持续时间效果或无限游戏效果转换为周期性游戏效果, 只需将其周期更改为非零值即可。

健康水晶的周期性游戏效果 GE_CrystalHeal

打开 GE_CrystalHeal image

细节-持续时间-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] image

每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 设置周期。 只有持续效果或无线效果才可以设置为周期性效果。

魔力水晶

image

基于 GameplayEffect 类新建蓝图 GE_CrystalMana 持续时间游戏效果 持续治疗水晶

打开 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]

image

在 3秒的持续时间内,每隔0.1秒魔力值增加1,并在开始时立即增加1。3秒后不再增加。

基于 AuraEffectActor 新建蓝图 BP_ManaCrystal

打开 BP_ManaCrystal 细节-Duration Gameplay Effect Class-GE_CrystalMana 游戏效果

添加 StaticMesh 静态网格体组件:CrystalMesh 到 根组件 SceneRoot

CrystalMesh-细节-静态网格体-静态网格体-SM_Shard_debris CrystalMesh-细节-变换-缩放-0.2 不缩放根组件。

CrystalMesh-细节-碰撞-碰撞预设-NoCollision 【可以省略这个设置】 防止网格体与其他物体碰撞。

添加 Sphere Collision 球体体碰撞:Sphere 到 根组件 SceneRoot

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

image

魔力值在3秒内 将从 10 增加到41后停止。魔力水晶消失。

6. Effect Stacking 效果叠加

打开 GE_CrystalMana 魔力水晶 游戏效果 细节-Stacking-Stacking Type-None: 将 3个 BP_ManaCrystal 魔力水晶放入关卡并互相靠近。 与角色重叠时,角色魔力快速增加。但这不是真的堆叠。 这只是应用了3次游戏效果。效果相互独立。 3个是1个的3被效果。

假设我们想拾取一颗水晶,而该水晶的持续时间为两秒。 如果在那两秒钟内我们拿起另一个水晶怎么办? 我们不希望我们的法力以两倍的速度增长。 我们仍然希望以相同的速度增加法力,但我们希望刷新该持续时间。

假设我们拿起第一个水晶,一秒钟后,我们拿起第二个水晶。 也许我们只是想刷新持续时间,这样我们的法力就能再增加两秒。

Stacking Type 堆叠类型:

将3个魔力水晶堆叠,使玩家同时接触3个。 image

细节-持续时间-Period-Period 周期-0.1 细节-持续时间-Duration Magnitude-Scalable Float Magnitude-1 【持续时间改为1秒。】 细节-持续时间-Period-Execute Periodic Effect on Application 对应用程序执行周期性效果-禁用

这意味着在一秒钟内我们将执行修饰符十次, 每次法力值增加了 1 点,即每0.1秒增加1,法力值在一秒的时间内总共增加了 10 点法力值.

Aggregate by Source 按来源聚合

来源实际上指的是导致这种情况的技能系统组件。 玩家的技能系统组件存在与玩家状态,敌人的存在于敌人。各自只有一个。 在我们的例子中,无论我们选择源还是目标,我们都会得到堆栈限制2。魔力只能增加20. 每个来源限制2个同时生效,当前只有一个来源【玩家状态】 如果玩家同时接触3个。则只有2个生效,一共增加20点魔力。

image 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。 如果相同的源应用相同的效果,这将再次是第三个堆栈,但这超出了极限。 所以这个效果不能应用。 现在我们按源聚合,这意味着每个源都会保留该特定堆栈的计数源造成的影响。

image

Aggregate by Target 按目标聚合

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 清除整个堆栈

每个目标都有自己的堆叠 image

每个目标限制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 刷新持续时间 这本质上使效果的持续时间无限,因为它不断刷新。

GE_CrystalMana 魔力水晶 游戏效果

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点。因为之前的水晶效果被之后的延长了。 image

GE_CrystalHeal 健康水晶 游戏效果

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 清除整个堆栈

7. Infinite Gameplay Effects 无限游戏效果

Source/Aura/Public/Actor/AuraEffectActor.h

protected:
        // 无限时间游戏效果 在蓝图中指定
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
    TSubclassOf<UGameplayEffect> InfiniteGameplayEffectClass;

BP_FireArea 火焰区域

在Blueprints/Actor/Area目录下: 基于 AuraEffectActor 新建蓝图 BP_FireArea image

打开 BP_FireArea 添加 Box Collision 盒子组件 Box 到 Root组件下。

添加 Niagara 粒子系统组件到 Root组件下。 Niagara 粒子系统组件-细节-Niagara-Niagara系统资产-NS_Fire

当角色与火焰区域重叠时,应用无限游戏效果。

基于 GameplayEffect 类新建蓝图 GE_FireArea 火焰区域游戏效果

打开 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

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 节点

当玩家与火焰区域重叠时,健康值会无限减少。 但玩家离开火焰区域依然在减少健康值。 必须手动删该无限效果。

8. Instant and Duration Application Policy 即时和持续时间效果应用策略

9. Infinite Effect Application and Removal 无限效果的应用和删除

一旦您应用了游戏效果,该游戏效果就会变为活动状态,并且这些应用功能返回该效果的句柄。 所以我们以后总是可以使用该句柄,例如如果它是无限时间游戏效果,则将其效果删除。

需要小心我们这样做的方式。 可能有多个参与者重叠。 其中一些可能有技能系统组件,有些可能没有。 我们不能仅仅假设目标 actor 和重叠是任何给定效果句柄的正确 actor。 如果多个Actor重叠并且我们设置一个效果句柄变量,那么我们将覆盖它并丢失该变量之前存储的效果句柄。

当我们应用游戏效果时,如果该游戏效果是无限持续时间游戏效果,我们可以存储手柄,但我们也可以存储我们正在应用这个效果的Actor。实际上可以存储技能系统组件,代替拥有ASC的Actor。

C++

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

打开 BP_FireArea 事件图表: 修改节点 删除事件外的全部节点。 添加 调用函数-On Overlap 函数节点。【在父类中处理ApplyEffectToTarget等等】

选择Box组件,添加事件 On Component End Overlap(Box) 添加 调用函数-On End Overlap 函数节点。 image

配置BP_FireArea类 image

此时离开火焰区域将停止减少健康值。

GE_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个火焰在一起。 image 重叠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点健康 方便测试

10. PreAttributeChange 预属性更改

https://docs.unrealengine.com/5.3/en-US/API/Plugins/GameplayAbilities/UAttributeSet/PreAttributeChange/

对属性修改添加限制。 预属性更改是我们可以修改属性当前值的更改的地方。 并且该函数在更改实际发生之前被调用。 预属性更改是由属性更改触发的,无论是通过属性访问器还是 游戏效果创建的设置器。 从字面上看,任何改变属性的东西都可以触发这个函数。

这不会永久更改给定属性的修饰符。 它只是查询属性修饰符返回的值。

我们可以在变化发生之前钳制变化,但是后面有一些操作发生在预属性更改之后, 这些将重新计算所有涉及的修饰符的当前值。

因此,如果由于任何原因,这之后的修改器导致健康值发生变化,将重新计算健康,这意味着我们的夹紧将不起作用,我们将有稍后再次夹紧。

所以预属性改变并不是实现最终夹紧改变最有效的选择。

最有效的限制是 PostGameplayEffectExecute 这是在游戏效果更改属性之后发生的事件。 image

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());
    }
}

11. PostGameplayEffectExecute 游戏效果后执行

游戏效果后执行,在游戏属性改变后执行。 可以访问到大量属性信息。

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);
}

12. Curve Tables for Scalable Floats 数值曲线表

为 GE_PotionHeal 健康治疗效果添加 不同等级的治疗数值曲线表 CT_PotionHeal

打开 GE_PotionHeal 游戏效果 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude- 25,使用 Curve Tables 曲线表 image

在 内容-Blueprints-Actor-Potion 目录下新建 曲线表格 右键-其他-曲线表格-创建曲线表-CT_PotionHeal

image image image

打开 CT_PotionHeal 曲线表格

image

添加新列: image

表头表示等级 1-10,下面表示该等级对应的治疗量 5.0 - 100.0 不同等级的健康药剂对玩家增加不同的健康值。 image

切换到曲线视图: 线性 image 重命名曲线名:HealingCurve image

GE_PotionHeal 健康治疗效果 使用 CT_PotionHeal 治疗曲线表格

打开 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 image 细节-Gameplay Effect-Modifiers-索引0-Modifier Magnitude-Scalable Float Magnitude-选择曲线 HealingCurve image

治疗效果增加的值为 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,

image

C++ 每个 AuraEffectActor 效果Actor 需要有自己的等级值 ActorLevel

代码更新为使用等级变量 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

打开 BP_HealthPotion 蓝图-细节-Applied Effects-Actor Level-1 可修改等级 image

在关卡中拖入多个 BP_HealthPotion 实例,可与i单独修改每个实例的 Actor Level image

在一个曲线表格中包含多个曲线 包含健康曲线,魔力曲线

将 CT_PotionHeal 重命名为 CT_Potion CT_Potion 添加曲线 ManaCurve image 曲线视图中,可以拖动点修改数值。

GE_PotionMana 游戏效果使用 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 image

WangShuXian6 commented 10 months ago

7. Gameplay Tags / Gameplay标签 分层标签系

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 是最具体的。

1. Gameplay Tags / Gameplay标签

定义Gameplay标签

你必须将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 (+)) 按钮。

点击新索引的下拉菜单并选择你的数据表。

使用C++定义标签

你可以使用 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标签容器(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标签查询(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 文件。

简化C++中的标签访问

你可以使用IGameplayTagAssetInterface改进你的Gameplay标签实现。该接口提供了以下优势:

你不用显式将对象转型就可以获取对象的标签。

你可以为每种可能的类型编写自定义代码。

实现该接口并重载GetOwnedGameplayTags函数,就能创建一种能够被蓝图访问的方法,来为Gameplay标签容器填充与该对象关联的标签。在大部分情况下,这意味着将基类中的标签复制到新容器中,但你的实现可以从多个容器收集标签,或调用蓝图函数以访问蓝图声明的标签或你的对象需要的任意内容。

关于该实现的示例,请参阅Lyra示例游戏项目中的 ALyraTaggedActor 类。

Gameplay标签

image

游戏标签本质上是分层的。 层次结构的每个级别都用点分隔。 类型:FGameplayTag

使用游戏标签容器存储游戏标签。 游戏标签容器具有标签映射计数的概念, 标签映射计数告诉我们容器中存在多少给定标签。

2. Creating Gameplay Tags in the Editor 在编辑器中创建游戏标签

游戏标签是整个项目范围内的。 是在游戏标签管理器中注册的。

项目设置-项目-GameplayTags image

Gameplay标签-Gameplay标签列表-管理Gameplay标签- 有默认的标签 image 点击加号添加新标签 image 添加标签 Attributes.Vital.Health
注释:属性.重要.健康 玩家在死亡前可以承受的伤害量 源:DefaultGameplayTages.ini image image

添加标签 Attributes.Vital.Mana 添加标签 Attributes.Vital.MaxMana 添加标签 Attributes.Vital.MaxHealth
image

标签存在于项目文件默认配置中 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="")

3. Creating Gameplay Tags from Data Tables 从数据表资产导入标签

在目录 内容-Blueprints-AbilitySystem-GameplayTags 下 右键-其他-数据表格 选择行结构: GameplayTagTableRow 名称:DT_PrimaryAttributes image image image

打开 游戏标签表格行 数据表 DT_PrimaryAttributes

添加新行 玩法标签-标签-Attributes.Primary.Strength 玩法标签-开发评论-力量 提高物理伤害

image

添加新行 玩法标签-标签-Attributes.Primary.Intelligence 玩法标签-开发评论-智力 提高魔法伤害

添加新行 玩法标签-标签-Attributes.Primary.Resilience 玩法标签-开发评论-恢复力/韧性 提高护甲和护甲穿透力

添加新行 玩法标签-标签-Attributes.Primary.Vigor 玩法标签-开发评论-活力,提高健康值 image

制作好标签表格后,需要将其转换为游戏标签。

项目设置-项目-GameplayTags-Gameplay标签-Gameplay标签列表 数组- 新建一组标签 下拉选择 游戏标签表格行 数据表 DT_PrimaryAttributes image

Gameplay标签-Gameplay标签列表-管理Gameplay标签-可以看到从数据表导入的新标签 image

5. Apply Gameplay Tags with Effects 应用带有游戏效果的游戏标签

如何将游戏标签添加到任何给定的技能系统组件

通过游戏效果 GE_CrystalHeal 将游戏标签添加到任何给定的技能系统组件的游戏效果上

打开 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 image

细节-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-空 image

此时 该游戏效果 GE_CrystalHeal 上将具由游戏标签 Attributes.Vital.Health

C++

6. Gameplay Effect Delegates 游戏效果委托

在玩家和敌人的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();
}

7. Get All Asset Tags 获取所有资产标签

游戏效果带有游戏标签。可在技能系统组件中访问到效果规格, 再从效果规格中访问到游戏标签。 获取所有资产标签,存储到标签容器中,标签容器比数组优化更多。 控件控制器依赖技能系统组件。可接受技能系统组件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 image

8. Broadcasting Effect Asset Tags 广播效果资产标签

在 技能系统组件中定义委托 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 image image

9. UI Widget Data Table / UI 控件数据表

定义一个用于在UI显示提示消息的消息属性数据表行结构

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;
};

数据图表 DT_MessageWidgetData

在 内容-Blueprints-UI-Data 目录下新建 右键-其他-数据图表-选择 UIWidgetRow 这是在 Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h 中定义的结构体 struct FUIWidgetRow image 名称 DT_MessageWidgetData image

打开 DT_MessageWidgetData image 包含了C++ struct FUIWidgetRow 结构体定义的4个属性

添加新行,可设置各个属性 image

添加一些消息游戏标签

项目设置-项目-GameplayTags Gameplay标签-Gameplay标签列表-管理Gameplay标签-

添加标签:Message.HealthPotion 使用默认源。 image image 继续添加标签: Message.HealthCrystal Message.ManaCrystal Message.ManaPotion image

让覆盖控件WBP_Overlay知道覆盖控件控制器已接受到消息标签,先添加 消息控件数据表资产变量

Source/Aura/Public/UI/WidgetController/OverlayWidgetController.h

protected:
    // 在子类蓝图中指定消息控件数据表资产
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Widget Data")
    TObjectPtr<UDataTable> MessageWidgetDataTable;

为 BP_OverlayWidgetController 覆层控件控制器指定 消息控件数据表资产变量

打开 BP_OverlayWidgetController 细节-Widget Data-Message Widget Data Table-DT_MessageWidgetData 数据表 image 之后可以在C++和蓝图中访问该数据表

10. Retrieving Rows from Data Tables 从数据表中检索行

建议使用数据资产替代数据表。此处仅作学习数据表使用。

为 DT_MessageWidgetData 添加数据

打开 数据表 DT_MessageWidgetData

第1行: Row Name 行命名 : Message.HealthCrystal Messgae Tag : Message.HealthCrystal Message :捡起健康水晶 Message Widget : None Image : T_HealthCrystal 纹理 image

第2行: Row Name 行命名 : Message.HealthPotion Messgae Tag : Message.HealthPotion Message :捡起健康药剂 Message Widget : None Image : T_Potion_red 纹理 image

第3行: Row Name 行命名 : Message.ManaCrystal Messgae Tag : Message.ManaCrystal Message :捡起魔力水晶 Message Widget : None Image : T_ManaCrystal 纹理 image

第4行: Row Name 行命名 : Message.ManaPotion Messgae Tag : Message.ManaPotion Message :捡起魔力药剂 Message Widget : None Image : T_Potion_Blue 纹理 image

image 之后将通过行名称/标签名称查找数据。 在 覆层控件控制器的 EffectAssetTags.AddLambda 函数中执行查找。

为 游戏效果 添加对应的消息标签

为 游戏效果 健康药剂 GE_PotionHeal 添加对应的消息标签 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.HealthPotion image

为 游戏效果 魔力药剂 GE_PotionMana 添加对应的消息标签 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.ManaPotion image

为 游戏效果 健康水晶 GE_CrystalHeal 添加对应的消息标签 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.HealthCrystal image

为 游戏效果 魔力水晶 GE_CrystalMana 添加对应的消息标签 Message.ManaCrystal

打开 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 image

现在拾起对应物体会显示效果自带的游戏标签。

在 覆层控件控制器的 EffectAssetTags.AddLambda 函数中通过行名称/标签名称在数据表中查找对应的文本消息等数据。

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 中后续将消息行广播到控件

11. Broadcasting Data Table Rows 广播数据表行

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 上绑定消息委托,接受消息行数据

打开 WBP_Overlay 事件图表: 添加序列 Sequence 先将 控制器转换为 Cast To BP_OverlayWidgetController . 当子类是覆层的时候这将成功。 保存为变量 BPOverlayWidgetController

BPOverlayWidgetController 绑定消息委托 assign Message Widget Row Delegate

Message Widget Row Delegate 输出一行消息数据。拆分该数据,打印其中的Message文本 BPGraphScreenshot_2024Y-01M-10D-23h-45m-04s-170_00

捡起对应药剂将打印对应消息。

image

12. Message Widget 消息控件

基于 AuraUserWidget 创建一个消息控件 WBP_EffectMessage

用于显示控件接受到的消息数据 在目录 内容-Blueprints-UI-Overlay-Subwidget 下右键-用户界面-控件蓝图-创建 image 名称:WBP_EffectMessage

打开 WBP_EffectMessage 设计器: 填充屏幕改为自定义 image 随意调整 控件根的大小 image

添加 Horizontal Box 水平框:HorizontalBox_Root

添加 Image 图片控件: Image_Icon 到 水平框 Image_Icon-外观-笔刷-图像-T_Potion_Red Image_Icon-外观-笔刷-图像大小-x,y -75 设置为变量 image

添加 Text 文本 控件: Text_Message 到 水平框 Text_Message-内容-文本:捡起了一个健康药剂 预览效果 Text_Message-插槽-垂直对齐-垂直居中对齐 Text_Message-外观-背景颜色-A-0 设置为变量

添加 Space 间隔区 到 水平框 放置在 图片与文本之间 外观-尺寸-20,0 image

将 WBP_EffectMessage 添加到 WBP_Overlay

打开 WBP_Overlay 添加 WBP_EffectMessage 到画布面板 image image

WBP_EffectMessage 设置变量

打开 WBP_EffectMessage 图表: 左侧添加方法:SetImageAndText image

SetImageAndText-细节-输入: 添加输入: Image - Texture2D 纹理2D Text - 文本 image

拖入 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 的 文本 BPGraphScreenshot_2024Y-01M-11D-00h-41m-20s-474_00

DT_MessageWidgetData 数据表使用 WBP_EffectMessage 控件

打开 WBP_EffectMessage 为每一行的 Message Widget 指定:WBP_EffectMessage image

WBP_Overlay 的绑定消息事件 获取 Message Widget 并渲染

打开 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 以输出至视图

BPGraphScreenshot_2024Y-01M-11D-00h-56m-46s-127_00 BPGraphScreenshot_2024Y-01M-11D-00h-57m-02s-853_00

删除设计器上的测试 WBP_EffectMessage 控件 WBP_EffectMessage 控件的高度缩放至屏幕高度,发生变形。

调整 WBP_EffectMessage 控件

打开 WBP_EffectMessage 控件 HorizontalBox_Root-右键-包裹-覆层 这在HorizontalBox_Root的父级添加了覆层控件 这时 WBP_EffectMessage 不在变形 image image image

打开 WBP_Overlay

图表: 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 这将 消息控件的左上角定位在屏幕中心。

BPGraphScreenshot_2024Y-01M-11D-01h-10m-02s-256_00 image

13. Animating the Message Widget 设置消息控件的动画

WBP_EffectMessage 控件的图像输入为可选

打开 WBP_EffectMessage 控件

图表: 图像输入为可选 Set Image and Text 的 Image 拖出 工具-Is Valid Is Valid 的 Is Valid 连接 Set Brush 的执行节点

BPGraphScreenshot_2024Y-01M-11D-01h-32m-06s-335_00

为 WBP_EffectMessage 控件添加动画效果

打开 WBP_EffectMessage 控件 设计器-动画 image image

添加动画-MessageAnimation image

image

为 Text_Message 添加过渡动画 轨道-所有已命名控件-Text_Message image image

Text_Message 右侧加号-变换 image image

移动时间轴到0.25 image 选择 Text_Message - 变换-平移-y- -160 使文本上移 自动创建关键帧。

image

移动时间轴到0.45 选择 Text_Message - 变换-平移-x- 150 使文本右移 image

点击曲线编辑器 调节曲线 曲线上-右键-可以添加关键帧 调节细节 image

WBP_EffectMessage 图表 Set Image and Text

添加 变量-动画-message animation message animation 拖出 用户界面-动画-play animation

没有图片也需要播放动画。

BPGraphScreenshot_2024Y-01M-11D-02h-05m-45s-380_00

WBP_EffectMessage 事件图表

因为不能使用定时事件去销毁控件。

添加 custom event 自定义事件

名称 DestroyDelay DestroyDelay 拖出 Delay Delay : Duration =0.5 Delay 拖出 remove from parent 0.5秒后执行销毁。

image

WBP_EffectMessage 图表 Set Image and Text

添加 DestroyDelay play animation 的执行连接 DestroyDelay

BPGraphScreenshot_2024Y-01M-11D-02h-07m-35s-099_00

现在消息控件动画可以执行,然后销毁。不会造成内存泄露。

WBP_EffectMessage 添加透明动画

设计器-打开动画 MessageAnimation-Text_Message 右侧加号-渲染不透明度 image image

时间轴0-不透明度改为1 - 表示全显示 image

时间轴拖到最后-不透明度改为0 - 表示不显示 image

WBP_EffectMessage 的 Image_Icon 添加动画

选择 MessageAnimation 轨道-所有已命名控件-Image_Icon image

Image_Icon -右侧加号-变换 image image

移动时间轴到0.25 选择 Image_Icon - 变换-平移-y- -160 使图片上移 自动创建关键帧。

移动时间轴到0.45 选择 Image_Icon - 变换-平移-x- 150 使图片右移

image

WBP_EffectMessage 的 Image_Icon 添加透明动画

MessageAnimation-Text_Message 右侧加号-颜色和不透明度 image

时间轴0-颜色和不透明度-A-改为1 - 表示全显示 image

时间轴拖到最后--颜色和不透明度-A-改为0 - 表示不显示 image

14. Replacing Callbacks with Lambdas 用Lambdas替换回调

对所有属性使用相同的委托

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 的绑定

打开 WBP_HealthGlobe 图表: OnHealthChanged 事件-右键-刷新节点 image OnHealthChanged 事件 New Value 输出至 Set Health image

OnMaxHealthChanged 事件-右键-刷新节点 OnMaxHealthChanged 事件 New Value 输出至 Set Max Health image

BPGraphScreenshot_2024Y-01M-11D-02h-49m-10s-189_00

修复 WBP_ManaGlobe 的绑定

图表: OnManaChanged 事件-右键-刷新节点 OnManaChanged 事件 New Value 输出至 Set Mana OnMaxManaChanged 事件-右键-刷新节点 OnMaxManaChanged 事件 New Value 输出至 Set Max Mana BPGraphScreenshot_2024Y-01M-11D-02h-49m-57s-938_00

15. Ghost Globe

优化 WBP_GlobeProgressBar 健康与魔力进度球的动画过渡,加入插值更平滑。 打开 WBP_GlobeProgressBar

加入一张背景,延时插值到最终的位置。

M_FlowingGlobe

M_FlowingGlobe 材质能用于界面显示,是因为其 材质-材质域 -用户界面 image image

16. Properly Clamping Attributes 正确夹紧属性

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()));
    }
}
WangShuXian6 commented 10 months ago

8. RPG Attributes RPG属性

1. Initialize Attributes from a Data Table 使用数据表初始化属性

在 构造函数中直接初始化函数值不是首选方案。 使用数据表初始化属性也不是,此处使用数据表仅作学习。 【使用游戏效果初始化属性是推荐方案】

初始化主属性。

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 蓝图设置属性数据表

打开 BP_AuraPlayerState 技能系统组件已公开可见 image

为玩家状态初始化属性 技能系统组件-细节-attribute test-添加 image image 这需要具由正确行结构的数据表。

创建属性数据表 DT_InitialPrimaryValues

在目录 Blueprints-AbilitySystem-Data 下 右键-其他-数据表格-AttributeMetaData image 名称 DT_InitialPrimaryValues image 打开 DT_InitialPrimaryValues

添加一行,必须在名称中指定属性 行命名:AuraAttributeSet.Strength Base Value:10 image

为玩家状态 BP_AuraPlayerState 蓝图设置属性数据表 DT_InitialPrimaryValues

打开 BP_AuraPlayerState 技能系统组件-细节-attribute test-索引0-Attributes-AuraAttributeSet 技能系统组件-细节-attribute test-索引0-Default Starting Table-DT_InitialPrimaryValues image

运行-输入命令- showdebug abilitysystem 力量 Strength 属性 已被初始化为 10 image

属性数据表 DT_InitialPrimaryValues 添加其他属性

AuraAttributeSet.Intelligence AuraAttributeSet.Resilience AuraAttributeSet.Vigor

image image

2. Initialize Attributes with Gameplay Effects 使用游戏效果初始化属性

建议方法。 在游戏开始时设置即可。

玩家状态 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初始化属性值 新建效果 GE_AuraPrimaryAttributes

不同的玩家角色具由不同的初始化属性值

当前只为Aura角色。

在目录 Blueprints-AbilitySystem-GameplayEffects-PrimaryAttributes 下 基于 GameplayEffect 新建 游戏效果蓝图 GE_AuraPrimaryAttributes

打开 GE_AuraPrimaryAttributes

细节-Gameplay Effect-Modifiers- 添加4组元素对应4个主属性 image

细节-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

image

分别设置 AuraAttributeSet.Intelligence AuraAttributeSet.Resilience AuraAttributeSet.Vigor image

玩家角色 Aura 使用 游戏效果 GE_AuraPrimaryAttributes

打开 BP_AuraCharacter 细节-属性-Default Primary Attribute-GE_AuraPrimaryAttributes image 这对应 AuraCharacterBase 的 TSubclassOf<UGameplayEffect> DefaultPrimaryAttributes

运行游戏; showdebug abilitysystem image

3. Attribute Based Modifiers 基于属性的修改器

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 image

Attribute Based Modifiers 基于属性的修改器 能够根据其他属性更改属性。 这使得拥有从其他属性派生属性变得非常容易。

GE_TestAttributeBased

基于 Gameplay Effect 新建 GE_TestAttributeBased 蓝图类

BP_TestActor

在 目录 Blueprints-Actor-TestActor 下 基于 AuraEffectActor 新建蓝图类 BP_TestActor 打开BP_TestActor 添加 Box Collision 组件到根

事件图表: 选择 Box 添加事件 On Component Begin Overlap (Box) 添加 Apply Effect to target 从左侧选择拖入变量 Instant Gameplay Effect Class image 将 BP_TestActor 拖入关卡

细节-Applied Effects-InstantGameplayEffectClass-GE_TestAttributeBased

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 是否应该快照属性-不启用 它与何时捕获属性有关,我们何时应该使用该属性值? 应该在创建导致此游戏效果的游戏效果规格时使用它还是在它被应用时。 快照决定了我们捕获该属性的确切时间。 image

当前玩家 健康值50,活力值9,当玩家与BP_TestActor重叠时玩家应用效果 GE_TestAttributeBased,健康值增加9,成为59. image image

再 增加一个 Modifier 修改器

细节-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

image image

4. Modifier Order of Operations 修改器操作顺序

将健康初始值改为 10 方便计算

Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp

UAuraAttributeSet::UAuraAttributeSet()
{
    InitHealth(10.f);
    InitMaxHealth(100.f);
    InitMana(10.f);
    InitMaxMana(50.f);
}

再 增加一个 Modifier 修改器

细节-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 是否应该快照属性-不启用 image

image

将第2个修改器即索引1的修改器 Modifier Op-设置为 Multiply 乘法

细节-Gameplay Effect-Modifiers-索引-Modifier Op- Multiply 结果为( (Health + Vigor) * Strength)+Resilience =202 按顺序基于上一个修改器的结果进行操作。 image

为方便测试,暂时取消 Source/Aura/Private/AbilitySystem/AuraAttributeSet.cpp中的限制大小 PreAttributeChange PostGameplayEffectExecute

image

将第3个修改器即索引2的修改器 Modifier Op-设置为 Divide 除法

image 细节-Gameplay Effect-Modifiers-索引-Modifier Op- Divide 结果为( (Health + Vigor) * Strength)/ Resilience =15.83

image

再 增加一个 Modifier 修改器

细节-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

5. Modifier Coefficients 修改器系数

Modifier Magnitude-Attribute Based Magnitude-Coefficient 系数 系数到属性计算

Modifier Magnitude-Attribute Based Magnitude-Pre Multiply Additive Value 预乘叠加值 属性计算的附加值,在应用系数之前添加

Modifier Magnitude-Attribute Based Magnitude-Post Multiply Additive Value 乘法后叠加值 添加值到属性计算中,在系数应用后添加

工作原理: image 计算方式:(系数 * (捕获的游戏属性 + 预乘叠加值))+ 乘法后叠加值 image

多个修改器: 个修改器分别计算出效果值,再依次为前一个效果器叠加。 image image

删除第4个修改器

image image image

6. Secondary Attributes 次要属性

部分属性完全依赖其他属性 image

主要属性

主要属性将是不依赖于任何其他属性的独立属性。 可以根据不同的情况增加或减少它们。

所有其他属性都以某种方式依赖于这些属性。

四个主要属性:力量 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。可获得的最大法力值

C++

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);
}

7. Derived Attributes 派生属性

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()));
    }
}

为次要游戏属性制作无限时间游戏效果 GE_SecondaryAttribute

次要游戏属性始终跟随主属性变化。

重命名目录 PrimaryAttributes 为 DefaultAttributes

在 目录 Blueprints-AbilitySystem-GameplayEffects-DefaultAttributes 下 基于 Gameplay Effect 新建 GE_SecondaryAttribute 蓝图类

为 BP_AuraCharacter 指定 GE_SecondaryAttribute

BP_AuraCharacter-细节-属性-Default Secondary Attributes-GE_SecondaryAttribute image

打开 GE_SecondaryAttribute 设置修改器

细节-持续时间-Duration Policy-Infinite image

armor 跟随 Resilience 变化

细节-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

image

ArmorPenetration 跟随 Resilience

细节-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

BlockChance 依赖 Armor

细节-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

CriticalHitChance 依赖 ArmorPenetration

细节-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

CriticalHitResistance 依赖 Armor

细节-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

CriticalHitDamage 依赖 ArmorPenetration

细节-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

HealthRegeneration 依赖 Vigor

细节-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

ManaRegeneration 依赖 Intelligence

细节-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

MaxHealth 依赖 Vigor

细节-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

MaxMana 依赖 Intelligence

细节-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

image

8. Custom Calculations 自定义计算

image

image

9. Player Level and Combat Interface 玩家等级和战斗接口

在玩家状态中添加玩家等级.

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;

新建 战斗接口 CombatInterface

在敌人与玩家控制器之间共享

在 interaction 目录下新建C++ CombatInterface,基于 Unreal 接口 image

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;

10. Modifier Magnitude Calculations 修改器幅值计算

制作一个自定义计算类,以便我们可以获得最大生命值和最大生命值

MMC_MaxHealth 自定义最大生命值效果修改器

在 C++ AbilitySystem-ModMagCalc 目录新建 基于 GameplayModMagnitudeCalculation

用于通过蓝图或本机代码执行自定义游戏效果修饰符计算的类

image image

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 使用自定义修改器 MMC_MaxHealth

打开 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

image image 最大健康值 112.5

MMC_MaxMana 自定义最大魔力值效果修改器

在 C++ AbilitySystem-ModMagCalc 目录新建 基于 GameplayModMagnitudeCalculation

用于通过蓝图或本机代码执行自定义游戏效果修饰符计算的类

image image

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 使用自定义修改器 MMC_MaxMana

打开 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

image image

11. Initializing Vital Attributes 初始化重要属性

健康值和魔力值是即时效果。 在最大健康值和最大法力已经初始化或至少第一次设置后执行此操作。 将健康值设置为最大健康值,并将你的法力值设置为最大法力值。

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()
{
}

创建 重要属性的默认游戏效果 GE_AuraVitalAttributes

在 目录 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 使用 游戏效果 GE_AuraVitalAttributes

打开 BP_AuraCharacter Default Vital Attributes-GE_AuraVitalAttributes

image 运行游戏,健康和魔力进度球都为100%。 image image

WangShuXian6 commented 10 months ago

9. Attribute Menu 属性菜单

1. Attribute Menu - Game Plan 属性菜单-游戏规划

image image

2. Attribute Menu - Framed Value 属性菜单-框架值 WBP_FramedValue

在 目录 Blueprint-UI-AttributeMenu 基于 AuraUserWidget-右键-用户界面-控件蓝图 创建 控件蓝图 WBP_FramedValue image

打开 WBP_FramedValue 设计器:

添加 尺寸框:SizeBox_Root SizeBox_Root 设置为变量。 模式:desired 所需 宽:80,高:45 image

图表:参数化 新建变量: BoxWidth 浮点 分组-Frame Properties 默认值-80 BoxHeight 浮点 分组-Frame Properties 默认值-45 image

拖入节点 SizeBox_Root Set Width Override BoxWidth

拖入节点 SizeBox_Root Set Height Override BoxHeight

image

折叠到函数 UpdateFrameSize image

设计器: 添加 覆层 :Overlay_root 到 SizeBox_Root 添加 image : Image_Background到 覆层 -水平填充,垂直填充 。设置为变量 image-外观-笔刷-图像-MI_FlowingUIBG image

图表:参数化 拖入节点: Image_Background Set Brush Set Brush 的 In Brush 拖出提升为变量 BackgroundBrush 分组-Frame Properties BackgroundBrush 分组-Frame Properties 默认值-图像-MI_FlowingUIBG

image image 折叠到函数 UpdateBackgroundBrush image

设计器: 准备添加边框 拖入节点: Image :Image_Border到 Overlay_root ,水平填充,垂直填充 , image-外观-笔刷-图像-Border_1_png image-外观-笔刷-绘制为-边界 image-外观-笔刷-边缘-0.5,0.5,0.5,0.5 image image

拖入 Text 文本:TextBlock_Value 到 Overlay_root 内容-文本-99 水品居中对齐,垂直居中对齐 对齐-将文本中对齐 设置为变量 image

3. Attribute Menu - Text Value Row 属性菜单-文本值行 WBP_TextValueRow

在 目录 Blueprint-UI-AttributeMenu 基于 AuraUserWidget-右键-用户界面-控件蓝图 创建 控件蓝图 WBP_TextValueRow

打开 WBP_TextValueRow

设计器: 添加 尺寸框 :SizeBox_Root 模式-所需 宽度重载 启用 高度重载 启用 设为变量

图表:参数化 新建变量: BoxWidth 浮点 分组-Row Properties 默认值-720 BoxHeight 浮点 分组-Row Properties 默认值-60 点击变量右侧眼睛图表,公开变量 image

拖入节点 SizeBox_Root Set Width Override BoxWidth

拖入节点 SizeBox_Root Set Height Override BoxHeight

image image

拖入节点 水平框:HorizontalBox 到 SizeBox_Root

拖入节点 文本 到 HorizontalBox 水平向左对齐,垂直居中对齐 内容-文本-Attribute

拖入节点 WBP_FramedValue 到 HorizontalBox 插槽-尺寸-填充 水平向右对齐,垂直居中对齐

拖入节点 间隔区: Space 外观-尺寸-40,1

拖入节点 命名的插槽 Named Slot 到 HorizontalBox 当其他控件继承该控件时,可在插槽处添加自定义控件,例如按钮。 image

4. Attribute Menu - Text Value Button Row 属性菜单-文本值按钮行 WBP_TextValueButtonRow

基于 WBP_TextValueRow 创建 控件 WBP_TextValueButtonRow image

打开 WBP_TextValueButtonRow 默认已存在部分控件 image

设计器: 添加控件: 覆层 :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

image

5. Attribute Menu - Construction 属性菜单-构造

WBP_AttributeMenu

在 目录 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 【使文本换行】 水平居中对齐,垂直填充 image

添加 间隔区 到 两个文本之间 外观-尺寸-750,20 用以换行 image

拷贝间隔区 到 Wrap Box

WBP_TextValueRow 到 Wrap Box Row Properties-Box Width-750 Row Properties-Box Height-60 image image

WBP_TextValueButtonRow 到 Wrap Box image

尺寸框:SizeBox_Scroll用以限制滚动框大小 水平居中对齐,垂直居中对齐 插槽-子布局-宽度重载-600 插槽-子布局-高度重载-240 插槽-填充空白空间-启用 image

滚动框 Scroll_Box 到 SizeBox_Scroll 内部控件大小自适应宽度。

WBP_TextValueRow 到 Scroll_Box 10个

image

6. Button Widget 按钮控件

关闭 属性框的按钮

添加 尺寸框 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 将文本中对齐

公开全部属性变量

image image

按钮控件 WBP_Button

在 目录 Blueprint-UI-Button 下 基于 AuraUserWidget-右键-用户界面-控件蓝图 创建 控件蓝图 WBP_Button

打开 WBP_Button

设计器:

将属性框添加到视图 WBP_Overlay

打开 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

image

折叠到函数 UpdateFrameSize

image

设计器: 模式-所需

覆层 Overlay_Root

Image_Border 到 Overlay_Root 设置为变量 外观-笔刷-Button_Border 水平填充,垂直填充

Button 到 Overlay_Root 水平填充,垂直填充 设置为变量

Text 到 Overlay_Root 设置为变量

图表: Image_Border Set Brush BorderBrush :图像-Button_Border 分类-Button Propertites

image 折叠到函数 UpdateBorderBrush image

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

image

折叠导函数 UpdateButtonBrushes image image

Text Set Text Box Text 分类-Button Propertites 默认X 默认值 X

Text Set Font Make SlateFontInfo

Make FontOutlineSetting BPGraphScreenshot_2024Y-01M-12D-20h-07m-05s-874_00

Font Family- 默认 roboto Outline Size- FontSize Letter Spacing

折叠到函数 UpdateText image image image

WBP_AttributeMenu 控件

删除 SizeBox_Close 下的 全部子节点 添加 WBP_Button 到 SizeBox_Close image

7. Wide Button Widget 宽按钮控件

打开 WBP_TextValueButtonRow 删除插槽全部节点,替换为 WBP_Button image

设置 WBP_TextValueButtonRow中的 WBP_Button-Button Propertites-Box Text-+ image image

宽按钮控件

基于 WBP_Button 创建控件蓝图 WBP_WideButton 打开 WBP_WideButton

图表: BoxWidth 默认值 200

BoxHeight 默认值 65

BorderBrush 默认值 清除留空,着色-透明

ButtonNormalBrush 默认图像-WideButton ButtonHoveredBrush 默认图像-WideButton_Highlighted ButtonPressedBrush 默认图像-WideButton_Pressed_2 ButtonDisabledBrush 默认图像-WideButton_GrayedOut

ButtonText 默认文字 按钮 image

WBP_Overlay

打开 WBP_Overlay 添加 WBP_WideButton 名称 AttributeMenuButton image image

WBP_Overlay - AttributeMenuButton -Button Propertites- Button Text- 属性 image image

8. Opening the Attribute Menu 打开属性菜单

打开 WBP_Overlay

AttributeMenuButton 设置为变量

图表: Event Construct 事件

拖出 AttributeMenuButton AttributeMenuButton 拖出 get button 获取其中的按钮部分 get button 的 button 输出 assign on clicked image

添加自定义事件 add custom event - AttributeMenuButtonClicked OnClicked_Event 调用 AttributeMenuButtonClicked 函数 拖出 AttributeMenuButton AttributeMenuButton 拖出 get button 获取其中的按钮部分 get button 的 button 输出 控件-set is enabled

AttributeMenuButtonClicked 事件内禁用按钮,即 界面的属性按钮点击时,属性按钮自身被禁用。

image image

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 image image

优化 WBP_AttributeMenu

打开 WBP_AttributeMenu

Overlay_Root 重命名为 Overlay_Box

SizeBox_Root -右键-包裹为 覆层:Overlay_Root image

此时属性控件不在缩放至全屏了 image 覆层可以保证子级控件正确的尺寸。

Image_Background -填充-5

打开 WBP_Overlay 调整 属性控件

图表: create WBP Attribute Menu widget 输出 set position viewport set position viewport 的 position:25,25 image image

BPGraphScreenshot_2024Y-01M-12D-22h-33m-30s-337_00

9. Closing the Attribute Menu 关闭属性菜单 WBP_AttributeMenu

打开 WBP_AttributeMenu 添加关闭属性控件事件

WBP Button 改为 CloseButton 设为变量 image

图表: Event Construct 事件: 拖出 CloseButton CloseButton 输出 get button

get button 的 button 输出 assign on clicked

OnClicked_事件 调用 remove from parent

关闭属性控件后 启用属性按钮的点击状态

属性菜单不应依赖覆盖层。 因为覆盖层依赖属性菜单。 否则会循环依赖。

覆盖层可以创建事件委托。

WBP_AttributeMenu 属性控件广播关闭通知/委托

打开 WBP_AttributeMenu 图表: 事件分发器-创建一个事件委托-AttributeMenuClosed

image

每当销毁属性菜单时,广播这个事件。

添加事件 event destruct [控件销毁事件]

event destruct 调用 call Attribute Menu Closed 委托 image

订阅 call Attribute Menu Closed 委托 的其他蓝图都会获得通知 例如 属性控件广播自身销毁关闭。 覆盖层订阅该关闭委托。启用属性按钮。

image image image

覆盖层接受/订阅属性控件的委托通知

打开 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 启用 image BPGraphScreenshot_2024Y-01M-12D-23h-07m-40s-399_00

动态创建,销毁控件性能消耗不高。 也可以隐藏控件,但是这会在后台保留点击事件,不推荐。

10. Plan for Displaying Attribute Data 10.显示属性数据的规划

可以在空间控制器中广播属性变更,例如健康值变更。 然后对应 的控件接受广播,更新UI。 但是属性过多,每次添加新属性时,都需要修改控件控制器和控件。 所以不推荐此方法。

推荐方式: 控件控制器广播一个包含属性,描述等等多种信息的通用结构类型的游戏标签。 控件订阅该委托,检索游戏标签是否包含自身所需的标签。 控件已绑定特有标签。 image image image image 1.创建次要属性Gameplay标签 (在C++中更好地处理标签) 2创建属性数据资产 3.创建FAuraAttributelnfo结构 4.填写每个属性的数据资产 5.创建UAttributeMenuWidgetControlle

11. Gameplay Tags Singleton 游戏标签单例

集中存取标签。

AuraGameplayTags 游戏标签单例

基于 C++ None/无 创建 C++ 游戏标签单例 AuraGameplayTags image image

该单例标签为结构体

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 减少受到的伤害,提高格挡几率"));
}

12. Aura Asset Manager / Aura资产管理

https://docs.unrealengine.com/5.3/zh-CN/asset-management-in-unreal-engine/

资产加载与卸载

C++ 资产管理 AuraAssetManager

基于 Asset Manager 创建 C++ AuraAssetManager image image

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();
}

将 AuraAssetManager 资产管理器设为项目的资产管理器

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

此时 AuraAssetManager 资产管理器 中初始化的标签将可见

编辑-项目设置-项目-GameplayTags-管理Gameplay标签- attributes-Secondary-Armor 上,将提示:Native 这是在C++资产管理器中初始化的游戏标签。是由C++原生定义的标签。 image 其他标签没有 Native 提示,不是有C++原生定义。 image

清除从 DT_PrimaryAttributes 数据资产加载的游戏标签

这将使这些标签失效,之后通过标签管理器加载标签。 在C++ AbilityActorInfoSet 设置技能Actor时,加载主属性游戏标签。

image image

游戏标签管理器中增加 Armor 标签 变量 用以存储原生标签

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 减少受到的伤害,提高格挡几率"));
}

C++ AbilityActorInfoSet 设置技能Actor中 获取辅助标签 Armor

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 标签,然后打印。 image

13. Native Gameplay Tags 原生游戏标签

为所有属性制作原生游戏标签

删除项目设置中的MaxHealth MaxMana 标签 image image

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 提示 蓝图中也可以使用

image image

去除调试信息 Source/Aura/Private/AbilitySystem/AuraAbilitySystemComponent.cpp

void UAuraAbilitySystemComponent::AbilityActorInfoSet()
{
    // 为游戏效果应用委托绑定函数 EffectApplied
    OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAuraAbilitySystemComponent::EffectApplied);
}

14. Attribute Info Data Asset 属性信息数据资产

C++ 属性信息数据资产 AttributeInfo

基于 DataAsset 新建 C++ AttributeInfo image image

信息数据资产可以存储在蓝图中

存储属性信息的结构 用以在属性变化时,将此属性结构广播到控件蓝图中 控件蓝图以此更新自身信息

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();
}

创建属性信息资产 DA_AttributeInfo

创建一个简单的资产,在该类的实例中存储与特定系统相关的数据资产可以在内容浏览器中使用任何继承自它的原生类来创建。如果你希望拥有数据继承性或一个复杂的层级,则应该创建纯数据蓝图类

在目录 Blueprints-AbilitySystem-Data 右键-其他-数据资产-AttributeInfo 【C++中创建的类】 名称:DA_AttributeInfo image image image

打开 DA_AttributeInfo image AttributeValue属性值未公开在编辑器,只能在蓝图中可读。

添加属性 Intelligence

Attribute Tag-Attributes.Primary.Intelligence Attribute Name-智力 Attribute Description-增加魔法伤害

image image

依次添加所有主要属性,辅助属性, 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; 最大魔力值 可获得的最大魔法量

image image

15. Attribute Menu Widget Controller 属性菜单控件控制器 C++ AttributeMenuWidgetController

基于 C++ AuraWidgetController 类创建 属性菜单控件控制器 C++ AttributeMenuWidgetController image image

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()
{

}

16. Aura Ability System Blueprint Library / Aura技能系统蓝图函数库

Aura技能系统蓝图库 作为一个单例 用以访问控件控制器

AuraAbilitySystemLibrary / Aura技能系统蓝图函数库

基于 C++ BlueprintFunctionLibrary 新建 C++ AuraAbilitySystemLibrary image image

静态函数可以直接调用. 因为静态函数所属的类本身可能不存在于世界上。 静态函数无法访问世界上存在的任何对象。 所以需要一个世界上下文对象。

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 获取 WBP_Overlay 控件控制器

打开 WBP_Overlay 控件

图表: 右键-AuraAbilitySystemLibrary image

添加 this 节点-get a refference to self 表示世界变量

image

仅做测试,删除。

17. Constructing the Attribute Menu Widget Controller 构造属性菜单控件控制器

在 AuraHUD 设置 属性菜单控件控制器 必须的参数

必须的参数:

属性菜单 控件变量 属性菜单 控件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;
}

在 AuraAbilitySystemLibrary 蓝图函数库中创建 获取属性控件控制器的方法

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;
}

BP_AttributeMenuWidgetController 属性菜单控件控制器蓝图

在目录 Blueprint-UI-WidgetController

基于 AttributeMenuWidgetController 类创建 属性控件控制器蓝图类 如果查找不到 AttributeMenuWidgetController 类,需要为类AttributeMenuWidgetController 声明UCLASS(BlueprintType, Blueprintable)可蓝图化

image image

BP_AuraHUD 指定 BP_AttributeMenuWidgetController 控制器类

打开 BP_AuraHUD 细节-AuraHUD-AttributeMenuWidgetControllerClass-BP_AttributeMenuWidgetController

image

在 WBP_AttributeMenu 属性菜单控件中使用 技能系统蓝图函数库 AuraAbilitySystemLibrary 获取 WBP_AttributeMenu 控件控制器

打开 WBP_AttributeMenu

图表: 右键-AttributeMenuWidgetController image 添加 this 节点-get a refference to self 表示世界变量

set widget controller 用来存储属性菜单控件控制器

get widget controller 就可以获取属性菜单控件控制器 image BPGraphScreenshot_2024Y-01M-13D-17h-13m-32s-809_00

AuraHUD 中包含 变量 AttributeMenuWidgetController,蓝图函数库通过获取 AuraHUD,在其中在获取 AttributeMenuWidgetController。 控件控制器将使用 BindCallbacksToDependencies绑定技能系统组件,玩家状态等变量。

18. Attribute Info Delegate 属性信息委托

控件控制器准备信息并广播属性信息资产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 配置 属性信息资产

打开 BP_AttributeMenuWidgetController AttributeInfo-DA_AttributeInfo image

之后需要为属性菜单空间的每一个属性绑定标签

打开 WBP_TextValueRow 控件,添加设置标签名称的功能

设计器: 重命名标签文本节点为 TextBlock_label TextBlock_label 设置为变量 image image

图表: 左侧新建函数 SetLabelText image

SetLabelText 函数 细节-输入-添加输入 LabelText 文本类型 image

拖出 TextBlock_label 拖出 SetText(Text)

SetLabelText 的 LabelText 输出至 SetText(Text) 的 In Text

image

在属性菜单条形控件 WBP_TextValueButtonRow 中订阅属性信息委托

WBP_TextValueButtonRow 继承自 WBP_TextValueRow ,也有设置标签名称函数 SetLabelText

打开 WBP_TextValueButtonRow 图表: 右键-AttributeMenuWidgetController image 添加 this 节点-get a refference to self 表示世界变量

从 get Attribute Menu Widget Controller 的输出 拖出 assign Attribute Info Delegate [订阅控件控制器中定义的委托] image

Attribute Info Delegate_事件 的 Info 输出 拖出 break AuraAttribute image

收到广播时,设置WBP_TextValueButtonRow标签控件名称。 名称来自广播的属性信息的 Attribute Name 字段

添加 SetLabelText

image

显示 WBP_AttributeMenu 属性菜单控件时, 广播属性信息

打开 WBP_AttributeMenu 图表: 在 get Attribute Menu Widget Controller 拖出 Broadcast Initial Values 函数 image 当前该函数只广播了力量信息作为示例。

现在打开菜单时,将显示力量标签名 image

打开 WBP_TextValueRow 控件,添加设置标签的属性具体值的功能

打开 WBP_TextValueRow

可设置小数或整数,2个版本。

打开 WBP_TextValueButtonRow 图表: 左侧新建函数 SetNumericalValueInt image SetNumericalValueInt -细节-输入 添加输入参数 Value 整数类型 image

拖出 WBP_FramedValue WBP_FramedValue 拖出 TextBlock_Value TextBlock_Value 拖出 控件-setText(text)

SetLabelText 函数 细节-输入-添加输入 LabelText 文本类型 image

拖出 TextBlock_label

SetNumericalValueInt 的 value 拖出 to text (interger) image

在属性菜单条形控件 WBP_TextValueButtonRow 中订阅属性信息委托后获取属性的值,渲染

打开 WBP_TextValueButtonRow 图表: Set Numerical Value Int 函数 BPGraphScreenshot_2024Y-01M-13D-18h-19m-36s-465_00

力量值已正确设置 image

19. Widget Attribute Tags 控件属性标签

WBP_TextValueRow 公开 AttributeTag 变量

打开 WBP_TextValueRow 图表: 左侧添加变量-AttributeTag 类型:gameplay标签 公开此变量 使其可被外部编辑 image

WBP_AttributeMenu 为每一个 WBP_TextValueButtonRow 控件添加标签属性

打开 WBP_AttributeMenu

设计器:

将主要属性区域的每个 WBP_TextValueButtonRow 控件重明名为对应的属性名称: Row_Strength Row_Intelligence Row_Resilience Row_Vigor image

将辅助属性区域的每个 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

为其他控件执行同样操作 BPGraphScreenshot_2024Y-01M-13D-19h-36m-53s-879_00

WBP_TextValueButtonRow

图表

break aureAttributeInfo 的 Attribute Tag 拖出 Matches Tag

变量-默认-attribute tag

branch BPGraphScreenshot_2024Y-01M-13D-19h-43m-36s-163_00

如果当前WBP_TextValueButtonRow 的标签属性等于 从广播接受的标签,那么使用接受的标签设置标签名称和值。

当前只广播了力量属性信息。所以属性菜单只有力量栏显示正确。

image

将 WBP_TextValueButtonRow 的绑定事件拷贝至 WBP_TextValueRow

WBP_TextValueButtonRow image

打开 WBP_TextValueRow 图表: event construct 事件: 右键-AttributeMenuWidgetController 添加 this 节点-get a refference to self 表示世界变量

从 get Attribute Menu Widget Controller 的输出 拖出 assign Attribute Info Delegate [订阅控件控制器中定义的委托] image

Attribute Info Delegate_事件 的 Info 输出 拖出 break AuraAttribute

BPGraphScreenshot_2024Y-01M-13D-19h-59m-32s-577_00

当前只广播了力量属性信息。所以属性菜单只有力量栏显示正确。 image

20. Mapping Tags to Attributes 将标签映射到属性

从属性信息资产获取所有标签,广播到控件。

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);
    }
}

21. Responding to Attribute Changes 响应属性变化

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);
}

现在可显示所有属性,并且实时更新属性值。 image

WangShuXian6 commented 10 months ago

10. Gameplay Abilities 游戏技能

1. Gameplay Abilities 游戏技能

image image image 游戏技能: 定义技能或技能的类

必须被授予 -在服务器上授予 -Spec复制到拥有客户端

必须激活才能使用

成本和冷却时间

技能异步运行 -一次多个活动

技能任务 -异步执行操作xin

2. Granting Abilities 2.授予技能

基于 C++ GameplayAbility 新建 C++ AuraGameplayAbility 技能基类

目录 Public/AbilitySystem/Abilities image image

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();
}

新建技能 GA_TestGameplayAbility

在目录 Blueprints-AbilitySystem-GameplayAbilities

基于 AuraGameplayAbility 新建蓝图类 GA_TestGameplayAbility image

打开 GA_TestGameplayAbility image

图表: 技能激活事件

image

为玩家 BP_AuraCharacter 蓝图添加技能 GA_TestGameplayAbility

打开 BP_AuraCharacter Abilities-StartupAbilities-添加一组技能 GA_TestGameplayAbility image

游戏运行时将激活技能 image

结束技能

打开 GA_TestGameplayAbility 图表 添加 delay 添加 end ability 5秒后结束技能 image image

3. Settings on Gameplay Abilities 设置游戏技能

打开 技能 GA_TestGameplayAbility image

技能标签

细节-标签 image

游戏标签 描述
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 目标阻止标签 如果目标参与者/组件具有以下标签中的任何一个,则阻止此技能

高级-Instancing Policy 实例化策略

技能在执行时是如何实例化的。这限制了一个技能在其实现中所能做的事情。

选项: Non Instanced 不实例化: 此技能从未实例化。执行该技能的任何东西都在CDO上运行。

Instanced Per Actor 每个参与者都实例化 每个 Actor都有自己的这种技能的实例。状态可以保存,复制是可能的

Instanced Per Execution 每次执行都实例化 每次执行这个技能时,我们都会实例化它。可以复制,但不建议复制。

实例化策略 描述 详细资料
每个参与者实例化 将为该技能创建单个实例。每次激活都会重复使用。 可以存储持久数据。每次都必须手动重置变量。
每次执行实例化 每次激活时创建的新实例 在激活之间不存储持久性数据。比每个参与者实例化性能更低
不实例化 只使用类默认对象不创建实例。 无法存储状态,无法绑定到技能任务委托上。三个选项中性能最佳。

Net Execution Policy 网络执行策略

Net Execution Policy 描述
Local Only 仅限于本地 只在本地客户端运行。服务器不运行的技能。
Local Predicted 局部预测 在本地客户端上激活,然后在服务器上激活。利用预测。服务器可以回滚无效的更改。
Server Only 仅服务器 服务器上运行。
Server Initiated 服务器启动 先在服务器上运行,然后在所属的本地客户端上运行

Replication Policy

默认即可,游戏会自动复制。

不应使用的选项

image

4. Input Config Data Asset 输入配置数据资产

通过输入激活技能

1-旧版输入:【弃用】

将输入直接绑定到技能系统组件。 按下一个按钮,一个技能会自动接收该输入,然后激活或执行 做什么取决于你如何编码该技能。 这是通过创建一个具有技能输入常量的枚举来完成的。 将这些枚举常量硬编码为技能输入枚举,并且这些输入将被链接到那些特定的技能。

2-增强输入:

输入操作通过输入映射上下文绑定到输入。

可以决定如何激活技能来响应这些输入,可以用任何方式做到这一点。

Lyra,Epic Games 提供的多人射击游戏示例项目,提供了一个极佳示例, 将增强输入与该项目的游戏技能系统技能联系起来。

3-当前项目:【增强输入】 采用数据驱动的方法。简化的Lyra。 能够在运行时更改技能映射的输入。 例如能够在运行时进行更改,按下鼠标左键来激活特定的技能。

需要创建一个包含输入操作的数据资产,将输入操作与游戏标签联系起来。 为每个输入添加一个游戏标签。例如,一键、鼠标左键、鼠标右键等。 在运行时,我们应该能够为我们的游戏技能分配各种标签。 然后当我们激活技能时,每个技能都会与其输入标签相关联,这可以进行检查和更改。

第一步就是把这个数据资产化。

C++ 输入配置数据资产 AuraInputConfig

基于 DataAsset 创建 C++ AuraInputConfig Public/Input image image

创建一个结构体,将一组输入操作链接到游戏标签。 它将包含一个输入操作和一个游戏标签。

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")
        );

}

输入操作 数据资产 DA_AuraInputConfig

在目录 Blueprints-Input-

右键-其他-数据资产-选择 AuraInputConfig 名称 DA_AuraInputConfig

鼠标左键 输入操作 IA_LMB

技能输入是一维的。

在 Blueprints-Input-InputActions 目录 右键-输入-输入操作 名称: IA_LMB

打开 IA_LMB 值类型-Axis 1D(浮点) image

其他输入操作

鼠标右键 输入操作 IA_RMB 1键 IA_1 2键 IA_2 3键 IA_3 4键 IA_4

值类型-Axis 1D(浮点)

image

IMCAuraContext 输入映射情景关联输入操作 IA

打开 IMC_AuraContext

添加 映射: 选择 数据资产 IA_1 绑定键盘 1键 image

添加 映射: 选择 数据资产 IA_2 绑定键盘 2键

添加 映射: 选择 数据资产 IA_3 绑定键盘 3键

添加 映射: 选择 数据资产 IA_RMB 绑定鼠标右键

添加 映射: 选择 数据资产 IA_LMB 绑定鼠标左键

image

DA_AuraInputConfig 输入数据资产中配置输入操作与输入标签的映射

运行时可以动态修改输入数据资产。

打开 DA_AuraInputConfig

依次配置 image image

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

image

5. Aura Input Component 自定义输入组件

将回调函数绑定到输入

可以将回调绑定到游戏标签,然后找到对应的输入。

增强输入组件 C++ AuraInputComponent

在 Public/Input 基于 EnhancedInputComponent 新建 C++ image image

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"

6. Callbacks for Ability Input 技能输入的回调

玩家控制器中调用输入回调 按下时触发 根据标签识别按键

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 image

image

玩家控制器BP_AuraPlayerController 设置输入配置数据资产 DA_AuraInputConfig

打开 BP_AuraPlayerController 输入- Input Config-DA_AuraInputConfig Input Config 是 C++中定义的变量。 image

运行游戏,按下,长按,松开 1,2,3,4,鼠标左键,鼠标右键 可触发对应回调函数。 image image

7. Activating Abilities 激活技能

游戏启动时的技能输入标签

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

打开 GA_TestGameplayAbility

细节-input-startup input tag-InputTag.LMB 这将设置初始技能标签为 InputTag.LMB image

所以一旦我按下鼠标左键,我就会激活这个技能,然后它将处于激活状态,然后五秒钟后它会自行结束。 只能在它尚未激活的情况下激活它。

运行游戏,按下左键会激活技能。5秒后技能结束。 image

如果去掉延时。 将可以频繁触发技能激活。 因为技能在输入时就激活,然后自动取消激活,可以再次激活。

8. Click To Move 单击移动

9. Setting Up Click to Move 设置 单击移动

鼠标左键为特殊按键, 可以激活技能,也可以使角色自动奔跑。

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);
        }
    }
}

此时,只要长按鼠标,玩家则向目标不断移动。
而且在多人模式下,服务端可客户端都可以正常运行。

10. Setting Up Auto Running 10.设置自动奔跑

释放鼠标左键时,鼠标跟随时间小于短按阙值时,是短按, 则生成曲线导航路径,自动奔跑至目标点。

导航系统模块 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;
    }
}

添加 体积-导航网格体体边界体积

image image image

按 P 键使 导航网格体体边界体积 可视化 image

放大到整个关卡 image

添加障碍物,释放左键时,可在玩家和目的地之间新城样条曲线路径点。 此时不能自动奔跑。 image

11. Implementing Auto Running 实现自动奔跑

为客户端启用导航 项目设置-引擎-导航-运行客户端导航-启用 否则生成的导航路径无效 image

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 忽略。 这将使鼠标跟踪穿透障碍物到达被障碍物遮挡的地面。 image

此时,短按一次鼠标左键时,玩家将自动奔跑至目的地。

12. Code Clean Up 代码清理

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);
        }
    }
}

13. Aura Projectile 玩家 技能投射物

让玩家角色可以发射各类投射物,例如魔法球

魔法投射物 C++ AuraProjectile

在目录 Public/Actor 基于 Actor 新建 C++ AuraProjectile

image

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)
{

}

魔法火球蓝图 BP_FireBolt

在目录 Blueprints-AbilitySystem-GameplayAbilities-Fire-FireBolt 基于 AuraProjectile 新建蓝图 BP_FireBolt

image

打开 BP_FireBolt image

添加 Niagara 组件 到 Sphere

image sphere 过大会与玩家网格体重叠,导致攻击到玩家自身。【后期可以为玩家单独添加的通道种类】

Niagara 组件 -细节-Niagara-Niagara 系统资产-NS_Fire_3 火球资产 image image

将 BP_FireBolt 火球放置到关卡,火球具由速度,将会朝面向的方向飞远。 image

准备创建可以产生 BP_FireBolt火球的技能。

14. Aura Projectile Spell 投射物攻击魔法技能

基于 AuraGameplayAbility 创建 魔法投射物攻击技能 C++ AuraProjectileSpell

在目录 Public/AbilitySystem/Abilities image

image

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);
}

技能蓝图 GA_FireBolt 火球术

在目录 Blueprints-AbilitySystem-GameplayAbilities-Fire-FireBolt 基于 AuraProjectileSpell 新建技能蓝图 GA_FireBolt

image image

打开 GA_FireBolt

激活时打印文字 image

细节-输入-Startup input tag -InputTag.LMB 将左键单击输入标签绑定到初始输入标签属性。

image

为玩家BP_AuraCharacter指定技能 GA_FireBolt火球术

打开 BP_AuraCharacter 细节-Abilities- startup Abilities-删除测试技能,选择 技能 GA_FireBolt火球术 image 玩家初始时将在单击敌人时激活,施放火球技能。 image c++和蓝图版本技能都已激活。

15. Spawning Projectiles 生成火球

投射物 AuraProjectile 基类需要可网络复制

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);
}

在魔法技能 AuraProjectileSpell 中生成投射物

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 image

打开 SKM_Staff 武器尖端带有一个插槽-TipSocket image

为玩家或敌人设置使用的武器插槽名称

打开 玩家 BP_AuraCharacter 细节-Combat-Weapon Tip Socket Name-TipSocket image Weapon Tip Socket Name 是基类定义的变量。 将通过此插槽名获取武器上该插槽的世界位置,用以在此处生成技能投射物,例如火球。

为 GA_FireBolt 火球术 技能设置投射体类 ProjectileClass ,使用火球蓝图

ProjectileClass 在技能基类中定义

打开 GA_FireBolt 火球术技能 细节-Aura Projectile Spell-Projectile Class-BP_FireBolt image

现在,点击敌人,激活技能GA_FireBolt 火球术,将会在玩家武器处生成一个火球,发射出去。 此时火球方向错误。尚未设置。 只能发射一次,因为技能激活后永远没有结束。 image

WangShuXian6 commented 10 months ago

11. Ability Tasks 技能任务

https://docs.unrealengine.com/5.3/zh-CN/gameplay-ability-tasks-in-unreal-engine/

1. Ability Tasks

技能任务(C++类"UAbilityTask")是更常规的技能任务类的特殊形式,旨在使用游戏性技能。使用游戏性技能系统的游戏通常包括各种自定义技能任务,这些任务实施其独特的游戏功能。它们在游戏性技能执行过程中执行异步工作,并且能够通过调用委托(Delegate) (在本地C++代码中)或移经一个或多个输出执行引脚(在蓝图中)来影响执行流。这使技能能够跨多个帧执行,并可在同一帧内执行多个不同的函数。大部分技能任务都有一个即时触发的执行引脚,使蓝图能够在任务开始后继续执行。此外,通常还有一些特定于任务的引脚,它们会在延迟后或在可能发生或不发生的某个事件之后触发。

技能任务可以通过调用"EndTask"函数自行终止,或者等待运行它的游戏性技能结束,此时它会自动终止。这可以防止幻影技能任务运行,有效地泄漏CPU周期和内存。例如,某个技能任务可能播放一个施法动画,而另一个任务则在玩家的瞄准点处放置一个靶向标线。如果玩家点击确认输入来施放该法术,或者等待动画结束而未确认该法术,游戏性技能就会结束。虽然它们可以在任何时候自动终止,但是技能任务保证最晚在主要技能结束时终止。

技能任务设计用于网络环境和非网络环境,但它们不会通过网络直接更新自己。它们通常是间接保持同步的,因为它们是由游戏性技能(会进行复制)创建,并且使用复制的信息(例如玩家输入或网络变量)来确定它们的执行流。

近战攻击游戏性技能,在蓝图中实施。中央的"播放蒙太奇并等待事件"技能任务是ActionRPG样本的一部分。 image

在技能 GA_FireBolt 中 使用预制游戏任务

技能任务像用来完成游戏技能的工人,要么立即完成,要么跨越一段时间。 游戏技能系统中已经存在许多任务。 也可以自己创建游戏任务。

打开 GA_FireBolt 技能 图表:

: 动画-蒙太奇- play montage

image

它有一个输入执行引脚,也有一个常规输出执行引脚。 到达节点后,右上角的常规输出执行引脚将立即执行。 这是同步任务。

但也允许我们在到达目标后立即继续调用其他函数和事物节点。

但是,有许多情况可以触发这些其他输出执行线引脚。 例如 on completed 这只是一个蓝图潜在节点,也称为异步任务。 GAS具有这些潜在节点的更专门的形式,并且这些在技能系统中更加根深蒂固。

例如节点:ability-tasks-play montage and wait image

它将播放蒙太奇,然后等待蒙太奇中的其中一件事情完成或混合或被中断或取消。

基于 Cast_FireBolt 动画序列创建动画蒙太奇 AM_Cast_FireBolt

E:/Unreal Projects 532/Aura/Content/Assets/Characters/Aura/Animations/Abilities/Cast_FireBolt.uasset Cast_FireBolt-右键-创建-创建动画蒙太奇 AM_Cast_FireBolt image AM_Cast_FireBolt 具由默认插槽 image

ABP_Aura 动画蓝图也在使用默认插槽,所以可以播放该动画。 image

打开 GA_FireBolt 技能 使用 动画蒙太奇 AM_Cast_FireBolt

ability-tasks-play montage and wait 节点 montage to play 选择资产 AM_Cast_FireBolt image

激活此技能时将播放 AM_Cast_FireBolt 此时立即生成火球,时间过早。

AM_Cast_FireBolt

打开 AM_Cast_FireBolt 资产详情-混合选项-混合-混合时间-0 不混合 资产详情-混合选项-混处-混合时间-0.1 image 这样蒙太奇播放的速度更快更灵敏。

2. Sending Gameplay Events 发送游戏事件

AM_Cast_FireBolt 监听蒙太奇通知事件

打开 AM_Cast_FireBolt 图表: ability-tasks-wait gameplay event

具由该技能并且已激活的actor 将监听 动画蒙太奇的通知事件

在 动画蒙太奇AM_Cast_FireBolt中 发送一个带有标签 Event.Montage.FireBolt 的事件,通知技能生成火球。

新增标签 Event.Montage.FireBolt

项目设置-gameplayTags-管理gameplay标签- 添加标签 Event.Montage.FireBolt 默认源。

image

AM_Cast_FireBolt 监听带标签Event.Montage.FireBolt的事件

打开 AM_Cast_FireBolt 图表: ability-tasks-wait gameplay event event tag - Event.Montage.FireBolt image

自定义动画通知蓝图 AN_MontageEvent

在 目录 Content/Blueprints/AnimNotifies/ 基于 AnimNotify 动画通知 新建 AN_MontageEvent 动画通知蓝图

image 用于在动画蒙太奇发送任意游戏事件

打开 AN_MontageEvent 左侧-我的蓝图-函数-重载-已接受通知 Received_Notify image image 可以覆盖收到的通知。 决定通知到达时发生什么。 当接受到达通知并且有与关联的网格组件时,将调用此函数执行此通知的动画蒙太奇,

mesh comp -始终可以获得该网格的所有者。

image

发送游戏事件:ability-send gameplay event to actor

当向演员发送游戏事件时,不仅发送识别标签,还可以通过有效负载发送附加数据。 image

GA_FireBolt 技能 的 wait gameplay event 可以接受该负载。

左侧新建变量 EventTag 类型:gameplay标签 公开变量 EventTag EventTag变量 输出至 send gameplay event to actor - Event Tag 节点

send gameplay event to actor - payload 拖出 Make GameplayEventData image

AM_Cast_FireBolt 发送通知

打开 AM_Cast_FireBolt 在武器挥出时间点-添加通知-AN_MontageEvent image image

选择通知 AN_MontageEvent-细节-动画通知-event tag-Event.Montage.FireBolt image 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 从 蒙太奇收到。 image

之后 actor 的 GA_FireBolt 技能的 wait gameplay event 的 event Received 节点 将接收到该事件 。 因为 wait gameplay event 已设置为 要过滤接受 带有 标签 Event.Montage.FireBolt 的事件。所以可以收到该事件。

image image

所以应当在 GA_FireBolt 技能的 wait gameplay event 事件接受到通知时才生成火球,此时动作适合生成火球。 而非技能开始后立即生成火球。动画不协调。

3. Spawn FireBolt from 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 技能的 wait gameplay event 事件接受到通知时使用 SpawnProjectile 生成火球

打开 GA_FireBolt 技能 图表: SpawnProjectile

End Ability 结束技能,使技能可再次释放 BPGraphScreenshot_2024Y-01M-14D-22h-07m-37s-457_00

此时动画合适时才会生成火球。 并可以多次释放技能。

4. Custom Ability Tasks 自定义技能任务

TargetDataUnderMouse 自定义技能任务 C++

在目录 Public/AbilitySystem/AbilityTasks/ 基于 AbilityTask 新建 C++ TargetDataUnderMouse

image

获取光标跟踪命中结果

这是一个 异步蓝图技能任务节点 创建多个输出执行引脚的方式是通过广播委托。 任何输出执行引脚都需要作为此类的委托变量存在。

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 中调用 TargetDataUnderMouse 异步技能任务

打开 GA_FireBolt 图表: 可以添加C++ 中定义的蓝图节点 TargetDataUnderMouse

一旦执行到 TargetDataUnderMouse 节点, 就会调用 activate,将立即执行引脚 ValidData 广播有效数据 Data 。 Data 为光标跟踪的位置信息。 所以使用 ValidData 引脚,而非右上角引脚。 image image

这在多人游戏中存在问题,客户端的光标位置在服务器端显示为世界中心点0,0,0。 因为客户端的光标位置不会发送到服务端。

5. Target Data 目标数据

image 技能在客户端激活后,也将在服务端激活,需要时间TimeActive。

技能在客户端激活后,将会执行到节点TargetDataUnderMouse,以获取光标跟踪位置,然后将此位置信息通过RPC同步到服务端,需要时间 TimeRPC。

1-如果 TimeActive 小于 TimeRPC,那么服务端将获取不到正确的 TargetDataUnderMouse 的数据,只能为0,0,0. 客户端的技能激活先发送到服务端。

2-如果 TimeRPC 小于 TimeActive, TargetDataUnderMouse 的数据RPC更早到达服务端,那么服务端将可获取正确的TargetDataUnderMouse 的数据。 客户端的目标数据RPC先发送到服务端。

但是 技能激活 TimeActive ,目标数据RPC TimeRPC 发送到服务端的时间无法确定。无法保证先后顺序。

GAS 目标数据系统

技能系统通过目标数据系统来保证服务端获取正确数据: image image

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 委托执行广播。 将触发委托的回调,然后服务器才可以接受到 复制来的目标数据。

如果强制触发委托依然未成功,说明目标数据尚未到达服务器。 需要等待

6. Send Mouse Cursor Data 发送鼠标光标数据

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);
    }
}

7. Receiving Target Data 服务端接收目标数据 【非本地控制】

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 中 从目标数据句柄获取目标数据后绘制调试球

打开 GA_FireBolt 图表: get hit result from target data break hit result BPGraphScreenshot_2024Y-01M-15D-16h-03m-23s-655_00

使用服务器+客户端模式启动游戏 这将启动一个服务端和一个客户端 此时客户端点击敌人显示调试球,服务端上的该客户端点击的敌人也会显示调试球。 目标数据由客户端发送到了服务端。 image

image

使用 服务器 + 2客户端模式 image

客户端的数据将会复制到服务端,但此时,服务端不会把该数据复制到所有客户端。 客户端1点击敌人,客户端1敌人出现调试球。 服务端敌人出现该调试球。 客户端2不会出现该调试球。 image

服务器此时可以向正确的方向发射火球。

8. Prediction in GAS / GAS中的预测

多人游戏中,服务器应该有权对游戏状态进行任何重大更改。 如果客户可以改变重要因素,那么这就留下了一个可供作弊者利用的漏洞。 但是,如果客户端必须等待服务器确认其更改请求的有效, 那么客户端执行操作之间就会存在明显的延迟。

游戏利用预测解决该问题。

预测是指客户继续进行更改的时间。 移动角色、打开门、射击武器并将该变化通知服务器。 当服务器获取信息时,它会确保它是有效的更改。 如果更改无效,服务器将撤消这些更改。 快节奏的多人游戏必须利用预测。

开发人员不想要的是拥有充满分支的技能,就像如果权威做x,否则做 x 的预测版本。

相反,他们想要或多或少自动的预测。

并不是所有事情都需要预测。

例如,脚步声不需要预测,但诸如健康变化,法力造成伤害之类的重大事情需要预测。

Gas自动预测技能激,活触发事件游戏效果应用程序包括属性修改,但不执行计算。

得益于虚幻中的新角色运动系统,游戏还预测了蒙太奇和运动引擎。

但GAS不能预测游戏效果、移除效果或游戏周期性效果。 这些是一些尚未克服的技术挑战。

Gas 预测以预测密钥的概念为中心。

预测密钥只是一个唯一的 ID,并且这些被存储在客户端的中心位置。 当客户端执行预测操作时,它将向服务器发送预测密钥并与该键关联它的客户端预测操作和副作用。

服务器将接受或拒绝该密钥。 然后,它将把任何服务器端创建的副作用与该密钥关联起来,并响应客户端,通知密钥 效果已被接受或拒绝。 从客户端发送到服务器的 F 预测密钥将始终到达服务器。

复制仅发生从服务器到客户端,但是那指的是复制变量。 变量在其属性宏中标记有“replicated”或“replicated using”。 对于这些,复制仅从服务器到客户端,

但这里我们讨论的是发送服务器的预测密钥为此,为了方便起见,我们经常使用术语“复制”。 所以我们说这个密钥正在被复制到服务器,这常见于GAS. 当服务器将预测密钥发送回时,它只会返回给发送它的客户端.

9. Orienting the Projectile 投射物定向

投射技能生成投射物 在蓝图中使用时接受位置信息

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 火球技能设置生成投射物的方向

打开 GA_FireBolt 图表: 缓存获取的目标数据位置信息未 ProjectileTargetLocation ProjectileTargetLocation 输出给 SpawnProjectile 的 ProjectileTargetLocation

TargetDataUnderMouse 异步任务的 valid Data 引脚 连接后续节点。

因为 TargetDataUnderMouse 的 ValidData.Broadcast(DataHandle) 广播完成后才执行到该引脚。

wait gameplay event 的 event received 引脚连接后续节点 因为此为异步任务,需要等待接受到数据后再生成投射物

BPGraphScreenshot_2024Y-01M-15D-17h-35m-30s-254_00 此时客户端无法发射火球,因为服务端引脚执行完后立刻终止了技能。

放置角色对相机的block,导致相机放大。

打开 BP_AuraCharacter 胶囊体组件-碰撞预设- 此处默认阻止了相机。应当设置为忽略相机。 image 网格体组件也阻止了相机。

在 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);
}

按住shift + 左键点击 也可以向任意方向发射技能

默认需要点击敌人才可以发射火球

Content/Blueprints/Input/InputActions/IA_SHIFT.uasset

在 目录 Content/Blueprints/Input/InputActions/ 右键-输入-输入操作 新建 IA_SHIFT

打开 IA_SHIFT 值类型-Axis1D(浮点) image

打开 IMC_AuraContext 输入操作上下文 添加一个映射 将 输入操作 IA_SHIFT 与 shift 键映射在一起 绑定输入操作 IA_SHIFT 绑定键-左shift和右shift image

在控制器中绑定 输入操作 IA_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 设置 ShiftAction 输入操作变量 为 输入操作资产 IA_SHIFT

打开 BP_AuraPlayerController 细节-输入- Shift Action-IA_SHIFT image

此时,服务器端 按住shift ,然后点击左键,可以向任意方向发射火球技能。 客户端存在问题 image

10. Motion Warping 运动扭曲

https://docs.unrealengine.com/5.0/zh-CN/motion-warping-in-unreal-engine/ 运动扭曲 是一种可以动态调整角色的根骨骼运动以对齐目标的功能

在角色蓝图中创建运动扭曲逻辑,在动画蒙太奇中分配运动扭曲窗口,并链接到指定位置。

让角色面向技能发射的方向

概述

先决条件

必须启用 运动扭曲(Motion Warping) 插件。如需详细了解插件及其安装方法,请参阅:使用插件

image 运动扭曲利用了蓝图动画蒙太奇工作流程。因此,你需要了解这些功能。

你的项目中有角色蓝图、输入功能按钮和动画,可用于创建Gameplay示例。

运动扭曲概述

运动扭曲的整体功能可分为两大区域:

动画蒙太奇(Animation Montage) ,你可以在其中创建具备动画通知状态的运动扭曲窗口。 image

蓝图逻辑(Blueprint Logic) ,你可以在其中设置逻辑来分配扭曲目标并播放蒙太奇。 image

动画蒙太奇

动态蒙太奇可供你指定运动扭曲区域,自定义其行为,并对其命名。

创建

要新建一个运动扭曲区域,右键点击一个 通知(Notifies) 轨道,选择 添加通知状态...(Add Notify State...)> 运动扭曲(Motion Warping) 。

image 这些是带有开始和结束时间的可自定义区域,你可以将其对齐到动画中最适合应用扭曲的区域。

比如,在这个覆盖蒙太奇中,当角色把手放在障碍物上时,你可能需要确保起始扭曲区域覆盖整个区域。 image

细节

动画通知 的 细节(Details) 面板包含运动扭曲正常运行所需的属性和设置。选择你的运动扭曲分段以显示这些细节。 image

细节名称 说明
根骨骼运动修饰符(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) 。点击即可添加。

image 现在将该组件从组件(Components)面板拖放到 事件图表(Event Graph) ,即可在你的蓝图图表中引用该组件。

image

节点

从运动扭曲引用拖移链接后,你可以浏览与之相关的函数和事件。它们位于运动扭曲类别中。 image

你可以在蓝图中使用以下运动扭曲节点:

节点名称 节点图像 说明
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 节点,以及用于禁用所有根骨骼运动修饰符的节点。

运动扭曲示例

本小节介绍了如何设置角色扭曲到击打目标的简单运动扭曲示例。

禁用扭曲

image

开始之前,确保你打算使用的动画启用了根骨骼运动。具体做法是打开动画资产并启用 EnableRootMotion 。 image

设置目标位置

第一步是创建并放置一个你要扭曲到的目标。此示例使用了圆柱体。

在 放置Actor(Place Actors) 面板中,点击 所有类(All Classes) 并找到 目标点(Target Point) 。将其拖放到关卡中以添加目标点。确保它对齐并旋转到你所需的扭曲点。

image

设置动画蒙太奇

接下来,创建动画蒙太奇资产。要想从现有动画派生此类资产,有一个简单的方法,那就是是右键点击你的动画资产,并选择 创建(Create) > 创建动画蒙太奇(Create AnimMontage) 。创建蒙太奇之后,打开资产。 image 现在蒙太奇已打开,你可以在序列中推移以预览你的动画。下一步是在通知轨道下添加运动扭曲窗口。具体做法是,右键点击轨道区域并选择 添加通知状态...(Add Notify State...) > 运动扭曲(Motion Warping) 。 image 现在已创建运动扭曲窗口,你可以使用其中的控点设置开始和结束范围。

设置此运动扭曲窗口的范围,让它在动画开头附近开始,在角色攻击的那一刻结束。你还可以在移动 通知 关键帧时按住 Shift 键预览那个时刻的当前动画。

image 接下来,选择运动扭曲关键帧,并找到 细节(Details) 面板。你将在此处设置此关键帧的一些属性。

将 根骨骼运动修饰符配置(Root Motion Modifier Config) 设置为 倾斜(Skew)扭曲(Warp)** 。此操作用于指定扭曲类型。

为 扭曲目标名称(Warp Target Name) 设置名称。此操作用于用名称标识此扭曲。 image

获取目标位置

现在打开你的角色蓝图资产。在事件图表中,创建映射到所需输入操作的 Input Action 节点。具体做法是,右键点击图表,并从 输入(Input)> 操作事件(Action Events) 中选择你的输入事件。

在本示例中,有一个用于打击的输入操作事件(Input Action Event for Punch)。

image 接下来,你需要获取你早先放在此示例中的目标点的位置。你有几种办法可以选择,但对于本示例,请创建 Get All Actors Of Class 节点。将 Actor类(Actor Class) 设置为 目标点(Target Point) 。最后,将Input Action节点中的 按下(Pressed) 输出引脚挂接到Get All Actors of Class函数的输入执行引脚。 image 最后,添加 Get(副本)节点以连接到输出Actor(Out Actors)数组数据引脚。你还要创建 Get Actor Location 函数,并将其输入 目标数据(Target data) 引脚连接到 Get 节点的输出数据引脚。

image

扭曲目标

现在你要创建逻辑来获取目标点的位置。

首先,将运动扭曲组件添加到角色蓝图。具体做法是,点击组件(Components)面板中的 (+) 添加(*(+) Add) ,在移动(Movement)类别下找到运动扭曲(Motion Warping)。点击它以添加组件。 image 接下来,从组件(Components)面板将运动扭曲组件(Motion Warping Component)拖放到事件图表中。

image 从运动扭曲(Motion Warping)引用引脚拖移,以添加 Add or Update Warp Target from Transform 节点。创建后,将其输入事件引脚连接到 Get All Actors Of Class 节点。你还要确保将扭曲目标名称分配到 名称(Name) 引脚。此名称必须匹配你早先在蒙太奇中定义的扭曲目标名称。 image 你还需要将 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)引脚(由橙色引脚表示的变换值)时,将创建转换节点。如果存在不同值类型,但它们在转换后可兼容,则虚幻引擎会在连接引脚时自动创建转换节点。 image

播放蒙太奇

现在,你可以在事件图表中引用 骨骼网格体(Skeletal Mesh) 组件,并在其上播放蒙太奇。将 骨骼网格体(Skeletal Mesh) 组件拖放到事件图表中。

右键点击图表并选择 动画(Animation)> 蒙太奇(Montage)> 播放蒙太奇(Play Montage) 以添加 Play Montage 节点。然后将你的蒙太奇资产分配到 要播放的蒙太奇(Montage to Play) 引脚。 image

结果

现在你运行关卡时,应该能够看到角色在播放其打击动画时扭曲到相应的点。 image

你可以在下面看到本页用于将运动扭曲实现到简单扭曲目标位置的角色蓝图逻辑大图。 image

打开 AM_Cast_FireBolt 蒙太奇使用的动画序列 Cast_FireBolt

细节-根运动-启用根运动-启用 使其成为根运动动画,才可以使用 Motion Warping 运动扭曲 引擎功能 image

为角色添加 Motion Warping 运动扭曲 组件

打开 BP_AuraCharacter 添加 Motion Warping 运动扭曲 组件 image

AM_Cast_FireBolt 动画蒙太奇

将通知轨道 1 重命名为 Events

再添加一个通知轨道 Motion Warp image

调整时间 到 发射技能起始姿势处 在轨道 Motion Warp 添加通知状态-Motion Warping

image

调节通知状态 的末端覆盖技能动画的发射部分 image

选择运动扭曲关键帧-细节-

根骨骼运动修饰符配置(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

这样,动画将旋转根骨骼以面向目标,我们将其称为面向目标。 image

在角色中设置扭曲目标

打开 BP_AuraCharacter 事件图表

右键 custom event 添加自定义事件 SetFacingTarget SetFacingTarget 事件-细节-输入- 添加输入参数 FacingTarget 类型 vector 向量 image

拖入扭曲组件 扭曲组件 拖出 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 技能中旋转角色方向

打开 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

BPGraphScreenshot_2024Y-01M-15D-19h-30m-47s-112_00

现在释放技能时,角色将面向火球发射的方向。

现在 技能依赖角色蓝图。 但技能不应依赖角色,而应依赖角色实现的战斗接口。 为了分离这种依赖,在战斗接口中创建函数 这样,敌人也可以使用该接口实现扭曲。

战斗接口 CombatInterface 中 面向目标

Source/Aura/Public/Interaction/CombatInterface.h

UINTERFACE(MinimalAPI,BlueprintType)

public:
    // 面向目标 在蓝图子类中实现
    // native 事件不能标记为虚拟
    UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
    void UpdateFacingTarget(const FVector& Target);

BP_AuraCharacter 实现战斗接口的 UpdateFacingTarget

打开 BP_AuraCharacter 事件图表 添加事件-UpdateFacingTarget 连接到 add or update warp target from location

删除事件 SetFacingTarget

image

GA_FireBolt 技能使用 战斗接口的 面向目标

打开 GA_FireBolt 删除 SetFacingTarget 删除 cast to BP_AuraCharacter

get avatar actor from actor info 转化为 cast to CombatInterface cast to CombatInterface 拖出 UpdateFacingTarget BPGraphScreenshot_2024Y-01M-15D-19h-43m-45s-730_00

断开最后的 end Ability 结束技能节点,使客户端也可以释放火球。 但此时有问题,只能释放一次,因为客户端技能始终没有结束,所以不能再次激活。 BPGraphScreenshot_2024Y-01M-15D-19h-49m-34s-787_00

11. Projectile Impact 炮弹撞击

火球撞击敌人时产生效果

启用 Niagara 模块

Source/Aura/Aura.Build.cs

PrivateDependencyModuleNames.AddRange(new string[] { "NavigationSystem","Niagara"});

AuraProjectile

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-Impact Effect-NS_FireExplosion1 火球爆炸特效

细节-AuraProjectile-Impact Sound-sfx_FireBolt_Impact 火球爆炸音效

image

此时玩家可以向敌人发射火球

为 蒙太奇动画AM_Cast_FireBolt添加音效通知

打开 AM_Cast_FireBolt

添加 Sound 轨道 添加通知-播放音效 选择 playSound-细节-音效-sfx_FireBolt image image

火球飞行时循环播放的音效

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火球投射物设置循环音效

打开 BP_FireBolt 细节-AuraProjectile-Looping Sound-sfx_FireBoltHiss 火球循环音效 image

12. Projectile Collision Channel 弹丸碰撞通道

为障碍物生成重叠事件 防止角色,火球穿过障碍物

选择障碍物-搜索 generate 细节-物理-碰撞-生成重叠事件-启用 image

场景中大多数物体的 碰撞预设-对象类型 为 WorldStatic,WorldDynamic 所以需要一个 自定义对象类型

自定义对象类型 Projectile

项目设置-引擎-碰撞-Object channels-新建object通道 命名-Projectile 默认响应-Ignore

设置为忽略响应后,可以手动他对其他对象的具体响应类型。

您最多可以拥有18个自定义通道(包括对象和检测通道)。这是您项目的对象类型列表。如果删除游戏正在使用的对象类型,那么此类型将变回WorldStatic,

image image image

定义 投射碰撞通道 宏名称 方便C++使用

Source/Aura/Aura.h

// 投射碰撞通道
// 第一次创建 所以占用通道1,以此类推
#define ECC_Projectile ECollisionChannel::ECC_GameTraceChannel1

蓝图中 Projectile 对象类型 对应 c++ 中 ECC_Projectile 通道

药剂蓝图忽略 Projectile 通道

打开 BP_ManaPotion-box碰撞组件 细节-碰撞-碰撞预设-custom 细节-碰撞-碰撞预设-物体响应-Projectile-忽略 【与Projectile通道不会重叠或阻挡】 image

障碍物 与 Projectile 通道 重叠

细节-碰撞-碰撞预设-物体响应-Projectile-重叠

image

BP_FireBolt 火球投射物 的 碰撞预设-对象类型 改为 Projectile

打开 BP_FireBolt-Sphere球体碰撞组件 细节-碰撞-碰撞预设-custom 碰撞预设-对象类型 -Projectile image

投射物基类 C++ 碰撞预设-对象类型 均为 ECC_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 类型,默认忽略一切。 现在所有的药剂都会忽略火球 但敌人也会被火球忽略。

敌人与 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 网格体组件-碰撞-生成重叠事件-启用 image

此时 火球可以与敌人触发重叠事件。 火球可以攻击敌人。

13. Projectile Gameplay Effect 投射物游戏效果

制作一个基本的游戏伤害效果 GE_Damage ,用来影响健康属性

仅作学习。

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 image

使投射物携带游戏效果句柄 [轻型版本]

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();
}

为 敌人BP_EnemyBase 设置属性类

注意,GE_SecondaryAttributes 游戏属性重命名为 GE_AuraSecondaryAttributes

打开 BP_EnemyBase 暂时共享玩家的游戏属性。

Default Primary Attributes-GE_AuraPrimaryAttributes Default Secondary Attributes-GE_AuraSecondaryAttributes Default Vital Attributes-GE_AuraVitalAttributes

image

火球技能GA_FireBolt 设置 伤害效果类

打开 GA_FireBolt Damage Effect Class-GE_Damage image

此时 火球与敌人重叠时,会减少敌人的健康属性值 image

为了多次查看技能效果,在 技能 GA_FireBolt 中,生成火球后延迟0.5秒结束技能。

仅作测试多次技能伤害用 打开 GA_FireBolt image

敌人的健康值在不断变少。 服务器端和客户端均正常。

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

火区域与玩家的胶囊体和网格体都发生了重叠事件,导致玩家一次重叠受到2次伤害

应该只有其中之一与火区域重叠。

取消 胶囊体组件 的 生成重叠事件

打开 BP_AuraCharacter 选中 胶囊体组件-细节-碰撞-生成重叠事件 -不启用 image

此时,只有网格体组件会与火区域发生重叠。 角色网格体组件需要设置对 WorldDynamic 阻挡。 因为火区域为 WorldDynamic 对象类型。 image

在角色C++类中统一设置 胶囊体不生成重叠事件,防止多次触发重叠事件,因为网格体组件已设置了重叠事件

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);
}

14. Enemy Health Bar 敌方健康条

AuraEnemy C++ 敌人类中构造 健康条控件

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());
}

创建 敌人健康条控件蓝图 WBP_ProgressBar

Content/Blueprints/UI/ProgressBar/WBP_ProgressBar.uasset

右键-用户界面-控件蓝图-AuraUserWidget 名称 WBP_ProgressBar image

打开 WBP_ProgressBar

设计器: 尺寸框-SizeBox_Root 设为变量 所需 宽度重载 60 高度重载 6 image image

图表: 事件 event pre construct

SizeBox_Root 布局-尺寸框-set width override 函数

提升为变量 BoxWidth 分组 Progress Bar Properties 默认值 60

SizeBox_Root 布局-尺寸框-set height override 函数

提升为变量 BoxHeight 分组 Progress Bar Properties 默认值 6

image

折叠到函数 UpdateBoxSize image

设计器: 覆层-Overlay_Root

进度条-ProgressBar_Front 设为变量 水平填充 垂直填充

image ProgressBar-样式-样式-背景图-着色-A-0 ProgressBar-进度-百分比-30 image

图表: 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 默认值-着色-红色 BPGraphScreenshot_2024Y-01M-16D-00h-03m-35s-421_00 折叠导函数 UpdateFrontFillBrush image image image

为BP_EnemyBase敌人蓝图添加健康条控件

打开 BP_EnemyBase 选择 Health Bar 组件-细节-用户界面-控件类-WBP_ProgressBar 组件-细节-用户界面-以所需大小绘制-启用 image image

image

调整 Health Bar 组件 位置,防止被敌人网格体盖住 image image 旋转敌人面对视图,因为控件只能对正面视角显示。 image

将敌人自身设置为 健康条控件的 控件控制器

用以广播数据到健康控件

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());
    }
}

基于 WBP_ProgressBar 创建健康专用的健康条 WBP_EnemyHealthBar

Content/Blueprints/UI/ProgressBar/WBP_EnemyHealthBar.uasset 右键-用户界面-控件蓝图-WBP_ProgressBar 名称 WBP_EnemyHealthBar image

打开 WBP_EnemyHealthBar

BP_EnemyBase 使用 WBP_EnemyHealthBar

打开 BP_EnemyBase 选择 Health Bar 组件-细节-用户界面-控件类-WBP_EnemyHealthBar image

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

BPGraphScreenshot_2024Y-01M-16D-00h-50m-31s-040_00

现在对敌人施放火球技能,敌人的健康条会实时显示健康百分比

可能需要在敌人蓝图中调整 health bar 的缩放 image

15. Ghost Bar 幽灵进度条

幽灵进度条 留空待做。

WangShuXian6 commented 10 months ago

12. RPG Character Classes / RPG角色职业

1. RPG Character Classes / RPG角色职业

image image

2. Character Class Info 角色职业信息

存储所有相关角色的所有数据 CharacterClassInfo C++ 数据资产

基于 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);
}

基于 CharacterClassInfo 新建 DA_CharacterClassInfo 职业信息 资产蓝图

Content/Blueprints/AbilitySystem/Data/DA_CharacterClassInfo.uasset

右键-其他-数据资产-CharacterClassInfo 名称 DA_CharacterClassInfo image

打开 DA_CharacterClassInfo 新建3个 职业信息 最后一个再使用默认职业,否则无法多次创建。

这将主要属性游戏效果子类化

SecondaryAttributes ,VitalAttributes 在所欲职业之间共享。

现在有了数据资产,可以用游戏效果填充它们。

3. Default Attribute Effects 默认属性效果

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

image

次要属性的游戏效果是所有职业共享的相同效果。作为派生属性,可以在主要属性的修改器中调整参数。

如果想为敌人使用不同与玩家的次要属性修改器,可以单独为敌人新建次要属性游戏效果。 例如 GE_SecondaryAttributes_Elementalist

此处不使用不同,玩家与敌人使用相同。 将玩家 的 次要属性 重要属性 移动至共享文件夹,与敌人共享 重命名。去掉Aura部分表示共享 Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/GE_SecondaryAttributes.uasset

Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/GE_VitalAttributes.uasset

DA_CharacterClassInfo 配置游戏效果

打开 DA_CharacterClassInfo

各职业使用对应的主属性游戏效果

共享属性使用玩家共享的游戏效果 image

Warrior 职业主属性-GE_PrimaryAttributes_Warrior Ranger 职业主属性-GE_PrimaryAttributes_Ranger Elementalist 职业主属性-GE_PrimaryAttributes_Elementalist

所有职业通用次属性 SecondaryAttributes - GE_SecondaryAttributes 所有职业通用重要属性 VitalAttributes - GE_VitalAttributes

4. Curve Tables - CSV and JSON / 使用 曲线表-CSV和JSON 为各职业主属性游戏效果添加修改器

为 敌人Elementalist魔法师职业 的游戏效果 GE_PrimaryAttributes_Elementalist 的修改器使用曲线表

打开 GE_PrimaryAttributes_Elementalist

需要为4个主属性分别添加修改器,每个都使用曲线表,曲线表包含不同的等级对应的值

CT_PrimaryAttributes_Elementalist 魔法师职业主属性效果曲线表格

右键-其他-曲线表格-Cubic 三次曲线 不会自动插值 Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/Enemy/CurveTables/CT_PrimaryAttributes_Elementalist.uasset image image

打开 CT_PrimaryAttributes_Elementalist

力量属性曲线:Attributes.Primary.Strength

与游戏标签保持一致 image

曲线上-右键-添加关键帧 image

image

选择关键帧:修改值 x-1,y-5 表示力量 1级是,5点力量。 虽然x显示为时间单位秒,但可以当作x值。 image

单击 缩放匹配,当前关键帧居中显示。 image

在曲线上继续添加关键帧,x-5,y-7 image

选择2个关键帧,在第一个关键帧点上-右键-自动 【三次插值,自动切线】 可以使关键帧之间的线段平滑 image

image

在曲线上继续添加关键帧,x-10,y-9.5

选择曲线视图模式-规格化视图模式 规格化视图,显示与规格化为[-1,1]的Y轴重叠的所有曲线 可显示所有的关键帧 image image

在曲线上继续添加关键帧,x-15,y-12.5 在曲线上继续添加关键帧,x-20,y-14 在曲线上继续添加关键帧,x-40,y-25

全选关键帧-自动 使其平滑 image

智力属性曲线 Attributes.Primary.Intelligence

添加曲线 Attributes.Primary.Intelligence

添加关键帧: 1,15 5,19 10,21 20,25 40,40

全选-自动 image

韧性属性曲线 Attributes.Primary.Resilience

添加曲线 Attributes.Primary.Resilience

添加关键帧: 1,11 40,20 全选-自动

活力属性曲线 Attributes.Primary.Vigor

添加曲线 Attributes.Primary.Vigor

添加关键帧: 1,7 40,14

全选-自动

GE_PrimaryAttributes_Elementalist 魔法师主属性游戏效果

打开 GE_PrimaryAttributes_Elementalist

细节-Gameplay Effect-Modifiers 添加4组 对应4个主属性

Attributes.Primary.Strength 力量

使用曲线表格 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

image

Attributes.Primary.Intelligence 智力

使用曲线表格 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

Attributes.Primary.Resilience 韧性

使用曲线表格 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

Attributes.Primary.Vigor 活力

使用曲线表格 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 CT_PrimaryAttributes_Elementalist.csv image

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 image

为其他属性的等级补全值 image

将CSV文件导入到曲线表 CT_PrimaryAttributes_Elementalist

同名曲线将被覆盖

打开 CT_PrimaryAttributes_Elementalist image 从源文件中重导入曲线表。所有变更将丢失。此操作不可撤消 image 4个曲线都被修改。 导入的曲线无法使用 自动 选项。

拷贝 CT_PrimaryAttributes_Elementalist.csv 文件

重命名为 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

新建曲线表cubic类型- CT_PrimaryAttributes_Ranger

Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/Enemy/CurveTables/CT_PrimaryAttributes_Ranger.uasset

从文件 CT_PrimaryAttributes_Ranger.csv 导入曲线 image

将曲线表导出为json文件

CT_PrimaryAttributes_Elementalist-右键-导出为json CT_PrimaryAttributes_Elementalist.json image

[
    {
        "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
    }
]

拷贝 CT_PrimaryAttributes_Elementalist.json

重命名为 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
    }
]

导入json文件到引擎

点击内容栏-导入 image 选择 Data/CT_PrimaryAttributes_Warrior.json 文件

导入为-CurveTable 选择曲线插值类型-Cubic 应用

image

自动创建曲线表格 CT_PrimaryAttributes_Warrior 与json文件同名 image

打开 CT_PrimaryAttributes_Warrior image

使用json格式可以修改曲线。 而csv无法修改曲线。

为 GE_PrimaryAttributes_Ranger 游侠属性效果使用曲线表 CT_PrimaryAttributes_Ranger

打开 GE_PrimaryAttributes_Ranger

细节-Gameplay Effect-Modifiers 添加4组 对应4个主属性

Attributes.Primary.Strength 力量

使用曲线表格 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

image

Attributes.Primary.Intelligence 智力

使用曲线表格 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

Attributes.Primary.Resilience 韧性

使用曲线表格 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

Attributes.Primary.Vigor 活力

使用曲线表格 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 战士属性效果使用曲线表 CT_PrimaryAttributes_Warrior

打开 GE_PrimaryAttributes_Warrior

细节-Gameplay Effect-Modifiers 添加4组 对应4个主属性

Attributes.Primary.Strength 力量

使用曲线表格 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

image

Attributes.Primary.Intelligence 智力

使用曲线表格 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

Attributes.Primary.Resilience 韧性

使用曲线表格 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

Attributes.Primary.Vigor 活力

使用曲线表格 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 职业数据应用属性游戏效果

打开 DA_CharacterClassInfo image

敌人的辅助属性游戏效果

玩家的主要属性可以在运行时变化, 玩家的辅助属性基于重要属性,应用了无限时间游戏效果随主属性变化。

但敌人的主要属性不需要在运行时变化, 敌人的主要属性根据其等级初始化,然后不再变化。 所以不需要为敌人辅助属性应用无限时间游戏效果。

拷贝 GE_SecondaryAttributes 辅助属性效果 重命名为 GE_SecondaryAttributes_Enemy 作为敌人的辅助属性游戏效果

Content/Blueprints/AbilitySystem/GameplayEffects/DefaultAttributes/GE_SecondaryAttributes_Enemy.uasset

打开 GE_SecondaryAttributes_Enemy 持续时间-duration policy-instant 即时 image

打开 DA_CharacterClassInfo SecondaryAttributes-GE_SecondaryAttributes_Enemy image

将 GE_SecondaryAttributes 移动至 Aura文件夹

5. Initializing Enemy Attributes 初始化敌人属性

每个角色都需要数据资产,将其存储在一个中心位置. 游戏模式是决定游戏规则的地方。 从某种意义上说,这个数据资产包含了很多规则,比如敌人根据他们的等级应该取什么属性值。 因此,可以在游戏模式上存储敌人职业数据资产,这样它就只存在于单个类中。 仅占用其中一项数据资产的内存。 将数据资产存储在游戏模式上,并能够从蓝图中进行设置。

游戏模式中存储职业数据资产

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 设置 职业信息数据资产 DA_CharacterClassInfo

打开 BP_AuraGameMode Character Class Info-DA_CharacterClassInfo image

运行游戏,各类职业的敌人将拥有默认属性。

为敌人角色使用不同的职业 CharacterClass

image

WangShuXian6 commented 10 months ago

13. Damage 伤害

1. Meta Attributes 元属性

到目前为止,影响健康的游戏效果正在影响属性集并改变健康的值。 在更复杂的角色扮演游戏中,通常不会这样做。

因为,例如,每当我们对敌人造成伤害时,都会进行许多计算. 并且这些计算与攻击者和受害者的属性有关.

为了保持数学简单,我们经常使用占位符属性,其行为作为中介,允许我们实际执行设定健康值之前需要执行的所有数学运算。 这些中间值称为元属性。

元属性和普通属性之间的区别:

普通属性通常会被复制,并且可以在服务器上更改并复制到客户端。

元属性不会被复制,它实际上只是一个临时占位符。 仅在服务器上使用这些来执行计算,执行这些计算后可以将更改并应用于重要的真实属性上。 image

例如,在损坏的情况下,我们可能有一个称为传入损坏的元属性。 元属性已经不再复制。 假设游戏效果将施加伤害。 假设这个游戏效果显示我对你造成 11 点伤害。 游戏效果将增加元属性生命值,而不是减去受害者的 11。 元属性执行所有修改器的计算。 然后得出最终值。 之后归零,开始对元属性做出响应。 例如弹出提示文字,眩晕玩家,减少健康值。

2. Damage Meta Attribute 伤害元属性

使用元属性伤害替代游戏效果对敌人直接造成的伤害。

属性集中添加 元属性 IncomingDamage 传入伤害

由于元属性不会复制,因此它不会收到代表通知。 例如 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 效果 使用使用传入的伤害,而不是直接设置生命值属性

打开 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 image

使用使用传入的伤害,不直接修改健康值: 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 image

现在通过伤害元属性 IncomingDamage 来修改敌人的健康值。

伤害元属性首先被清零,每一帧都被消耗用于计算伤害。

3. Set By Caller Magnitude 由调用者设置大小

这个大小将由创建技能效果规范的代码/蓝图显式设置

GE_Damage 效果使用 Set By Caller ,代码直接设置伤害

打开 GE_Damage

Modifiers-索引0-Attribute-AuraAttributeSet.Health Modifiers-索引0-Modifier Op-Add Modifiers-索引0-Modifier Magnitude-Magnitude calculation Type-Set By Caller image

调用者幅度设置的是它们的键值对, 需要一个游戏标签来根据调用者的大小来识别属性集合。 因此,首先要为伤害创建一个游戏标签。

为伤害创建一个游戏标签

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")
        );
}

魔法投射技能游戏效果规格句柄携带着一个键值对:伤害游戏标签,其值为 50

只需要在应用时能够从游戏效果中访问该键值对

// 使用技能系统本身造成伤害,而非游戏效果
        // 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 效果使用 Set By Caller ,由调用者设置伤害大小

打开 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 点伤害

image

现在攻击敌人可造成50点伤害。

现在我们的游戏技能系统可以控制伤害的大小, 它不仅仅是在游戏效果本身中设置的,它现在是我们通过代码控制的东西。

4. Ability Damage 技能伤害

为技能系统增加与等级关联的伤害

Source/Aura/Public/AbilitySystem/Abilities/AuraGameplayAbility.h

public:
    // 与等级关联的可扩展伤害
    // FScalableFloat 表示可设置缩放值和曲线表
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Damage")
    FScalableFloat Damage;

子类技能 GA_FireBolt 中可设置 Damage 属性

可以设置缩放值。 并且可以使用曲线表格使 Damage 可扩展,随等级变化

GA_FireBolt-细节-损害-Damage image

为 火球术技能 GA_FireBolt 的 Damage 属性创建json格式曲线表格 CT_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 应用 image image

火球术技能GA_FireBolt 的 Damage 属性 使用 曲线表格 CT_Damage

打开 GA_FireBolt

损害- Damage 1, CT_Damage,Abilities.FireBolt

image

魔法投射技能基类中使用 Damage 扩展值

        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);
    }
}

5. Enemy Hit React 敌人命中反应

为敌人新增命中响应技能,该技能播放命中相应蒙太奇。 击中敌人时,为其附加原生游戏标签,触发敌人命中响应技能,触发蒙太奇动画。

敌人命中反应游戏效果 GE_HitReact

基于 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")
        );

}

敌人命中反应游戏效果 GE_HitReact 使用 Effects.HitReact 效果标签

细节-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

image 游戏效果 GE_HitReact 将为目标授予 Effects.HitReact 效果标签。

在 Aura Enemy 监听技能系统组件上的游戏标签更改 响应 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;
}

为敌人新增命中响应技能 GA_HitReact

基于 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

BPGraphScreenshot_2024Y-01M-16D-20h-06m-44s-099_00

战斗接口 获取 命中相应蒙太奇指针

Source/Aura/Public/Interaction/CombatInterface.h

class UAnimMontage;

public:
    // 获取 命中相应蒙太奇指针
    // BlueprintNativeEvent 蓝图本地事件,c++无需virtual 声明,C++ GetHitReactMontage_Implementation 实现
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
    UAnimMontage* GetHitReactMontage();

角色基类继承 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;
}

GA_HitReact 命中响应技能从接口获取蒙太奇动画

图表: cast to CombatInterface-as combat Interface 拖出 GetHitReactMontage
GetHitReactMontage 的return value 输出至 playMontageAndWait-Montage to play

BPGraphScreenshot_2024Y-01M-16D-20h-22m-31s-235_00

创建 命中相应蒙太奇动画

基于动画序列 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

为敌人设置 HitReactMontage 命中相应动画蒙太奇

打开 BP_Goblin_Spear combat-HitReactMontage-AM_HitReact_GoblinSpear

image

打开 BP_Goblin_Slingshot combat-HitReactMontage-AM_HitReact_GoblinSlingshot image

6. Activating the Enemy Hit React Ability 激活敌方命中反应技能

在敌人受到伤害且为死亡时激活技能 GA_HitReact

将 技能 GA_HitReact 添加到职业信息 CharacterClassInfo 中

职业信息由敌人共享,可以包含共享的技能

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数据资产添加 通用技能-命中响应技能

打开 DA_CharacterClassInfo

Common Abilities-添加一个技能 GA_HitReact image

为 GA_HitReact 技能 添加标签 Effects.HitReact

通过标签激活技能 GA_HitReact 时,技能必须拥有该标签。 TargetASC->TryActivateAbilitiesByTag(TagContainer);

打开 GA_HitReact 细节-标签-Ability Tags-Effects.HitReact image

对目标的健康属性集造成伤害,如果这种伤害不是致命的, 尝试使用包含 Effects_HitReact 标签的标签容器通过标签激活技能 GA_HitReact。 GA_HitReact 技能将其中的 GE_HitReact 效果应用在目标上。 然后获取,播放蒙太奇动画

现在攻击敌人,敌人将播放受击蒙太奇动画。

调整蒙太奇

AM_HitReact_GoblinSlingshot,AM_HitReact_GoblinSpear 混合选项混入-混合时间-0.05 更快的混合 混出-混合时间-0.05 更快的混合 image

GA_HitReact 技能

打开 GA_HitReact 技能 高级-Instancing Policy 实例化策略-Instanced Per Actor 每个参与者实例化 第一次激活时,只会实例化一次技能。后续使用缓存。

image

蒙太奇完成,被打断,取消时,结束 GE_HitReact 游戏效果,结束 GA_HitReact 技能

打开 GA_HitReact 技能 图表 通过句柄移除游戏效果 RemoveGameplayEffectFromOwnerWithHandle Active GE Hit React 输出至 RemoveGameplayEffectFromOwnerWithHandle-handle

然后再结束技能 end ability BPGraphScreenshot_2024Y-01M-16D-21h-19m-10s-649_00

在蒙太奇混出时也结束效果和技能,则可以使敌人播放完整动画,减少受击动画频次。 但是不使用混出时也结束效果更好。更符合直觉。

BPGraphScreenshot_2024Y-01M-16D-21h-21m-38s-762_00

7. Enemy Death 敌人死亡

死亡时玩家和敌人的通用功能

战斗接口增加死亡方法

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秒后消失。 image

8. Dissolve Effect 溶解效果

M_DissolveEffect 溶解材质 BPGraphScreenshot_2024Y-01M-16D-22h-01m-21s-917_00

将主框中的节点复制到其他材质,可使其具由溶解效果。

敌人溶解材质 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();
}

为角色设置材质实例,创建时间轴 实现 StartDissolveTimeline

打开 BP_EnemyBase

事件图表:

添加事件 StartDissolveTimeline 【此处实现基类的定义】

add timeline 重命名 DissolveTimeline

双击 DissolveTimeline 打开 时间轴编辑器

image 添加浮点型轨道 DissolveTrack 添加关键帧 时间:0 值:-0.1 表示溶解开始 image

image

添加关键帧 时间:3 值:0.55 表示完全溶解

image

ctrl 多选2个关键帧-右键-自动 使过渡平滑 image

打开 MI_GoblinDissolve 查看控制溶解的参数名称

image

事件图表:

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

BPGraphScreenshot_2024Y-01M-16D-22h-45m-37s-971_00

为角色,敌人设置溶解材质实例

打开 BP_Goblin_Spear Dissolve Material Instance-MI_GoblinDissolve Weapon Dissolve Material Instance-MI_Spear_Dissolve

image

BP_Goblin_Slingshot Dissolve Material Instance-MI_GoblinDissolve Weapon Dissolve Material Instance-MI_Slingshot_Red_Dissolve image

9. Floating Text Widget 浮动文本控件

伤害提示文本控件蓝图 WBP_DamageText

不需要控件控制器

右键-用户界面-控件蓝图-UserWidget WBP_DamageText Content/Blueprints/UI/FloatingText/WBP_DamageText.uasset

打开 WBP_DamageText 设计器: 大小为-所需 覆层-Overlay_Root

文本-Text_Damage 内容-文本-999 设为变量 水平居中对齐 垂直居中对齐 image

添加动画 DamageAnim

DamageAnim-添加轨道-Text_Damage Text_Damage-轨道加号-变换

第一帧-变换-平移-x 0,y -80 使文本上移 image

移动时间轴到0.2 变换-平移-x 90,y -80 使文本右移 image

移动时间轴到0.4 变换-平移-x 112,y -96

移动时间轴到0.7 变换-平移-x 88,y -212 image

移动时间轴到1 变换-平移-x -100,y -300 image

打开曲线编辑器 查看 image

设置缩放动画 移动时间轴到0.1 变换-缩放-x 2.25,y 2.25

移动时间轴到0.2 变换-缩放-x 1,y 1

移动时间轴到1 变换-缩放-x 0.4,y 0.4 image

设置不透明度动画 Text_Damage-轨道加号-渲染不透明度 image 移动时间轴到1 渲染不透明度 0 image

图表

damage anim play animation image

左侧添加函数 UpdateDamageText

UpdateDamageText-输入参数-增加 Damage 类型 浮点

Text_Damage setText(Text)

to text(float) to text(float)-Maximum Fractional Digits-3

image

伤害提示文本组件 C++

基于 WidgetComponent 新建C++ DamageTextComponent

image image

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"

基于 DamageTextComponent 创建 BP_DamageTextComponent 伤害提示文本控件组件蓝图

Content/Blueprints/UI/FloatingText/BP_DamageTextComponent.uasset

打开 BP_DamageTextComponent 设置控件类 细节-用户界面-控件类-WBP_DamageText image

事件图表: 实现 SetDamageText 添加事件 event Set Damage Text

get user widget object 获取控件 cast to WBP_DamageText

UpdateDamageText

image

10. Showing Damage Text 显示伤害文本

属性集的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);
    }
}

属性集检测到属性变化时 调用控制器上的显示文本组件方法ShowDamageNumber

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_AuraPlayerController Damage Text Component Class-BP_DamageTextComponent image

BP_DamageTextComponent 生成控件后,延时删除控件

打开 BP_DamageTextComponent 图表 delay destroy component 销毁组件,也会销毁控件 BPGraphScreenshot_2024Y-01M-17D-00h-19m-37s-571_00

防止文本被遮挡

细节-用户界面-空间-屏幕 控件在屏幕中渲染,完全在场景之外,从不会被遮挡。

默认为 场景 控件在场景中被渲染为网格体,其能够像场景中的其他网格体一样被遮挡. image

现在敌人受到伤害会显示伤害文本 image

防止文本控件像素模糊化

打开 WBP_DamageText 动画- 确保缩放不超过1 所有的缩放关键帧的值都成被缩小。 缩放值:0.4 1 0.4 0.2

设计器: 将实际文本放大,不会像素化。

Text_Damage 控件-细节-外观-字体-尺寸-放大为 60 image

11. Execution Calculations 执行计算

最定制化的计算方式:UGameplayEffectExecutionCaculation

image image

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 仅在服务器上执行本地预测、服务器启动和仅服务器网络执行策略的游戏播放能力

Execution Calculation 执行计算 常用于复杂的伤害计算

image

12. Damage Execution Calculation 伤害执行计算

根据游戏效果执行计算定制的计算类。 这就是所谓的伤害执行计算。

伤害执行计算 C++ ExecCalc_Damage

基于 GameplayEffectExecutionCalculation 新建 C++ ExecCalc_Damage image image

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();
}

13. ExecCalcs - Capturing Attributes / ExecCalcs-捕获属性

捕获执行计算中的属性

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 伤害效果中使用 执行计算修改器 ExecCalc_Damage

打开 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 游戏效果条件

image

14. Implementing Block Chance 实现格挡几率

根据调用者大小计算伤害值

属性集监测到的属性变更值,是经过执行计算修改器修改后的值 例如伤害值

执行计算修改器经过计算的伤害值,应用到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

创建测试效果测试执行计算 GE_SecondaryAttributes_TEST

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

image

DA_CharacterClassInfo 职业数据资产使用测试效果 GE_SecondaryAttributes_TEST

打开 DA_CharacterClassInfo Common Class Defaults-Secondary Attributes- GE_SecondaryAttributes_TEST image

此时固定百分百格挡,伤害为2.5

恢复使用原辅助效果 Common Class Defaults-Secondary Attributes- GE_SecondaryAttributes_Enemy

15. Implementing Armor and Armor Penetration 实现护甲和护甲穿透

伤害计算顺序 -格挡几率-护甲穿透

护甲穿透抵消护甲的百分比

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);

}

16. Damage Calculation Coefficients 伤害计算系数

系数可能随等级变小。 在职业信息数字资产中保存每个系数的曲线表。 因为这是共享资产。

为伤害计算新建系数曲线表格资产 CT_DamageCalculationCoefficients

右键-其他-曲线表格-constant CT_DamageCalculationCoefficients 没有平滑过渡 Content/Blueprints/AbilitySystem/Data/CT_DamageCalculationCoefficients.uasset image

打开 CT_DamageCalculationCoefficients 默认曲线:ArmorPenetration 护甲穿透系数

添加2列 第1列列名-1 值0.25 第2列列名-10 值0.25 表示等级1-10 系数均为0.25 这会将每100点护甲穿透减少25点。 image 由于护甲穿透随等级提高,系数可以随等级降低。

第2列列名-10 值0.15

第3列列名-20 值0.085

第4列列名40 值0.035 image

阶梯状曲线图表示: image

添加曲线:EffectiveArmor 有效护甲

1,0.333 10,0.25 20,0.15 40,0.085 image

在职业信息数据资产中添加 伤害计算系数属性

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 image

使用 技能系统蓝图函数库 获取数据资产

该数据资产存在于游戏模式中,如果可以访问游戏模式,那就可以访问数据资产。 不应该每次需要该数据资产的某种值时都必须获取游戏模式。

使用更简单的方法来获取该数据资产:技能系统蓝图函数库

先获取职业信息,再从其中获取曲线表资产数据

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;
}

在 执行计算修改器ExecCalc_Damage C++中使用系数曲线 CT_DamageCalculationCoefficients

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);
}

现在,使用了系数曲线。 必须设置系数曲线资产,否则让游戏崩溃,更容易找到崩溃原因。

17. Implementing Critical Hits 实现致命一击【暴击】

捕获一些属性并确定命中是否至关重要。 为此使用暴击几率, 并使用暴击抗性来降低有效暴击几率。 如果你受到暴击,则伤害加倍,并为暴击伤害添加暴击值。 添加致命一击抵抗系数计算系数的曲线表,调整您的致命一击抵抗力。

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

打开 CT_DamageCalculationCoefficients 添加曲线 CriticalHitResistance 系数随等级降低 0.15 0.1 0.08 0.06

image

执行计算中的暴击属性,无法在属性集变更中访问。

现在攻击敌人将有几率造成暴击。

WangShuXian6 commented 10 months ago

14. Advanced Damage Techniques 高级伤害技术

1. The Gameplay Effect Context 游戏效果情景

魔法投射技能中,为游戏情景添加技能,源对象,投射物,技能命中结果。

弱对象指针是智能指针。 它们是可以存储指针的包装器,而弱对象指针有一个属性,就是不获取它所存储的指针的所有权。 即,它不参与引用计数。垃圾收集不会跟踪它。 弱引用指针就代表“我指向这东西,但这东西什么时候释放不关我事儿……”

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);
    }
}

2. Custom Gameplay Effect Context 自定义游戏效果情景

从代码编辑器添加文件 游戏效果情景类型 AuraAbilityTypes

用来存储 是否暴击,是否格挡等变量

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;
}

3. NetSerialize 网络序列化

image 左侧表示网络传输流,右侧表示布尔变量

存储- 从右向左执行 即 序列化布尔变量 image

加载- 从左向右 ,即 左侧被反序列化,存储在右侧布尔值中。 image

image

image image

image image image

image image image

用来存储布尔值的紧凑方式: 为真则翻转指定位

RepBits=0;
if(A){
RepBits |= 1<<0;
}
RepBits |= 1<<1;
RepBits |= 1<<2;

image

4. Implementing Net Serialize 实现网络序列化

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;
}

5. Struct Ops Type Traits 结构操作类型特征

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
    };
};

6. Aura Ability System Globals / Aura全局技能系统

自定义Aura全局技能系统 C++ AuraAbilitySystemGlobals

基于 AbilitySystemGlobals 新建 自定义Aura全局技能系统 C++ AuraAbilitySystemGlobals

存放需要全局访问的变量。

image image

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();
}

配置项目使用 自定义Aura全局技能系统

Config/DefaultGame.ini 新增内容

[/Script/GameplayAbilities.AbilitySystemGlobals]
+AbilitySystemGlobalsClassName="/Script/Aura.AuraAbilitySystemGlobals"

查看是否使用了 自定义Aura全局技能系统

在 EffectContextHandle 效果情景上断点调试 查看其内容【调试模式】 Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp

SpawnProjectile()
{
EffectContextHandle.AddHitResult(HitResult);
}

玩家发射一个火球技能触发该函数。 以下变量为自定义Aura全局技能系统所添加的专有属性

bIsBlockedHit = {bool} false
bIsCriticalHit = {bool} false

image

可以在游戏任何地方使用。例如属性集变更处。

7. Using a Custom Effect Context 使用自定义效果情景

蓝图函数库 添加 设置 自定义效果情景方法

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 参数,获取是否格挡变量。

在 执行计算修改器 ExecCalc_Damage 中为自定义效果情景设置是否暴击,是否格挡属性

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);
        }
    }
}

8. Floating Text Color 浮动文本颜色

更新文本提示组件

伤害文本组件

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 控件中实现是否暴击,格挡

打开 WBP_DamageText

图表: 进入 Update Damage Text 函数: 为 Update Damage Text 函数添加输入参数: BlockedHit 布尔类型 CriticalHit 布尔类型 image

左侧新建 函数 GetColorBasedOnBlockAndCrit 为GetColorBasedOnBlockAndCrit输入添加输入参数: IsBlocked 布尔类型 IsCrit 布尔类型

IsBlocked 提升为变量 Blocked IsCrit 提升为变量 Crit

为GetColorBasedOnBlockAndCrit输入添加输出: Color: Slate颜色类型 提升为变量 Out Color image

节点: 流程控制-branch

数学-布尔-and bool 数学-布尔-not bool

set Out Color

make SlateColor

流程控制-branch

流程控制-branch

流程控制-branch

格挡,未暴击-蓝色 暴击,未格挡-红色 暴击,格挡-黄色 未暴击,未格挡-白色

BPGraphScreenshot_2024Y-01M-17D-22h-43m-18s-507_00

Update Damage Text 事件图表:

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

BPGraphScreenshot_2024Y-01M-17D-19h-36m-19s-990_00

BP_DamageTextComponent 控件使用新变量 :是否暴击,格挡

打开 BP_DamageTextComponent 图表: 将时间获取的参数输出至 Update Damage Text 函数

BPGraphScreenshot_2024Y-01M-17D-19h-01m-56s-864_00

9. Hit Message 命中消息

WBP_DamageText 控件

设计器:

添加控件: 文本:Text_HitMessage 设为变量 水平居中对齐 垂直居中对齐 将文本中对齐 轮廓大小:2 外观-字体-尺寸-78 image

动画:

新建动画: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 BPGraphScreenshot_2024Y-01M-17D-21h-27m-08s-439_00

Update Damage Text 函数

Text_HitMessage 控件-set text(text) 函数

image

左侧添加函数 GetHitMessageBasedOnBlockAndCrit GetHitMessageBasedOnBlockAndCrit 添加输入参数

IsBlocked 布尔类型 IsCrit 布尔类型

IsBlocked 提升为变量 Blocked_2 IsCrit 提升为变量 Crit_2

为GetColorBasedOnBlockAndCrit输入添加输出: Message 文本类型 提升为变量 OutMessage

从 Get Color Based on Block And Crit 拷贝所有条件分支 image

右键将拷贝的变量替换为本地变量

OutMessage BPGraphScreenshot_2024Y-01M-17D-22h-37m-33s-952_00

Update Damage Text 函数

Update Damage Text 函数 的输入提升为本地变量 Blocked,Crit,Demage

GetHitMessageBasedOnBlockAndCrit GetHitMessageBasedOnBlockAndCrit-Message 提升为本地变量 Message

Text_HitMessage 外观-set color and opacity Text Color

BPGraphScreenshot_2024Y-01M-17D-22h-41m-17s-670_00

GA_HitReact

打开 GA_HitReact Instancing Policy 实例化策略-Instanced Per Execution 每次执行实例化 受击立刻重新播放受击蒙太奇动画。 轻量级可以这样设置。

10. Damage Types 伤害类型

使用游戏标签识别伤害类型。 游戏标签可以在整个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);
}

基于 C++ AuraGameplayAbility 创建 AuraDamageGameplayAbility 伤害型技能 C++ 基类

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;
};

所有伤害类型技能例如 AuraProjectileSpell 的父级C++都改为 AuraDamageGameplayAbility

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 火球技能

打开 GA_FireBolt 损害-DamageTypes-添加一个伤害键值对 损害-DamageTypes-键-Damage.Fire 损害-DamageTypes-值-系数:1,曲线表格:CT_Damage ,曲线:Abilities.FireBolt image

这替代了原先的可扩展类型Damage,但效果一样。 只是处理方式不同,并且新方式支持多个伤害叠加。

现在 火球术作为 伤害型技能的基类 可包含多种类型的伤害型标签。且包含可扩展伤害值。 现在已包含 Damage.Fire 标签,表示 火属性伤害。

火球术 的 DamageEffectClass 伤害技能效果类 为 GE_Damage , GE_Damage 使用了 执行计算 ExecCalc_Damage 修改器。 执行计算 ExecCalc_Damage 修改器 中 获取了 DamageTypes 包含的所有伤害标签例如 Damage.Fire, 对目标的健康值造成伤害。

之后可以为火球术 DamageTypes 添加 更多伤害类型的【标签-伤害曲线】键值对。 例如暗属性的伤害。 ExecCalc_Damage 修改器 都可以接收到。

11. Mapping Damage Types to Resistances 将伤害类型映射到韧性/抗性

有了伤害类型的概念。 就有与之对抗的抗性类型。

定义所有的伤害类型标签,并为每种伤害类型映射一个抵抗类型标签。

伤害-抗性组,为每种伤害类型技能标签映射一个抵抗类型属性标签: 将之前的 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")
        );
}

执行计算修改器从 伤害-抗性 map 获取伤害技能标签 然后从标签获取伤害值

    // 根据调用者大小计算伤害值 累加
    // 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);
}

12. Resistance Attributes 抗性属性

属性集中添加抗性属性【辅助类别】

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 辅助属性效果中的抗性属性计算

【可以单独新建抗性效果处理抗性计算,此处未使用该方式】 抗性可以依赖多种属性。

打开 GE_SecondaryAttributes

添加新的 修改器 Modifier 火抗属性 依赖 韧性 Resistance

每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 image

添加新的 修改器 Modifier 火抗属性 依赖 智力 Intelligence 【可选,为保持简单,当前不使用该修改器】

每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

添加新的 修改器 Modifier 光抗属性 依赖 韧性 Resistance

每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

添加新的 修改器 Modifier 秘抗属性 依赖 韧性 Resistance

每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

添加新的 修改器 Modifier 物抗属性 依赖 韧性 Resistance

每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_AttributeMenu 设计器 滚动框 控件下继续添加 WBP_TextValueRow

Row_FireResistance Row_LightningResistance Row_ArcaneResistance Row_PhysicalResistance 设为变量 image

图表: sequence

Row_FireResistance Row_LightningResistance Row_ArcaneResistance Row_PhysicalResistance

set attribute tag

分别使用标签: Attributes.Resistance.Fire Attributes.Resistance.Lightning Attributes.Resistance.Arcane Attributes.Resistance.Physical

BPGraphScreenshot_2024Y-01M-18D-16h-11m-36s-590_00

优化蓝图: BPGraphScreenshot_2024Y-01M-18D-16h-17m-55s-144_00

分别折叠到函数: SetPrimaryAttributeTags SetSecondaryAttributeTags SetResistanceAttributeTags

image

DA_AttributeInfo 属性信息数据资产中 新增抗性的 标签-属性名-描述 信息结构

打开 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-增加对物理属性伤害的抗性

image 现在属性菜单可看到抗性属性 image

13. Resistance Damage Reduction 减少抗性伤害

执行计算修改其中 获取抗性值 参与伤害计算

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 添加抗性属性

打开 GE_SecondaryAttributes_TEST

添加新的 修改器 Modifier 火抗属性 依赖 韧性 Resistance

每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

image

14. Multiplayer Test 多人测试

由于游戏模式仅存在于服务端,现在游戏在多人模式中,客户端游戏模式为空,将崩溃。

敌人类的技能和属性应当只在服务端初始化

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);
    }
}
WangShuXian6 commented 10 months ago

15. Enemy AI 敌人AI

https://docs.unrealengine.com/5.3/zh-CN/artificial-intelligence-in-unreal-engine/ AI系统——一种可用于在项目中创建高真实度AI实体的系统。

1. Enemy AI Setup 设置 敌人AI

image

2. AI Controller Blackboard and Behavior Tree / AI控制器黑板和行为树

启用 AI模块 AIModule

Source/Aura/Aura.Build.cs

PrivateDependencyModuleNames.AddRange(new string[] { "NavigationSystem", "Niagara", "AIModule" });

AI控制器: 基于 AI Controller 新建 AuraAIController C++

image 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);
}

基于 AuraAIController 创建 BP_AuraAIController 蓝图

image

敌人蓝图BP_EnemyBase 使用 BP_AuraAIController

打开 BP_EnemyBase pawn-AI Controller class-BP_AuraAIController

image

创建黑板 BB_EnemyBlackboard

右键-人工智能-黑板 BB_EnemyBlackboard image Content/Blueprints/AI/BB_EnemyBlackboard.uasset image

创建行为树 BT_EnemyBehaviorTree

右键-人工智能-行为树 BT_EnemyBehaviorTree image

Content/Blueprints/AI/BT_EnemyBehaviorTree.uasset image

行为树具由黑板资产

BP_EnemyBase 使用 行为树 BT_EnemyBehaviorTree

打开 BP_EnemyBase AI-Behavior Tree-BT_EnemyBehaviorTree image

AuraEnemy 中运行行为树,让行为树控制敌人

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 行为树的行为快速测试

打开 BT_EnemyBehaviorTree 行为树: 拖出节点: selector 选择器 选择器会从左至右按顺序执行其并列的子节点

tasks-paly animation paly animation-细节-节点-要播放的动画-Attack_Spear image image

运行游戏,敌人将播放攻击动画 Attack_Spear。

删除测试节点。

3. Behavior Tree Service 行为树服务

选择器的行为

image

selector 选择器会从左至右按顺序执行其并列的子节点。 一个播放动画节点完成,它将转下一个节点。 行为树中的节点具有成功或失败的概念, 节点是否成功或失败取决于节点如何定义成功或失败。 如果一个节点成功或失败,它会将成功或失败信息返回给其父节点, 然后父节点将决定如何处理该信息。

选择器将依次执行其子级,直到它的一个子级成功了,它将成功返回到ROOT。 一旦成功返回到根,那么整棵树就会重新开始。

具有三个子级的选择器将首先执行其第一个子级。 如果那个子级成功了,其他子级就永远不会成功。 一旦它成功返回根并重新开始。 如果它再次成功,那么将永远不会尝试执行其他两个子节点。 只要第一个节点保持成功,它就会一直执行。 一旦失败,选择器将尝试执行其下一个子节点并继续从那里开始这个过程。

择器称为复合节点,我们可以将某些类型的节点附加到复合节点上

附加到选择器的这些类型的节点之一称为服务。

右键单击选择器,可以添加装饰器和服务。 image 装饰器可以附加到节点,服务可以附加到节点。

选择器被认为是一个分支,即使它有多个子级连接到它, 选择器及其所有子级视为一个分支。

一个服务一旦附加到节点(例如选择器),只要分支正在被执行。 则附加到它的任何服务都将被执行,并且服务以我们指定的频率执行。

基于 BTService_BlueprintBase 创建服务 BTService_FindNearestPlayer C++

一个服务 获取所有玩家控制的角色并找到最近的玩家,这样它就可以去攻击该玩家。

由于服务经常在行为树上使用,并且拥有该节点的行为树组件具有它自己的主人。 可以访问拥有该特定节点的参与者和控制器。

image image

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());
}

基于 BTService_FindNearestPlayer 创建 BTS_FindNearestPlayer 服务蓝图

Content/Blueprints/AI/BTS_FindNearestPlayer.uasset

打开 BTS_FindNearestPlayer 事件图表:

左侧可以重载事件函数。

细节面板:

描述-节点名称-FindNearestPlayer

image

左侧重载函数 接收Tick AI image

image

将 BTS_FindNearestPlayer 服务 添加到 BT_EnemyBehaviorTree 行为树

打开 BT_EnemyBehaviorTree 行为树: 选择器-右键-添加服务-BT Enemy Behavior Tree

image image

现在让我们使用蓝图版本 BT 查找最近的玩家,注意它显示“FindNearestPlayer”。 该名称在服务中0节点名称 定义.

选择 带有该服务的选择器的 FindNearestPlayer 部分 细节-服务-间隔 0.5 表示执行频率 image

运行游戏: image

BTS_FindNearestPlayer 服务 的 AIOwner ,ActorOwner 都是 BP_AuraAIController

4. Blackboard Keys 黑板键/黑板关键帧

服务和行为树不存储变量。 服务将收集的信息存储在黑板键资产中。例如最近的玩家。

BB_EnemyBlackboard

BB_EnemyBlackboard 有默认关键帧 SelfActor image

行为树可以访问与其关联的黑板。 在行为树中,可以访问这些值并根据这些值进行行为。

BTS_FindNearestPlayer 添加黑板键类型的变量

打开 BTS_FindNearestPlayer 新增变量 公开变量 SelfActorKey 类型-黑板键选择器

image

BT_EnemyBehaviorTree 行为树的 BTS_FindNearestPlayer 服务上可设置 SelfActorKey

打开 BT_EnemyBehaviorTree 选择 FindNearestPlayer 细节-默认-SelfActorKey-SelfActor image

SelfActor 是行为树的黑板的默认键 这样 ,服务可以获取到行为树的黑板的黑板键

BTS_FindNearestPlayer 服务获取行为树的黑板的黑板键

打开BTS_FindNearestPlayer

image image 屏幕打印出各个敌人的蓝图名称 :BP_Goblin_Spear 表示 行为树的黑板的黑板键 SelfActor 自动设置为 BP_Goblin_Spear 敌人角色

这因为: 敌人类 包含行为树,包含AI控制器。 行为树包含了黑板。包含了服务。 服务可以获取到行为树的黑板的黑板键。【黑板的黑板键 SelfActor 自动设置为 BP_Goblin_Spear 敌人角色】

删除测试节点和SelfActorKey变量。

BB_EnemyBlackboard 黑板 添加跟踪目标键

打开 BB_EnemyBlackboard

添加键 Obejct

名称 TargetToFollow image

TargetToFollow-细节-键类型-基类-Actor 这用来限制 TargetToFollow 的类型

TargetToFollow-细节-实例已同步-不启用 如设为true,则此域将在此黑板的所有实例之间同步。

如果有多个敌人都使用这个黑板,即时同步将使他们之间共享变量。 如果其中一个敌人设置了它,那么所有的人都可以访问同一个变量。

如果未选中,则黑板的每个实例都有自己的该变量版本。 image

添加键 浮点 DistanceToTarget

到目标的距离 在找到最近的目标后立即设置它, 然后我们可以根据距离采取行动。 image

服务 创建 黑板键选择器类型变量 准备与黑板蓝图上存在的实际键关联

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 image

打开 BP_AuraCharacter actor-高级-标签-Player image

5. Finding the Nearest Player 寻找最近的玩家

BT_EnemyBehaviorTree 行为树 中 服务变量与黑板键关联

打开 BT_EnemyBehaviorTree

选择 FindNearestPlayer 服务-

可以看到 继承自 BTService_FindNearestPlayer C++ 基类中的属性: TargetToFollowSelector DistanceToTargetSelector image

分别设置值为黑板中的键 TargetToFollow,DistanceToTarget

TargetToFollowSelector - TargetToFollow DistanceToTargetSelector - DistanceToTarget

image

此时服务可获取并修改黑板的键。

服务中 找到最近的目标和距离

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

打开行为树 BT_EnemyBehaviorTree 运行游戏

行为树的黑板键会实时显示其值:每0.5秒更新一次。 image

行为树图表:

添加节点: Move To Move To-细节-黑板-黑板键-TargetToFollow image

敌人将跟随目标玩家

image

6. AI and Effect Actors 人工智能和效果Actor

优化敌人行动 BP_EnemyBase 使控制器按照视线的方向进行定向移动。

打开 BP_EnemyBase 旋转全部不启用。 image

使用角色移动组件控制移动 选择 角色移动组件 角色移动(旋转设置)-使用控制器所需的旋转-启用 image 如为tue,朝控制器的理想旋转(通常为Controler->ControlRotation)平滑地旋转角色,将RotationRate用作旋转的变化率被OrentRotationToMovement覆盖 通常需要确保将其他设置清除,如角色上的bUseControllerRotationYaw。

AuraEnemy 使用控制器所需的旋转 使角色平滑移动,转向

    // 使用控制器所需的旋转
    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());
}

避免敌人拾取药剂等物体 ,AuraEffectActor 检查重叠目标的 标签来区分玩家,敌人

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);
        }
    }
}

调整 效果actor的属性

BP_FireArea

Applied Effects-Destroy On Effect Application-不启用 Applied Effects-Apply Effects To Enemies-启用 image

BP_HealthPotion ,BP_ManaPotion,BP_HealthCrystal,BP_ManaCrystal

Applied Effects-Destroy On Effect Application-启用 Applied Effects-Apply Effects To Enemies-不启用 image

图表 删除 destroy actor 节点 image

需要优化 WBP_GlobeProgressBar

image

7. Behavior Tree Decorators 行为树装饰器节点

https://docs.unrealengine.com/5.3/zh-CN/unreal-engine-behavior-tree-node-reference-decorators/

装饰器节点(在其他行为树系统中也称为条件语句)连接到合成(Composite)任务(Task)节点,并定义树中的分支,甚至单个节点是否可以执行。

黑板装饰器

image 黑板(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) | 节点应该在行为树图表中显示的名称。

BT_EnemyBehaviorTree

行为树有一个找到最近玩家的服务,它正在设置几个黑板键的值,每半秒左右一次。

可以使用这些黑板键值来确定下一步应该做什么。

装饰器也可以附加到节点上,它们可以为我们提供条件。像 if 语句, 根据黑板键值,允许执行给定的分支或阻止执行给定分支。

删除 move to 节点

添加节点: selector selector-右键-添加装饰器-blackBoard image image

该装饰器选择器使用黑板键作为条件。 image

选择选择器的 装饰器选择器 部分 image

细节-黑板-黑板键-TargetToFollow 细节-黑板-键查询-已设置 如果它被设置为已设置,则必须设置要遵循的目标【黑板键-TargetToFollow】才能执行此选择器。 因此,如果我有子类连接到选择器,那么只有在跟随目标时它们才会被执行。 即 找到了最近的玩家时,才会执行。黑板键-TargetToFollow 有值。

image

敌人被击中后是否有反应

黑板 添加键 HitReacting 布尔类型 image

AuraEnemy C++中 AI控制器设置黑板键 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 添加黑板装饰器

打开 BT_EnemyBehaviorTree 继续添加黑板装饰器到选择器上 细节-黑板-黑板键-HitReact 细节-黑板-键查询-未设置

黑板-添加键 RangedAttacker 布尔类型

远程攻击

行为树

添加节点 sequence 表示 RangedAttacker 远程攻击 sequence 表示 MeleeAttacker 近战攻击 sequence 表示 Move to target

BPGraphScreenshot_2024Y-01M-19D-00h-01m-02s-924_00

如果不是战士,则设置黑板键 RangedAttacker 远程攻击为true

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);
}

BT_EnemyBehaviorTree

行为树

RangedAttacker sequence -是否远程攻击

添加黑板装饰器 细节-黑板-黑板键-RangedAttacker 细节-黑板-键查询-已设置 节点名称-我是远程攻击吗 image

只有 RangedAttacker 为真,才表示是远程攻击,才执行此分支,否则执行下一个节点。

添加黑板装饰器 细节-黑板-黑板键-DistanceToTarget 细节-黑板-键查询-小于或等于 细节-黑板-键值-600 节点名称-到达攻击距离了吗 远程攻击也有距离限制

MeleeAttacker sequence 是否近战攻击

添加黑板装饰器 细节-黑板-黑板键-DistanceToTarget 细节-黑板-键查询-小于 细节-黑板-键值-500 节点名称-足够近以攻击 image

目标距离小于500时,表示近战攻击,则执行此分支,否则执行下一个节点。

Move to target sequence

前2各都失败时执行此分支 与目标距离在500-4000时,开始移动至目标。

1-足够接近目标时,才向目标移动 添加黑板装饰器 细节-黑板-黑板键-DistanceToTarget 细节-黑板-键查询-小于或等于 细节-黑板-键值-4000 节点名称-足够近以攻击 image

2-如果太近,则失败,重新开始整个节点 添加黑板装饰器 细节-黑板-黑板键-DistanceToTarget 细节-黑板-键查询-大于或等于 细节-黑板-键值-500 节点名称-足够远以攻击 image

3-添加节点 wait wait -等待-等待时间-0 wait -等待-随即偏差-0.5 约0.5秒的延迟后执行后面的节点 image

4-move to move to-黑板-黑板键-TargetToFollow move to-节点-可接受半径-50 添加到目标到达测试中A[和目标位置之间闯值的固定距离

image

BPGraphScreenshot_2024Y-01M-19D-13h-45m-21s-344_00

8. Attack Behavior Tree Task 攻击行为树任务

基于 BTTask_Blueprint 新建行为树任务C++ BTTask_Attack

image image

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);
}

基于 BTTask_Attack 创建 近战攻击行为树任务蓝图 BTT_Attack

Content/Blueprints/AI/BTT_Attack.uasset

打开 BTT_Attack 重载 接收执行AI event receive execute AI

image

事件图表: ai-行为树-finish execute finish execute-success-启用 任务必须返回成功或失败 image

controlled pawn 拖出 get object name print string image

BT_EnemyBehaviorTree

打开 BT_EnemyBehaviorTree

近战节点下拖出节点 BTT_Attack BPGraphScreenshot_2024Y-01M-19D-13h-07m-10s-366_00

近战攻击时,打印 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 敌人位置绘制调试球

打开 BTT_Attack

变换-get actor location 渲染-调试-draw debug sphere

image 此时只有近战职业会显示调试球。 image image

BT_EnemyBehaviorTree

move to target -足够远以攻击-流控制-观察期中止-self

目标距离大于等于500时中止自身节点和子类节点。不再继续跟踪。返回行为树。 这样敌人跟踪时,最终会停在500距离处,不会无限贴近目标。

image image

melee attacker

拖出节点 move to

细节-黑板-黑板键-TargetToFollow 节点-可接受半径-20 节点名称-接近 近战攻击战士,在距离目标20单位时,才开始攻击。 image

BTT_Attack-节点名称-Attack image

BPGraphScreenshot_2024Y-01M-19D-13h-57m-28s-477_00

近战攻击者会贴近玩家,远程攻击者会在远处。 敌人之间会互相妨碍。远程敌人可能会阻止近战敌人行走。 敌人脚下没有生成孔洞。 image

9. Find New Location Around Target 在目标周围寻找新位置

优化敌人动画蒙太奇 ABP_Goblin_Spear -权重速度

防止动画切换时的抖动 打开 ABP_Goblin_Spear 找到使用的混合空间 BS_GoblinSpear_IdleRun image 打开 BS_GoblinSpear_IdleRun

取样平滑-权重速度-4 表示在4分之一秒内切换动画

如果大于0,这是允许取样权重变化的速度。 速度为1意味着取样权重可以在一秒内从0变为1(或从1变为0)。 速度为2意味着这需要半秒钟。 这允许混合空间切换到新参数而不经过中间状态有效地混合它原来的位置和新目标的位置。举例而言,想象我们有一个运动的混合空间,向左、向前和向右移动。现在如果内插混合空间本身的输入从一个极端到另一个极端,你会从左向前、向右。作为另一种选择,将此取样权重速度值设为高于0它将直接从左到右,而不需要先向前移动。 值越小,样本权重调整越慢,因此更平滑然而,0值将完全禁用此平滑。

BT_EnemyBehaviorTree 近战敌人靠近目标后,在目标周围随即寻找新地点开始攻击目标

打开 BT_EnemyBehaviorTree 黑板-添加 向量 黑板键-MoveToLocation

行为树-新建任务-BTTask_Blueprint-BTT_GoAroundTarget Content/Blueprints/AI/BTT_GoAroundTarget.uasset image 基于蓝图的任务节点的基类。不可用来创建本地C++类! 任务接收到中止事件时,所有与此实例相关的所有延迟操作将被移除这可以防止Execute执行继续操作,但不处理外部事件请谨慎使用(终止时注销),存疑时调用IsTaskExecuting()

BTT_GoAroundTarget 任务 为近战敌人在靠近玩家时在玩家周围找到一个新位置

重载 接收执行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 BPGraphScreenshot_2024Y-01M-19D-15h-33m-41s-740_00

BT_EnemyBehaviorTree 将任务 BTT_GoAroundTarget 的 NewLocation 与行为树的黑板键 MoveToLocation 关联

打开 BT_EnemyBehaviorTree

1 melee attacker 拖出任务 wait 等待-等待时间-1 等待-随即偏差-0.5 image

2 melee attacker 拖出任务 BTT_GoAroundTarget BTT_GoAroundTarget-默认-NewLocation-MoveToLocation BTT_GoAroundTarget-默认-Target-TargetToFollow 节点名称-在目标周围寻找新位置 image

3 melee attacker 拖出任务 move to move to-黑板-黑板键-MoveToLocation move to 右键添加 TimeLimit 装饰器 TimeLimit 装饰器-时间限制-2 使敌人在玩家周围移动时不能停留太久,超时则中止。 防止被其他敌人卡住位置无法接近目标。 image image

image BPGraphScreenshot_2024Y-01M-19D-15h-45m-43s-347_00

最终,近战敌人接近目标后开始攻击,等待1秒左右,移动到目标周围的新位置,不断循环。再次开始攻击。 移动到目标周围的新位置时如果被物体卡住,最多等待2秒,重新找新位置。

10. Environment Query System 场景查询系统

https://docs.unrealengine.com/5.3/zh-CN/environment-query-system-in-unreal-engine/

这些数据可给人工智能提供决策数据,用于后续动作的决策过程。

场景查询系统(EQS) 是虚幻引擎5(UE5) AI系统的一个功能,可将其用于从环境中收集数据。在EQS中,可以通过不同种类的测试向收集的数据提问,这些测试会根据提出问题的类型来生成最适合的项目。

可以从行为树中调用EQS查询,并根据测试的结果将其用于后续操作的决定。EQS查询主要由生成器节点(用于生成将被测试及加权的位置或Actor)和情境节点(被用作各种测试和生成器引用的框架)组成。可以用EQS查询指引AI角色找到能够发现玩家并发起攻击的最佳位置、找到距离最近的体力值或弹药拾取物,或找到最近的掩体(以及其他可进行的动作)。

防止敌人出现 无视与目标之间的障碍物 就开始攻击的错误

image image 使用场景查询系统对目标位置过滤,找出合适的位置。

11. Environment Queries 场景查询

重新分组 AIController BehaviorTree Services Tasks EQS image

新建场景查询蓝图 EQ_FindRangedAttackPosition

寻找远程攻击位置 右键-人工智能-场景查询-EQ_FindRangedAttackPosition Content/Blueprints/AI/EQS/EQ_FindRangedAttackPosition.uasset image

基于 EQSTestingPawn 创建 BP_EQSTestingPawn 蓝图

专门用来测试场景查询 image

E:/Unreal Projects 532/Aura/Content/Blueprints/AI/EQS/BP_EQSTestingPawn.uasset

创建basic地图 EQS_TestingMap 用来测试 场景查询

Content/Maps/EQS_TestingMap.umap

拖入 BP_EQSTestingPawn

BP_EQSTestingPawn-EQS-查询模板-EQ_FindRangedAttackPosition

image

EQ_FindRangedAttackPosition 场景查询

打开 EQ_FindRangedAttackPosition 查询利用生成器生成一堆actor或位置的项目,并且我们可以在我们的环境中使用这些位置, 根据我们设置的任何规则进行查询。

点击-根-拖出生成器 points:pathing grid

围绕查询目标pawn 在其周围生成点 pawn 通常为运行此场景查询的pawn。 image

网格半大小-500 之间的空间-100

场景中选中 BP_EQSTestingPawn, 可以看到 在半径1000的正方体范围内,每隔100单位生成一个点 image

点击-根-拖出生成器 points:circle

image 场景中选中 BP_EQSTestingPawn, image

测试将获取这些点,对其评估。

使用 points:pathing grid

12. EQS Tests 场景查询测试

对 points:pathing grid 生成的点进行测试

打开 EQ_FindRangedAttackPosition points:pathing-右键-添加测试-trace image trace将跟踪每个点到选择的某个目的地,默认情况下,它会跟踪可见性通道。 image 测试目的 决定了我们对每个点的处理方式。 如果我们追踪某个目标,我们能否击中该目标,或者是否有什么东西阻碍了我们? 可以选择过滤掉所有无法追踪到给定目标或相反目标的点。

测试目的-仅过滤

基于 EnvQueryContext_BlueprintBase 创建 场景查询情景 EQS_PlayerContext 蓝图

Content/Blueprints/AI/EQS/EQS_PlayerContext.uasset

需要这个场景查询情景能够准确地确定我们应该能够追踪并尝试达到的目标。

打开 EQS_PlayerContext 重载 提供Actor集 provide actors set 返回Actor对象引用的数组 image

get all actors of class get all actors of class-actor class-BP_AuraCharacter 这将获取 BP_AuraCharacter 类的所有角色 image

EQ_FindRangedAttackPosition 场景查询使用 EQS_PlayerContext 场景查询情景

打开 EQ_FindRangedAttackPosition pathing grid 的 trace 测试 检测-情景-EQS_PlayerContext 将追踪当前玩家的情景可见性 image

即,这些点如果可以看到 玩家,则为蓝色,否则红色。

将 BP_EQSTestingPawn 上移超过地板。防止被地板挡住。

默认地图没有玩家 BP_AuraCharacter 红色 image

地图拖入 玩家 BP_AuraCharacter 蓝色 image

pathing grid 的 trace 测试 布尔匹配 这个布尔值来确定是否要过滤掉或保留基于此跟踪的点。 赋予“ScoringFActo分数所需要匹配的布尔值。如不匹配此值,分数将不会发生改变

如果不启用,即时关卡有 BP_AuraCharacter,所有点也是红色。

pathing grid 的 trace 测试 布尔匹配-不启用

关卡拖入障碍物 看不到玩家的点为蓝色,看得到玩家的点为红色。 image

布尔匹配-启用 看不到玩家的点为红色,看得到玩家的点为蓝色。 image

我们只想要可以看到玩家的点,可以畅通无阻地追踪到玩家的点,所以 布尔匹配-不启用

注意,障碍物内部的点也是红色,可以追踪到玩家。因为障碍物没有阻挡点的可见性通道。

需要为障碍物添加 打开障碍物网格体SM_Block2x2x1

显示简单碰撞 添加盒体简单碰撞

image

关卡添加 体积-导航网格体边界体积

NavMeshBoundsVolume image 放大至包裹整个关卡

选择 NavMeshBoundsVolume 按P键显示导航网格 image

障碍物已经不再导航网格内。

保存 EQ_FindRangedAttackPosition 使其重新计算。 现在障碍物内部不再生成点。 image image

路径与寻路有关,而寻路与人工智能有关,人工智能使用导航网格边界体积。 因此,路径网格不会给我们任何不可导航的点,这使人工智能角色无法继续行走。

13. Distance Test 距离测试

测试出最好的点

EQ_FindRangedAttackPosition 场景查询 添加新测试测试点的分数

打开 EQ_FindRangedAttackPosition

pathing grid 节点右键-添加测试-distance 距离允许我们计算每个点到查询或我们的查询中的距离. case 是测试池或某些查询上下文。 image 测试目的-仅得分 默认:得分是每个点到我们在这里设置的情景的距离 image

距离-到此距离-EQS_PlayerContext 得分因数- -1【负一】 距离玩家越近,得分越高。 image

得分因数- 1 【正一】 距离玩家越远,得分越高。 image

游侠,远距离攻击 的 EQ_FindRangedAttackPosition 场景查询设置

距离-到此距离-EnwQueryContext_Querier EnwQueryContext_Querier 表示 场景查询情景的所有点的中心。 这使游侠呆在场景查询情景的所有点的中心 image

得分因数- -1【负一】 这使游侠不会被障碍物挡住。 image

14. Using EQS Queries in Behavior Trees 在行为树中使用EQS查询

BT_EnemyBehaviorTree 行为树运行场景查询

打开 BT_EnemyBehaviorTree

1 RangedAttacker 远程攻击节点-拖出 run EQS query run EQS query-EQS-EQS请求-查询模板-EQ_FindRangedAttackPosition 场景查询 节点名称-获取攻击位置 黑板-黑板键-MoveToLocation 运行查询后,它将设置我们指定的黑板键之一 MoveToLocation 的值为 EQ_FindRangedAttackPosition 场景查询 查询过滤后的点的位置之一。 image

2 RangedAttacker 拖出 move to

移动到该位置

move to-黑板-黑板键-MoveToLocation 节点名称-到达攻击位置

image

3 RangedAttacker 拖出 BTT attack

4 RangedAttacker 拖出 wait 等待时间-1 随即偏差-0.5

image

回到主关卡

远程攻击敌人将始终在远处准备攻击,并且视线不会被障碍物挡住。

image

修复玩家自动寻路错误

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;
    }
}

BPGraphScreenshot_2024Y-01M-19D-17h-43m-42s-702_00

障碍物 阻挡可见性通道 visibility/ 忽略相机 camera

选中障碍物-碰撞 image

WangShuXian6 commented 10 months ago

16. Enemy Melee Attacks 敌人近战攻击

1. Melee Attack Ability 近战攻击技能

基于 C++ AuraDamageGameplayAbility 创建 C++ AuraMeleeAttack 近战攻击技能

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());
    }
}

基于 AuraMeleeAttack 创建近战攻击技能蓝图 GA_MeleeAttack

Content/Blueprints/AbilitySystem/GameplayAbilities/GA_MeleeAttack.uasset

打开 GA_MeleeAttack

通过技能标签激活技能

细节-tags-ability tag-Abilities.Attack image

DA_CharacterClassInfo 职业信息资产 为战士职业配置 初始技能 GA_MeleeAttack

打开 DA_CharacterClassInfo warrior- Startup Abilities-GA_MeleeAttack image

攻击任务 BTT_Attack中为战士职业激活技能 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

BPGraphScreenshot_2024Y-01M-19D-18h-37m-29s-028_00

GA_MeleeAttack 技能 攻击时绘制调试球

打开 GA_MeleeAttack 事件图表: get avatar actor from actor info get actor location draw debug sphere end ability

细节-高级-Instancing Policy-Instanced Per Actor 只会实例一次,不会每次执行都实例化。 BPGraphScreenshot_2024Y-01M-19D-18h-43m-28s-393_00

近战攻击绘制绿色调试球

image

2. Attack Montage 攻击蒙太奇

基于 Attack_Spear 动画序列 制作近战攻击蒙太奇 AM_Attack_GoblinSpear

Content/Assets/Enemies/Goblin/Animations/Spear/AM_Attack_GoblinSpear.uasset

默认通知轨道改为-MotionWarping MotionWarping轨道-右键-添加通知状态-Motion Warping image

控制 MotionWarping 通知条的长度,覆盖 攻击开始到结束 image

选择 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 旋转以面对该目标

image

GA_MeleeAttack 近战技能播放攻击蒙太奇

打开 GA_MeleeAttack play montage and wait play montage and wait-montage to play-AM_Attack_GoblinSpear

蒙太奇完成时结束技能 end ability

image

为敌人 BP_EnemyBase 实现战斗接口的 运动扭曲 使其攻击时面朝目标

打开 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

image

还需要为敌人添加攻击目标用于扭曲旋转面向目标

3. Combat Target 作战目标

扭曲运动需要使用根运动 Attack_Spear 动画启用 更运动

打开 Attack_Spear 动画序列

根运动-启用根运动-启用 image

为敌人增加攻击目标变量

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 攻击任务中为敌人设置攻击目标

实际目标来自行为树。

打开 BTT_Attack

新增 黑板键选择器 变量 CombatTargetSelector 公开变量

CombatTargetSelector get blackboard value as actor 工具-is valid 攻击目标有效时才激活技能

类-enemy interface-set Combat Target BPGraphScreenshot_2024Y-01M-19D-20h-00m-15s-889_00

BT_EnemyBehaviorTree 行为树中关联 CombatTargetSelector

打开 BT_EnemyBehaviorTree

MeleeAttacker 的 attack 分支-默认- Combat Target Selector-TargetToFollow 节点名称-近战攻击 image

RangedAttacker 的 BTT_attack 分支-默认- Combat Target Selector-TargetToFollow 节点名称-远程攻击 image

GA_MeleeAttack 技能中 使敌人面向目标

打开 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 面向目标扭曲

BPGraphScreenshot_2024Y-01M-19D-20h-14m-33s-735_00

现在敌人近战攻击玩家时,始终面向玩家。

4. Melee Attack Gameplay Event 近战攻击游戏事件

使用近战攻击蒙太奇向演员发送游戏事件标签。

修改C++ 使蓝图中可访问攻击武器的插槽位置

将获取攻击插槽位置事件改为蓝图版本

CombatInterface

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;
}

AuraCharacterBase

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);
}

使用 GetCombatSocketLocation 的蓝图版本

    // 参数:实现该接口的 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 image

AM_Attack_GoblinSpear 蒙太奇发送事件标签

打开 AM_Attack_GoblinSpear 添加 Events 通知轨道 在武器刺出的时间点上添加通知 右键-添加通知-AN_MontageEvent 【自定义的通知】 image image

选择 AN_MontageEvent -动画通知-event tag-Event.Montage.Attack.Melee 用以在游戏中监听 image

GA_MeleeAttack 技能中 监听 带 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

BPGraphScreenshot_2024Y-01M-19D-20h-50m-31s-856_00

监听到攻击事件后在武器插槽位置处画出调试球。

image

此时,武器插槽位置错误,因为没有设置插槽名称

查看 SKM_Spear 武器骨骼网格体的武器插槽名称

根据 BP_Goblin_Spear 的 weapon 组件定位到武器 SKM_Spear 打开 SKM_Spear 插槽名-TipSocket image

BP_Goblin_Spear 设置 武器插槽名称

打开 BP_Goblin_Spear

Combat-Weapon Tip Socket Name-TipSocket image

此时位置正确 image

5. Get Live Players Within Radius 获取攻击武器插槽位置半径内的存活actor

战斗接口定义 是否死亡蓝图函数

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;
}

蓝图函数 获取攻击武器插槽位置半径内的存活actor

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 近战攻击技能中 使用蓝图本机函数 GetLivePlayersWithinRadius 获取攻击武器插槽处半径内存活玩家

打开 GA_MeleeAttack Get Live Players Within Radius for each loop 变换-get actor location

BPGraphScreenshot_2024Y-01M-19D-22h-35m-59s-476_00

现在 ,敌人攻击玩家时,在敌人武器插槽位置半径处,使用不可见球体查询与球体重叠的动态类型actor,获取存活actor,在actor中心渲染调试球体。即在玩家中心绘制球体。敌人也会被重叠查询到。

image

6. Causing Melee Damage 造成近战伤害

伤害技能基类中 AuraDamageGameplayAbility 添加 造成伤害方法

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

打开 CT_Damage 添加曲线 Abilities.Melee 1,5 2,7.5 40,50

自动平滑 image

GA_MeleeAttack 近战攻击技能 设置 damage effect class 伤害效果类

打开 GA_MeleeAttack 细节-damage effect class-GE_Damage image

GE_Damage 具由即时效果,使用执行计算,遍历所有伤害类型的效果。

伤害类型组 【标签-伤害曲线值】

细节-damage-damage types: key-Damage.Physical value-1.0 , CT_Damage, Abilities.Melee

image

通过调用者设置伤害大小

事件图表: make outgoing gameplay effect spec damage effect class image Assign Tag Set by Caller Magnitude Assign Tag Set by Caller Magnitude-data tag-Damage.Physical image

以上为蓝图版本的使用伤害效果。此处仅作展示,需要删除这3个节点。 使用C++定义的 CauseDamage 替代。

CauseDamage 对重叠中的存活目标循环使用 造成伤害函数 应用伤害【伤害标签-伤害曲线值】 BPGraphScreenshot_2024Y-01M-19D-23h-28m-58s-462_00

此时,近战敌人近战攻击玩家,将使玩家健康值减少。

可以改变 敌人的 level 等级,应用不同等级的伤害 image

玩家被攻击时显示文本

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);
        }
    }
}

7. Multiplayer Melee Test 多人近战测试

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);
    }
}

8. Montage Gameplay Tags 蒙太奇游戏标签

添加蒙太奇标签 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")
        );
}

9. Tagged Montage 标记蒙太奇

标签关联关联蒙太奇。 标签可指代事件,插槽。

战斗接口包含 一个连接蒙太奇和游戏标签的结构

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;
}

在角色中设置 AttackMontages 以关联标签和蒙太奇

打开 BP_Goblin_Spear combat-Attack Montages-添加一组映射: montage-AM_Attack_GoblinSpear montage tag-Montage.Attack.Weapon image

GA_MeleeAttack 技能中

不再硬编码要播放的蒙太奇动画名称

打开 GA_MeleeAttack get attack montages 可以获取 标记蒙太奇组

需要参数 get avatar actor from actor info

随机选择一个蒙太奇 length -1 random integer in range

get(a ref) break TaggedMontage

BPGraphScreenshot_2024Y-01M-20D-00h-32m-18s-207_00

AM_Attack_GoblinSpear 使用对应的蒙太奇事件标签

打开 AM_Attack_GoblinSpear 选择通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.Weapon

image

技能中将监听该事件标签。【wait gemeplay event】【但是此标签从角色中标记蒙太奇组获取的】 由于角色已设置该蒙太奇与该标签的关联,所以此处标签必须一致。

10. Multiple Attack Sockets 多个攻击插槽

通过标签找到插槽

战斗接口 通过蒙太奇标签获取插槽

例如攻击插槽:武器插槽,左手插槽,右手插槽

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 技能中将蒙太奇标签传递给 GetCombatSocketLocation

打开GA_MeleeAttack BPGraphScreenshot_2024Y-01M-20D-01h-00m-32s-963_00

项目设置中 删除不再使用的标签 Event.Montage.Attack.Melee

为食尸鬼 SK_Ghoul 左右手添加攻击插槽

打开 SK_Ghoul 骨骼

Wrist-L:LeftHandSocket image

Wrist-R:RightHandSocket image

移动插槽到手部 image

11. Ghoul Enemy 食尸鬼敌人

新建文件夹 Content/Blueprints/Character/Ghoul

设置敌人行走速度 蓝图可编辑

Source/Aura/Public/Character/AuraEnemy.h

public:
    // 基本步行速度
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat")
    float BaseWalkSpeed = 250.f;

混合空间 BS_IdleWalk

右键-动画-混合空间-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

image

取样平滑-权重速度-4

动画蒙太奇

基于 Assets/Enemies/Ghoul/Animations/HitReact.HitReact'动画序列制作动画蒙太奇 AM_HitReact_Ghoul Content/Assets/Enemies/Ghoul/Animations/AM_HitReact_Ghoul.uasset

打开 AM_HitReact_Ghoul

基于 BP_EnemyBase 新建子蓝图 BP_Ghoul

Content/Blueprints/Character/Ghoul/BP_Ghoul.uasset

打开 BP_Ghoul 网格体 mesh 组件-骨骼网格体资产-SKM_Ghoul image

调整网格体 面向前方, image

选择主组件BP_Ghoul-combat-life span-5 combat-Left Hand Socket name-LeftHandSocket combat-Right Hand Socket name-RightHandSocket hit react montage-AM_HitReact_Ghoul image

character class-warrior

attributes 分类下的属性已不再使用,改用职业信息资产中的设置

调整胶囊体组件 半高,半径 适应网格体大小 image

基于 ABP_Enemy 创建带骨架的子动画蓝图 ABP_Ghoul

选择 骨骼 SK_Ghoul Content/Blueprints/Character/Ghoul/ABP_Ghoul.uasset image

资产覆盖编辑器-混合空间播放器-BS_IdleWalk image

BP_Ghoul 使用 ABP_Ghoul

打开 ABP_Enemy 网格体组件: 细节-动画-动画类-ABP_Ghoul

image BaseWalkSpeed-125

角色移动-旋转速率-0,0,150

GA_MeleeAttack 技能中检查攻击标签蒙太奇组长度

打开 GA_MeleeAttack

branch end ability

BPGraphScreenshot_2024Y-01M-20D-01h-56m-13s-108_00

12. Ghoul Attack Montages 食尸鬼攻击蒙太奇

动画序列 Attack_L,Attack_R使用根运动

打开 Attack_L,Attack_R 根运动-启用根运动-启用

image

AM_Ghoul_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 旋转以面对该目标 image

缩短 Motion Warping 扭曲动画时间 image

添加通知轨道:Events

右键-添加通知-AN_MontageEvent 【自定义的通知】 在预计击中位置 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.RightHand 用以在游戏中监听

image

AM_Ghoul_Attack_L 动画蒙太奇 添加运动扭曲

基于动画序列 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 旋转以面对该目标 image

缩短 Motion Warping 扭曲动画时间 image

添加通知轨道:Events

右键-添加通知-AN_MontageEvent 【自定义的通知】 在预计击中位置 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.LeftHand 用以在游戏中监听

BP_Ghoul 中设置标签蒙太奇组

打开 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

image

GA_MeleeAttack 技能对左右手攻击的处理

打开 GA_MeleeAttack

随机数提升为变量 TaggedMontage 使随机数固定。

删除 break TaggedMontage 从 TaggedMontage 拖出 TaggedMontage

play montage and wait -Stop when Ability Ends-不启用 防止攻击动画意外停止,不再循环

修复敌人攻击被打断时,无法结束技能的错误

在攻击蒙太奇完成,中断,取消时结束技能。 image

BPGraphScreenshot_2024Y-01M-20D-02h-56m-40s-042_00

GA_HitReact 在敌人受击时也结束敌人的攻击技能

打开 GA_HitReact

细节-标签-Cancel Abilities with Tag-Ability.Attack 执行此技能时,具有这些标签的技能会取消。 image GA_HitReact 技能表示敌人受击,当敌人受击时,敌人如果同时具由一个带 Ability.Attack 标签的技能,那么这个技能将自动结束,不会执行。 敌人攻击时,中途被击打导致受击,则敌人的攻击技能结束。防止出现技能无法结束的情况。 GA_MeleeAttack 技能拥有标签 Ability.Attack

13. Melee Polish 近战完善

蓝图函数库 判断是否友军

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 近战攻击节能中防止友军伤害

打开 GA_MeleeAttack

IsNotFriend get avatar actor from actor info branch

BPGraphScreenshot_2024Y-01M-20D-03h-19m-11s-449_00

BPGraphScreenshot_2024Y-01M-20D-03h-19m-54s-782_00

调整敌人健康值

GE_SecondaryAttributes_Enemy 系数改为0.25 image

溶解材质

复制材质 M_DissolveEffect 自发光颜色和不透明蒙版处的节点

拷贝 M_Ghoul 材质为 M_GhoulDissolve 打开 M_GhoulDissolve 选中主输出节点-细节-材质0混合模式-已遮罩 image

To Emissive Color 连接到 自发光颜色 image

To Opacity Mask 连接到 不透明蒙版 image

Dissolve 值改为 -2 每种网格体都不一样,需要通过材质实例测试得出完全不溶解的值 image

BPGraphScreenshot_2024Y-01M-20D-03h-41m-46s-028_00

基于 M_GhoulDissolve 创建材质实例 MI_GhoulDissolve

BP_Ghoul 配置

打开 BP_Ghoul Dissolve Material Instance-MI_GhoulDissolve

image

BP_EnemyBase 使用 回避 防止敌人互相阻挡导致无法移动

打开 BP_EnemyBase 细节-角色移动:回避-使用RVD回避-启用 image 设置后组件将使用RVO回避。这只在服务器上运行。仅适用与人工智能。 敌人将会互相回避。但不会在旋转时定向。

WangShuXian6 commented 10 months ago

17. Enemy Ranged Attacks 敌人的远程攻击

修正碰撞预设设置

地板

image

BP_AuraCharacter

胶囊体组件

胶囊体组件 启用碰撞 处理碰撞功能,但忽略技能抛射物。 image

网格体组件

网格体组件 纯查询 处理常见的世界对象的阻挡,处理与技能抛射物的重叠。 image

weapon

武器 无碰撞 处理阻挡,忽略技能抛射物的重叠。因为武器产生技能抛射物 image

BP_Goblin_Spear

胶囊体组件

image

网格体组件

image

weapon

image

BP_Goblin_Slingshot

胶囊体组件

image

网格体组件

image

weapon

image

BP_FireBolt

Sphere 组件

Sphere 组件 纯查询 处理与 世界物体,玩家敌人的重叠。 其他都忽略。 image

球体半径-12 过大则会与玩家自身重叠触发事件,伤害自身。 【也可以使用标签判断,新增通道等方式避免重叠事件】 image

BP_SlingshotRock

Sphere 组件

image

RockMesh

石块网格体不应该和任何物体触发重叠事件,因为这由石块的Sphere组件触发 image

SM_SlingshotRock

image

1. Ranged Attack 远程攻击

整理文件夹 基于 AuraProjectileSpell 创建远程攻击技能蓝图 GA_RangedAttack Content/Blueprints/AbilitySystem/Enemy/Abilities/GA_RangedAttack.uasset

2. Rock Projectile 岩石投射

基于 AuraProjectile 创建游侠投射的石块物体 BP_SlingshotRock 蓝图

Content/Blueprints/AbilitySystem/Enemy/Abilities/BP_SlingshotRock.uasset

选中石块的 静态网格体 SM_SlingshotRock image

打开 BP_SlingshotRock 添加静态网格体组件(BP_SlingshotRock) 名称-RockMesh 这会自动将石块网格体设置为静态网格体组件的-静态网格体 image image image image

ProjectileMovement 组件: 细节-抛射物-初始速度-1000 最大速度-1000 发射物重力范围-1

image

设置技能 GA_RangedAttack

打开 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);
    }

image

损害-Damage Types-

输入-Startup Input Tag-

输入-Replicate Input Directly-

3. Ranged Damage Curve 远程伤害曲线

CT_Damage 曲线表格 添加 Abilities.Ranged 远程伤害曲线

打开 CT_Damage 曲线表格

添加 Abilities.Ranged 远程伤害曲线 1,7.5 40,35

全选,自动平滑

image

设置技能 GA_RangedAttack

打开 GA_RangedAttack

损害-Damage Types-添加一组 gameplay tag-Damage.Physical 表示物理伤害

scaleable float-1, CT_Damage,Abilities.Ranged image

基类将遍历 Damage Types 将伤害值赋予当前技能使用的技能效果 GE_Damage

4. Granting Ranged Attacks 赋予远程攻击

当远程攻击被授予技能 GA_RangedAttack 行为树将根据技能标签激活攻击任务。

行为树通过技能标签激活当前技能

细节-tags-ability tag-Abilities.Attack image

行为树的激活技能处示例: image

DA_CharacterClassInfo 职业信息 为Ranger 职业添加初始技能信息

打开DA_CharacterClassInfo ranger-startup abilities-添加一组初始技能 :GA_RangedAttack 远程攻击技能 image

GA_RangedAttack 远程攻击技能 在技能激活时绘制调试球

打开 GA_RangedAttack 远程攻击技能 事件图表: event ActivateAbility get avatar actor from actor info get actor location draw debug sphere

image

添加手拿弹弓的敌人 BP_Goblin_Slingshot character class -Ranger 游侠职业 游戏敌人在到达攻击范围内停下,开始攻击,绘制调试球。

image

5. Slingshot Attack Montage 弹弓攻击蒙太奇

Attack_Slingshot 弹弓动画序列使用根动画

打开 Attack_Slingshot 根运动-启用根运动-启用 image

基于 Attack_Slingshot 弹弓动画序列 创建动画蒙太奇 AM_Attack_Goblin_Slingshot

Content/Assets/Enemies/Goblin/Animations/Slingshot/AM_Attack_Goblin_Slingshot.uasset

AM_Attack_Goblin_Slingshot 动画蒙太奇设置运动扭曲

打开 AM_Attack_Goblin_Slingshot

默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人的拉弹弓开始攻击 - 到 拉弹弓到最大攻击结束 image

选择 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 旋转以面对该目标

image

添加通知轨道:Events 决定发射弹弓的时机

右键-添加通知-AN_MontageEvent 【自定义的通知】 在预计弹弓发射的位置 【弹弓拉到最长】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.Weapon 用以在游戏中监听 image image 标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇武器上的插槽位置,生成抛射物。 播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.Weapon。 后续通过标签监听此事件。

BP_Goblin_Slingshot 配置 标签蒙太奇键值对数组

打开 BP_Goblin_Slingshot

combat-attack Montages -添加一组标签蒙太奇映射 montage-AM_Attack_Goblin_Slingshot montage tag-Montage.Attack.Weapon 这与之前创建的蒙太奇,以及蒙太奇的标签一致 image

6. Playing the Ranged Attack Montage 播放远程攻击蒙太奇

伤害技能中 添加 从标签蒙太奇组中随机选取一对标签蒙太奇对返回方法

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 中将 从标签蒙太奇组中随机选取一对标签蒙太奇对返回 节点组替换为C++版本

打开 GA_MeleeAttack 事件图表: GetRandomTaggedMontageFromArray BPGraphScreenshot_2024Y-01M-20D-17h-07m-03s-604_00

BP_EnemyBase 敌人基类统一 设置攻击时武器上生成抛射物的武器插槽名称

打开 BP_EnemyBase combat-weapon tip socket name-TipSocket image

这样,无需在敌人子类中设置插槽名,除非需要在子类中覆盖。 例如 BP_Goblin_Slingshot

BP_Goblin_Slingshot 的弹弓武器 SK_Slingshot 添加插槽 PouchSocket

打开 SK_Slingshot Pouch 骨骼添加插槽 PouchSocket image

BP_Goblin_Slingshot 覆盖父类的插槽名称

打开 BP_Goblin_Slingshot combat-weapon tip socket name-PouchSocket image

GA_RangedAttack 技能中播放远程攻击蒙太奇

打开 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 这将在拉弹弓时在弹弓处绘制调试球 BPGraphScreenshot_2024Y-01M-20D-17h-26m-26s-223_00 image

7. Spawning the Rock Projectile 生成投掷石块

GA_RangedAttack 技能中生成抛射物石块

打开 GA_RangedAttack 事件图表: spawn projectile

get avatar actor from actor info enemy interface-get combat target get actor location

BPGraphScreenshot_2024Y-01M-20D-17h-39m-57s-200_00

EQS_TestingMap 测试地图中测试

世界场景设置-游戏模式重载-BP_AuraGameMode image 删除场景的角色 拖入 player start 玩家出生点 拖入 BP_Goblin_Slingshot 职业 ranger 现在游侠可以抛射石块伤害玩家

使石头边缘发光更容易被看到

打开 SM_SlingshotRock 打开 石头主材质 M_SlingshotRock 添加节点 : fresnel multiply multiply 向量3 输出至 自发光颜色

image

BP_SlingshotRock 通过 yaw,row 随即旋转石块

打开 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

BPGraphScreenshot_2024Y-01M-21D-14h-54m-07s-246_00

防止敌人的技能石块伤害敌人

防止友军伤害也将阻止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;
    }
}

8. Slingshot Animation Blueprint 弹弓动画蓝图

使用弹弓骨骼 SK_Slingshot 为弹弓 新建动画蓝图 ABP_Slingshot

image

image

Content/Blueprints/Character/Goblin_Slingshot/ABP_Slingshot.uasset

打开 ABP_Slingshot

添加状态机 Main 动画-状态机-state machine image

进入 状态机 Main 添加 Slingshot_Idle image

进入主状态 使用默认插槽运行动画 Main 拖出 slot defaultSlot image

BP_Goblin_Slingshot 右手插槽

使用动画蓝图控制骨骼位置 希望我的骨骼位置能够跟随持有弹弓的妖精的骨骼位置。 需要知道妖精手的变换,位置,旋转。 在妖精的右手上至少有一个插槽。

打开 BP_Goblin_Slingshot 找到 其骨骼 SK_Goblin 打开 SK_Goblin

将右手的Hand-RSocket插槽重命名为 RightHandSocket image

动画蓝图 ABP_Slingshot 获取所属pawn,再获取骨骼,再获取右手插槽

打开 动画蓝图 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 防止每一帧都获取插槽

BPGraphScreenshot_2024Y-01M-21D-15h-27m-56s-695_00

主动画使用敌人右手插槽位置修改弹弓拉伸的特定骨骼位置

transform (Modify) Bone

添加 HandSocketTransform 后分割结构体引脚 用来修改弹弓骨骼的位置和旋转

指定要修改的弹弓骨骼 选择 transform (Modify) Bone -细节- 骨骼控制-要修改的骨骼-Pouch 平移-平移模式-替换现有项 平移空间-世界场景空间 【因为事件图表中是在世界空间中变换插槽 get socket transform】

旋转-旋转模式-替换现有项 旋转空间-世界场景空间

缩放-缩放模式-忽略 缩放-缩放引脚-取消选择公开为引脚 用以不公开缩放引脚

透明度-透明度引脚-取消选择公开为引脚 用以不公开透明度引脚

image

通过主状态变换弹弓骨骼 BPGraphScreenshot_2024Y-01M-21D-15h-40m-20s-467_00

BP_Goblin_Slingshot

打开 BP_Goblin_Slingshot Weapon组件-细节-动画-动画模式-使用动画蓝图 -动画-动画类-ABP_Slingshot

image

这样 弹弓的Pouch骨骼将实时使用敌人右手的RightHandSocket插槽位置 image

网格体组件-细节-动画-动画模式-使用动画蓝图 -动画-动画类-AM_Attack_Goblin_Slingshot

image image

9. Slingshot Attack Montage 弹弓攻击蒙太奇

ABP_Goblin_Slingshot 发射石块

打开 ABP_Goblin_Slingshot 添加布尔变量 HoldingPouch 表示正在拉紧弹弓准备弹射 默认为true image

HoldingPouch 为true时,不修改骨骼姿势,而使用默认空闲动画姿势

主动画: 添加 动画 Slingshot_Idle blend poses by bool HoldingPouch BPGraphScreenshot_2024Y-01M-21D-16h-12m-43s-253_00

AM_Attack_Goblin_Slingshot 弹弓发射动画蒙太奇中添加通知用以修改 HoldingPouch 变量

在 WeaponHandSocket 插槽处添加预览资产 SKM_Slingsshot image

新增通知轨道Rock

在释放弹弓处添加通知 ReleaseRock image

在抓住弹弓皮带起始处 添加通知 GrabPouch image da

公开角色的武器变量到蓝图

Source/Aura/Public/Character/AuraCharacterBase.h

protected:
    //TObjectPtr 与原始指针相似 但有附加功能:访问跟踪指针,可选的延迟加载资产
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Combat")
    TObjectPtr<USkeletalMeshComponent> Weapon;

基于 Slingshot_Attack 动画序列制作弹弓射出石块动画蒙太奇AM_Slingshot_Attack

E:/Unreal Projects 532/Aura/Content/Assets/Enemies/Goblin/Slingshot/Centered/AM_Slingshot_Attack.uasset

在 AM_Attack_Goblin_Slingshot 动画蓝图中响应通知 ReleaseRock , GrabPouch

image

打开 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 这获取了弹弓武器动画 BPGraphScreenshot_2024Y-01M-21D-17h-57m-26s-988_00

响应武器动画的动画通知 设置HoldingPouch

释放石块时

右键 ReleaseRock 添加 动画通知事件 Event AnimNotify_ReleaseRock image ABP_Slingshot ABP_Slingshot 转换为有效get image

设置 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 BPGraphScreenshot_2024Y-01M-21D-17h-42m-48s-502_00

拉紧弹弓时:

右键 GrabPouch 添加 动画通知事件 Event AnimNotify_GrabPouch ABP_Slingshot ABP_Slingshot 转换为有效get

设置 ABP_Slingshot 的变量 HoldingPouch set HoldingPouch 如果攻击刚开始 拉紧弹弓 设置 HoldingPouch 为true image 此时 ABP_Slingshot 动画蓝图一定有效

完整: BPGraphScreenshot_2024Y-01M-21D-17h-57m-52s-143_00

此时游戏敌人可以正确播放弹弓投射石块动画。 这个概念可用于任务武器运动动画。

BP_SlingshotRock 石块网格体不应该和任何物体触发重叠事件,因为这由石块的Sphere组件触发

打开 BP_SlingshotRock RockMesh组件-碰撞-碰撞预设-无碰撞 NoCollision image image

WangShuXian6 commented 10 months ago

18. Enemy Spell Attacks 敌人的法术攻击

1. Goblin Shaman 哥布林萨满/魔法师

职业 Elementalist 魔法师

右键基于 BP_EnemyBase 创建子蓝图 BP_Shaman

Content/Blueprints/Character/Shaman/BP_Shaman.uasset

使用骨骼 SK_Shaman 右键基于 ABP_Enemy 创建带骨架的子动画蓝图 ABP_Shaman

Content/Blueprints/Character/Shaman/ABP_Shaman.uasset image

BP_Shaman

打开 BP_Shaman 网格体组件-骨骼网格体资产-SKM_Shaman image

调整胶囊体组件 半高 52,半径 和位置 image

调整网格体组件面向前方 image

weapon 组件 骨骼网格体资产-SKM_ShamanStaff

SK_Shaman 骨骼添加武器插槽用以固定武器位置

打开 SK_Shaman 将 左手的 插槽 LeftHandSocket 重命名为 WeaponHandSocket image

使用 SK_Shaman 创建 混合空间 BS_IdleWalk

右键-=动画-混合空间-SK_Shaman BS_IdleWalk

Content/Assets/Enemies/Shaman/Animations/BS_IdleWalk.uasset 打开 BS_IdleWalk 水平坐标名称-Speed 最大幅值-75 image

拖入 Shaman_Idle 到 0,50 位置

拖入 Shaman_Walk 到 75,50 image

细节-取样平滑-权重速度-4 image

ABP_Shaman

打开 ABP_Shaman 资产覆盖编辑器-混合空间播放器-BS_IdleWalk image

BP_Shaman 使用动画 ABP_Shaman

网格体-动画-动画类-ABP_Shaman image

BP_Shaman 职业

细节-character class -Elementalist image 这样魔法师将使用 Elementalist 职业 的属性值和节能初始化自身。

2. Shaman Attack Montage 萨满攻击蒙太奇

Shaman_Attack 攻击动画序列

细节 -根运动-启用根运动

image

基于 Shaman_Attack 攻击动画序列 创建动画蒙太奇 AM_Attack_Shaman

Content/Assets/Enemies/Shaman/Animations/AM_Attack_Shaman.uasset

AM_Attack_Shaman 动画蒙太奇 设置扭曲运动

打开 AM_Attack_Shaman

默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人的魔杖抬起开始攻击 - 到 魔杖落下攻击结束 image 选择 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 旋转以面对该目标

image

添加通知轨道:Events 决定魔杖生成魔法球的时机

右键-添加通知-AN_MontageEvent 【自定义的通知】 在预计弹弓发射的位置 【弹弓拉到最长】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.Weapon 用以在游戏中监听

image image

标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇武器上的插槽位置,生成抛射物。 播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.Weapon。 后续通过标签监听此事件。

BP_Shaman 配置 标签蒙太奇键值对数组

打开 BP_Shaman

combat-attack Montages -添加一组标签蒙太奇映射 montage-AM_Attack_Shaman montage tag-Montage.Attack.Weapon 这与之前创建的蒙太奇,以及蒙太奇的标签一致 image

BP_Shaman 武器插槽名称 TipSocket

image

法杖 SM_ShamanStaff 添加 武器插槽名称 TipSocket

打开 SKM_ShamanStaff

添加插槽 TipSocket

image

调整插槽到法杖尖端 image

3. Shaman Attack Ability 萨满攻击技能

基于 技能 GA_RangedAttack 右键创建子蓝图类 GA_EnemyFireBolt

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

其他设置使用继承来的默认值

image

现在敌人有了发射火球的技能

DA_CharacterClassInfo 职业信息资产中为 Elementalist 魔法师 职业分配初始技能 GA_EnemyFireBolt

打开 DA_CharacterClassInfo Elementalist -startup abilities- GA_EnemyFireBolt image

关卡拖入 BP_Shaman

image

BP_Shaman 行走速度,旋转速度

打开 BP_Shaman combat-base walk speed-75 image

角色移动组件-细节- 角色移动-旋转速率-0,0,200 image

攻击动画蒙太奇中添加攻击声音通知

打开 AM_Attack_Shaman 添加通知轨道 Sound 在火球生成处 添加通知-播放音效- 选中 PlaySound 通知-细节-动画通知-音效-sfx_FireBolt image

打开 AM_Attack_Goblin_Slingshot 添加通知轨道 Sound 在石块发射处 添加通知-播放音效- 选中 PlaySound 通知-细节-动画通知-音效-sfx_Slingshot_Fire image

4. Dead Blackboard Key 死亡黑板键

行为树检查敌人是否死亡 防止敌人死亡后仍然发射技能

BB_EnemyBlackboard 添加是否变量

打开 BB_EnemyBlackboard 添加 布尔类型 Dead 黑板键 image

BT_EnemyBehaviorTree 行为树设置 Dead 黑板键

打开 BT_EnemyBehaviorTree 重命名节点 我是否没有激活 受击技能 我是否有攻击目标

image

添加blackboard黑板装饰器 名称-我是否存活 我是否存活-黑板-黑板键-Dead 我是否存活-黑板-键查询-未设置

如果死亡,则中止一切行为 我是否存活-流控制-通知观察者-值改变时 我是否存活-流控制-观察器中止-both 死亡时,整个行为树中止。 image BPGraphScreenshot_2024Y-01M-21D-19h-28m-14s-896_00

C++中 死亡时 设置黑板键Dead的值为true

Source/Aura/Private/Character/AuraEnemy.cpp

void AAuraEnemy::Die()
{
    // 设置寿命
    SetLifeSpan(LifeSpan);
    // 设置黑板键Dead
    if (AuraAIController) AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("Dead"), true);
    Super::Die();
}

5. Enemies Multiplayer Testing 敌人多人游戏测试

    // 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;
    }
}
WangShuXian6 commented 10 months ago

19. Enemy Finishing Touches 敌人的最终润色

1. Goblin Spear - Sound Notifies 长矛哥布林-声音通知

拷贝 sfx_Footsteps 为 sfx_Footsteps_quite

打开 sfx_Footsteps_quite VolumeMultiplier-默认-0.05 image

Run_Spear_fix 动画序列 添加脚步声

打开 Run_Spear_fix

默认通知轨道-Sound 添加通知-播放音效 音效-sfx_Footstepssfx_Footsteps_quite image

配置 MetaSound源声音 sfx_Template_multi

拷贝 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 目录下的音效全部拖入 image

VolumeMultiplier-0.5 image

重命名为 sfx_Swoosh

AM_Attack_GoblinSpear 蒙太奇添加声音通知

打开 AM_Attack_GoblinSpear

默认通知轨道-Sounds 添加通知-播放音效 音效-sfx_Swoosh image

2. Impact Effects 撞击效果

长矛刺中目标时产生粒子特效和音效 在长毛尖端处生成特效【不太准确的方式】

战斗接口获取血液粒子变量

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;
};

AuraCharacterBase 角色添加血液粒子变量

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 设置 血液粒子 BloodEffect

打开 BP_AuraCharacter combat-BloodEffect-NS_BloodImpact image

BP_EnemyBase 设置 血液粒子 BloodEffect

打开 BP_EnemyBase combat-BloodEffect-NS_BloodImpact image

使用 音效模板制作长矛撞击音效

拷贝 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个音效 image image VolumeMultiplier-0.25

BP_Goblin_Spear 设置 TaggedMontage 类型的标签蒙太奇组 Acttack Montages 的 ImpactSound 撞击音效

长矛刺中目标的音效

打开 BP_Goblin_Spear cpmbat-Acttack Montages ImpactSound -sfx_Swipe image

技能 GA_MeleeAttack 中播放长矛撞击音效和撞击粒子

打开 GA_MeleeAttack 新建布尔变量 HasHitTarget 造成伤害时设为true 技能刚激活时设为false HasHitTarget image

get combat socket location 提升为变量 CombatSocketLocation

循环刺中 的每个目标时,刺中完成时播放撞击音效 需要已命中目标 HasHitTarget 为true Branch 撞击音效来自TaggedMontage 的标签蒙太奇音效组 image

play sound at location 音效位置来自武器上生成的检测重叠目标的重叠球的插槽 TaggedMontage 分割结构体引脚 CombatSocketLocation

对每个目标造成伤害时播放血液粒子 get BloodEffect niagara-spawn system at location CombatSocketLocation

BPGraphScreenshot_2024Y-01M-21D-21h-15m-29s-510_00

敌人攻击玩家时,客户端无法产生击中粒子和音效。 这是由因为技能GA_MeleeAttack在服务器端运行AI行为树产生。 技能只复制到拥有该技能的客户端。 而没有任何玩家可以拥有由人工智能控制的角色。

所以AI控制的技能不会复制到客户端。 技能中的粒子和音效也不会复制到客户端。

技能系统使用gameplay cue 来复制AI控制的粒子和音效到客户端。

3. Melee Impact Gameplay Cue 近战冲击游戏提示

Gameplay Cue 游戏性Cue显示

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显示 有多种类型 image

基于 GameplayCueNotify_Static 创建 静态游戏性Cue显示 GC_MeleeImpact

GameplayCueNotify_Static 不会实例化 Content/Blueprints/AbilitySystem/Enemy/Cues/GC_MeleeImpact.uasset image

打开 GC_MeleeImpact 指定执行cue发生什么 重载函数 on execute image image 同时调用父函数,类似 super image

拆分 参数parameters image

执行带有参数的游戏性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

BPGraphScreenshot_2024Y-01M-21D-23h-18m-52s-676_00

通过标签识别执行 游戏性Cue显示

游戏性Cue显示标签 必须与"GameplayCue"开头的游戏性标记相关联,且GameplayCue必须是顶层标签。

项目设置中添加标签 GameplayCue.MeleeImpact image

类默认值-gameplay cue-gameplay cue tag-GameplayCue.MeleeImpact

image

GA_MeleeAttack 中执行 游戏性Cue显示 GC_MeleeImpact, 通过标签 GameplayCue.MeleeImpact

打开 GA_MeleeAttack

execute gameplayCueWithParams on owner execute gameplayCueWithParams on owner-gameplay cue tag-GameplayCue.MeleeImpact

make Gameplay Cue Parameters CombatSocketLocation BPGraphScreenshot_2024Y-01M-21D-23h-12m-20s-150_00

现在,服务端和客户端都可以显示硬编码的粒子和音效。 这些效果都可以复制到客户端。

将战斗目标作为来源参数传入 Cue。 cue 中就可以通过来源参数获取血液粒子。

get combat target ability-get avatar actor from actor info BPGraphScreenshot_2024Y-01M-21D-23h-27m-44s-321_00 BPGraphScreenshot_2024Y-01M-21D-23h-28m-15s-808_00

GC_MeleeImpact 中使用来源的血液粒子

打开 GC_MeleeImpact get blood effect BPGraphScreenshot_2024Y-01M-21D-23h-32m-22s-356_00 现在血液粒子不再硬编码。

玩家受伤使用绿色血液效果 NS_BloodImpact_green

复制 NS_BloodImpact 为 NS_BloodImpact_green 打开 NS_BloodImpact_green Initialize particle-color-绿色 image

打开 BP_AuraCharacter combat - blood effect-NS_BloodImpact_green image 换回 NS_BloodImpact

GA_MeleeAttack 为 游戏性Cue显示 GC_MeleeImpact 传入攻击音效

打开 GA_MeleeAttack 删除原来的额播放音效节点和生成粒子节点 image image

通过标签从标签蒙太奇音效组中获取音效 TaggedMontage break TaggedMontage make gameplay tag container from tag image

BPGraphScreenshot_2024Y-01M-21D-23h-54m-16s-713_00

GC_MeleeImpact 通过 aggregated source tags 标签获取的音效

打开 GC_MeleeImpact 循环 aggregated source tags 标签 for each loop get debug string from gameplay tag container print string BPGraphScreenshot_2024Y-01M-21D-23h-53m-56s-373_00

蒙太奇标签组需要通过插槽添加不同类型的标签。区分蒙太奇标签和音效标签。

4. Montage and Socket Tags 蒙太奇和插槽标签

将 用于抛射物生成位置的战斗插槽蒙太奇标签 MontageAttack 重命名为插槽 CombatSocket_

    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近战技能中战斗标签改用 蒙太奇标签组中的 SocketTag

用来监听蒙太奇战斗事件通知 和 用作生成抛射物用的位置 打开GA_MeleeAttack image

为敌人蓝图设置蒙太奇标签组的蒙太奇,蒙太奇关联的蒙太奇攻击标签,战斗插槽位置标签 【用以生成抛射物或直接生成攻击目标重叠检测球】

BP_Ghoul 左右手攻击标签

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

image

BP_Goblin_Slingshot

combat-attack montages-

montage-AM_Attack_Goblin_Slingshot montage tag-Montage.Attack.1 Socket tag-CombatSocket.Weapon image

BP_Goblin_Spear

combat-attack montages-

montage-AM_Attack_GoblinSpear montage tag-Montage.Attack.1 Socket tag-CombatSocket.Weapon Impact Sound-sfx_Swipe image

BP_Shaman

combat-attack montages-

montage-AM_Attack_Shaman montage tag-Montage.Attack.1 Socket tag-CombatSocket.Weapon image

GA_MeleeAttack 技能中 使用 蒙太奇标签组的 montage tag 标签监听 蒙太奇的战斗通知事件

打开 GA_MeleeAttack 蒙太奇标签组break taggedMontage 的 montage tag 输出至 wait gamepley event 的 event tag

image BPGraphScreenshot_2024Y-01M-22D-18h-19m-34s-982_00

AM_Ghoul_Attack_L 攻击蒙太奇动画使用 montage tag 的标签值 Montage.Attack.1 来发送攻击事件通知

选中自定义事件通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.1 image

AM_Ghoul_Attack_R 攻击蒙太奇动画使用 montage tag 的标签值Montage.Attack.2来发送攻击事件通知

选中自定义事件通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.2 image

AM_Attack_Goblin_Slingshot 攻击蒙太奇动画使用 montage tag 的标签值 Montage.Attack.1 来发送攻击事件通知

选中自定义事件通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.1 image

AM_Attack_GoblinSpear 攻击蒙太奇动画使用 montage tag 的标签值 Montage.Attack.1 来发送攻击事件通知

选中自定义事件通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.1 image

AM_Attack_Shaman 攻击蒙太奇动画使用 montage tag 的标签值 Montage.Attack.1 来发送攻击事件通知

选中自定义事件通知 AN_MontageEvent 细节-动画通知-event tag-Montage.Attack.1 image

游戏性Cue显示中使用蒙太奇标签组的攻击蒙太奇标签MontageTag获取对应的 攻击音效标签

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 中使用 GetTaggedMontageByTag

打开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

BPGraphScreenshot_2024Y-01M-22D-18h-20m-17s-219_00

现在 服务端和客户端的AI敌人都可以播放撞击音效和血液粒子。

5. Goblin Spear - Hurt and Death Sounds 长矛哥布林-伤害和死亡音效

使用 sfx_Template_multi 模板制作受伤音效 sfx_GoblinHurt

复制 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 image VolumeMultiplier-0.25

AM_HitReact_GoblinSpear 受击蒙太奇动画添加通知 sfx_GoblinHurt

默认通知轨道-Sounds 添加通知-播放音效- sfx_GoblinHurt image

角色基类添加死亡音效变量

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

复制 sfx_Template_multi 为 sfx_GoblinDeath Content/Assets/Sounds/Enemies/Goblin/Death/sfx_GoblinDeath.uasset

打开 sfx_GoblinHurt 使用 同目录下3个音效 配置 inputArray image VolumeMultiplier-0.25

BP_Goblin_Spear 配置死亡音效 DeathSound

打开 BP_Goblin_Spear 细节-combat-Death Sound-sfx_GoblinDeath image

6. Goblin Slingshot - Sound Notifies 弹弓哥布林-声音通知

Running_Slingshot 动画序列添加通知

默认轨道-Sounds 添加通知-播放音效-sfx_Footsteps_quite image

AM_HitReact_GoblinSlingshot 动画蒙太奇添加受击音效

默认轨道-Sounds 添加通知-播放音效-sfx_GoblinHurt image

BP_Goblin_Slingshot 配置死亡音效 DeathSound

打开 BP_Goblin_Spear 细节-combat-Death Sound-sfx_GoblinDeath image

7. Rock Impact Effects 游侠敌人石块撞击效果

使用 sfx_Template_multi 模板制作石块撞击音效 sfx_RockHit

复制 sfx_Template_multi 为 sfx_RockHit Content/Assets/Sounds/Enemies/Goblin/RockHit/sfx_RockHit.uasset

打开 sfx_RockHit 使用 同目录下3个音效 配置 inputArray image VolumeMultiplier-0.08 image

BP_SlingshotRock 石块抛射物 配置石块撞击效果,音效

BP_SlingshotRock impact sound-sfx_RockHit image

石块抛射物撞击地板音效

地板对 Projectile 抛射物通道产生重叠响应 地板生成重叠事件 image

BP_SlingshotRock 石块抛射物撞击地板粒子效果

BP_SlingshotRock impact effect-NS_SlingshotImpact image

防止多次生成石块撞击音效

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;
    }
}

8. Goblin Shaman - Sound Notifies 哥布林萨满-声音通知

Shaman_Walk 动画序列添加行走音效

默认轨道-Sounds 添加通知-播放音效-sfx_Footsteps image

基于 Shaman_HitReact 动画序列 制作 萨满受击动画蒙太奇AM_Shaman_HitReact

Content/Assets/Enemies/Shaman/Animations/AM_Shaman_HitReact.uasset

打开 AM_Shaman_HitReact 默认轨道-Sounds 添加通知-播放音效-sfx_GoblinHurt image

BP_Shaman 使用受击蒙太奇动画 AM_Shaman_HitReact

打开 BP_Shaman

Hit React montage-AM_Shaman_HitReact image

BP_Shaman 配置死亡音效 DeathSound

打开 BP_Shaman 细节-combat-Death Sound-sfx_GoblinDeath image

9. Ghoul - Sound Notifies 食尸鬼-声音通知

Walk 动画序列添加脚步声

默认轨道-Sounds 添加通知-播放音效-sfx_Footsteps image

使用 sfx_Template_multi 模板制作攻击音效 sfx_GhoulAttack

复制 sfx_Template_multi 为 sfx_GhoulAttack Content/Assets/Sounds/Enemies/Ghoul/Attack/sfx_GhoulAttack.uasset image VolumeMultiplier-0.15

AM_Ghoul_Attack_L ,AM_Ghoul_Attack_R动画蒙太奇添加攻击音效通知

添加轨道-Sounds 添加通知-播放音效- sfx_GhoulAttack image

BP_Ghoul 配置撞击音效

打开 BP_Ghoul impact sound-sfx_Swipe image

攻击音效可以早于撞击音效发生。

使用 sfx_Template_multi 模板制作受击音效 sfx_GhoulHurt

复制 sfx_Template_multi 为 sfx_GhoulAttack Content/Assets/Sounds/Enemies/Ghoul/Growl/sfx_GhoulHurt.uasset image

VolumeMultiplier-0.18

AM_HitReact_Ghoul 动画蒙太奇添加受击音效通知 sfx_GhoulHurt

默认轨道-Sounds 添加通知-播放音效- sfx_GhoulHurt image

使用 sfx_Template_multi 模板制作死亡音效sfx_GhoulDeath

复制 sfx_Template_multi 为 sfx_GhoulDeath Content/Assets/Sounds/Enemies/Demon/Death/sfx_GhoulDeath.uasset 与demon共用音效 VolumeMultiplier-0.25

image

BP_Ghoul 配置死亡音效

BP_Ghoul death sound-sfx_GhoulDeath image

10. Ghoul - Swipe Trail 食尸鬼攻击拖尾效果

SK_Ghoul 添加用于 生成攻击拖尾效果的骨骼插槽

打开 骨骼 SK_Ghoul

在骨骼 Wrist-R 添加插槽 RightTrailSocket 在骨骼 Wrist-L 添加插槽 LeftTrailSocket

插槽 RightTrailSocket ,LeftTrailSocket 移动到指尖 image image

AM_Ghoul_Attack_L 添加 拖尾效果通知

添加轨道 Trail 右键-添加通知状态-定时Niagara效果 覆盖攻击动作时间范围 Niagara系统-NS_CombatTrail 插槽命名-LeftTrailSocket image image

AM_Ghoul_Attack_R 添加 拖尾效果通知

添加轨道 Trail 右键-添加通知状态-定时Niagara效果 覆盖攻击动作时间范围 Niagara系统-NS_CombatTrail 插槽命名-RightTrailSocket

11. Demon Blueprint 恶魔蓝图

基于 BP_EnemyBase 创建子蓝图类 BP_Demon

Content/Blueprints/Character/Demon/BP_Demon.uasset

基于动画蓝图 ABP_Enemy 创建 带骨架的子动画蓝图 ABP_Demon ,使用骨架 SK_Demon

Content/Blueprints/Character/Demon/ABP_Demon.uasset

image

BP_Demon 配置网格体

BP_Demon-网格体组件-骨骼网格体资产-SKM_Demon 调整位置,面向前方 image

胶囊体组件-半高-56,半径 26 image

ABP_Demon 动画蓝图使用混合空间

打开 ABP_Demon 资产覆盖编辑器-混合空间播放器-BS_Demon_IdleRun image

BP_Demon

打开 BP_Demon 网格体组件-动画-动画模式-使用蓝图动画 动画类-ABP_Demon image

SK_Demon 骨骼添加攻击插槽

打开 SK_Demon 在尾巴末端骨骼 添加插槽 TailSocket image

BP_Demon 配置攻击插槽

BP_Demon weapon tip socket name-TailSocket image

BP_Demon 配置行走速度

base walk speed-175 image

12. Demon Melee Attack 恶魔近身攻击

尾巴近战攻击 左旋 -基于 动画序列 Demon_Attack_L 创建 动画蒙太奇 AM_Attack_Demon_L

Content/Assets/Enemies/Demon/Animations/AM_Attack_Demon_L.uasset

尾巴近战攻击 右旋 -基于 动画序列 Demon_Attack_R 创建 动画蒙太奇 AM_Attack_Demon_R

Content/Assets/Enemies/Demon/Animations/AM_Attack_Demon_R.uasset

AM_Attack_Demon_L 的扭曲运动

打开 AM_Attack_Demon_L 默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人的尾巴开始攻击 - 到 尾巴攻击结束

image

选择 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 旋转以面对该目标

image

添加通知轨道:Events 决定尾巴攻击的时机

右键-添加通知-AN_MontageEvent 【自定义的通知】 在尾巴击中目标的位置 【尾巴旋转范围扇形的最长,敌人完全背对目标时】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.1 用以在游戏中监听 需要调整,使尾巴尖击中目标的位置在目标身体中心位置

image image image image

标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇尾巴上的插槽位置,生成尾巴重叠虚拟球。 播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.1 后续通过标签监听此事件。

添加通知轨道:Sounds 攻击音效

添加通知-播放音效- sfx_GhoulAttack image image

AM_Attack_Demon_R 的扭曲运动

打开 AM_Attack_Demon_R 默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人的尾巴开始攻击 - 到 尾巴攻击结束

image

选择 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 旋转以面对该目标

image

添加通知轨道:Events 决定尾巴攻击的时机

右键-添加通知-AN_MontageEvent 【自定义的通知】 在尾巴击中目标的位置 【尾巴旋转范围扇形的最长,敌人完全背对目标时】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.2 用以在游戏中监听 需要调整,使尾巴尖击中目标的位置在目标身体中心位置 image

image

标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇尾巴上的插槽位置,生成尾巴重叠虚拟球。 播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.2 后续通过标签监听此事件。

添加通知轨道:Sounds 攻击音效

添加通知-播放音效- sfx_GhoulAttack image image

用于恶魔的尾巴攻击的插槽

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();
}

BP_Demon 配置尾巴攻击时的插槽名称

Tail Socket Name-TailSocket

image

BP_Demon 配置标签蒙太奇组

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

image

GA_MeleeAttack 近战攻击添加调试球显示尾巴击中的目标位置

draw debug sphere image

BP_Demon 配置职业为 战士warrior

character class-warrior image

BS_Demon_IdleRun 恶魔混合空间优化防止攻击位置偏移

打开 BS_Demon_IdleRun 取样平滑-权重速度-4 image

关卡拖入 BP_Demon

image

13. Demon Ranged Attack 恶魔远程攻击

Demon_Throw 投掷动画序列 启用根运动

打开 Demon_Throw 根运动-启用根运动-启用 image

基于 动画序列 Demon_Throw 创建 动画蒙太奇 AM_Demon_Throw

AM_Demon_Throw Content/Assets/Enemies/Demon/Animations/AM_Demon_Throw.uasset

AM_Demon_Throw 的扭曲运动

打开 AM_Demon_Throw 默认通知轨道名:Motion Warping 添加通知状态-Motion Warping Motion Warping 覆盖敌人投掷动作的开始攻击 - 到 投掷动作攻击结束 image

选择 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 旋转以面对该目标

image

添加通知轨道:Events 决定扔出石块攻击的时机

右键-添加通知-AN_MontageEvent 【自定义的通知】 在尾巴击中目标的位置 【尾巴旋转范围扇形的最长,敌人完全背对目标时】 选择 AN_MontageEvent -动画通知-event tag-Montage.Attack.1 用以在游戏中监听

image image

标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇尾巴上的插槽位置,生成尾巴重叠虚拟球。 播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.1 后续通过标签监听此事件。

添加通知轨道:Sounds 攻击音效

添加通知-播放音效- sfx_Swipe

image image

近战恶魔,运程恶魔名称

近战 恶魔 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 职业为 ranger

打开BP_Demon_Ranger character class-ranger image

SK_Demon 骨骼添加左手插槽用于生成攻击投掷物

SK_Demon 在骨骼 Wrist-L处添加插槽 LeftHandSocket 调整位置到掌心 image

BP_Demon_Ranger 配置标签蒙太奇组

BP_Demon_Ranger 删除原标签组 添加新组 combat-attack montages-

montage-AM_Demon_Throw montage tag-Montage.Attack.1 Socket tag-CombatSocket.LeftHand impact sound-sfx_Swipe image

BP_Demon_Ranger 配置 左手攻击插槽名

Left Hand Socket Name-LeftHandSocket image

默认只在 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);
}

技能中指定生成投掷物的插槽名称/或用于攻击的插槽名称

GA_RangedAttack

删除 get combat socket location

break taggedMontage -Socket tag 输出至 spawn projectile-Socket Tag

BPGraphScreenshot_2024Y-01M-23D-00h-08m-00s-327_00

GA_FireBolt

火球术传入硬编码的插槽即可

make literal gameplay tag make literal gameplay tag-value-CombatSocket.Weapon image BPGraphScreenshot_2024Y-01M-23D-00h-10m-28s-592_00

BP_Demon_Ranger 改变材质颜色

打开 BP_Demon_Ranger

材质-Mesh-元素0-M_DemonDark image image

14. Demon - Sound Notifies 恶魔-声音通知

15. Demon - Dissolve Effect 恶魔-溶解材质

16. Shaman Summon Locations 萨满召唤地点

萨满召唤恶魔技能 基于 AuraGameplayAbility C++ 创建派生C++ AuraSummonAbility

该技能不生成投掷物,不需要伤害变量。所以不基于 AuraProjectileSpell。 而是基于 AuraGameplayAbility image

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;
}

基于 AuraSummonAbility 创建召唤技能蓝图 GA_SummonAbility

image

Content/Blueprints/AbilitySystem/Enemy/Abilities/GA_SummonAbility.uasset

打开 GA_SummonAbility

事件图表: 技能激活时获取召唤物位置数组 event activateAbility GetSpawnLocations image

使用标签Abilities.Attack激活技能GA_SummonAbility

打开 GA_SummonAbility 类默认值-细节 -标签-ability tags-Abilities.Attack image

这可以使其与行为树攻击任务一起使用。

为魔法师职业Elementalist赋予召唤技能 GA_SummonAbility

打开职业信息数据资产 DA_CharacterClassInfo

Elementalist-startup abilites- 将技能 GA_EnemyFireBolt 火球术替换为 GA_SummonAbility image

关卡添加萨满 BP_Shaman

image

17. Async Spawn Times 异步生成时间

GA_SummonAbility 召唤技能

事件图表: 将 召唤物位置组提升为变量 SpawnLocations

新建整数类型 召唤物位置索引变量 SpawnLocationIndex

branch SpawnLocationIndex less SpawnLocations length draw debug sphere SpawnLocations get(a copy) SpawnLocationIndex ++ delay shuffle

BPGraphScreenshot_2024Y-01M-23D-14h-30m-47s-611_00

确保召唤物位置在地面上

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;
}

在山坡上测试召唤物位置

地板需要简单盒体碰撞 image

选中部分地板-右键-组 旋转使该部分地板倾斜 image

18. Summoning Particle Effect 召唤粒子效果

GA_SummonAbility 召唤技能

时间图表: for each loop spawn system at location spawn system at location-system template-NS_GroundSummon delay

BPGraphScreenshot_2024Y-01M-23D-14h-57m-04s-567_00

19. Select Minion Class at Random 随机选择召唤物职业

设置 GA_SummonAbility 召唤技能 的召唤物职业种类

打开 GA_SummonAbility 召唤技能 类默认设置-细节-Minion Classes 添加2组职业 BP_Demon_Ranger BP_Demon_Warrior

image

从技能设置的职业中随机选择用以召唤物

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 召唤技能 随机选择召唤物职业

打开 GA_SummonAbility 召唤技能 时间图表: GetRandomMinionClass spawnActor from class spawnActor from class 固定生成,忽略碰撞

提高生成位置,防止陷入地板 add

手动在世界中生成的actor,不会获取到分配给他的控制器,所以直接生成的敌人不会攻击玩家。

分配使用默认控制器 pawn-spawn default controller

BPGraphScreenshot_2024Y-01M-23D-15h-14m-25s-708_00

20. Minion Summon Montage 召唤小兵蒙太奇

Shaman_Summon 动画序列启用根运动

image

基于动画序列 Shaman_Summon 创建召唤动画蒙太奇 AM_Shaman_Summon

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。 image

image

标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。

播放此蒙太奇可触发通知事件,带有事件标签Montage.Attack.1 后续通过标签监听此事件。

GA_SummonAbility 召唤技能 中播放蒙太奇动画 AM_Shaman_Summon

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

BPGraphScreenshot_2024Y-01M-23D-15h-33m-02s-642_00

21. Minion Count 小兵数量

为技能创建新的召唤类型技能标签属性,以区别于 Ability tags 攻击类型技能标签

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 召唤技能设置技能标签为 召唤类型 Abilities.Summon

表明不是攻击技能,而是召唤技能

GA_SummonAbility 细节- ability tags-Abilities.Summon image

22. Elementalist Behavior Tree 魔法师职业行为树

之前的 BT_EnemyBehaviorTree 行为树中的 BTT_Attack 任务使用攻击类型标签Abilities.Attack来激活攻击技能, 没有使用 Abilities.Summon 标签 来激活召唤技能。

拷贝 BT_EnemyBehaviorTree 行为树为 BT_EnemyBehaviorTree_Elementalist

Content/Blueprints/AI/BehaviorTree/BT_EnemyBehaviorTree_Elementalist.uasset

BP_Shaman 萨满使用 魔法师行为树 BT_EnemyBehaviorTree_Elementalist

打开 BP_Shaman AI-Behavior Tree-BT_EnemyBehaviorTree_Elementalist image

配置职业信息资产的魔法师职业的初始技能为攻击技能和召唤技能

打开 DA_CharacterClassInfo Elementalist-startup abilites-2个技能 GA_EnemyFireBolt 火球术 GA_SummonAbility 召唤术

image

设置 BP_FireBolt 的 sphere 组件蓝图可见

Source/Aura/Public/Actor/AuraProjectile.h

protected:
    // 投射物球体碰撞组件
    // 投射物球体碰撞组件
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
    TObjectPtr<USphereComponent> Sphere;

BP_FireBolt 火球抛射物,发射后再启用重叠

设置 BP_FireBolt sphere组件默认无碰撞,不检测重叠 打开BP_FireBolt sphere组件-碰撞-碰撞预设-custom 碰撞已启用-无碰撞 对象类型-Projectile image image

火球发射后短暂延时后再启用查询 时间图表: event beginplay delay Sphere Sphere 右键 转换为有效get 碰撞-set collision enabled set collision enabled-new type-纯查询(无物理碰撞) draw debug sphere get actor location sphere get sphere radius

image

23. Elementalist Attack Task 魔法师攻击任务

在小兵低于一定数量时,才激活召唤技能,开始召唤小兵。 双击 BTT_Attack 打开 BTT_Attack 任务

拷贝 BTT_Attack 任务为 BTT_Attack_Elementalist

Content/Blueprints/AI/Tasks/BTT_Attack_Elementalist.uasset 打开 BTT_Attack_Elementalist

事件图表: event receive execute AI-controlled pawn 提升为变量 ControlledPawn

添加 gameplay 标签 变量 SummonTag 表示召唤 公开 SummonTag 默认值 -Abilities. Summon image

添加 gameplay 标签 变量 AbilityTag
检查条件根据小兵数量后设置 AbilityTag

GetMinionCount ControlledPawn

添加整数变量 MinionSpawnThreshold 小兵数量最低值 默认值 2 表示小兵数量小于2时,激活召唤技能

less MinionSpawnThreshold branch set AbilityTag SummonTag

如果小兵数量不少于2,AbilityTag 设为攻击标签 Attack Tag,表示攻击技能

最终使用AbilityTag的技能标签激活技能 image BPGraphScreenshot_2024Y-01M-23D-17h-16m-06s-726_00

生成小兵时,更新当前小兵数量

通过传递负数,表示减少数量

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 召唤技能中每次生成小兵都递增小兵数量

打开GA_SummonAbility 事件图表: IncremenetMinionCount get avatar actor from actor info

BPGraphScreenshot_2024Y-01M-23D-17h-19m-56s-602_00

BT_EnemyBehaviorTree_Elementalist 行为树

RangedAttacker 远程攻击下的 BTT_Attack 替换为 BTT_Attack_Elementalist

BTT_Attack_Elementalist分支-默认- Combat Target Selector-TargetToFollow 节点名称-魔法师攻击

image image

MeleeAttacker 近战攻击下的 BTT_Attack 替换为 BTT_Attack_Elementalist

BTT_Attack_Elementalist分支-默认- Combat Target Selector-TargetToFollow 节点名称-魔法师攻击 image

24. Decrementing Minion Count 减少小兵计数

GA_SummonAbility 召唤技能中监听小兵销毁事件

打开 GA_SummonAbility 在生成小兵后,监听其销毁事件 事件图表: assign on destroyed [有一点延迟] IncremenetMinionCount image BPGraphScreenshot_2024Y-01M-23D-17h-59m-55s-142_00

BP_SlingshotRock 石块,发射后再使用查询

打开 BP_SlingshotRock 在开始时禁用碰撞 image

sphere组件-碰撞-碰撞预设-custom 碰撞已启用-无碰撞 对象类型-Projectile image

事件图表: Sphere【可省略,由以上设置代替】 set collision enabled-无碰撞 【可省略,由以上设置代替】 延迟一秒后启用查询 delay Sphere set collision enabled-纯查询(无物理碰撞) image BPGraphScreenshot_2024Y-01M-23D-17h-33m-10s-761_00

抛射石块时,使角度向上偏移,防止过早落地,无法击中目标

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);
}

GA_RangedAttack 设置抛射向上偏移角度

SpawnProjectile-OverridePitch-true SpawnProjectile-PitchOverride-35 image

25. Adding Juice with Tweening 召唤物的补间过渡效果

BP_EnemyBase 游戏开始时使用补间动画

召唤物生成时 出现缩放动画。

事件图表

event beginPlay add timeline 名称 SpawnTimeline

进入 SpawnTimeline

添加浮点型轨道 ScaleTrack

添加关键帧: 0,0 0.1,0

全选2个关键帧-自动平滑

调整过渡角度为山峰曲线,最高点到1.5 image

添加关键帧: 0.2,0 0.3,0 全选2个关键帧-自动平滑 调整过渡 全选首末之外的所有点,使最低点为1,末点为1

image

事件图表

使用时间轴的曲线值设置网格体的缩放。

make vector mesh set relative scale 3d

image BPGraphScreenshot_2024Y-01M-23D-18h-18m-12s-498_00

26. Enemies Final Polish 敌人最终润色

GA_RangedAttack 修复萨满发射的 GA_EnemyFireBolt 火球过高问题

GA_EnemyFireBolt 火球术默认未设置 火球角度,导致使用了默认值的角度,过高。 需要为 基类 GA_RangedAttack 设置角度默认为false

打开 GA_RangedAttack

spawn projectile -OverridePitch 提升为变量 ShouldOverridePitch 默认为false image

spawn projectile -PitchOverride提升为变量PitchOverride

BPGraphScreenshot_2024Y-01M-23D-18h-32m-08s-705_00

修复内存泄漏导致的fps 不断下降,最终降至0的问题

默认 AAuraProjectile::OnSphereOverlap 击中目标重叠时产生的石块和粒子不断累计,越来越多。

GA_RangedAttack 技能中 等待蒙太奇的通知事件设置为只触发一次

wait gameplay event-only trigger once-启用 image

这样,每次投掷石块蒙太奇动画触发一次事件。 否则一次蒙太奇会触发多次通知事件。 BPGraphScreenshot_2024Y-01M-23D-18h-41m-36s-622_00

NS_SlingshotImpact 石块击中目标的粒子

默认情况: 粒子更新-particle state-无限 表示粒子生成后将一直存在 image

image

粒子更新-particle state-Kill Particles When Lifetime Has Elapsed 当生命周期已过时系死粒子 -启用 此时无限符号消失。 2处都需要启用。 image image

执行计算中,未找到相关属性时不发出警告

当前项目属性不是通用的。

    // 参数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);
}
WangShuXian6 commented 10 months ago

20. Level Tweaks 等级调整

1. Level Lighting and Post Process 关卡照明及后处理

新建basic地图 Dungeon

删除默认地板 向上移动 DirectionalLight SkyAtmosphere SkyLight VolumetricCloud

拖入SM_Tile_3x3_A 地板 location 0,0,0

拖入 SM_Tile_3x3_B地板

所有地板和建筑物添加简单碰撞

捕捉网格启用, 开始复制制作地板

世界场景设置-游戏模式重载-BP_AuraGameMode

添加 玩家出生点

DirectionalLight

光源-强度-1 lux 光源颜色-蓝紫 使用色温-启用 温度-6400 image

SkyLight

光源颜色-蓝 光源-强度范围-50

添加 体积-后期处理体积 PostProcessVolume

后期处理体积设置-无限范围-启用

渲染功能-后期处理材质-资产引用 PP_Highlight image

镜头-exposure-min EV100-0 镜头-exposure-max EV100-0 使光照恒定,不模拟眼睛

颜色分级-temperature- 色温类型-色温 色温-7500 着色-0.1

颜色分级-global-

颜色分级-shadows-

胶片粒度

拖入点光源 到火粒子

光源颜色-金黄

2. Texture Streaming Pool Over Budget 纹理流池超预算

它的出现是提示场景中的 Texture 使用的内存超出了为它们分配的额定内存(UE4 默认是1000MB),因此引擎已经开始通过降低纹理的质量以进行补偿。同时你还会看到场景里有些材质现实质量会变得很差,很模糊。

纹理流送

https://docs.unrealengine.com/5.3/zh-CN/texture-streaming-in-unreal-engine/ 纹理流送用于在运行时在内存中加载和卸载纹理的系统。

关卡拖入更多物体

障碍物碰撞预设

需要简单碰撞盒 image

添加 体积-导航网格体边界体积

覆盖整个关卡

为障碍物添加碰撞盒可阻止导航体积

压缩场景使用的纹理 防止出现 纹理流池超预算

打开 纹理 Tileset1_low_DefaultMaterial_BaseColor

image

压缩-高级-最大纹理尺寸-512 此时纹理会变小

过滤 tile 纹理,批量压缩

方法2:DefaultEngine.ini

DefaultEngine.ini

[/Script/Engine.RenderSettings]
r.TextureStreaming=True
r.Streaming.PoolSize=1000

3. Flame Pillar Actor 火焰柱actor

基于 actor 创建蓝图 BP_FlamePillar

Content/Blueprints/Actor/FlamePillar/BP_FlamePillar.uasset

BP_FlamePillar

添加 静态网格体 Pillar 作为根组件 使用 SM_Pillar image

添加 Niagara :Flame 使用 NS_Fire image

添加 PointLight 点光源组件 FireLight image 调整位置到火中心 image 光源颜色-金黄 image

动态改变光源强度

BP_FlamePillar-actor tick-启用tick并开始-不启用 image

FireLight-细节-移动性-固定 image

事件图表: add timeline :FlameIntensityTimeline_1

进入时间轴 长度-5 添加浮点型轨道-FlameIntensity 自动平滑 image

事件图表: FireLight set Intensity set Intensity-new Intensity 提升为变量-BaseIntensity 默认值5000 ,为灯光的强度 multiply 折叠到函数 ScaleFlameIntensity 输入参数改为 Intensity

add timeline :FlameIntensityTimeline_2

进入时间轴 长度-3

添加浮点型轨道-FlameIntensity 关键帧与第一个不同 自动平滑 image

使不同的火焰柱子火光变化不一样

添加自定义事件 custom event :StartTimeline_1 添加自定义事件 custom event :StartTimeline_2。 添加自定义事件 custom event :ChooseTimeline

random integer in range switch on int StartTimeline_1 StartTimeline_2 ChooseTimeline BPGraphScreenshot_2024Y-01M-23D-22h-17m-02s-654_00

Pillar 组件碰撞

官方 projectile 为阻挡, 实际需要忽略。 image

4. Fade Actor 淡出

所有障碍物碰撞预设为忽略相机 防止相机缩放 image

障碍物挡住视线时,淡化,基于 actor 创建淡出蓝图 BP_FadeActor

Content/Blueprints/Actor/FadeActor/BP_FadeActor.uasset 添加 静态网格体 Mesh 使用 SM_Beacon

复制 材质 M_Beacon 为 M_Beacon_f

打开 材质 M_Beacon_f 选择 主节点 细节-混合模式-已遮罩 已遮罩 比不透明 消耗更少 image

添加 标量1 转换为参数Fade 默认1 DitherTemporalAA [抖动时间用于溶解纹理] 控制不透明度 image

基于 M_Beacon_f 创建材质实例 MI_Beacon_f

BP_FadeActor 使用材质实例 MI_Beacon_f

BP_FadeActor 事件图表

打开 事件 construction script Mesh get materials 提升为变量 OriginalMaterials for each loop create dynamic material instance 新建 材质动态实例数组类型变量 DynamicMaterialInstance image DynamicMaterialInstance add unique

DynamicMaterialInstance clear

BPGraphScreenshot_2024Y-01M-23D-22h-47m-55s-768_00

主事件图表

event beginplay DynamicMaterialInstance for each loop Mesh set material 折叠到函数 SetMaterialToDynamicInstance image

DynamicMaterialInstance for each loop set scalar parameter value set scalar parameter value-parameter name-Fade set scalar parameter value-value-0.5 BPGraphScreenshot_2024Y-01M-23D-22h-57m-12s-566_00

新建 材质实例数组类型变量 FadeMaterialInstance

设置 BP_FadeActor 的FadeMaterialInstance 属性 为 MI_Beacon_f 淡出版本材质

打开 事件 construction script FadeMaterialInstance BPGraphScreenshot_2024Y-01M-23D-23h-01m-43s-311_00

主事件图表

add timeline: FadeTimeline 添加浮点类型轨道 Fade 0,1 1,0 自动平滑 image

添加自定义事件 custom event : FadeOut 添加自定义事件 custom event : FadeIn

= branch

OriginalMaterials for each loop Mesh set material 折叠到函数 ResetMaterials image

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 BPGraphScreenshot_2024Y-01M-23D-23h-21m-43s-229_00

完整: BPGraphScreenshot_2024Y-01M-23D-23h-23m-40s-871_00

5. Fading Out Obstructing Geometry 淡出障碍物网格体

新建蓝图接口 BI_FadeInterface

Content/Blueprints/Actor/FadeActor/BI_FadeInterface.uasset

添加函数 FadeOut FadeIn

BP_FadeActor 使用蓝图接口 BI_FadeInterface

打开 BP_FadeActor 类设置-已实现的接口 BI_FadeInterface image

事件图表: 删除 自身的 FadeOut,FadeIn 使用接口的 event FadeOut,event FadeIn

BPGraphScreenshot_2024Y-01M-24D-00h-06m-07s-184_00

BP_AuraCharacter 决定何时淡入淡出障碍物

SpringArm 组件下添加 Box 碰撞盒

Box 碰撞盒 要比胶囊体组件更窄

Box 碰撞盒 -盒体范围-217,22,32 image

Box 碰撞盒-碰撞预设-custom 生成叠事件 碰撞已启用-纯查询 对象类型-WorldDynamic 只重叠 WorldStatic 其他全部忽略 image

事件图表: 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

BPGraphScreenshot_2024Y-01M-24D-00h-21m-10s-779_00

BP_FadeActor 的 Mesh 组件碰撞-对象类型为 WorldStatic

官方projectile 为阻挡。 image

删除 FadeFinished 函数的 设置通道节点 BPGraphScreenshot_2024Y-01M-24D-00h-24m-09s-515_00

BP_FadeActor 的 Mesh 组件 淡入淡出不投影

BP_FadeActor 的 Mesh 组件 细节-光照-投射阴影-不启用 image

基于 BP_FadeActor 创建子蓝图类 FA_Tile_3x3x2 用于其他障碍物的淡入淡出版本

Content/Blueprints/Actor/FadeActor/FA_Tile_3x3x2.uasset

打开 FA_Tile_3x3x2 Mesh 组件使用原障碍物的网格体即可 配置 Fade Material Instance 属性 为淡入淡出版本得到对应材质实例即可 多个材质实例,顺序要保持一致 image

替换场景中的障碍物为淡入淡出版本。

BP_FadeActor 添加 阻止可视性的开关参数

打开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

BPGraphScreenshot_2024Y-01M-24D-00h-44m-18s-212_00

BP_FadeActor 阻止可视性 以进行移动导航。

BP_FadeActor-细节-BlockVisibility-启用 image

mesh组件-碰撞预设-Visibility-阻挡 image

这样,才可以在点击该淡入淡出版本的网格体时,进行移动导航。

WangShuXian6 commented 10 months ago

21. Cost and Cooldown 技能消耗和冷却

1. Health Mana Spells Widget 生命魔力控件

基于 AuraUserWidget 创建控件蓝图 WBP_HealthManaSpells

Content/Blueprints/UI/Overlay/Subwidget/WBP_HealthManaSpells.uasset

填充屏幕-自定义 宽:1140 高:216 image

设计器: 覆层:Overlay_Root image

horizontal box 水平框:BaseBox 水平填充,垂直填充

BaseBox 子级:

horizontal box 水平框:HealthBox 插槽-尺寸-填充 0.2 image

horizontal box 水平框:CentralBox 插槽-尺寸-填充 0.6

horizontal box 水平框:ManaBox 插槽-尺寸-填充 0.2

填充使3个水平框按自定义比例占用父空间 image

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

完整: image image

2. Spell Globe 施法球

基于 AuraUserWidget 创建控件蓝图 WBP_SpellGlobe

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

image image

设计器: SizeBox_Root 子级: 覆层:Overlay_Root

Overlay_Root 子级: 图像:Image_Glass 水平填充,垂直填充

图像:Image_Ring 水平填充,垂直填充

图表: 添加变量 RingBrush 类型 Slate笔刷 分类 GlobelProperties 默认图像 SkillRing_1

Image_Ring set brush RingBrush image 折叠导函数 UpdateRingBrush image image image

设计器: Image_Glass 笔刷-图像-MI_EmptyGlobe 插槽-填充-7 image

图表: Image_Glass slot as overlay slot set padding make margin 添加变量 GlassPadding 分类 GlobelProperties 默认值 7

image

折叠到函数 UpdateGlobePadding image

设计器: Overlay_Root 子级: 图像:Image_Background 水平填充,垂直填充 笔刷-图像-MI_FireSkillBG 插槽-填充-7 image

图表 UpdateGlobePadding 内:

Image_Background slot as overlay slot set padding image

image

设计器: Overlay_Root 子级: 图像:Image_SpellIcon 水平填充,垂直填充 笔刷-图像-FireBolt 插槽-填充-7

image

图表 UpdateGlobePadding 内: Image_SpellIcon slot as overlay slot set padding

BPGraphScreenshot_2024Y-01M-24D-14h-58m-33s-616_00

主图表: 添加变量 SpellIconBrush 类型 Slate笔刷 分类 GlobelProperties 默认图像 FireBolt

Image_SpellIcon set brush SpellIconBrush 折叠到函数 UpdateSpellIconBrush image image

设计器: Overlay_Root 子级: 文本:Text_Cooldown 水平居中对齐,垂直居中对齐 文本 空 尺寸 24 轮廓大小1

image image

显示冷却倒计时时,图标背景变暗

主图表: Image_Background set brush tint color make slatecolor 灰色 make LinearColor A为1

折叠到函数 SetBackgroundTint 输入:Tint 浮点 BPGraphScreenshot_2024Y-01M-24D-15h-13m-12s-059_00 BPGraphScreenshot_2024Y-01M-24D-15h-14m-16s-811_00

添加变量 TransparentBrush 类型 Slate笔刷 分类 GlobelProperties 默认值-着色-A 为0 全透明

添加函数 ClearGlobe ClearGlob打开 Image_SpellIcon Image_Background set brush TransparentBrush

主图表: ClearGlobe image

添加函数 SetIconAndBackground

输入1 IconBrush 类型 Slate笔刷 输入2 BackgroundBrush 类型 Slate笔刷

Image_SpellIcon Image_Background set brush set brush

BPGraphScreenshot_2024Y-01M-24D-15h-35m-08s-107_00

主图表:

BPGraphScreenshot_2024Y-01M-24D-15h-36m-59s-022_00 image

3. Adding Spell Globes 添加施法球

WBP_HealthManaSpells

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

image

image image

WBP_Overlay

事件图表: 删除 健康/魔力进度球控件 部分节点 image

设计器: 移除 健康,魔力 进度球

添加 WBP_HealthManaSpells 插槽-尺寸x 1140 尺寸y 218 锚点 下居中 插槽-对齐- 0.5,0 插槽-位置X- 0 插槽-位置y- -240

image image image

事件图表: WBP_HealthManaSpells set widget controller get widget controller image

WBP_HealthManaSpells

时间图表 event widget controller set sequence

获取 健康,魔力 控件控制器,设置给控件

WBP_HealthGlobe WBP_ManaGlobe set widget controller get widget controller image

关卡中添加 BP_FireArea 测试健康进度控件 此时健康控件正常。

image

4. XP Bar 经验栏

基于 AuraUserWidget 创建控件蓝图 WBP_XPBar

Content/Blueprints/UI/ProgressBar/WBP_XPBar.uasset

设计器: 填充屏幕-自定义 宽度 880 高度 50 image

覆层:Overlay_Root

Overlay_Root 子级: 图像: 水平填充,垂直填充 笔刷-图像-xp_frame

进度条 progress bar 水平填充,垂直填充 插槽-填充-21 9.5 21 21 样式-背景图-着色-0,0,0,0.5

样式-填充图-图像-xp_bar

进度-百分比-0.5 image image

WBP_Overlay

设计器: WBP_XPBar

插槽-锚点-下中 插槽-尺寸x-880 插槽-尺寸y-50 插槽-对齐-0.5,0 插槽-位置x-0 插槽-位置y- -45

image image image

5. Ability Info Data Asset 技能信息数据资产

自定义日志类别 LogAura

代码编辑器 aura-右键-添加文件 image

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);

AttributeInfo 使用自定义日志类别 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();
}

为技能栏创建 技能信息数据资产 ,基于 DataAsset C++ 创建 AbilityInfo C++

image

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

右键-其他-数据资产-AbilityInfo DA_AbilityInfo image Content/Blueprints/AbilitySystem/Data/DA_AbilityInfo.uasset

BP_OverlayWidgetController 控件控制器设置 技能信息数据资产 为 DA_AbilityInfo

打开 BP_OverlayWidgetController Ability Info-DA_AbilityInfo image

新建 火球术技能标签

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")
        );
}

技能信息数据资产蓝图 DA_AbilityInfo 添加一组技能信息

AbilityTag-Abilities.Fire.FireBolt InputTag // 不应公开给蓝图,应在代码中设置,通过技能获取,可以运行时改变 Icon-FireBolt BackgroundMaterial-MI_FireSkillBG image

现在控件控制器可以通过技能标签查找技能信息数据,然后广播给控件。

6. Initialize Overlay Startup Abilities 初始化覆盖层的初始技能信息

使用委托,使技能系统组件 和 覆层控件控制器的通信,传递技能信息

赋予技能时,广播一个事件

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;
}

7. For Each Ability Delegate 循环每个技能广播

循环委托,替代单独广播 简化代码

直接循环可激活的所有技能 在技能系统中执行回调,替代在控件控制器中执行回调

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);
}

8. Binding Widget Events to the Ability Info Delegate 将Widget事件绑定到技能信息委托

覆层控件监听 来自控件控制器的技能信息广播

WBP_SpellGlobe 添加 gameplay标签变量 InputTag

image

WBP_HealthManaSpells 包含输入标签 InputTag 名称 ,设置 WBP_SpellGlobe的 InputTag 变量

打开 WBP_HealthManaSpells 设计器: 将各技能栏设置为与 InputTag 一致的名称,并设为变量用以访问

主动技能 SpellGlobe_LMB SpellGlobe_RMB SpellGlobe_1 SpellGlobe_2 SpellGlobe_3 SpellGlobe_4

被动技能 SpellGlobe_Passive_1 SpellGlobe_Passive_2

image

事件图表: 添加函数 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 BPGraphScreenshot_2024Y-01M-24D-21h-42m-46s-825_00

主事件: event pre construct SetSpellGlobeInputTags image

BPGraphScreenshot_2024Y-01M-24D-21h-44m-27s-361_00

WBP_SpellGlobe 在控件设置好控件控制器的时候,在控件控制器上绑定一个监听函数

打开 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 BPGraphScreenshot_2024Y-01M-24D-21h-55m-59s-328_00 image

WBP_HealthManaSpells

添加函数 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 子控件 在预构建事件中设置了输入标签。

BPGraphScreenshot_2024Y-01M-24D-22h-07m-09s-285_00

主事件: SetGlobeWidgetControllers BPGraphScreenshot_2024Y-01M-24D-22h-08m-19s-618_00

为火球术 GA_FireBolt 配置技能标签

打开 GA_FireBolt 标签-ability tags-Abilities.Fire.FireBolt image

此时运行游戏,技能栏将可以使用获取的技能信息设置UI

image

因为 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 输入标签与技能标签的配对

打开 GA_FireBolt 当前 输入-startup inputt tag-InputTag.LMB 标签-ability tags-Abilities.Fire.FireBolt image

表示 InputTag.LMB 左键 触发技能 当前技能 Abilities.Fire.FireBolt 所以技能栏显示为 LMB 显示火球术。 image

如果把输入标签设置为 InputTag.RMB 输入-startup inputt tag-InputTag.RMB, 则技能栏 RMB显示为火球术,只能用右键触发火球术 image

灵活的系统。 可以在运行时更改 输入-startup inputt tag 的标签

9. Gameplay Ability Cost 游戏技能消耗

GA_FireBolt

打开 GA_FireBolt 游戏技能消耗和冷却 通过技能效果实现 costs-cost gameplay effect class- image

基于 GameplayEffect 创建火球术的 技能消耗效果 GE_Cost_FireBolt

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点。 image

GA_FireBolt 配置 消耗节能功能效果 GE_Cost_FireBolt

costs-cost gameplay effect class-GE_Cost_FireBolt

image

使消耗效果生效:commitAbility

事件图表: avant activateAbility commitAbility commitAbility 返回布尔值 提交技能需要有对应的消耗火冷却资源,否则会失败,导致技能激活也失败。 image image BPGraphScreenshot_2024Y-01M-24D-23h-01m-26s-936_00

现在每次施放火球术时,立即消耗20点魔力,如果魔力不足,将无法施放技能。激活技能事件将撤销。

为技能消耗效果创建曲线表格 CT_Cost

右键-其他-曲线表格-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 image image

技能消耗效果 GE_Cost_FireBolt 使用曲线表格 CT_Cost的Fire.FireBolt曲线

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 image

10. Gameplay Ability Cooldown 游戏技能冷却

技能冷却需要一个游戏标签 Cooldown.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")
        );
}

基于 GameplayEffect 创建火球术的 技能冷却效果 GE_Cooldown_FireBolt 蓝图

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秒,然后自行消失】

image

为 GA_FireBolt 火球术技能配置技能冷却效果

打开 GA_FireBolt Cooldowns- Cooldown Gameplay Effect Class-GE_Cooldown_FireBolt image

事件图表: 移除delay延时节点 image

但这会立即结束技能,使客户端在产生火球之前就结束了技能,所以客户端将不会触发技能。

应该在施法蒙太奇动画 的几个事件后结束技能。这确保生成火球。 image

修复GA_FireBolt火球蒙太奇被剪短

使技能结束后依然可以继续播放火球术蒙太奇动画。 如果再次激活技能,将重新播放蒙太奇动画。 打开 GA_FireBolt

play montage and wait-Stop when Ability Ends-取消 image

BPGraphScreenshot_2024Y-01M-25D-12h-23m-24s-783_00

11. Cooldown Async Task 冷却异步任务

冷却倒计时

WBP_SpellGlobe

了解冷却的开始时间,持续时间,结束时间,自身运行定时器逻辑。 不需要每一帧都查询技能系统组件。 这需要异步任务。类似播放蒙太奇。

基于蓝图异步行为基础 BlueprintAsyncActionBase 创建 冷却异步任务 C++ WaitCooldownChange

在冷却开始,结束时得到通知。

image

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);
        }
    }
}

WBP_SpellGlobe

事件图表

WaitCooldownChange

接收到技能信息资产时,可获取技能冷却标签 break AuraAbilityInfo

也可以 临时硬编码一个冷却标签,用以关联冷却效果 make literal gameplay tag-Cooldown.Fire.FireBolt

BPOverlayWidgetController get ability system component

image

12. Cooldown Tags in Ability Info 技能信息中的冷却标签

冷却函数返回已完成任务的引用,用以结束

Source/Aura/Public/AbilitySystem/AsyncTasks/WaitCooldownChange.h

// AsyncTask  冷却函数返回已完成任务的引用,用以结束
UCLASS(BlueprintType, meta = (ExposedAsyncProxy = "AsyncTask"))
class AURA_API UWaitCooldownChange : public UBlueprintAsyncActionBase

WBP_SpellGlobe

事件图表 AsyncTask 提升为变量 WaitCooldownChangeTask

在WaitCooldownChange的各异步任务调用之前调用其结束任务,防止覆盖导致内存泄露。

WaitCooldownChangeTask 转换为有效get EndTask image

技能信息资产中设置冷却标签

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;
};

DA_AbilityInfo 技能信息资产配置冷却标签

CooldownTag - Cooldown.Fire.FireBolt image

WBP_SpellGlobe

事件图表 ReceiveAbilityInfo 函数中 CooldownTag 提升为变量 CooldownTag 只在当前技能栏操作控件与技能的输入标签匹配时才设置变量。

BPGraphScreenshot_2024Y-01M-25D-13h-54m-17s-616_00

主事件: 接收到技能信息后: sequence 检查 WaitCooldownChange ,然后结束它 然后再开始 WaitCooldownChange 内的 冷却开始,结束事件。 防止多次执行委托事件。 CooldownTag

BPGraphScreenshot_2024Y-01M-25D-13h-59m-07s-873_00

13. Showing Cooldown Time in the HUD 在HUD中显示冷却时间

WBP_SpellGlobe

事件图表 冷却开始时 TimeRemaining 提升为变量 TimeRemaining

SetBackgroundTint Tint-0.05 SetBackgroundTint-Tint 提升为变量 CooldownTint Text_Cooldown set render opacity In Opacity -1

折叠到函数 SetCooldownState BPGraphScreenshot_2024Y-01M-25D-14h-07m-24s-032_00

SetBackgroundTint Tint-1 Text_Cooldown set render opacity In Opacity -0 折叠到函数 SetDefaultState BPGraphScreenshot_2024Y-01M-25D-14h-09m-24s-069_00

使用计时器更新冷却时间 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

set TimeRemaining TimeRemaining

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 BPGraphScreenshot_2024Y-01M-25D-14h-36m-41s-281_00

设计器: Text_Cooldown-渲染变换-渲染-渲染不透明度-1

image

事件图表: Text_Cooldown set render opacity 折叠到函数HideCooldownText BPGraphScreenshot_2024Y-01M-25D-14h-41m-14s-083_00

完整: BPGraphScreenshot_2024Y-01M-25D-14h-41m-37s-753_00

14. Modeling Mode 建模模式

WangShuXian6 commented 10 months ago

22. Experience and Leveling Up 经验与升级

1 Experience and Leveling Up 经验与升级

image 经验数据资产中包含升级需要得到经验,奖励,属性点奖励,技能点奖励。

数学公式等级实现跨级比较复杂。 经验数据资产更适合实现跨级升级。

经验放置在玩家状态上。

image

2. Level Up Info Data Asset 升级信息数据资产

image

基于 DataAsset 创建 升级信息数据资产 C++ LevelUpInfo

image

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

右键-其他-数据资产-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

image

3. Adding XP to the Player State 将XP添加到玩家状态

玩家状态上的 经验值在服务端执行,复制到客户端

image

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);
}

4. Listening for XP Changes 监听XP更改

image

玩家状态设置等级升级信息

Source/Aura/Public/Player/AuraPlayerState.h

class ULevelUpInfo;

public:
    // 等级升级信息
    UPROPERTY(EditDefaultsOnly)
    TObjectPtr<ULevelUpInfo> LevelUpInfo;

控件控制器监听 XP更改

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);
    }
}

5. Awarding XP Game Plan 奖励XP游戏计划

image

奖励XP 1.XP奖励敌人 (和一种获得它的方法) 2.传入XP元属性 3.被动游戏技能,GA监听事件 (并授予它) a.响应事件应用的游戏效果 4.当伤害致命时从属性集奖励经验值 5.在属性集中处理传入的XP,并在玩家状态下增加XP

6. XP Reward for Enemies 敌人经验值奖励

敌人拥有攻击者在杀死敌人时获得的 XP 值。

1.创建XP奖励曲线表 2.每个职业的可缩放浮动值【XP奖励】 3.技能系统组件中获取角色的职业和等级 4.在战斗接口中添加GetCharacterClass函数 BlueprintNativeEvent和BlueprintCallable)

创建XP奖励曲线表格 CT_XP_Reward

右键-其他-曲线表格 -Cubic CT_XP_Reward Content/Blueprints/AbilitySystem/Data/CT_XP_Reward.uasset

3个职业曲线: image

魔法师 Elementalist

战士 Warrior

游侠 Ranger

Warrior曲线: 1,20 40,1000

自动平滑

Ranger曲线: 1,25 40,1500 自动平滑

Elementalist曲线: 1,35 40,2500 自动平滑

职业信息资产数据结构添加 XP奖励曲线 属性

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();
};

函数库中 通过职业和等级获取 XP奖励值 方法

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;

角色基类中实现 获取 XP奖励值

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;
}

DA_CharacterClassInfo 职业信息资产中为各职业设置 XPReward :XP奖励曲线表格 CT_XP_Reward

魔法师Elementalist XPReward-1,CT_XP_Reward,Elementalist

战士Warrior XPReward-1,CT_XP_Reward,Warrior

游侠Ranger XPReward-1,CT_XP_Reward,Ranger

image

7. Incoming XP Meta Attribute 传入XP元属性

属性集中设置XP元属性 元属性不会被复制。

属性集中设置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);
    }
}

8. Passively Listening for Events XP被动技能监听事件

基于 GameplayAbility 创建被动技能蓝图 GA_ListenForEvent XP被动技能监听

只在服务器运行,不需要复制到客户端。 激活后将一直运行。 Content/Blueprints/AbilitySystem/Aura/Abilities/Passive_Startup/GA_ListenForEvent.uasset

高级-Instancing Policy-Instanced Per Actor 每个actor实例一个技能

Net Execution Policy-Server Only 仅服务器运行,不复制到客户端 image

激活技能时,开始监听游戏事件

事件图表: 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 可以获取其中的标签和值,来创建游戏效果。 image

添加 XP元属性标签

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);
    }
}

玩家一开始就激活 GA_ListenForEvent 技能

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);
}

基于 gameplayEffect 创建基于事件的效果 GE_EventBasedEffect 用于监听游戏事件技能

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 监听该事件。 image

GA_ListenForEvent 应用游戏效果 GE_EventBasedEffect

打开 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

BPGraphScreenshot_2024Y-01M-26D-14h-34m-24s-985_00

为BP_AuraCharacter 玩家配置被动技能 GA_ListenForEvent

打开 BP_AuraCharacter Abilities-Startup Passive Abilities-GA_ListenForEvent image

玩家将具由监听事件技能。该被动技能只在服务端激活,只在服务端监听。不会复制。 现在可以监听游戏事件,只其他地方等待发送游戏事件。

9. Sending XP Events 发送XP事件

属性集中 添加 发送XP事件 方法

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())中实际设置玩家的经验值。 然后广播到控件。

10. Showing XP in the HUD 在HUD中显示XP

玩家状态接收经验值。 属性集发送经验值事件。 属性集不应依赖玩家状态。 所以需要接口。

基于Unreal接口 创建 C++ 玩家接口 PlayerInterface

image

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"

玩家实现 PlayerInterface

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 进度百分比设置为0

WBP_Overlay经验条控件监听控件控制器的经验值变更广播事件

监听到后设置 WBP_XPBar 进度百分比

WBP_Overlay 中为 WBP_XPBar 设置控件控件控制器

打开 WBP_Overlay 事件:

WBP_XPBar set widget controller get widget controller

BPGraphScreenshot_2024Y-01M-25D-20h-32m-11s-519_00

WBP_XPBar 监听 设置控件控件控制器 事件

设计器: 进度条名称: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

BPGraphScreenshot_2024Y-01M-25D-20h-42m-11s-292_00

BP_AuraPlayerState 玩家状态设置升级信息

打开BP_AuraPlayerState Level Up Info-DA_LevelUpInfo image

现在杀死一个敌人将提升经验百分比。

11. Level Up Interface Function 升级接口功能

玩家接口添加升级功能

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()
{

}

改用静态调用, BlueprintNativeEvent 蓝图本机函数

可以判断是否实现接口。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);
        }
    }
}

12. Leveling Up 升级

玩家接口获取经验值, 等级,

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);
        }
    }
}

13. Showing Level in the HUD 在HUD中显示等级

控件控制器中绑定等级变更事件

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_ValueGlobe

复制 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)

image ![BPGraphScreenshot_2024Y-01M-25D-23h-12m-42s-317_00] image image

BPGraphScreenshot_2024Y-01M-25D-23h-13m-08s-609_00

(https://github.com/WangShuXian6/blog/assets/30850497/22ace08c-23fc-408e-b82f-0d1a55cf8127)

WBP_Overlay 中添加 WBP_ValueGlobe

打开 WBP_Overlay 设计器: WBP_ValueGlobe :ValueGlobe_Level

图表: ValueGlobe_Level get widget controller set widget controller image BPGraphScreenshot_2024Y-01M-25D-23h-17m-28s-226_00

CT_XP_Reward

打开 CT_XP_Reward 增大一级的经验奖励用来测试等级UI

现在,UI将实时显示等级,并可以升级。

头像UI WBP_PictureFrame

复制 WBP_GlobeProgressBar 为 WBP_PictureFrame

Content/Blueprints/UI/Overlay/Subwidget/WBP_PictureFrame.uasset

14. Level Up Niagara System 升级Niagara系统

角色升级特效

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);
    }
}

BP_AuraCharacter 设置 Niagara 组件

Niagara - Niagara 系统资产-NS_LevelUp image

将盒体碰撞移动到 CameraBoom 弹簧臂子级

删除蓝图添加的弹簧臂 SpringArm 和相机组件 Camera

image

SpringArm-目标臂长度-800 绝对旋转-0,-45,0 位置-0,0,0 否则弹簧臂将抖动 image image

15. Level Up HUD Message 升级HUD信息

创建 升级提示控件蓝图 WBP_LevelUpMessage

右键-用户界面-控件蓝图-AuraUserWidget WBP_LevelUpMessage image

Content/Blueprints/UI/Overlay/Subwidget/WBP_LevelUpMessage.uasset 设计器:

覆层:

覆层-包裹框:

内容自动换行

覆层-包裹框-间隔区:

外观-尺寸-1900,150

覆层-包裹框-垂直框:VerticalBox_Message

插槽-填充-填充空白空间-启用

覆层-包裹框-垂直框-文本:

插槽-填充-填充空白空间-启用

水平居中对齐 垂直对齐

将文本中对齐

覆层-包裹框-垂直框-水平框:

水平居中对齐 垂直对齐 插槽-填充-填充空白空间-启用 强制换新行-启用

覆层-包裹框-垂直框-水平框-文本:

插槽-填充-填充空白空间-不启用 强制换新行-启用

水平向右对齐 垂直对齐

将文本中对齐

覆层-包裹框-垂直框-水平框-文本:Text_Level

插槽-填充-填充空白空间-不启用 强制换新行-不启用

水平向左对齐 垂直对齐

将文本中对齐

image image

WBP_Overlay 升级时构造 WBP_LevelUpMessage

打开 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

BPGraphScreenshot_2024Y-01M-26D-00h-52m-14s-054_00

image

WBP_LevelUpMessage 自行删除自己

打开 WBP_LevelUpMessage event construct delay remove from parent image

音效

Assets/Sounds/LevelUp/sfx_Template_single.sfx_Template_single' sfx_Template_single 重命名为 sfx_LevelUpSound

WBP_LevelUpMessage 播放音效

play sound 2D play sound 2D-sound-sfx_LevelUpSound image

WBP_LevelUpMessage 升级动画

添加动画 MessageAnimation 添加轨道 VerticalBox_Message

VerticalBox_Message 添加变换

缩放: 时间轴0:x 0, y 0 时间轴0。5:x 1 , y 1

image

WBP_LevelUpMessage 播放动画

play animation get MessageAnimation BPGraphScreenshot_2024Y-01M-26D-01h-04m-11s-292_00

WangShuXian6 commented 10 months ago

23. Attribute Points 属性点

1. Attribute Points Member Variable 属性点成员变量

为玩家状态添加属性点变量,广播出去

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);
        }
    );
}

2. Showing Attribute Points in the HUD 在HUD中显示属性点

每次打开属性点UI都会执行UI构建事件并广播属性点初始值。

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_AttributePointsRow

复制 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)

BPGraphScreenshot_2024Y-01M-26D-11h-21m-10s-875_00

WBP_AttributeMenu

设计器: 属性下的控件 WBP Text Value Button Row 替换为 WBP_AttributePointsRow image

图表: 在广播初始值之前需要先为 WBP_AttributePointsRow 设置控件控制器, 才能使 WBP_AttributePointsRow 监听到初始值

WBP_AttributePointsRow set widget controller image BPGraphScreenshot_2024Y-01M-26D-11h-27m-53s-196_00 image

升级时获取属性点

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);
}

现在每次升级都会获取属性点。

3. Attribute Upgrade Buttons 属性升级按钮

WBP_TextValueButtonRow 按钮状态

图表: 添加函数 SetButtonEnabled 输入参数 Enabled 布尔

WBP_Button get button set is enabled

BPGraphScreenshot_2024Y-01M-26D-12h-06m-56s-667_00

WBP_AttributeMenu 根据可用属性点启用或禁用 WBP_TextValueButtonRow 属性增加按钮

只需要在一个地方监听属性点变更,控制所有 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

BPGraphScreenshot_2024Y-01M-26D-12h-09m-04s-377_00

主事件: BPGraphScreenshot_2024Y-01M-26D-12h-09m-36s-663_00

现在升级后,属性点不为0,主属性按钮将可用

4. Upgrading Attributes 更新属性

为属性增加按钮创建蓝图可调用函数

点击按钮后,发送按钮的游戏标签信息到控件控制器,附带按钮信息。 控件控制器使用技能系统组件通过标签更新对应的属性值。 因为技能系统组件能够执行与属性相关的操作,例如应用效果或发送游戏事件。 所以我要在技能系统组件上创建一个函数,我们可以调用它来升级属性。

当我们在小部件控制器中单击此按钮时,会调用系统组件的函数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);
}

GA_ListenForEvent 被动事件中,可以监听指定标签的事件,设置当前技能的技能效果得到set by caller修改器的值,然后应用效果到技能自身。

此技能的技能效果中需使用set by caller 指定修改器类型。

GE_EventBasedEffect 被动技能效果再添加4个 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 监听该事件。

image image

WBP_TextValueButtonRow 调用UpgradeAttribute触发件控制器事件

打开 WBP_TextValueButtonRow

图表: sequence get Attribute Menu Widget Controller 提升为变量 AttributeMenuWidgetController AttributeMenuWidgetController

WBP_Button get Button assign on clicked

AttributeMenuWidgetController UpgradeAttribute get Attribute Tag

image BPGraphScreenshot_2024Y-01M-26D-14h-22m-42s-107_00

现在 点击增加按钮可以消耗总属性点,增加指定主属性的点。 对应的辅助属性也会增加。

5. Top Off Our Fluids 升级后加满健康值,魔力值

游戏后处理效果完成后,等级才会实际增加,此时获取的最大健康值依然不是最新的值

PostAttributeChange 中获取最大健康值,魔力值

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;
    }
}

GA_ListenForEvent被动技能接收到标签事件时,检查,未接收的的事件设置 Set By Caller 修改器值为0

否则或提示异常 未设置 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;

image

监听到标签事件后循环该数组,未接受的标签设置 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 BPGraphScreenshot_2024Y-01M-26D-15h-19m-18s-071_00

6. Attribute Menu Polish 完善属性菜单

WBP_Button 点击,悬浮音效

设计器: 选中-Button 添加 点击时,悬浮时 事件 image

图表: 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 公开

BPGraphScreenshot_2024Y-01M-26D-15h-35m-40s-280_00

WBP_Overlay -AttributeMenuButton

设计器: AttributeMenuButton-细节-On Clicked Sound-SFX_UI_ButtonClick_01 image image

On Hovered Sound-SFX_UI_Hover_01 image

WangShuXian6 commented 10 months ago

24. Spell Menu 技能菜单

1. Spell Menu Design 技能菜单设计

image

2. Spell Globe Button 技能球按钮 WBP_SpellGlobe_Button

复制 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 因为默认的盒体有边框 image image

外观-样式-已悬停-绘制为-图像 图像-SkillRing_1_Glow image

外观-样式-已按压-绘制为-图像 图像-SkillRing_1_Glow 着色-变暗

外观-样式-已禁用-绘制为-图像 图像-SkillRing_1

添加图像控件:Image_Selection 水平填充,垂直填充 图像-SelectionCircle 填充-4,2,4,2 渲染-渲染不透明度-0

image

添加动画 SelectAnimation 添加轨道 Image_Selection 轨道添加变换

缩放: 0 - x 1.55 ,y 1.5

0.05 - x 1.3 ,y 1.25

image image BPGraphScreenshot_2024Y-01M-26D-16h-29m-00s-414_00

3. Offensive Spell Tree 攻击性技能树

创建控件蓝图WBP_OffensiveSpellTree,使用 AuraUserWidget

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

BPGraphScreenshot_2024Y-01M-26D-16h-38m-34s-788_00

设计器: 添加控件 包裹框: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_Overlay

添加控件 WBP_OffensiveSpellTree image

image image

4. Passive Spell Tree 被动技能树

被动技能树 WBP_PassiveSpellTree

复制 WBP_OffensiveSpellTree 为

Content/Blueprints/UI/SpellMenu/WBP_PassiveSpellTree.uasset

image SizeBox_Root 525 * 90

BoxHeight ,默认值 90

添加控件 WBP_SpellGlobe_Button *3 BoxWidth-60 BoxHeight-60 image

glass padding -4.5 image

添加控件间隔区 *2 尺寸-100,50

image image

WBP_Overlay 添加 WBP_PassiveSpellTree

打开 WBP_Overlay

添加控件 WBP_PassiveSpellTree image

修复 WBP_SpellGlobe_Button 悬浮状态抖动

打开 WBP_SpellGlobe_Button 将 Button_Ring 置于末端,最顶层 image

5. Equipped Spell Row 已装备的技能行

已装备的技能行 WBP_EquippedSpellRow

复制 WBP_PassiveSpellTree 为 WBP_EquippedSpellRow

Content/Blueprints/UI/SpellMenu/WBP_EquippedSpellRow.uasset

image

设计器: 添加控件 包裹框: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

image

复制 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

image image

WBP_Overlay 添加 WBP_EquippedSpellRow

打开 WBP_Overlay

添加控件 WBP_EquippedSpellRow image image

6. Spell Menu Widget 技能按钮控件

技能按钮控件 WBP_SpellMenu

复制 WBP_AttributeMenu 为 WBP_SpellMenu Content/Blueprints/UI/SpellMenu/WBP_SpellMenu.uasset

image image image image

WBP_Overlay 添加 WBP_SpellMenu

打开 WBP_Overlay

添加控件 WBP_SpellMenu 删除测试控件

7. Spell Description Box 技能描述框

WBP_SpellMenu 添加 技能描述框

image image

8. Spell Menu Button 技能按钮

WBP_Overlay

WBP_Overlay 删除测试控件

image

点击主界面的技能按钮时,将技能按钮禁用,创建技能控件,显示在视口。

添加 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 BPGraphScreenshot_2024Y-01M-26D-23h-20m-38s-869_00

WBP_SpellMenu

关闭按钮重命名 CloseButton image

点击技能控件关闭按钮时,销毁技能控件 图表: event construct get CloseButton get button assign on clicked:OnSpellMenuButtonClicked remove from parent

添加事件分发器 SpellMenuClosed

触发一个技能控件关闭事件 event destruct call SpellMenuClosed

BPGraphScreenshot_2024Y-01M-26D-23h-01m-17s-293_00

WBP_Overlay

监听技能控件关闭事件 SpellMenuClosed 图表: assign SpellMenuClosed SpellMenuButton get button set is enabled

BPGraphScreenshot_2024Y-01M-26D-23h-10m-28s-014_00 BPGraphScreenshot_2024Y-01M-26D-23h-21m-01s-170_00

WBP_Overlay 显示技能菜单时,禁用玩家控制

图表: 添加表示属性菜单是否打开的变量 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-取消

BPGraphScreenshot_2024Y-01M-26D-23h-40m-24s-432_00

branch AttributeMenuOpen get player controller set input mode game and UI set input mode game and UI-hide cursor during capture-取消

BPGraphScreenshot_2024Y-01M-26D-23h-39m-41s-860_00

BPGraphScreenshot_2024Y-01M-26D-23h-41m-09s-823_00

9. Spell Menu Widget Controller 技能菜单控件控制器

WBP_SpellMenu 添加装备按钮

用以将选择的技能装备到技能栏

设计器: 添加控件 WBP Wide Button image

右键 AuraWidgetController C++ 派生 WBP_SpellMenu 控件控制器 SpellMenuWidgetController C++

image

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()
{

}

将技能信息委托C++功能从覆层控制器移动到基类控制器

AuraAbilitySystemComponent

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();
    }
}

AuraWidgetController

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;
}

OverlayWidgetController

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);
    }
}

AttributeMenuWidgetController

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());
}

10. Constructing the Spell Menu Widget Controller 构建技能菜单控件控制器

全局HUD上添加技能HUD

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;
}

SpellMenuWidgetController

Source/Aura/Public/UI/WidgetController/SpellMenuWidgetController.h

UCLASS(BlueprintType, Blueprintable)

BP_AttributeMenuWidgetController 配置技能信息资产

配置技能信息资产 ability info-DA_AbilityInfo

image

基于 SpellMenuWidgetController 创建 技能控件控制器 蓝图 BP_SpellMenuWidgetController

Content/Blueprints/UI/WidgetController/BP_SpellMenuWidgetController.uasset

配置技能信息资产 ability info-DA_AbilityInfo image

BP_AuraHUD 配置技能控件控制器类

SpellMenuWidgetControllerClass-BP_SpellMenuWidgetController image

现在可以构建 技能控件,设置他的控件控制器。

WBP_SpellMenu 设置控件控制器

图表: sequence get SpellMenuWidgetController set widget controller image BPGraphScreenshot_2024Y-01M-27D-14h-56m-56s-171_00

11. Equipped Row Button 装备按钮

被动技能标签

虽然被动技能不通过输入操作来激活。

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_EquippedRow_Button

复制 WBP_SpellGlobe_Button 为 WBP_EquippedRow_Button Content/Blueprints/UI/SpellGlobes/WBP_EquippedRow_Button.uasset

WBP_EquippedSpellRow 装备栏使用 WBP_EquippedRow_Button

打开WBP_EquippedSpellRow WBP Spell Globe Button 替换为 WBP_EquippedRow_Button

image image

为每个按钮重命名 Globe_LMB Globe_RMB Globe_1 Globe_2 Globe_3 Globe_4

Globe_Passive_1 Globe_Passive_2 image

技能控件控制器广播技能信息

Source/Aura/Private/UI/WidgetController/SpellMenuWidgetController.cpp

void USpellMenuWidgetController::BroadcastInitialValues()
{
    BroadcastAbilityInfo();
}

WBP_SpellMenu 在广播初始值之前设置好 WBP_EquippedSpellRow 的控件控制器

图表:

设置技能菜单控件控制器,设置装备栏控件控制器

get WBP_EquippedSpellRow set widget controller get widget controller

cast to BP_SpellMenuWidgetController 提升为变量 BPSpellMenuWidgetController

BPSpellMenuWidgetController

广播初始值 技能信息

BPSpellMenuWidgetController Broadcast Initial values BPGraphScreenshot_2024Y-01M-27D-16h-18m-13s-523_00

WBP_EquippedSpellRow 装备栏在设置 控件控制器 事件时,为 每个 按钮设置控件控制器

图表: 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

BPGraphScreenshot_2024Y-01M-27D-15h-40m-50s-237_00

折叠到函数 SetWidgetControllers BPGraphScreenshot_2024Y-01M-27D-15h-41m-46s-025_00

BPGraphScreenshot_2024Y-01M-27D-15h-42m-03s-572_00

一旦设置了控件控制器,就可以将事件分配给技能信息委托。

WBP_EquippedRow_Button 为控件控制器设置监听事件,在控件控制器设置事件中。

图表: 控件控制器设置 get widget controller cast to BP_SpellMenuWidgetController 提升为变量 BPSpellMenuWidgetController

sequence

添加 监听事件 BPSpellMenuWidgetController assign ability info delegate

根据回调的技能信息设置技能图标 break AuraAbilituInfo

装备栏关注技能按钮的输入操作信息。不关注技能。 装备栏按钮每个技能的输入操作固定,技能不固定。 用以为技能分配输入操作。

技能栏关注技能按钮的技能信息。不关注输入操作。 因为技能栏按钮每个技能的位置固定。输入操作不固定。

添加 gameplay标签 变量 InputTag 公开。 用以在父级装备栏为每个技能按钮设置输入操作标签名

BPGraphScreenshot_2024Y-01M-27D-15h-55m-53s-918_00

WBP_EquippedSpellRow 装备栏 为每个技能按钮设置输入操作标签名

设计器: Globe_LMB-细节-InputTag-InputTag.LMB image

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

WBP_EquippedRow_Button 根据回调标签名获取技能信息

通过自身的技能输入标签,在回调技能信息中通过 InputTag 找到技能信息

图表: matches tag matches tag-exact match -启用 InputTag

branch Image_Icon set brush from texture

Image_Background set brush from material

BPGraphScreenshot_2024Y-01M-27D-16h-19m-09s-831_00

此时装备栏显示已装备的技能信息: image

改变火球术技能的输入标签,此处也会改变位置。

WBP_EquippedRow_Button 添加函数设置按钮外观

图表:

ClearGlobe函数

Image_Icon BPGraphScreenshot_2024Y-01M-27D-16h-31m-39s-365_00

未装备技能的按钮显示为空。 ClearGlobe BPGraphScreenshot_2024Y-01M-27D-16h-32m-19s-605_00

主图表: set brush set brush make slateBrush BPGraphScreenshot_2024Y-01M-27D-16h-36m-47s-496_00 image

折叠到函数 ReceiveAbilityInfo BPGraphScreenshot_2024Y-01M-27D-16h-39m-30s-239_00

BPGraphScreenshot_2024Y-01M-27D-16h-40m-38s-942_00

12. Ability Status and Type 技能状态和类型

image

技能状态和类型标签

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();
}

13. Showing Abilities in the Spell Tree 在技能树种显示所有技能

技能树的每个技能需要知道自己分配的技能信息。状态,非输入标签。

技能信息添加技能状态标签变量

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);
}

WBP_SpellGlobe_Button

图表: 添加技能标签变量识别相关技能信息 AbilityTag [gameplay标签] 公开

WBP_OffensiveSpellTree 技能书控件中为每个技能按钮设置 AbilityTag

打开 WBP_OffensiveSpellTree

设计器: 第一列:火属性技能 第二列:光属性技能 第三列:暗属性技能

从下到上开始

第一列第一个技能 WBP Spell Globe Button 细节-AbilityTag-Abilities.Fire.FireBolt image image

WBP_SpellMenu 为技能树控件 WBP_OffensiveSpellTree ,WBP_PassiveSpellTree 设置控件控制器

图表: get WBP_OffensiveSpellTree set widget controller get widget controller

get WBP_PassiveSpellTree set widget controller get widget controller

BPGraphScreenshot_2024Y-01M-27D-17h-32m-55s-175_00

WBP_OffensiveSpellTree 技能树使用控件控制器设置事件,为每个技能按钮设置控件控制器

图表:

Event Widget Controller Set WBP_SpellGlobe_Button 所有的技能按钮 set widget controller get widget controller

折叠到函数 SetWidgetControllers BPGraphScreenshot_2024Y-01M-27D-17h-39m-14s-206_00 BPGraphScreenshot_2024Y-01M-27D-17h-39m-31s-605_00

WBP_SpellGlobe_Button 技能按钮在控件控制器设置事件时,监听技能菜单带自身标签名称的广播初始信息事件

图表: 删除覆层控件控制器 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 BPGraphScreenshot_2024Y-01M-27D-18h-02m-03s-784_00

检查是否装备技能

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

BPGraphScreenshot_2024Y-01M-27D-18h-22m-36s-099_00

检查是否可用技能

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 BPGraphScreenshot_2024Y-01M-27D-18h-17m-07s-994_00

检查是否解锁技能

branch Status matches tag matches tag-tag two-Abilities.Status.Unlocked 检查是否解锁技能

or boolean

后续使用装备技能节点

折叠到函数 ReceiveAbilityInfo

BPGraphScreenshot_2024Y-01M-27D-18h-25m-56s-274_00

BPGraphScreenshot_2024Y-01M-27D-18h-23m-40s-798_00

BPGraphScreenshot_2024Y-01M-27D-18h-26m-17s-353_00

技能树可以显示技能

image

WBP_Overlay 修复 SetInputMode_GameAndUI需要一个有效的玩家控制器作为'PlayerController’目标

当打开一个属性菜单控件或技能菜单控件后不关闭控件,直接结束游戏,将导致此错误。

这是由于玩家控制器的销毁早于控件销毁导致。

打开 WBP_Overlay 图表: 控制器之后添加 工具-Is Valid 验证 这仅在菜单已关闭的情况下执行。否则导致错误。

image image BPGraphScreenshot_2024Y-01M-27D-18h-38m-24s-902_00

14. Ability Level Requirement 技能升级条件

技能升级时,需要等级符合条件。

初始技能不包括全部的符合升级条件的技能。

技能信息中添加升级的等级条件

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;
}

DA_AbilityInfo 技能信息资产中设置技能升级的等级要求 LevelRequirement和 技能

火球术 LevelRequirement-1

Ability-GA_FireBolt

image

BP_AuraGameMode 游戏模式中存储技能信息资产 AbilityInfo

Ability Info-DA_AbilityInfo image

创建雷电技能标签

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")
            );

}

基于AuraGameplayAbility 创建雷电技能蓝图 GA_Electrocute

Content/Blueprints/AbilitySystem/Aura/Abilities/Lightning/GA_Electrocute.uasset

图表: event activateAbility print string delay end ability

BPGraphScreenshot_2024Y-01M-27D-20h-05m-42s-851_00

类默认设置 Ability tag-Abilities.Lightning.Electrocute image

DA_AbilityInfo 技能信息资产中添加雷电技能

AbilityTag-Abilities.Lightning.Electrocute CooldownTag- InputTag- StatusTag- Icon-Shock BackgroundMaterial-MI_ShockSkillBG LevelRequirement-2 Ability-GA_Electrocute

image

WBP_OffensiveSpellTree 攻击技能树 为一个按钮设置雷电技能标签

设计器: 第二列下1: AbilityTag-Abilities.Lightning.Electrocute

image image

15. Update Ability Statuses 更新技能状态

一旦玩家等级到达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);
        }
    }
}

16. Updating Status in the Spell Menu 技能菜单中更新技能状态

升级时,更新技能状态

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级,雷电技能显示为灰色可用状态 image

17. Show Spell Points 显示技能点

技能菜单控制器添加技能点变更委托

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 技能菜单控件监听技能状态变更委托 SpellPointsChanged

打开 WBP_SpellMenu 设计器: 技能点 WBP Framed Value 重命名为 FramedValue_SpellPoints

image

图表: 在广播初始值之前监听。

BPSpellMenuWidgetController assign SpellPointsChanged

FramedValue_SpellPoints get TextBlock_Value SetText(Text)

折叠到函数 SpellPointsChanged

BPGraphScreenshot_2024Y-01M-27D-22h-02m-10s-931_00 image BPGraphScreenshot_2024Y-01M-27D-22h-03m-21s-567_00

18. Selecting Icons 选择技能按钮

WBP_SpellGlobe_Button 技能按钮 选择特效

图表: 添加函数 Select

Image_Selection set render opacity select animation play animation play sound 2D play sound 2D-sound-SFX_UI_ButtonClick_02

BPGraphScreenshot_2024Y-01M-27D-22h-20m-39s-526_00

WBP_SpellGlobe_Button 的 Button_Ring 控件 添加 点击时事件

image image

主图表: on clicked (Button_Ring) Select

image

选择一个技能时,其他技能按钮取消选择状态

添加函数 Deselect Image_Selection set render opacity BPGraphScreenshot_2024Y-01M-27D-22h-26m-11s-869_00

添加事件分发器 OnSpellGlobeSelected

OnSpellGlobeSelected 添加参数 SelectedGlobe 类型 WBP_SpellGlobe_Button

点击技能按钮时调用 OnSpellGlobeSelected

OnSpellGlobeSelected-SelectedGlobe -self image

BPGraphScreenshot_2024Y-01M-27D-22h-42m-29s-149_00

WBP_OffensiveSpellTree 技能树控件的每个技能按钮都监听 OnSpellGlobeSelected

打开 WBP_OffensiveSpellTree

图表: event contruct WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 至 WBP_SpellGlobe_Button_8

assign OnSpellGlobeSelected

每个按钮都执行 Deselect 事件返回的按钮执行 Select

BPGraphScreenshot_2024Y-01M-27D-22h-42m-56s-934_00

WBP_PassiveSpellTree 被动技能树的按钮选择状态

打开 WBP_PassiveSpellTree 图表:

WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 WBP_SpellGlobe_Button_2

assign OnSpellGlobeSelected

每个按钮都执行 Deselect 事件返回的按钮执行 Select

BPGraphScreenshot_2024Y-01M-27D-22h-47m-53s-405_00

19. Deselecting Icons 取消选择按钮

WBP_OffensiveSpellTree 攻击技能树控件

图表:

添加事件分发器 OnOffensiveSpellGlobeSelected

OnOffensiveSpellGlobeSelected 每次点击攻击技能按钮最后都调用 BPGraphScreenshot_2024Y-01M-27D-22h-54m-33s-289_00

添加函数 DeselectAll

WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 至 WBP_SpellGlobe_Button_8 Deselect BPGraphScreenshot_2024Y-01M-27D-23h-00m-28s-953_00

WBP_PassiveSpellTree 被动技能树控件

图表:

添加事件分发器 OnPassiveSpellGlobeSelected

OnPassiveSpellGlobeSelected 每次点击攻击技能按钮最后都调用 BPGraphScreenshot_2024Y-01M-27D-22h-54m-47s-939_00

添加函数 DeselectAll

WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 WBP_SpellGlobe_Button_2 Deselect BPGraphScreenshot_2024Y-01M-27D-23h-01m-48s-280_00

WBP_SpellMenu 技能菜单控件监听 OnOffensiveSpellGlobeSelected ,OnPassiveSpellGlobeSelected

图表:

WBP_OffensiveSpellTree assign OnOffensiveSpellGlobeSelected

WBP_PassiveSpellTree DeselectAll

WBP_PassiveSpellTree assign OnPassiveSpellGlobeSelected

WBP_OffensiveSpellTree DeselectAll

image BPGraphScreenshot_2024Y-01M-27D-23h-05m-11s-060_00

WBP_SpellGlobe_Button

设计器: Image_Selection-渲染-渲染不透明度-1

20. Spell Menu Buttons 技能菜单按钮

技能点为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_SpellMenu 打开技能菜单时,禁用使用技能点按钮和装备技能按钮

选择技能后才可以使用技能点按钮和装备技能按钮

设计器: WBP Wide Button 技能点按钮控件重命名为 Button_SpellPoints image

WBP Wide Button 装备技能按钮控件重命名为 Button_Equip image

图表: 预购键时,禁用2个按钮 event pre construct

Button_SpellPoints set is enabled

Button_Equip set is enabled

image image

选择技能树中的一个攻击技能时,控件通知控件控制器,发送选择的技能的技能标签。

为此需要一个蓝图可调用函数供选择技能使用。

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;
        }
    }
}

初始技能点为0

Source/Aura/Public/Player/AuraPlayerState.h

private:
    UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_SpellPoints)
    int32 SpellPoints = 0;

WBP_SpellGlobe_Button 技能按钮点击事件中,调用 技能菜单的 SpellGlobeSelected

图表: BPSpellMenuWidgetController SpellGlobeSelected 传入自身的技能标签 AbilityTag image

设置好控件控制器后从控件控制器监听 SpellGlobeSelected 中的 SpellGlobeSelectedDelegate 广播

从广播中接收两个按钮的状态 图表: BPSpellMenuWidgetController assign SpellGlobeSelectedDelegate

Button_SpellPoints set is enabled

Button_Equip set is enabled 折叠到函数 SetButtonsEnabled

重命名输入参数 SpendPointsEnabled EquipEnabled BPGraphScreenshot_2024Y-01M-28D-14h-44m-00s-382_00

image

预购键中使用 SetButtonsEnabled 简化节点

SetButtonsEnabled image

完整

BPGraphScreenshot_2024Y-01M-28D-14h-44m-42s-703_00

为被动技能树 WBP_PassiveSpellTree 的每个技能按钮设置控件控制器

打开 WBP_PassiveSpellTree 图表: Event Widget Controller Set WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 WBP_SpellGlobe_Button_2 get widget controller

折叠到函数 SetWidgetControllers BPGraphScreenshot_2024Y-01M-28D-14h-49m-27s-861_00 image BPGraphScreenshot_2024Y-01M-28D-14h-49m-48s-874_00

image

21. Selected Ability 选择技能

处理边缘情况,开启技能菜单时,选中一个技能,然后攻击敌人升级,获得技能点数 此时使用技能点按钮和装备技能按钮也需要实时更新状态

当技能点和技能状态改变时,手动广播技能按钮的选择事件内的行为

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);
}

22. Spending Spell Points 消耗技能点数

解锁,升级技能

服务端RPC 在 技能系统组件执行消耗技能点,仅在服务端执行

更新技能状态变更委托的参数 最终会广播技能信息到控件

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 技能菜单消耗技能点按钮的点击事件调用 控件控制器 的SpendPointButtonPressed

要在控件控制器设置之后执行该事件绑定

打开 WBP_SpellMenu 图表: Button_SpellPoints get button assign on clicked

BPSpellMenuWidgetController SpendPointButtonPressed

image BPGraphScreenshot_2024Y-01M-28D-15h-49m-23s-787_00

CT_Damage 修改 Abilities.FireBolt 火球术的伤害曲线,使其2级伤害更明显更大

整个节点上移 image

现在可以解锁和升级技能

23. Rich Text Blocks 多格式文本块

显示选定技能的当前等级和下一等级描述

WBP_SpellMenu

设计器:

添加 Rich text block 多格式文本块 控件: RichText_Description

添加 Rich text block 多格式文本块 控件: RichText_NextLevelDescription

image

为 多格式文本块 控件创建文本样式集 数据表格 DT_RichTextStyle

右键-其他-数据表格-RichTextStyleRow DT_RichTextStyle image

Content/Blueprints/UI/Data/DT_RichTextStyle.uasset

添加行 Default 可以设置样式属性 image

添加行 Damage image

WBP_SpellMenu 多格式文本块 使用 DT_RichTextStyle

设计器: RichText_Description-外观-文本样式集-DT_RichTextStyle image

内容-文本-默认技能描述</>13</> Default 标签为DT_RichTextStyle的行名称 image

24. Spell Descriptions 技能描述

技能增加获取描述属性方法

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;
}

技能菜单控件控制器调用函数 GetDescriptionsByAbilityTag

更新 能按钮选择委托,广播技能描述

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);
}

WBP_SpellMenu 技能菜单在技能按钮选择事件中获取技能描述

设计器: RichText_Description-细节-换行-自动包裹文本-启用 使文本块自动换行 image

RichText_NextLevelDescription-细节-换行-自动包裹文本-启用

图表: RichText_Description set text

RichText_NextLevelDescription set text

image BPGraphScreenshot_2024Y-01M-28D-16h-58m-03s-890_00

image

25. FireBolt Description 火球术技能描述

抛射技能添加描述

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);
}

26. Cost and Cooldown in Spell Description 技能描述中的消耗和冷却

将描述移动到抛射物技能子类

创建派生自 抛射技能 AuraProjectileSpell C++ 的子类 火球术技能 AuraFireBolt C++

image

移动 AuraProjectileSpell 的技能描述到 AuraFireBolt

Source/Aura/Public/AbilitySystem/Abilities/AuraProjectileSpell.h 删除 GetDescription,GetNextLevelDescription

Source/Aura/Private/AbilitySystem/Abilities/AuraProjectileSpell.cpp 删除 GetDescription,GetNextLevelDescription

AuraFireBolt

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

GA_FireBolt-细节-类设置-类选项-父类-AuraFireBolt image

技能上添加获取技能魔力消耗和冷却时间的方法

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);
}

WBP_SpellMenu 技能树控件描述布局

设计器: 添加图像到描述滚动下方 image 水平填充,垂直填充 全透明

图像-行为-可视性-非可命中测试(仅自身) 可见但无法进行命中测试(无法与指针交互),且不影响子项上(如有)的命中测试。 image 使被图像遮挡的滚动框可被滚动。

27. Self Deselect 自行取消选择

技能控件控制器创建 自行取消选择技能按钮函数

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());
}

WBP_SpellGlobe_Button 技能菜单技能按钮添加变量Selected表示自身是否选中

图表: 添加变量 Selected 布尔

Select 函数中: Selected BPGraphScreenshot_2024Y-01M-28D-19h-12m-04s-394_00

Deselect 函数中: Selected BPGraphScreenshot_2024Y-01M-28D-19h-12m-43s-895_00

主图表: 按钮点击事件中检查 Selected branch 未选择时执行后续事件

已选择时,执行控件控制器的 GlobeDeselect Deselect BPSpellMenuWidgetController GlobeDeselect

播放音效 play sound 2D-SFX_UI_Cancel_01 image

BPGraphScreenshot_2024Y-01M-28D-19h-18m-52s-688_00

不可用的技能不应显示技能描述,未解锁的技能应显示技能解锁条件

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;
}

28. Equipped Spell Row Animations 装备技能动画

WBP_EquippedSpellRow

设计器: 添加 Overlay 用于在装备栏覆盖控件

添加 图像 :Image_OffensiveSelecttionBox 默认-OffensiveSelectionBox

添加 图像 :Image_PassiveSelecttionBox 默认-PassiveSelectionBox image

添加动画 OffensiveSelecttionAnimation

添加 Image_OffensiveSelecttionBox 轨道 添加变换, 添加渲染不透明度

时间轴:0 缩放x,y-1.05,1.05 渲染不透明度-1

时间轴:1 缩放x,y-1,1 渲染不透明度-0 image

添加动画 HideOffensiveBox

添加 Image_OffensiveSelecttionBox 轨道 添加渲染不透明度

时间轴:0 渲染不透明度-1

时间轴:0.75 渲染不透明度-0 image

添加动画 PassiveSelecttionAnimation

添加 Image_PassiveSelecttionBox 轨道 添加变换, 添加渲染不透明度

时间轴:0 缩放x,y-1.05,1.05 渲染不透明度-1

时间轴:1 缩放x,y-1,1 渲染不透明度-0 image

添加动画 HidePassiveBox

添加 Image_PassiveSelecttionBox 轨道 添加渲染不透明度

时间轴:0 渲染不透明度-1

时间轴:0.75 渲染不透明度-0 image

图表

测试循环播放动画 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

image

删除节点

29. Ability Types 技能类型

技能信息中增加技能类型标签,用以区分主动,被动技能

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;
}

DA_AbilityInfo 技能信息资产中,为每个技能配置技能类型 AbilityType

AbilityType-Abilities.Type.Offensive image

WBP_SpellMenu 技能菜单,选中技能后,控件监听 WaitForEquipDelegate 等待装备技能事件

图表: 技能描述节点折叠到函数 SetDescriptions BPGraphScreenshot_2024Y-01M-28D-20h-26m-04s-331_00

选中技能后,控件监听 WaitForEquipDelegate 等待装备技能事件 BPSpellMenuWidgetController assign WaitForEquipDelegate

添加函数 OnWaitForEquip

输入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

BPGraphScreenshot_2024Y-01M-28D-20h-59m-30s-486_00

OnWaitForEquip

image

装备技能按钮点击事件中调用 EquipButtonPressed

Button_Equip get button assign on clicked BPSpellMenuWidgetController EquipButtonPressed image

BPGraphScreenshot_2024Y-01M-28D-20h-51m-12s-815_00

现在,选中一个技能后,点击装备,装备会显示动画。 提示应该要选择装备的输入之一。 image

WBP_EquippedSpellRow 动画部分 Image_OffensiveSelecttionBox,Image_PassiveSelecttionBox 默认全透明

Image_OffensiveSelecttionBox,Image_PassiveSelecttionBox 渲染-渲染不透明度-0 image

图像-行为-可视性-非可命中测试(仅自身) 防止遮挡装备栏的点击事件

image

WBP_SpellMenu取消选择技能时,关闭提示动画

图表:

BPSpellMenuWidgetController assign StopWaitingForEquipDelegate

添加函数 StopWaitingForEquip

输入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 隐藏动画只播放一次

BPGraphScreenshot_2024Y-01M-28D-21h-52m-21s-053_00

主图表

StopWaitingForEquip BPGraphScreenshot_2024Y-01M-28D-21h-30m-12s-112_00

30. Equipping Abilities 装备技能

技能树种选择技能,按下装备技能按钮时 检查技能树中选中的技能是否已装备。 如果已装备该技能,存储该技能对应的插槽,即输入标签到 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);
}

WBP_EquippedRow_Button 装备栏按钮添加技能类型变量

图表: 添加变量 AbilityType
gameplay标签类型 公开

WBP_EquippedSpellRow 装备栏

设计器: 为每个按钮指定技能类型标签 可多选批量设置

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

image

点击 装备栏按钮 WBP_EquippedRow_Button 时,调用 SpellRowGlobePressed

打开 WBP_EquippedRow_Button

图表: Button_Ring assign on clicked BPSpellMenuWidgetController SpellRowGlobePressed InputTag AbilityType

image

ReceiveAbilityInfo 函数 如果接收到的技能标签为空,则清理技能的背景

matches tag branch ClearGlobe

否则,设置了标签的按钮,在更换装备栏插槽时,背景会空白。 BPGraphScreenshot_2024Y-01M-28D-23h-44m-51s-773_00

full

BPGraphScreenshot_2024Y-01M-28D-23h-45m-32s-721_00

将WBP_OffensiveSpellTree 技能树的未实现的所有按钮的 AbilityTag 不设置标签,留空

打开 WBP_OffensiveSpellTree WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 至 WBP_SpellGlobe_Button_8 细节- AbilityTag-留空 但火球术,雷电术按钮除外,需要设置对应的技能。 image

否则,设置了标签的按钮,在更换装备栏插槽时,背景会空白。

将 WBP_PassiveSpellTree 技能树的所有按钮的 AbilityTag 不设置标签,留空

打开 WBP_PassiveSpellTree

WBP_SpellGlobe_Button WBP_SpellGlobe_Button_1 至 WBP_SpellGlobe_Button_2 细节- AbilityTag-留空

否则,设置了标签的按钮,在更换装备栏插槽时,背景会空白。

现在在技能树中选择技能,点击装备按钮,最后在装备栏点击要装备到的插槽即可。

31. Updating the Overlay When Equipping Abilities 装备技能时更新覆层控件

WBP_EquippedRow_Button 装备栏按钮 防止清除其他装备的技能

ReceiveAbilityInfo 函数图表: 检查当前标签之后再清理 BPGraphScreenshot_2024Y-01M-29D-00h-12m-28s-589_00

再覆层控件控制器中广播装备技能事件

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);
}

覆层使用的 WBP_SpellGlobe 技能图标 检查接收的技能信息的 AbilityTag 是否为 Ability.None

否则,设置了标签的按钮,在更换装备栏插槽时,背景会空白。

ReceiveAbilityInfo 函数图表: 检查接收的技能信息的 AbilityTag 是否为 Ability.None 如果是,则执行清理 branch matches tag ClearGlobe

清理后将冷却标签置空 set CooldownTag

BPGraphScreenshot_2024Y-01M-29D-00h-28m-30s-574_00

32. Globe Reassigned 技能图表重新分配

一旦装备了技能,则将该技能在技能树中取消选择。

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 按钮监听技能图表重新分配事件

打开 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 BPGraphScreenshot_2024Y-01M-29D-00h-46m-56s-084_00

主图表: OnSpellGlobeReassigned image BPGraphScreenshot_2024Y-01M-29D-00h-45m-11s-727_00

33. Unbinding Delegates 解除绑定

修复 重新分类技能时的取消状态音效不断累加的错误

WBP_SpellGlobe_Button 销毁时,解绑绑定到技能菜单的所有委托

图表: event destruct

解绑绑定技能菜单的重新分配委托 BPSpellMenuWidgetController unbind all events from SpellGlobeReassignedDelegate

解绑绑定技能信息委托 BPSpellMenuWidgetController unbind all events from AbilityInfoDelegate

image BPGraphScreenshot_2024Y-01M-29D-00h-58m-17s-614_00

WBP_ValueGlobe 销毁时,解绑绑定的所有委托

图表 event destruct BPOverlayWidgetController unbind all events from onPlayerLevelChangedDelegate image BPGraphScreenshot_2024Y-01M-29D-01h-00m-30s-906_00

WBP_SpellGlobe 销毁时,解绑绑定的所有委托

图表 event destruct BPOverlayWidgetController unbind all events from AbilityInfoDelegate image BPGraphScreenshot_2024Y-01M-29D-01h-03m-08s-477_00

WBP_EquippedRow_Button 销毁时,解绑绑定的所有委托

图表 event destruct BPSpellMenuWidgetController BPSpellMenuWidgetController 转换为有效get unbind all events from AbilityInfoDelegate image BPGraphScreenshot_2024Y-01M-29D-01h-14m-47s-881_00

WBP_SpellMenu 销毁时,解绑绑定的所有委托

图表 event destruct BPSpellMenuWidgetController unbind all events from SpellGlobeSelectedDelegate

BPSpellMenuWidgetController unbind all events from WaitForEquipDelegate

BPSpellMenuWidgetController unbind all events from StopWaitingForEquipDelegate

BPGraphScreenshot_2024Y-01M-29D-01h-09m-33s-304_00 BPGraphScreenshot_2024Y-01M-29D-01h-10m-11s-865_00

WBP_AttributeMenu 销毁时,解绑绑定的所有委托

图表 event destruct AttributeMenuWidgetController unbind all events from AttributePointsChangedDelegate image BPGraphScreenshot_2024Y-01M-29D-01h-12m-50s-309_00

WangShuXian6 commented 9 months ago

25. Combat Tricks 战斗技巧

1. Debuff Tags Debuff 标签

不同属性的技能,造成伤害时,在一段时间内对目标持续造成伤害的减益效果。

为简化代码,伤害型技能使用单一伤害类型

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);
}

GA_FireBolt火球术技能设置单一伤害类型 DamageType

类默认值: DamageType-Damage.Fire Damage-1, CT_Damage, Abilities.FireBolt image

GA_EnemyFireBolt 敌人火球术技能设置单一伤害类型 DamageType

类默认值: DamageType-Damage.Fire Damage-1, CT_Damage, Abilities.FireBolt image

GA_MeleeAttack 敌人近战技能设置单一伤害类型 DamageType

类默认值: DamageType-Damage.Physical Damage-1, CT_Damage, Abilities.Melee image

GA_RangedAttack 敌人远程范围技能设置单一伤害类型 DamageType

类默认值: DamageType-Damage.Physical Damage-1, CT_Damage, Abilities.Ranged image

添加减益效果标签

伤害类型,伤害抗性,伤害减益效果 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);

2. Debuff Parameters 减益效果参数

为伤害型技能添加减益效果参数

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")
        );

3. Damage Effect Params Struct 伤害效果参数结构

更好的伤害效果参数结构类型

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;
}

4. Using Damage Effect Params 使用伤害效果参数

优化投射物

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);
}

5. Determining Debuff 使用减益效果

执行计算中使用调用者设置的减益效果参数值

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);
}

为敌人辅助属性技能效果 GE_SecondaryAttributes_Enemy 添加火属性抵抗修改器

细节-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 image

6. Debuff Info in the Effect Context 效果情景中的减益效果信息

为效果情景,属性集 添加减益效果信息 减益效果需要网络序列化

效果情景添加属性

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();
}

7. Debuff in the Attribute Set 属性集中的减益效果信息

效果情景添加设置伤害类型属性

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);
    }
}

8. Dynamic Gameplay Effects 动态游戏效果

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秒造成一次伤害,持续一段时间。

9. Debuff Niagara Component 减益效果粒子

目标获得减益标签时激活粒子,失去标签时停用粒子。

如果减益粒子组件能获取当前粒子组件的拥有者的技能系统组件, 如果此时粒子组件的拥有者还没有技能系统组件, 那就立即创建一个委托,在粒子组件的拥有者获取到技能系统组件时广播该委托, 减益粒子组件不应依赖角色,应依赖战斗接口。

战斗接口定义技能系统组件注册委托,将用于减益粒子组件

Source/Aura/Public/Interaction/CombatInterface.h

class UAbilitySystemComponent;

// 声明技能系统组件注册委托,用于技能系统组件有效时获取通知
DECLARE_MULTICAST_DELEGATE_OneParam(FOnASCRegistered, UAbilitySystemComponent*)

public:
    // 返回委托
    virtual FOnASCRegistered GetOnASCRegisteredDelegate() = 0;

基于 NiagaraComponent 创建 DebuffNiagaraComponent C++

DebuffNiagaraComponent image

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;
}

角色在初始化技能actor信息时,广播技能系统组件注册事件

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();
}

敌人在初始化技能actor信息时,广播技能系统组件注册事件

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();
}

BP_EnemyBase 敌人配置减益粒子组件 BurnDebuffComponent

BurnDebuffComponent 组件-Niagara-Niagara系统资产-NS_Fire image image

BP_AuraCharacter 配置减益粒子组件 BurnDebuffComponent

BurnDebuffComponent 组件-Niagara-Niagara系统资产-NS_Fire

BP_Goblin_Spear 调整 BurnDebuffComponent 位置

BurnDebuffComponent 组件-激活-自动启用-启用 仅用于调整 BurnDebuffComponent 位置,测试 调整后需关闭。 image image

GA_Electrocute 雷电技能父类设置为 伤害型技能

类设置-类选项-父类-AuraDamageGameplayAbility image

现在,可以在 类默认值设置减益效果 image

10. Death Impulse Magnitude 死亡物理效果的力值

该值只与伤害型技能相关 角色死亡时受到的物理冲击值,使角色被弹开。

伤害效果参数携带 死亡物理效果的力值

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;
}

11. Death Impulse in the Effect Context 效果情景中的死亡冲击

角色死亡时,技能投射物在应用伤害效果到目标时,生成一个向量,将角色弹开。

自定义游戏效果添加死亡冲击向量

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;
}

12. Handling Death Impulse 处理死亡冲击

属性集中,受到致命伤害,使角色朝死亡冲击方向飞去。 属性集不应依赖角色。应依赖战斗接口。

战斗接口中,死亡时将死亡冲击向量发送给角色

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 的死亡冲击力度

GA_FireBolt-Death Impulse Magnitude-10000

image

伤害减益效果粒子优化

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的激活,使敌人在灼烧时无命中反应动画

GA_HitReact-类默认值-标签-Activation Blocked Tags-Debuff.Burn

image

13. Knockback 击退

伤害效果参数添加击退

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;
}

GA_MeleeAttack 近战攻击应用伤害效果

事件图表: Make Damage Effect Params from Class Defaults

apply damage effect image BPGraphScreenshot_2024Y-02M-02D-04h-24m-13s-729_00

GA_RangedAttack 应用伤害效果

远程伤害已经应用了伤害效果

设置GA_FireBolt火球术技能的击退参数

GA_FireBolt-类默认值-KnockbackChance -100 击退几率 100% 用于测试 image

ABP_Aura 动画蓝图 防止在击退时【位于空中】执行其他动画

打开 ABP_Aura 事件图表: sequence

character movement Is Falling提升为 IsFalling

BPGraphScreenshot_2024Y-01M-29D-22h-11m-20s-264_00

Main states

如果在空中,播放空中动画 InAir 右键-添加状态别名 ToInAir

ToInAir-IdleWalkRun-启用 ToInAir-Idle-启用 image

ToInAir 到 InAir 转换规则: IsFalling image

右键-添加状态别名 ToInAir InAir 到 Idle 转换规则: IsFalling not boolean image

image

防止角色超出地图边界

添加-体积-阻挡体积 防止在地图边界

image

WangShuXian6 commented 9 months ago

26. What a Shock

1. FireBolt Projectile Spread 火球传播

火球抛射物技能 添加生成多抛射物函数

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);
}

GA_FireBolt 火球术技能调用 SpawnProjectiles

事件图表: 断开原 SpawnProjectile

添加 SpawnProjectiles image

BPGraphScreenshot_2024Y-01M-30D-00h-14m-19s-110_00

2. Spawning Multiple Projectiles 生成多抛射物

将生成抛射物工具函数提取到技能系统库

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);
    }

}

image

火球技能生成多个火球抛射物

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);
    }
}

image

3. Homing Projectiles 追踪型抛射物

为BP_FireBolt火球抛射物增加重力,初始速度

BP_FireBolt-类默认设置-抛射物-发射物重力范围-1 初始速度-750 最大速度-750 image

现在火球会在发射后坠落到地面。

设置 GA_FireBolt 火球术的俯仰角

事件图表: 生成时设置俯仰角 override SpawnProjectiles-override pitch-启用 SpawnProjectiles-pitch override-65 image BPGraphScreenshot_2024Y-01M-30D-14h-36m-38s-430_00

GA_FireBolt 火球术 让抛射物追踪目标actor

事件图表: 抛射物重叠结果-hit actor 提升为变量 MouseHitActor MouseHitActor 作为 抛射物追踪目标 Homing Target BPGraphScreenshot_2024Y-01M-30D-14h-42m-17s-624_00

点击非敌人时使用 场景组件作为追踪目标 HomingTargetComponent

HomingTargetComponent 为弱引用指针 如果鼠标点击的actor不是敌人, 则创建新的场景组件作为追踪actor。 不能使用鼠标的点击目标,例如地板,中心点不在鼠标点击处。

由于 ProjectileMovement->HomingTargetComponent 是弱引用,其上创建的场景组件在抛射物销毁时不会垃圾回收 所以需要为附加到抛射物专用组件 Projectile->HomingTargetSceneComponent 上

Source/Aura/Public/Actor/AuraProjectile.h

public: 
    // 点击非敌人时使用 场景组件作为追踪目标
    // 用于垃圾回收
    UPROPERTY()
    TObjectPtr<USceneComponent> HomingTargetSceneComponent;

C++ 火球术技能处理追踪目标

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);
    }
}

现在,如果鼠标选择敌人,则火球会朝飞行。 指向地面,则飞向选中地点。撞击地面,地面需要与火球有重叠事件。生成重叠。

设置较低的发射俯仰角,较高的发射速度,较小的重力范围

提高火球术技能GA_FireBolt 俯仰角 使火球可以追踪到远处的敌人

俯仰角太高会无法击中近距离敌人 image

提高火球蓝图 BP_FireBolt 的发射速度,缩小重力范围 使火球可以追踪到远处的敌人

image

4. Click Niagara System 点击粒子特效

玩家控制器添加 左键短按点击指示目标点的粒子特效

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;
    }
}

BP_AuraPlayerController 玩家控制器蓝图配置 指示器粒子组件

类默认值-Click Niagara System-FX_Cursor image

5. Invoke Replicated Event 调用复制事件

BP_AuraCharacter 玩家蓝图雷电技能玩家动画事件

打开 BP_AuraCharacter 事件图表: 添加变量 InShockLoop 布尔类型 公开

玩家角色动画蓝图 ABP_Aura 配置雷电技能循环动画

右键长按不断施放雷电技能,并循环播放雷电技能动画

打开 ABP_Aura

Main States

Cast_Shock_Loop 动画序列

事件图表:

BP_Aura_Character get InShockLoop 提升为变量 InShockLoop image BPGraphScreenshot_2024Y-01M-30D-16h-14m-32s-658_00

Main States

根据 InShockLoop 转换 idle 和 Cast_Shock_Loop动画序列

idle 到 Cast_Shock_Loop 规则: InShockLoop image

Cast_Shock_Loop 到 idle 规则: 非 InShockLoop not boolean image

image

战斗接口中设置 InShockLoop

Source/Aura/Public/Interaction/CombatInterface.h

public:
    // 设置是否播放雷击动画
    UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
    void SetInShockLoop(bool bInLoop);

BP_AuraCharacter 初始技能添加雷电术作为测试

startup abilities- GA_Electrocute

image

设置雷电技能 GA_Electrocute

长按才能持续触发雷电技能

配置技能初始输入操作标签 细节-输入-startup input tag-InputTag.RMB 右键 image

事件图表: 清空测试节点

event activateAbility 测试释放按键触发技能 print string wait input release print string end ability BPGraphScreenshot_2024Y-01M-30D-16h-41m-10s-462_00 此时释放右键不能打印雷电技能

设置技能系统组件C++ 按下操作 确保 wait input release 按键释放有效

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 事件,开始之后的雷电技能逻辑。 image

设置雷电技能 GA_Electrocute

事件图表: 释放10秒后再结束技能。 delay

监听按下操作 wait input press wait input press-test already pressed-启用 将监听第一次下【用于激活技能的右键按下,否则不会监听第一次按下】 print string BPGraphScreenshot_2024Y-01M-30D-17h-09m-07s-736_00 第一次按下右键后,触发按下操作。只会触发一次按下监听。 然后激活技能,松开后执行技能逻辑。 10秒内只会触发一次按下监听。 image

由于控制器设置了右键按下开关标志,右键按下事件不会每一帧重复触发。

这是雷击技能的核心。 右键按下,雷击开始,直到松开右键,雷击才会结束,最多持续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 技能的追踪飞行加速度 使抛射物可以击中近处目标

GA_FireBolt HomingAccelerationMin - 6500

HomingAccelerationMax -7800

朝向归航目标的加速度大小。总体速度大小仍将受到MaxSpeed的限制

image

6. Aura Beam Spell 光束法术技能

为雷电技能创建父类技能C++,替换当前使用的父类 AuraDamageGameplayAbility 伤害型技能。 包含雷电技能相关的密集型计算函数。

创建派生自 C++ AuraDamageGameplayAbility 伤害技能的光束法术技能 AuraBeamSpell C++

AuraBeamSpell image

激活技能时,获取鼠标选中的目标数据。

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();
    }
}

GA_Electrocute 雷电术技能蓝图父类设置为 AuraBeamSpell

类设置-父类- AuraBeamSpell image

事件图表: 删除测试节点: 保留 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

BPGraphScreenshot_2024Y-01M-30D-18h-34m-42s-604_00

释放雷击技能后,自身播放雷击施法蒙太奇动画, 为目标的赋予触电蒙太奇标签。目标播放触电蒙太奇动画 目标所属的职业可以有自己专属的触电蒙太奇动画。

添加变量 Montage_Electrocute 类型 动画蒙太奇-对象引用 image image

7. Animation Blueprints 动画蓝图

Cast_Shock 雷击施法动画序列启用根运动

image

项目标签管理器添加用于雷击术的 蒙太奇 AN_MontageEvent -动画通知事件标签 Event.Montage.Electrocute

项目设置-gameplay tags-管理gameplay标签 image

右键 基于 Cast_Shock 雷击施法动画序列创建 雷击施法动画蒙太奇 AM_Cast_Electrocute

Content/Assets/Characters/Aura/Animations/Abilities/AM_Cast_Electrocute.uasset

AM_Cast_Electrocute的扭曲运动

打开AM_Cast_Electrocute 混合选项-混入-混合时间-0.1 混合选项-混出-混合时间-0.1 防止动画衔接抖动

动画-比率范围-0.5 放慢动画 image

image

默认通知轨道名: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 旋转以面对该目标

image

添加通知轨道:Events

决定施法攻击的时机

右键-添加通知-AN_MontageEvent 【自定义的通知】 在法杖尖位置 选择 AN_MontageEvent -动画通知-event tag-Event.Montage.Electrocute 用以在游戏中监听

image

标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇上的插槽位置,生成重叠虚拟球。 播放此蒙太奇可触发通知事件,带有事件标签 Event.Montage.Electrocute 后续通过标签监听此事件。

添加通知轨道:Sounds

攻击音效

添加通知-播放音效- sfx_Swipe

GA_Electrocute 雷电技能 播放动画蒙太奇 AM_Cast_Electrocute,使用扭曲动画

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 BPGraphScreenshot_2024Y-01M-30D-19h-58m-48s-761_00

GA_FireBolt 火球术也使用C++版本 Update Facing Target(message)

事件图表: 直接调用C++ 扭曲动画 Update Facing Target(message)带邮件标志 替换原 Update Facing Target 删除 cast to combatInterface BPGraphScreenshot_2024Y-01M-30D-20h-00m-50s-084_00

GA_Electrocute 雷电技能 冲击循环 检查是否实现战斗接口

事件图表: get avatar actor from actor info 检查是否实现战斗接口 does implement interface-interface-combat interface branch 未实现则结束技能 end ability 折叠到函数 EnforceImplementsCombatInterface BPGraphScreenshot_2024Y-01M-30D-20h-06m-35s-679_00 image

BP_AuraCharacter

事件图表: 监听动画蓝图设置释放雷击术循环变量事件 Event Set in shock loop set InShockLoop image BPGraphScreenshot_2024Y-01M-30D-20h-10m-52s-711_00

GA_Electrocute 雷电技能

事件图表: 开始播放攻击动画时立即设置变量 不许考虑动画混合时间问题 set InShockLoop-true get avatar actor from actor info image

松开键时设置 set InShockLoop 为false get avatar actor from actor info image

这样,在长按右键时,ABP动画蓝图将持续播放释放雷电术动画 AM_Cast_Electrocute,松开键时不再播放。 image

InShockLoop 存储在 BP_AuraCharacter 上,在技能与动画蓝图中传递。 BPGraphScreenshot_2024Y-01M-30D-20h-23m-46s-412_00

ABP_Aura 设置施法结束到闲置状态动画的过渡时间

选中 cast_shock_loop 到 idle 的规则 细节-混合设置-时长-0.1 时间越小,过渡越快,收起法杖动作越快 image

光束技能存储玩家

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

image

结束技能之前,启用移动 get OwnerCharacter get Character Movement set Movement mode-new Movement mode-Walking 行走 image

这样,长按雷电技能键时,无法移动,松开键即可移动。

如果在扭曲运动之前禁用移动,则扭曲运动失效。 应该在监听到施法动画蒙太奇发送的自定义事件之后禁用移动。 wait gameplay event wait gameplay event-event tag-Event.Montage.Electrocute wait gameplay event-onl;y trigger once-启用 只触发一次

BPGraphScreenshot_2024Y-01M-30D-20h-40m-20s-851_00

整理节点

PrepareToEndAbility BPGraphScreenshot_2024Y-01M-30D-20h-41m-34s-266_00

InShockLoop BPGraphScreenshot_2024Y-01M-30D-20h-42m-29s-072_00

UpdateFacingTarget BPGraphScreenshot_2024Y-01M-30D-20h-44m-34s-115_00

BPGraphScreenshot_2024Y-01M-30D-20h-44m-13s-831_00

8. Player Block Tags 玩家阻止标签

GA_Electrocute 修复 按住右键,同时可以点击左键触发其他技能的错误 技能拥有多种标签,actor的技能系统组件拥有这些标签,可用于激活技能,阻止技能 image 玩家控制器可以检查技能系统组件上的这些标签

创建禁用按键事件,光标追踪的相关标签

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 如果目标参与者/组件具有以下标签中的任何一个,则阻止此技能

GA_Electrocute 技能设置标签

Activation Owned Tags- Player.Block.CursorTrace Player.Block.InputHeld Player.Block.InputPressed

在激活雷电技能时,阻止鼠标追踪,左键单击,左键按住事件。

技能中在等待释放按键,所以不阻止 Player.Block.InputReleased

image 此时,右键长按,激活雷电技能。雷电技能的拥有者玩家被赋予标签 Player.Block.CursorTrace, Player.Block.InputHeld, Player.Block.InputPressed,

玩家控制器在使用光标追踪时,检测出技能系统组件包含标签 Player.Block.CursorTrace,所以光标追踪事件直接返回,不再执行。

单击时,检测出 Player.Block.InputPressed ,不再触发任意的按下输入操作,例如左键,右键,数字键,不会触发执行相应的技能例如火球术技能。 长按时,检测出 Player.Block.InputHeld,不再触发长按键输入操作,不会触发执行相应的技能。例如其他按键的长按技能。

雷电技能结束时,这些标签将从技能拥有者玩家删除。 不影响玩家右键长按再次触发雷电技能。

9. GameplayCue Notify Paths 游戏性Cue显示 提示通知路径

项目设置中添加 游戏性Cue显示标签 GameplayCue.MeleeImpact

通过标签识别执行 游戏性Cue显示 游戏性Cue显示标签 必须与"GameplayCue"开头的游戏性标记相关联,且GameplayCue必须是顶层标签。

项目设置中添加标签 GameplayCue.ShockBurst

image

基于 GameplayCueNotify_Static 创建 静态游戏性Cue显示 GC_ShockBurst 蓝图

静态游戏性Cue显示 不会生成实例 Content/Blueprints/AbilitySystem/GameplayCueNotifies/GC_ShockBurst.uasset

通过 GameplayCue.MeleeImpact 标签识别执行激活 游戏性Cue显示

类默认值-gameplay cue-gameplay cue tag-GameplayCue.MeleeImpact image image

事件图表: 指定执行cue发生什么 重载函数 on execute 同时调用父函数,类似 super get actor location play sound at location play sound at location-Sound-sfx_Shock BPGraphScreenshot_2024Y-01M-30D-22h-05m-00s-818_00

GA_Electrocute 雷电技能 中 通过标签 GameplayCue.MeleeImpact 激活 游戏性Cue显示 GC_ShockBurst ,播放音效

事件图表: 激活技能后就激活 游戏性Cue显示 execute gameplayCue on owner execute gameplayCue on owner-gameplay cue tag-GameplayCue.MeleeImpact

image BPGraphScreenshot_2024Y-01M-30D-22h-16m-41s-176_00

现在长按右键将播放雷击音效, 服务端和客户端都可以显示硬编码的粒子和音效。 这些效果都可以复制到客户端。

但此时会有警告: 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/。对于大型项目来说,这可能是缓慢的。请考虑指定要搜索的路径。

将所有游戏性Cue显示保存到特定文件夹 GameplayCueNotifies 中,优化搜索

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有最大数量限制,而 游戏性Cue显示 被赋值为 RPC,默认为2个RPC

image

设置 RPC发送数量限制为10个每次网络更新:不应超过10. Config/DefaultEngine.ini

[ConsoleVariables]
net.MaxRPCPerNetUpdate=10

所以, 游戏性Cue显示 只能用于必须用的地方,除此之外不应使用。以节省RPC。 根据带宽和游戏设置,每秒可以接收60-100次网络更新。

10. Gameplay Cue Notify Actor 游戏性Cue显示 通知Actor

雷电技能中,将法杖骨骼网格体传输给游戏性Cue显示。

游戏性Cue显示 生成雷电粒子系统,附加到 法杖插槽名上。

spawn system attached 用于将粒子系统附加到法杖的插槽上

项目设置中添加 游戏性Cue显示actor标签 GameplayCue.ShockLoop

image

基于 GameplayCueNotify_Actor 创建 游戏性Cue显示 GC_ShockLoop 蓝图

这是实例化的游戏性Cue显示。 image Content/Blueprints/AbilitySystem/GameplayCueNotifies/GC_ShockLoop.uasset

这种游戏性Cue显示可以添加用于显示的组件。 是通知Actor。可以实现其他接口。 image

配置标签 类默认值-gameplay cue-gameplay cue tag-GameplayCue.ShockLoop image

设置删除时自动销毁: Cleanup-Auto Destroy on Remove-启用 image

事件图表: on active 在首次激活具有持续时间的GameplayCue时调用,只有在客户端见证激活时才会调用 while active 当具有持续时间的GameplayCue第一次被视为活动时调用,即使它实际上并没有被应用(正在加入,等等)

例如: 瞬时爆炸效果,在新玩家加入游戏时,新玩家不应该在看到瞬时爆炸,这已成为过去。使用 on active

持续雷击效果,在新玩家加入游戏时,应该也可以看到该效果,使用 while active

所以雷击术使用 while active

指定执行cue发生什么

重载函数 while active

get object name print string BPGraphScreenshot_2024Y-01M-30D-22h-52m-46s-325_00

指定cue结束时发生什么

重载函数 on remove print string BPGraphScreenshot_2024Y-01M-30D-22h-59m-25s-529_00

GA_Electrocute 技能中 通过将 GC_ShockLoop 添加到Actor来使用它

打开 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 image

此时GameplayCue: GC_ShockLoop 出激活状态,如果不删除,之后的技能执行不能再次触发cue音效.

技能结束时删除 GameplayCue:GC_ShockLoop

PrepareToEndAbility 函数中:

remove GameplayCue from owner remove GameplayCue from owner-gameplay cue tag-GameplayCue.ShockLoop 【可以省略,会自行找到】 BPGraphScreenshot_2024Y-01M-30D-22h-57m-19s-478_00

战斗接口添加获取武器骨骼网格体组件函数

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;
}

NS_ElectricBeam

Beam Start,BeaM End 的蓝色 用户标志 : 一个只读值,可根据系统进行初始化并通过蓝图或c++在关卡中进行外部修改。 image

GA_Electrocute 技能中 为 GC_ShockLoop 发送参数

事件图表: GetWeapon ability-get avatar actor from actor info

将鼠标位置传入 Mouse Hit Location image

BPGraphScreenshot_2024Y-01M-30D-23h-29m-08s-119_00

GC_ShockLoop 使用 target attach component

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

BPGraphScreenshot_2024Y-01M-30D-23h-37m-40s-927_00

销毁时删除粒子 OnRemove 函数

BeamSystem-转换为有效get 如果有效则销毁 destroy component BPGraphScreenshot_2024Y-01M-30D-23h-34m-36s-108_00

现在,右键激活雷击节能,在法杖尖端与鼠标指向的地面或actor位置生成雷电,且在右键按住期间持续存在,且播放音效

image

11. Electrocute Looping Sound 雷击术循环音效

右键按住时,客户端施法动画可以正常复制。 但服务端的施法动画只会在客户端上显示一次周期,因为玩家角色蓝图的 InShockLoop没有正常复制到客户端。

修复 BP_AuraCharacter玩家角色蓝图的 InShockLoop 变量网络复制

打开 BP_AuraCharacter 事件图表: 左侧选择 InShockLoop 变量-细节-变量-复制-Replicated image image

现在 InShockLoop 现实了两个白色球标志表示会网络复制 image

GC_ShockLoop 持续播放循环音效

事件图表:

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

BPGraphScreenshot_2024Y-01M-31D-00h-03m-38s-866_00

销毁时删除循环音效 :On Remove 函数

ShockLoopSound 转换为有效get

淡出音量0.3秒后再销毁 fade out

BPGraphScreenshot_2024Y-01M-31D-00h-08m-02s-476_00

现在,长按释放技能时会持续播放音效,最后淡出音效。

12. Target Trace Channel 目标跟踪通道

默认墙体不会阻挡可视化通道,无法正确用雷击粒子末端位置参数。

需要为鼠标指针目标追踪新建对象通道

项目设置-引擎-碰撞-object channels 新建对象通道 Target 默认响应 Block image image

C++ 定义 Target 通道

占用第二个自定义通道 Source/Aura/Aura.h

#define ECC_Target ECollisionChannel::ECC_GameTraceChannel2

在行为树的任务:鼠标设置跟踪目标数据时使用 Target 通道

不再使用 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);
    }
}

现在 雷击术可以被所有物体阻挡,正确长生末端位置。不会穿透墙体。

为 空气墙设置 忽略 Target 通道

可以是鼠标追踪穿透空气墙,追踪到空气墙下方的物体。

image

为角色 BP_AuraCharacter 的碰撞盒子 Box组件 设置 忽略 Target 通道

BP_AuraCharacter Box组件-细节-碰撞预设-Target-忽略 image image

BP_FadeActor 在阻挡视线的墙体淡出到透明时,使其忽略 Target 通道 ,防止对雷击术产生交互

打开 BP_FadeActor 事件图表:

FadeFinished 函数事件图表: 将可视性通道替换为 Target 通道 删除 BlockVisibility 判断分支 BPGraphScreenshot_2024Y-01M-31D-00h-45m-31s-489_00

SM_Tile_3x3x2 静态网格体

显示简单碰撞 细节-碰撞-碰撞预设-碰撞复杂度- 将复杂碰撞用作简单碰撞 由于该静态网格体由多个网格组成,将会触发多次重叠事件。

只创建复杂形状(每个多边形)。将复杂形状用于所有场景查询和碰撞测试。只可用于静态形状的模拟中(即可被碰撞,但无法通过力或速度来移动。)

应该设置为: 细节-碰撞-碰撞预设-碰撞复杂度- 项目默认 使用项目物理设置(DefaultShapeComplexity) image

碰撞-添加盒体简化碰撞 image

这样,将只有一个碰撞盒体,只会触发1次重叠事件。 image

这样在视线被阻挡时,墙体不会闪烁。

13. First Trace Target 第一个跟踪目标

雷电技能中-使雷电击中第一个目标,如果鼠标选中目标与法杖中间有其他目标,则击中雷电经过的第一个目标

image

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();
            }
        }
    }

}

GA_Electrocute

事件图表: 为武器生成雷电粒子 折叠到函数 SpawnElectricBeam BPGraphScreenshot_2024Y-01M-31D-11h-57m-02s-568_00 BPGraphScreenshot_2024Y-01M-31D-12h-15m-35s-279_00

SpawnElectricBeam 函数事件图表: 首先调用C++技能跟踪鼠标选择的目标与武器之间的其他目标的第一个

TraceFirstTarget get MouseHitLocation BPGraphScreenshot_2024Y-01M-31D-12h-12m-19s-271_00

image

关闭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

BPGraphScreenshot_2024Y-01M-31D-14h-50m-54s-315_00

GC_ShockLoop 检查传入参数的源对象是敌人还是玩家

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

BPGraphScreenshot_2024Y-01M-31D-12h-52m-47s-964_00

GA_Electrocute

SpawnElectricBeam 函数图表:

does implement interface 结果提升为变量 FirstTargetImplementInterface

PrepareToEndAbility 函数图表

结束时删除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

这是因为,如果没有击中敌人,则不会将雷电传播到未来的目标。 例如击中地板不会继续传播雷电到其他目标。

BPGraphScreenshot_2024Y-01M-31D-13h-07m-01s-784_00

14. Additional Targets 雷电的其他目标

击中第一个敌人后,继续将雷电传播到第一个敌人中心半径内的其他敌人 需要为其他敌人添加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());
}

GA_Electrocute 获取存储的其他敌人

PrepareToEndAbility 函数图表

移除第一个目标的cue后,获取其他目标,仅做测试 StoreAdditionalTargets

循环为每个其他目标创建调试球

for each loop draw debug sphere get actor location

绘制限制半径 draw debug sphere get MouseHitLocation

image BPGraphScreenshot_2024Y-01M-31D-13h-42m-08s-272_00

image

15. Shock Loop Cues on Additional Targets 为其他目标添加cue

GA_Electrocute

PrepareToEndAbility 函数图表

移动 StoreAdditionalTargets 到函数 SpawnElectricBeam 中 BPGraphScreenshot_2024Y-01M-31D-13h-58m-30s-015_00

SpawnElectricBeam 函数图表

为第一个目标添加cue后,开始循环为其他目标添加cue image

添加函数 AddShockLoopCueToAdditionalTarget,将雷电也发射到其他目标

输入参数 AdditionalTarget 类型: Actor 对象引用 image

AdditionalTarget 提升为局部变量 TargetActor make Gameplay Cue Parameters

映射目标和目标参数 添加变量 AdditionalActorsToCueParameters 类型:映射型 Actor 对象引用 键类型为 Actor 对象引用 值类型为 Gameplay Cue Parameters image

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

BPGraphScreenshot_2024Y-01M-31D-14h-17m-31s-435_00

SpawnElectricBeam

StoreAdditionalTargets 的返回值提升为变量 AdditionalTargets 用于删除cue, 因为蓝图无法使用foe each loop 循环映射类型 image

BPGraphScreenshot_2024Y-01M-31D-14h-21m-14s-891_00

PrepareToEndAbility 函数图表

结束技能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 BPGraphScreenshot_2024Y-01M-31D-14h-30m-24s-240_00

image

BPGraphScreenshot_2024Y-01M-31D-14h-31m-20s-057_00

SpawnElectricBeam 函数图表 添加cue到每个目标

Add Shock Loop Cue to Additional Target

image

BPGraphScreenshot_2024Y-01M-31D-14h-48m-06s-683_00

现在,雷击术将击中5个敌人 image

修复击中地板时,依然会再次击中敌人的错误

SpawnElectricBeam 函数图表

击中地板,地板未实现战斗接口,之后不应该该继续查找附近的敌人和存储附近敌人 StoreAdditionalTargets StoreAdditionalTargets 之前应该检查 击中的第一个目标是否实现战斗接口。

FirstTargetImplementInterface branch image

BPGraphScreenshot_2024Y-01M-31D-14h-56m-44s-949_00

16. Electrocute Cost Cooldown and Damage 雷击成本冷却和伤害

CT_Damage 曲线表格添加雷击伤害曲线 Abilities.Electrocute

1,1 40,20 自动平滑 image

CT_Cost 为雷电技能魔力消耗创建曲线 Lighting.Electrocute

1,1.5,1.75,2,2.5,3,3.75,4.5,5.75,7 image

基于 GameplayEffect 创建雷电技能消耗效果 GE_Cost_Electrocute

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

每次释放雷击术,消耗对应魔力点。

image

添加雷电技能冷却效果标签

项目设置 -标签管理器-Cooldown.Lighting.Electrocute

image

基于 GameplayEffect 创建雷电技能冷却效果GE_Cooldown_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秒,然后自行消失】 image

设置 GA_Electrocute 雷击技能

打开 GA_Electrocute

配置伤害:

类默认值-细节-damage effect class-GE_Damage

damage type-Damage.Lighting

damage-1,CT_Damage,Abilities.Electrocute

debuff chance-0

image

Instancing Policy-Instanced Per Actor image

为 GA_Electrocute 技能配置技能冷却效果

Cooldowns- Cooldown Gameplay Effect Class-GE_Cooldown_Electrocute

为 GA_Electrocute 技能配置技能魔力消耗效果

costs-cost gameplay effect class-GE_Cost_Electrocute image

17. Applying Electrocute Cost and Damage 应用雷击技能的成本和伤害

公开伤害曲线给蓝图

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());
}

GA_Electrocute 技能,生成雷电粒子后,开始消耗魔力,产生伤害

事件图表: 每秒消耗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 image

true 则表示消耗魔力,开始应用伤害 此时长按右键,将持续消耗魔力,每0.1秒消耗一次即1点魔力 【1级时】

添加函数 ApplyDamage 造成伤害

首先对鼠标选中的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

BPGraphScreenshot_2024Y-01M-31D-16h-21m-50s-504_00

再将伤害循环应用到其他目标上 for each loop AdditionalTargets ApplyDamageSingleTarget BPGraphScreenshot_2024Y-01M-31D-16h-22m-11s-644_00

主事件图表:

如果消耗魔力成功,则可以调用 上面蓝图的 ApplyDamage 对所有目标造成伤害

如果鼠标选中目标死亡,则结束技能 添加变量 TargetDead 布尔类型

ApplyDamage 之前检查鼠标选中目标是否死亡,没有死亡才可以应用伤害 TargetDead branch

死亡则结束技能

清除计时器,结束技能等折叠到函数 ClearTimerAndEndAbility DamageAndCostTimer Clear and Invalidate Timer by Handle PrepareToEndAbility end ability BPGraphScreenshot_2024Y-01M-31D-16h-28m-31s-244_00 ClearTimerAndEndAbility

image

ApplyDamageSingleTarget 函数图表 优化

输入参数 Actor 提升为局部变量 DamageTarget

DamageTarget

BPGraphScreenshot_2024Y-01M-31D-16h-36m-24s-947_00

完整

BPGraphScreenshot_2024Y-01M-31D-16h-37m-40s-937_00

现在ji将对所有目标造成伤害 image image

BP_EnemyBase

Health Bar 组件-细节-用户界面-空间-屏幕 才可以使空间始终面向屏幕,始终可见。 场景选项,表示正面可见。 image image

18. Electrocute Polish 雷电光束

在目标死亡后结束光束粒子。 在主要目标或最后一个目标死亡后,结束雷电技能。

战斗接口中 创建目标死亡委托

原 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);
            }
        }
    }
}

GA_Electrocute 雷电技能蓝图中实现 PrimaryTargetDied,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

BPGraphScreenshot_2024Y-02M-01D-18h-30m-41s-386_00

优化目标死亡逻辑 消耗魔力成功后,仅应用伤害 image

提交冷却优化,移动到 ClearTimerAndEndAbility 函数中 image

ClearTimerAndEndAbility 函数事件

提交冷却 CommitAbilityCooldown

技能结束前,清空存储数组中的其他目标,否则,第一个目标死亡后,再对地板施法其他目标也会受伤害。因为此时其他目标仍存储在数组中。

清空其他目标数组 AdditionalTargets clear

清空对象参数映射 AdditionalActorsToCueParameters clear

设置鼠标目标为空 set Mouse Hit Actor

设置第一目标为false FirstTargetImplementInterface

BPGraphScreenshot_2024Y-02M-01D-19h-04m-30s-497_00

ApplyDamageSingleTarget 函数事件图表

对敌人应用伤害前,检查其是否存活,检查其技能系统组件是否有效 工具-is valid

将技能系统组件提升为局部变量 ASC 工具-is valid

检查伤害目标的技能系统组件 工具-is valid

BPGraphScreenshot_2024Y-02M-01D-18h-49m-03s-566_00

主事件图表

松开按键结束技能时,清除伤害计时器 image

一旦施放雷电技能,设置至少释放技能0.5秒。保证可以触发雷电粒子效果。 添加变量 MinSpellTime 浮点类型 默认值 0.5 等待按键松开事件中 检查按下的时间 time held

松开时设置 wait inout release-time held 提升为变量 检查其是否小于 MinSpellTime time held < branch

如果小于,获取差值,作为必须继续施放技能的时间 MinSpellTime

time held delay

如果不小于,直接清除计时器,结束技能

BPGraphScreenshot_2024Y-02M-01D-19h-15m-51s-642_00

ClearTimerAndEndAbility 函数事件

结束技能前清空 time held set time held BPGraphScreenshot_2024Y-02M-01D-19h-05m-19s-443_00

完整

BPGraphScreenshot_2024Y-02M-01D-19h-14m-20s-506_00

DA_AbilityInfo 技能信息中设置 雷电技能的 Cooldown tag ,以显示冷却时间

Cooldown tag-Cooldown.Lighting.Electrocute image 控件将响应该冷却标签,显示冷却倒计时。 现在每次松开右键或技能被结束后,将显示冷却倒计时。

19. Explode Dem FireBoltz

修复 命中敌人的额火球,在敌人死亡之后到达,火球不会消失。

在tick中修复。 但必须将tick值设置的较低,否则会消耗大量性能。不可使用。

BP_FireBolt 火球抛射物中 检查火球是否停止移动

事件图表: actor tick-tick 间隔(秒)-0 默认 这表示最大tick,每帧都执行。一般为每秒60-120次tick.现代计算机为120次tick. 此tick函数将被执行的频率(秒)。如小于或等于0,则其将每帧tick image

测试 tick 0

event tick get actor location get actor location 提升为变量 LocationThisFrame draw debug sphere

image

tick 0 将绘制一大堆调试球 image

测试 tick 0.1

actor tick-tick 间隔(秒)-0.1 表示每隔0.1秒执行一次tick,每秒执行10次tick. actor tick-tick 间隔(秒)-0.2 表示每隔0.2秒执行一次tick,每秒执行5次tick. image image

检查火球是否停止移动

添加变量 MinDistancePerFrame 浮点类型 默认10 表示火球每帧之间最小距离

get actor location 提升为变量 LocationLastFrame

将 LocationThisFrame 赋值给 LocationLastFrame LocationThisFrame

LocationThisFrame 减去 LocationLastFrame 是否小于等于 MinDistancePerFrame

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

BPGraphScreenshot_2024Y-02M-01D-21h-50m-14s-232_00

现在,如果在敌人死亡之后命中,则火球悬停一会后爆炸。 actor tick-tick 间隔(秒)-0.2

折叠到函数 MakeGoKaboomIfNoMove BPGraphScreenshot_2024Y-02M-01D-21h-56m-11s-570_00 BPGraphScreenshot_2024Y-02M-01D-21h-56m-30s-715_00

这是一种高性能解决方案。

20. Stun 眩晕

雷击技能的debuff 是敌人无法行动。

GA_FireBolt 修复 火球术之后立刻雷击再火球,火球会被打断的问题

标签-Block Abilities with Tag-Abilities 表示施放当前技能火球术时,其他的带Abilities标签的技能都会被阻止施放。 火球术施放完成后,才能施放其他技能。 image 可以阻止 Abilities.Fire 和 Abilities.Lighting 标签 image

GA_Electrocute 雷击技能中结束技能时应用眩晕减益效果cue

属性集会根据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

BPGraphScreenshot_2024Y-02M-01D-22h-23m-26s-899_00

现在技能上的 debuff 各个属性在技能结束时都会应用到目标 几率,频率,击退 image

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;
}

角色类中,初始化技能actor中,技能系统组件可用时,监听眩晕标签[注册游戏标签事件]

该逻辑不应放在角色基类,因为需要考虑调用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();
}

敌人类中,初始化技能actor中,技能系统组件可用时,监听眩晕标签[注册游戏标签事件]

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;
}

GetLifetimeReplicatedProps 将需要复制的Properties(UPROPERTY中标记为Replicated或replicatedUsing=)真正添加进复制列表

它的重写版本存在于每个有复制标记的属性的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);
}

ABP_Enemy 设置敌人受到雷击debuff时 播放眩晕动画

事件图表:

从敌人类获取是否被眩晕,判断是否被眩晕 原 BP AuraEnemy 重命名为 AuraEnemy

event blueprint update animation sequence AuraEnemy 转换为有效get IsStunned IsStunned 提升为变量 Stunned

BPGraphScreenshot_2024Y-02M-01D-23h-08m-50s-614_00

Main states 中根据 Stunned 来播放眩晕动画

添加状态 Stunned image

IdleWalkRun 到 Stunned 转换规则 Stunned 为true image

Stunned到 IdleWalkRun 转换规则 Stunned 为false not boolean image

Stunned 状态 需要序列播放器

添加 序列播放器 sequence player

image 之后,基于 此 ABP_Enemy 的动画蓝图都需要设置 序列播放器 sequence player

ABP_Goblin_Slingshot 动画蓝图设置 序列播放器 sequence player

资产覆盖编辑器-ABP_Enemy-AnimGraph-Main States-Stunned -序列播放器-Slingshot_Stun_Loop 【动画序列】 image

ABP_Goblin_Spear 动画蓝图设置 序列播放器 sequence player

资产覆盖编辑器-ABP_Enemy-AnimGraph-Main States-Stunned -序列播放器-Stun_Loop 【动画序列】

ABP_Shaman 动画蓝图设置 序列播放器 sequence player

资产覆盖编辑器-ABP_Enemy-AnimGraph-Main States-Stunned -序列播放器-Shaman_Stun 【动画序列】 image

ABP_Ghoul 动画蓝图设置 序列播放器 sequence player

资产覆盖编辑器-ABP_Enemy-AnimGraph-Main States-Stunned -序列播放器-Stun 【动画序列】 image

ABP_Demon 动画蓝图设置 序列播放器 sequence player

资产覆盖编辑器-ABP_Enemy-AnimGraph-Main States-Stunned -序列播放器-Demon_Stun 【动画序列】 image

ABP_Aura 设置受到雷击debuff时 播放眩晕动画

事件图表

event blueprint update animation 从 BP_Aura_Character 获取是否眩晕变量 IsStunned IsStunned 提升为变量 Stunned

Main states 状态机

右侧资产处 选择 动画序列 Stun 拖入成为状态Stun image

设置为循环动画 选择 序列播放器 Stun-细节-设置-循环动画-启用 image

Stunned到 Idle 转换规则 Stunned 为false image

添加状态别名 ToStun 包含stun之外的所有状态 image

ToStun 到 Stunned 转换规则 Stunned 为 true image

BPGraphScreenshot_2024Y-02M-01D-23h-30m-17s-583_00

现在敌人受到雷击debuff时,先播放受击动画,再播放眩晕动画。

ABP_Enemy 使眩晕动画自然

main states

IdleWalkRun 到 Stunned 转换规则-细节-混合设置-时长-增大到 0.4 image

加快AM_HitReact_GoblinSpear 动画蒙太奇 命中反应动画速度

找到 AM_HitReact_GoblinSpear 的 动画引用 HitReact_Spear 动画序列 复制 HitReact_Spear 为 HitReact_Spear_cut

HitReact_Spear_cut

时间轴移动到大约中间部分 移除后面的帧 image

根运动-启用根运动-启用 image

AM_HitReact_GoblinSpear

拖入 HitReact_Spear_cut 到 defaultGroup.defaultSlot 删除 HitReact_Spear image

BT_EnemyBehaviorTree 行为树中 使敌人在眩晕时无法移动,无法攻击

黑板

新建关键帧 Stunned 布尔类型 表示是否眩晕,用于判断 image

行为树

所有任务之前的选择器中 添加 blackboard 装饰器 配置与我是否没有激活受击技能装饰器相同 image

image

表示:未眩晕时,才会继续往下执行。

敌人C++ 在技能系统组件注册眩晕标签事件,在回调中设置 行为树的黑板键 Stunned

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);
    }
}

BT_EnemyBehaviorTree_Elementalist 行为树中 使敌人在眩晕时无法移动,无法攻击

行为树

所有任务之前的选择器中 添加 blackboard 装饰器 配置与我是否没有激活受击技能装饰器相同 image image

GA_MeleeAttack 为敌人战士职业添加雷击技能标签 用于测试击中玩家的眩晕效果

damage type-Damage.Lighting [原 Damage.Physical] debuff chance-100 image 这使 战士warrior职业击中玩家时,玩家眩晕5秒。 此时玩家不可移动,但却可以旋转方向,施放技能,需要修复。

将附带光属性减益效果的雷电技能的减益效果触发几率 Debuff_Chance 设为100,将频率 DebuffFrequency 设为1,持续时间DebuffDuration 设为 5,则表示受到雷击的目标在5秒内,每隔1秒触发一次减益效果,该效果导致眩晕cue。

GA_FireBolt 技能拥有者在被击晕时【被赋予了Debuff.Stun标签】不能触发该火球术技能

类默认值-标签-activation blocked tags-Debuff.Stun

image

GA_Electrocute 技能拥有者在被击晕时【被赋予了Debuff.Stun标签】不能触发该雷击术技能

类默认值-标签-activation blocked tags-Debuff.Stun image

现在玩家被击晕时,将不能施放火球术和雷击术技能。

玩家在被击晕时,阻止输入操作,防止移动玩家的方向

在玩家控制器的按键等输入操作中检查是否有阻止标签 ,该功能已拥有。

属性集中检查伤害类型,减益效果,授予减益标签,如果有眩晕减益标签,则添加 阻挡actor的鼠标选择和阻挡按键操作标签

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++创建的动态标签是无法复制的。所以这些阻止标签不会复制到客户端。

通过复制通知 ReplicatedUsing=OnRep_Stunned 来使客户端获取服务端返回的阻止标签

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()
{

}

角色中实现 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);
        }
    }
}

21. Stun Niagara System 眩晕粒子

GA_Electrocute 修复单击后立即松开按键时客户端不会删除雷击cue的问题

事件图表:

最小施法事件发生在清除计时器之前,这太早了。

检查是否是服务器端.只有服务器端才有权限为最小施法左延时,清除定时器,结束技能 HasAuthority

防止此时客户端光标不显示 如果不是在服务器,则执行 PrepareToEndAbility BPGraphScreenshot_2024Y-02M-02D-05h-18m-24s-850_00

BPGraphScreenshot_2024Y-02M-02D-05h-25m-03s-271_00

PrepareToEndAbility 函数图表

只有服务器才有权限应用伤害效果 HasAuthority

如果不是服务器,则绕过应用伤害效果继续执行 HasAuthority BPGraphScreenshot_2024Y-02M-02D-05h-22m-15s-694_00

技能抛射物 重叠时 伤害参数源技能系统组件需要左空检查,技能抛射物应该网络复制,运动也需要复制

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;
}

BP_FireBolt 再次检查运动复制

细节-复制-复制运动-启用 image

现在客户端可以正常复制火球

角色基类添加眩晕粒子组件

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);
}

BP_EnemyBase,BP_AuraCharacter 设置 StunDebuffComponent 眩晕粒子组件

image

niagara 系统资产-NS_Stars image

调整眩晕粒子组件高度到头部 image

激活-自动启用 用以测试效果

为每个敌人子类蓝图重新调整 眩晕粒子组件高度到头部

image

将眩晕粒子复制到客户端

默认眩晕减益标签未复制到客户端

客户端接收到服务端的 bIsStunned 变量后 根据 bIsStunned 激活,停用眩晕粒子

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();
    }
}

22. Shock Loop Animations 雷击循环动画

目标被雷击术命中时根据是否被雷击 播放雷击命中动画,非普通命中动画

接口中定义 否被雷击术击中 工具函数

Source/Aura/Public/Interaction/CombatInterface.h

public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
    bool IsBeingShocked() const;

    UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
    void SetIsBeingShocked(bool bInShock);

角色基类添加是否被雷击术击中 bIsBeingShocked 复制变量

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);
        }
    }
}

ABP_Enemy

事件图表

获取ABPEnemy 的 IsBeingShocked 提升为变量 IsBeingShocked image BPGraphScreenshot_2024Y-02M-02D-06h-30m-30s-869_00

main states

添加状态 BeingShocked image

BeingShocked 中添加序列播放器 sequence player image

IdleWalkRun 到 BeingShocked 规则 IsBeingShocked image

BeingShocked 到 IdleWalkRun 规则 IsBeingShocked not boolean image

为每个ABP_Enemy的子类动画蓝图设置序列播放器

ABP_Demon

序列播放器-DemonShockLoop image

设置 DemonShockLoop 动画序列的 动画-比率范围 可以改变动画速度

image

ABP_Ghoul

ShockLoop image

ABP_Goblin_Slingshot

ShockLoop image

ABP_Goblin_Spear

ShockLoop image

ABP_Shaman

Shaman_ShockLoop image

GA_Electrocute 雷击术中为目标设置 IsBeingShocked 变量的值

SpawnElectricBeam 函数事件图表:

生成cue后,为第一个鼠标选择的目标cue target设置 IsBeingShocked cue target set IsBeingShocked-in loop-true

其他目标循环设置 set IsBeingShocked-in loop-true

BPGraphScreenshot_2024Y-02M-02D-06h-54m-24s-126_00

PrepareToEndAbility 函数

删除cue设置为false 第一个目标 set IsBeingShocked-in loop-false

这是复制变量,不需要检查是否在服务器

其他目标循环设置 set IsBeingShocked-in loop-false

BPGraphScreenshot_2024Y-02M-02D-06h-53m-14s-078_00

WangShuXian6 commented 9 months ago

27. Passive Spells 被动技能

1. Passive Spell tags 被动技能标签

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")
    );

}

2. Aura Passive Ability 被动技能

技能系统组件创建结束被动技能委托

用以运行时被动技能栏动态更改被动技能 Source/Aura/Public/AbilitySystem/AuraAbilitySystemComponent.h

// 声明结束被动技能委托
DECLARE_MULTICAST_DELEGATE_OneParam(FDeactivatePassiveAbility, const FGameplayTag& /*AbilityTag*/);

public:
    // 结束被动技能委托
    FDeactivatePassiveAbility DeactivatePassiveAbility;

基于 AuraGameplayAbility 新建 被动技能 C++ AuraPassiveAbility

image

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);
    }
}

3. Passive Ability Info 被动技能信息

基于 AuraPassiveAbility 创建被动技能蓝图 GA_HaloOfProtection

Content/Blueprints/AbilitySystem/Aura/Abilities/PassiveSpells/GA_HaloOfProtection.uasset 通过 Abilities.Passive.HaloOfProtection 标签识别该被动技能

标签-AbilityTag-Abilities.Passive.HaloOfProtection image

基于 AuraPassiveAbility 创建被动技能蓝图 GA_LifeSiphon

Content/Blueprints/AbilitySystem/Aura/Abilities/PassiveSpells/GA_LifeSiphon.uasset

通过 Abilities.Passive.LifeSiphon 标签识别该被动技能

标签-AbilityTag-Abilities.Passive.LifeSiphon image

基于 AuraPassiveAbility 创建被动技能蓝图 GA_ManaSiphon

Content/Blueprints/AbilitySystem/Aura/Abilities/PassiveSpells/GA_ManaSiphon.uasset 通过 Abilities.Passive.ManaSiphon 标签识别该被动技能

标签-AbilityTag-Abilities.Passive.ManaSiphon image

DA_AbilityInfo 技能信息资产中添加3个被动技能信息

image

4. Passive Tags in Spell Tree 技能树中的被动技能标签

控件需要直到监听哪些信息用于被动技能信息显示

WBP_PassiveSpellTree 被动技能树 控件

设计器: 重命名按钮 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

现在可以响应技能信息广播

BPGraphScreenshot_2024Y-02M-02D-07h-46m-18s-741_00 BPGraphScreenshot_2024Y-02M-02D-07h-47m-03s-594_00

2级时被动可用,可以消耗技能点解锁。

image

在 WBP_HealthManaSpells 覆层装备栏中设置控件控制器 用以被动技能信息显示在装备栏

SetGlobeWidgetControllers 函数图表:

为被动按钮设置控件控制器 SpellGlobe_Passive_1 SpellGlobe_Passive_2 BPGraphScreenshot_2024Y-02M-02D-07h-53m-22s-467_00

SetSpellGlobeInputTags 为被动按钮设置输入操作标签

SpellGlobe_Passive_1 SpellGlobe_Passive_2 BPGraphScreenshot_2024Y-02M-02D-08h-01m-50s-117_00

现在可以装备解锁的被动技能 image image

6. Multiple Level Up Rewards 多级升级奖励

需要修复 一次性升级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);
    }
}

修复装备技能未使用客户端RPC的错误

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);
}

8. Passive Ability Activation 被动技能激活

被动技能不是通过输入操作激活,而是在装备时激活,在取消装备时取消激活。

配置被动技能GA_HaloOfProtection,GA_LifeSiphon ,GA_ManaSiphon通过服务器执行

类默认值-高级-net execution policy-server initiated 在服务器初始化,在客户端也执行。 这是因为被动技能的特殊性。

image

技能事件调试

事件图表: event activateAbility print string

event onEndAbility print string image

技能系统组件中激活,取消被动技能

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());
        }
    }
}

空间控制器修改对应的方法名 GetSlotFromAbilityTag

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);
    }
}

10. Passive Niagara Component 被动粒子

各种减益,被动粒子组件,监听对应的游戏标签变动事件。

技能系统组件添加被动效果激活委托

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);
    }
}

基于 NiagaraComponent 新建被动粒子组件 C++ PassiveNiagaraComponent

image

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);
}

为BP_AuraCharacter角色设置被动技能粒子组件的粒子资产和被动技能标签

HaloOfProtectionNiagaraComponent 组件-细节-Niagara系统资产-NS_Halo image image

HaloOfProtectionNiagaraComponent 组件 设置被动技能标签 HaloOfProtectionNiagaraComponent 组件-细节-PassiveSpellTag-Abilities.Passive.HaloOfProtection image

LifeSiphonNiagaraComponent 组件-细节-Niagara系统资产-NS_LifeSiphon LifeSiphonNiagaraComponent组件-细节-PassiveSpellTag-Abilities.Passive.LifeSiphon

ManaSiphonNiagaraComponent 组件-细节-Niagara系统资产-NS_ManaSiphon ManaSiphonNiagaraComponent 组件-细节-PassiveSpellTag-Abilities.Passive.ManaSiphon

现在装备被动技能会显示对应的粒子特效 image

WangShuXian6 commented 9 months ago

28. Arcane Shards 奥术碎片

1. Magic Circle 魔法阵

魔法阵技能,将为地面贴上圆形贴画Decal

创建奥术碎片文件夹 Content/Blueprints/AbilitySystem/Aura/Abilities/Arcane/ArcaneShards/

基于 Actor 创建奥术碎片的贴花组件Actor C++ MagicCircle

image

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);

}

基于 MagicCircle 创建魔法圆形贴花蓝图 BP_MagicCircle

Content/Blueprints/AbilitySystem/Aura/Abilities/Arcane/ArcaneShards/BP_MagicCircle.uasset

MagicCircleDecal 组件-细节-贴花-贴花材质-M_MagicCircle_1 image image

贴花在投射到物体上才能可视化。

将 BP_MagicCircle 拖入场景,放置在墙上上 这是因为该贴花面向侧面显示。不是从上到下显示。 image

BP_MagicCircle中 旋转 MagicCircleDecal 组件 使其X轴向上 image 这样可以在地面显示。 image

贴花立方体内的物体都会被贴上贴花材质,所需需要将贴花组件高度变小,使其尽量不贴在角色身上。 image

当前会贴在角色脚上 image

2. Spawning Magic Circles 生成魔法阵

使用玩家控制器,先在本地生成魔法阵

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;

设置 BP_AuraPlayerController 玩家控制器的魔法阵类为 BP_MagicCircle

Magic Circle Class-BP_MagicCircle image

事件图表中显示魔法阵

event beginPlay

ShowMagicCircle delay HideMagicCircle

BPGraphScreenshot_2024Y-02M-02D-19h-47m-29s-588_00

运行后,将显示魔法阵5秒,始终跟随鼠标位置。 image

旋转魔法阵的贴花 BP_MagicCircle

打开 BP_MagicCircle 事件图表: MagicCircleDecal add local rotation multiply BPGraphScreenshot_2024Y-02M-02D-19h-56m-14s-689_00

3. Magic Circle Interface Functions 魔法阵接口函数

魔法阵依赖玩家接口,不依赖玩家。 将魔法阵的材质设为参数,可定制。

公开魔法阵贴花

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();
    }
}

移除BP_AuraCharacter玩家控制的生成魔法阵测试节点

image

基于 M_MagicCircle_1 创建材质实例 MI_MagicCircle_1

BP_AuraCharacter 玩家生成魔法阵

事件图表: event beginPlay self ShowMagicCircle image

使用材质实例 MI_MagicCircle_1 [可选,否则使用默认材质] image

删除测试节点。

4. Arcane Shards Spell 奥术碎片技能

添加 奥术碎片技能标签 Abilities.Arcane.ArcaneShards

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")
        );
}

基于 AuraDamageGameplayAbility 创建奥术碎片技能蓝图 GA_ArcaneShards

Content/Blueprints/AbilitySystem/Aura/Abilities/Arcane/ArcaneShards/GA_ArcaneShards.uasset

设置技能标签 为 Abilities.Arcane.ArcaneShards ability tags-Abilities.Arcane.ArcaneShards image

伤害类 damage effect class-GE_Damage image

damage type-Damage.Arcane image

DA_AbilityInfo 技能信息中添加 GA_ArcaneShards

image

WBP_OffensiveSpellTree 技能树控件中添加 GA_ArcaneShards 的技能标签 Abilities.Arcane.ArcaneShards

选择第三列的 WBP_SpellGlobe_Button_6 细节-ability tag-Abilities.Arcane.ArcaneShards image

现在,技能树中可以使用解锁 GA_ArcaneShards 技能 image image image

5. Wait Input Press 等待按键按下

角色中操作显示魔法阵是显示隐藏光标

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;
    }
}

GA_ArcaneShards 技能 激活时显示魔法阵

事件图表: sequence ShowMagicCircle ability-get avatar actor from actor info

等待按下按键 wait input press 按下时隐藏魔法阵 ability-get avatar actor from actor info HideMagicCircle

现在,第一次按下奥术碎片技能输入键,生成魔法阵,再按一次技能输入键,魔法阵消失。

end ability

结束节能之前需要延迟一段时间,否则第二次按下会立即出发第二次技能生成魔法阵。 delay

BPGraphScreenshot_2024Y-02M-02D-21h-29m-42s-562_00

6. Anti Aliasing and Moving Decals 消除混叠和移动贴花

设置抗锯齿,使魔法阵贴花移动式更加顺畅

项目设置-引擎-渲染-默认设置-抗锯齿方法: 默认为 临时超分辨率(tsr) 这是最昂贵的抗锯齿方法,也是效果最好的。 但是,这与贴花不兼容。会导致移动贴花会显示尾迹。

应改为 多重取样抗锯齿(MSAA) 不再显示尾迹。 image

项目设置-引擎-渲染-默认设置-动态模糊-取消 防止移动时 贴花显示出现模糊化 image

7. Point Collection 收集点

奥术碎片技能,释放后会显示冰刺,按等级增多。需要自动或手动计算生成位置的点。 需要考虑再非平面的生成。

基于 Acot 创建 生成点Actor C++ PointCollection

image

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();
}

基于 生成点 PointCollection 创建生成点蓝图 BP_PointCollection

Content/Blueprints/AbilitySystem/Aura/Abilities/Arcane/ArcaneShards/BP_PointCollection.uasset

添加 billboard 公告板组件到 Pt 1组件下用于显示 添加 billboard 公告板组件到 Pt 0组件下用于显示 image 公告板组件0-精灵-sprint-TargetIcon 公告板组件1-精灵-sprint- TargetIconSpawn image

按顺序设置每个组件的位置 移动 pt1距离 pt0 250个单位 100,0,0 image

点和点的距离不小于250单位。任意方向都可以 因为奥术碎片技能效果半径约200,防止重叠。

依次为每个点添加 公告板组件,调整点位置 进入顶视图调整位置。 序号越小,距离中心点越近。 image

GA_ArcaneShards 技能中使用点集合位置生成点

事件图表:

从鼠标位置开始生成 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

BPGraphScreenshot_2024Y-02M-02D-23h-10m-17s-930_00

image 可以在斜坡上正确生成点 image

将获取的地面点提升为变量,防止每次随机循环生成新的地面点集合,导致位置布局与组件调整的设置不同 GetGroundPoints 提升为变量 GroundPoints 这样也可以使用缓存的GroundPoints,提升性能 BPGraphScreenshot_2024Y-02M-02D-23h-16m-50s-428_00

8. Async Point Locations 异步生成点位置

GA_ArcaneShards 使用时间轴按间隔一个个生成点

事件图表: 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

BPGraphScreenshot_2024Y-02M-02D-23h-41m-36s-432_00

9. Gameplay Cue Notify Burst

适合一次性游戏效果的cue,例如声音,碎片。 静态,未实例化,不能保持状态。 有自己的游戏队列通知。 无法执行延时等时间操作。

添加游戏性显示标签 GameplayCue.ArcaneShards

项目设置-标签管理器 image

基于 GameplayCueNotify_burst (GCN Burst) 创建游戏性显示 蓝图 GC_ArcaneShards

Content/Blueprints/AbilitySystem/GameplayCueNotifies/GC_ArcaneShards.uasset

该cue自动设置好 gameplay cue tag-GameplayCue.ArcaneShards 最后新建的标签 image 但在5.3中这会有bug, 必须先将此标签改为其他标签,再改回来才会生效。

重载函数 On Burst

image print string

image

GA_ArcaneShards 技能执行 GC_ArcaneShards

事件图表:

execute gameplayCueWithParams on owner execute gameplayCueWithParams on owner-gameplay cue tag-GameplayCue.ArcaneShards

make Gameplay Cue Parameters

image

施放技能后,每次生成1个点会执行依次 cue

GC_ArcaneShards

类默认值-GCN effect-burst effect-burst particles 添加一组

image

这将在玩家处生成粒子特效,数量与生成点一样。 image

类默认值-GCN effect-burst effect-burst sounds 添加一组 image

GA_ArcaneShards 设置生成位置

事件图表 get world location 提升为变量 ShardSpawnLocation 否则每次都会变更导致cue与生成点位置不同

ShardSpawnLocation BPGraphScreenshot_2024Y-02M-03D-00h-46m-27s-535_00

image

现在,释放技能可以生成粒子和音效。 服务端与客户端鼠标移动时的法阵贴花不会一致。因为这是预测键的性能优化行为。 服务端与客户端生成的调试球点位置不一致。因为调试信息不是预测动作。 但是粒子位置却一致。cue粒子是预测动作。

10. Arcane Shards Montage 奥术碎片动画蒙太奇

为奥术碎片技能添加事件标签 Event.Montage.ArcaneShards

项目设置-标签管理器-Event.Montage.ArcaneShards image

基于 Cast_ArcaneShards 创建动画蒙太奇 AM_Cast_ArcaneShards

Cast_ArcaneShards 启用根运动 Content/Assets/Characters/Aura/Animations/Abilities/AM_Cast_ArcaneShards.uasset

AM_Cast_ArcaneShards

扭曲运动

默认通知轨道名: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 旋转以面对该目标

image image

添加通知轨道:Events

决定施法攻击的时机

右键-添加通知-AN_MontageEvent 【自定义的通知】 在法杖尖位置 选择 AN_MontageEvent -动画通知-event tag-Event.Montage.ArcaneShards 用以在游戏中监听

image

标签蒙太奇键值对数组中会将此标签与此蒙太奇关联。 用以选择此蒙太奇上的插槽位置,生成重叠虚拟球。 播放此蒙太奇可触发通知事件,带有事件标签 Event.Montage.ArcaneShards 后续通过标签监听此事件。

添加通知轨道:Sounds 攻击音效

添加通知-播放音效-

GA_ArcaneShards 技能 播放动画蒙太奇 AM_Cast_ArcaneShards

事件图表: 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

BPGraphScreenshot_2024Y-02M-03D-01h-41m-30s-713_00

现在可以释放技能,生成粒子,音效。

11 Radial Damage Parameters 径向损伤参数

距离奥数碎片技能生成的水晶越远,伤害越小。 引擎自带该静态函数。

通过两个半径来计算伤害。 内半径伤害固定。 内半径至外半径伤害开始衰减。

添加径向损伤伤害参数类型

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;
}

12 Setting Radial Damage Parameters 设置径向损伤参数

技能系统组件库中添加设置径向损伤参数函数工具

并在应用伤害效果时设置径向损伤参数到效果情景中

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);
    }
}

13 Radial Damage with Falloff 径向损伤的伤害衰减

在效果情景句柄中使用,计算 径向损伤

战斗接口定义损伤委托

Source/Aura/Public/Interaction/CombatInterface.h

DECLARE_MULTICAST_DELEGATE_OneParam(FOnDamageSignature, float /*DamageAmount*/);

public:
    virtual FOnDamageSignature& GetOnDamageSignature() = 0; 

覆盖AuraCharacterBase中的TakeDamage*

受到伤害时,广播该事件,附带伤害值

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);
}

14 Tying Radial Damage All Together 将径向损伤捆绑在一起

准备在奥术碎片技能中造成径向损伤

伤害技能 制作伤害效果参数时添加技能生成物原点向量参数

删除变量 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;
}

CT_Damage 伤害曲线表添加奥术碎片伤害曲线 Abilities.ArcaneShards

1,10 40,200 自动 image

GA_ArcaneShards 奥术碎片技能设置伤害曲线值

damage-1,CT_Damage,Abilities.ArcaneShards 没有debuff RadialDamageInnerRadius-25 RadialDamageOuterRadius-250 image

事件图表:

绘制调试用的内球和外球 RadialDamageInnerRadius RadialDamageOuterRadius draw debug sphere image image

对外半径内的所有敌人目标造成伤害 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 BPGraphScreenshot_2024Y-02M-07D-23h-39m-57s-313_00 现在可以对碎片周围的敌人造成径向伤害。

计算伤害数组中 如果伤害值小于等于0,则不再继续计算本次伤害

防止后续多次绑定无效伤害委托

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);
}

GA_ArcaneShards 优化事件图表

ShardSpawnLocation image

折叠到函数 StoreShardSpawnLocation BPGraphScreenshot_2024Y-02M-07D-23h-53m-56s-518_00

变量 Overlapping Players 重命名为 PlayersToDamage PlayersToDamage image

折叠到函数 StorePlayersToDamage BPGraphScreenshot_2024Y-02M-07D-23h-58m-18s-268_00

折叠到函数 RadialDamageToPlayer

BPGraphScreenshot_2024Y-02M-08D-00h-03m-28s-536_00 BPGraphScreenshot_2024Y-02M-08D-00h-03m-49s-179_00

15 Ignore Enemies while Magic Circle Active 魔术阵激活时忽略敌人

激活魔术阵时,鼠标选中目标时,不再高亮目标.

鼠标默认跟踪可见性通道。 当激活魔法阵时,跟踪其他通道

创建通道,排除玩家和敌人,表示不跟踪玩家和敌人

项目设置-引擎-碰撞-object channels-添加 ExcludePlayers 通道,默认block image image

设置 BlockingVolume 空气墙阻挡体积碰撞预设忽略 ExcludePlayers 通道

image 当鼠标通道 设置为可见性通道或 ExcludePlayers 通道时,忽略鼠标。防止鼠标选中空气墙。

将 BP_EnemyBase 敌人基类胶囊体组件,网格体组件,武器组件 碰撞预设设置 忽略 ExcludePlayers 通道

表示在魔法阵激活时不被鼠标选中。 直接在基类中设置,不需要为每个敌人单独设置。

BP_EnemyBase-胶囊体组件-碰撞预设-忽略 ExcludePlayers 通道 image

BP_EnemyBase-网格体组件-碰撞预设-忽略 ExcludePlayers 通道 image

BP_EnemyBase-weapon组件-碰撞预设-忽略 ExcludePlayers 通道 image

将 BP_AuraCharacter 玩家基类胶囊体组件,网格体组件,武器组件,弹簧臂碰撞盒子 碰撞预设设置 忽略 ExcludePlayers 通道

表示在魔法阵激活时不被鼠标选中。

BP_AuraCharacter-胶囊体组件-碰撞预设-忽略 ExcludePlayers 通道 image

BP_AuraCharacter-网格体组件-碰撞预设-忽略 ExcludePlayers 通道 image

BP_AuraCharacter-weapon组件-碰撞预设-忽略 ExcludePlayers 通道 image

BP_AuraCharacter-弹簧臂碰撞盒子组件 Box-碰撞预设-忽略 ExcludePlayers 通道 image

为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();
    }
}

16 Knockback Force and Death Impulse Overrides 击退力和重写死亡冲动

奥术碎片生成时,BP_Goblin_Spear 敌人不应直接被垂直击飞,应该偏向水晶中心的外侧

基于BP_EnemyBase 重新创建 子蓝图 BP_Goblin_Spear

BP_Goblin_Spear 设置: 胶囊体 image

网格体 image

weapon image

溶解材质 image

强制删除旧版 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;
}

GA_ArcaneShards 奥术碎片技能中重新设置 MakeDamageEffectParamsFromClassDefaults 的参数

RadialDamageToPlayer 函数图表: 目标位置减去技能生成位置,作为击退方向和死亡冲击方向 get actor Location

ShardSpawnLocation

MakeDamageEffectParamsFromClassDefaults-PitchOverride-45 BPGraphScreenshot_2024Y-02M-08D-14h-52m-14s-167_00

现在,击飞时,敌人远离水晶中点。

17 Spell Descriptions 雷电技能和奥术碎片技能的技能描述

目前使用了 伤害技能C++作为雷电和奥术碎片的技能基类。 应该为此创建基于伤害技能的子类技能:雷电基类C++和奥术碎片基类C++

基于伤害技能 AuraDamageGameplayAbility C++ 派生C++类 ArcaneShards

image

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);  
}

基于光束技能 AuraBeamSpell C++ 派生C++类 Electrocute

image

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_Electrocute-类设置-父类-Electrocute image

设置GA_ArcaneShards奥术碎片技能的父类为 ArcaneShards

GA_ArcaneShards-类设置-父类-ArcaneShards image

置GA_ArcaneShards奥术碎片技能的 水晶数量

事件图表: MaxNumShards 替换 NumPoints 删除变量 NumPoints image image

18 Arcane Shards Cost and Cooldown 奥术碎片成本和冷却

创建技能消耗曲线表格 CT_SpellCost

用以替换之前的阶梯表格

image Content/Blueprints/AbilitySystem/Data/CT_SpellCost.uasset 其他-曲线表格-cubic image

曲线 FireBolt 1 2.5 40, 50 自动

曲线 Electrocute

1, 1 40, 28 自动

曲线 ArcaneShards

1,10 40,85

雷电技能设置 消耗技能效果 GE_Cost_Electrocute

1,CT_SpellCost,Electrocute image

火球术技能设置 消耗技能效果 GE_Cost_FireBolt

1,CT_SpellCost,FireBolt image

删除阶梯消耗数据表格 CT_Cost

基于 GameplayEffect 新建 奥术碎片消耗技能效果蓝图 GE_Cost_ArcaneShards

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

每次释放奥术碎片术,消耗魔力点。 image

GA_ArcaneShards 技能设置 消耗技能效果

costs gameplay effect class-GE_Cost_ArcaneShards image

添加奥数碎片技能冷却效果标签 Cooldown.Arcane.ArcaneShards

项目设置-标签管理器-添加 Cooldown.Arcane.ArcaneShards image

基于 GameplayEffect 新建 奥术碎片冷却技能效果蓝图 GE_Cooldown_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秒,然后自行消失】

image

GA_ArcaneShards 技能设置冷却效果

类默认值-cooldown gameplay effect class-GE_Cooldown_ArcaneShards image

DA_AbilityInfo 技能信息资产中配置奥数碎片技能的冷却标签

Cooldown Tag-Cooldown.Arcane.ArcaneShards image

修复GA_ArcaneShards 奥术碎片技能生成物位置错误

事件图表: 使用等级生成技能生成物数量,限制为MaxNumShards MaxNumShards min get ability level 提升为变量 NumShards

NumShards image

生成水晶时使用 NumShards 替换 MaxNumShards NumShards image BPGraphScreenshot_2024Y-02M-08D-16h-05m-05s-030_00

GA_ArcaneShards 奥术碎片技能应用冷却效果和消耗

在生成第一个碎片时执行技能消耗 commit ability cost image

激活技能时首先检查技能消耗 check ability cost 成功时才继续 branch image

删除清除计时器后的延时节点delay 销毁技能生成物后提交技能冷却效果 commit ability cooldown image BPGraphScreenshot_2024Y-02M-08D-16h-14m-18s-717_00

WangShuXian6 commented 9 months ago

29 Fire Blast 火焰爆炸

生成多个火球,冲向四周。

1 FireBlast Ability 火焰爆炸技能

基于 AuraDamageGameplayAbility c++ 创建派生C++ AuraFireBlast 火焰爆炸技能

image

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);
}

添加 FireBlast 技能 标签 Abilities.Fire.FireBlast

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")
        );
}

基于 AuraFireBlast 创建 GA_FireBlast 技能蓝图

Content/Blueprints/AbilitySystem/Aura/Abilities/Fire/FireBlast/GA_FireBlast.uasset

配置伤害效果和技能标签 image

image

配置技能信息数据 DA_AbilityInfo

添加新技能信息 image

WBP_OffensiveSpellTree 技能树分配新技能标签

Content/Blueprints/UI/SpellMenu/WBP_OffensiveSpellTree.uasset 火焰系技能第二个图标 image

image

image

升级到2级可解锁技能 GA_FireBlast image image image

FireBlast Cost and Cooldown 火焰爆炸成本和冷却时间

CT_SpellCost 添加火焰爆炸技能消耗曲线 FireBlast

image

1,15 40,150

自动平滑

基于 GameplayEffect 创建火焰爆炸技能消耗效果 GE_Cost_FireBlast 蓝图

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

image

GA_FireBlast 技能使用 GE_Cost_FireBlast 消耗效果

image

事件图表: 提交消耗效果 event activateAbility commitAbilityCost

image 现在施放技能将会消耗魔力。

创建火焰爆炸技能冷却标签 Cooldown.Fire.FireBlast

项目设置 image

基于 GameplayEffect 创建火焰爆炸技能冷却效果 GE_Cooldown_FireBlast 蓝图

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秒,然后自行消失】

image

GA_FireBlast 技能使用 GE_Cooldown_FireBlast 技能冷却效果

image

事件图表: 提交消耗和冷却效果 commitAbility end ability

image

配置技能信息数据 DA_AbilityInfo

添加 Cooldown.Fire.FireBlast 冷却 效果标签 image

现在释放技能后,显示冷却倒计时。 image

Aura Fire Ball 火球

GA_FireBlast 技能将生成多个火球,然后火球爆炸。

将 GA_FireBlast 技能 设为默认技能用于快速测试

设置默认输入操作标签 image

BP_AuraCharacter 添加一个默认技能 GA_FireBlast 用以快速测试

image

现在,初始将激活 GA_FireBlast 技能 image image

更新 父类 AuraProjectile 的 重叠事件,使其可重写,添加 virtual

因为 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);

基于 AuraProjectile C++ 派生 AuraFireBall C++ 表示 GA_FireBlast 技能生成的火球actor

image

这与火球术生成的火球不同,因为它具有额外的属性。

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)
{
}

AuraFireBlast 技能生成火球

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*>();
}

基于 AuraFireBall 创建 火球蓝图 BP_FireBall

Content/Blueprints/AbilitySystem/Aura/Abilities/Fire/FireBlast/BP_FireBall.uasset

添加 Niagara 粒子系统组件重命名为 FireBallEffect image

FireBallEffect-细节-Niagara系统资产-NS_Fireball

image image

BP_FireBall 根组件设置碰撞爆炸效果 impact

image

设置移动组件

该火球不使用自带的移动组件来移动,由技能控制火球移动

所以将速度设为0 image image

取消tick image

取消自动激活 image

GA_FireBlast 配置火球actor为 BP_FireBall

配置火球actor image

Spawning FireBalls 生成火球

AuraFireBlast 技能生成火球

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;
}

GA_FireBlast 生成火球

事件图表 SpawnFireBalls

image

BP_FireBall 暂时启用移动组件的火球速度和自动激活,tick用以测试

image image image image

现在施放火球爆炸技能将绕垂直地面360度均匀生成12个火球,发射到四周 image

FireBall Timelines 火球时间轴

通过时间线,使火球飞出去,减速,再飞回来,飞到玩家的位置,且定位到玩家的新位置。

AuraFireBall 火球actor添加时间线函数

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();
}

AuraProjectile 公开碰撞函数到蓝图

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;
}

火焰爆炸的 BP_FireBall 火球actor蓝图 实现 StartOutgoingTimeline

禁用移动组件的速度,tick,自动激活,防止火球通过移动组件自动运动。 image image image

事件图表:

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

自动平滑 image

获取初始位置和飞出去目标之间的向量 InitialLocation ApexLocation lerp(Vector) alpha =0 使用了a初始位置 alpha=1 使用了b 飞出去的目标位置

lerp(Vector) alpha 使用OutgoingTimeline 时间轴 的 轨道 OutgoingTrack 的值

set actor location 位置为初始位置和飞出去的顶点位置,通过时间轴插值 使用lerp返回值作为新位置

BPGraphScreenshot_2024Y-02M-24D-12h-38m-01s-672_00

现在施放火球爆炸技能,火球将360的方向向四周先加速后减速【时间轴控制速度】飞出1600cm然后停止。然后等待生命周期结束后销毁。

飞出后完成,开始返回时间轴 add timeline :ReturningTimeline

添加浮点轨道ReturningTrack 飞回时间 1 秒 0,0 1,1

自动平滑 先减速再加速 image

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

检查飞回后火球距离玩家的距离是否小于 ExplodeDistance get actor location 火球位置

ReturnToActor-get actor location 玩家位置

vector length <= ExplodeDistance 作为 branch 条件

折叠到函数 isWithinExplodeDistance 设为纯函数 image

BPGraphScreenshot_2024Y-02M-24D-13h-07m-29s-286_00

爆炸 on hit destroy actor

BPGraphScreenshot_2024Y-02M-24D-13h-09m-36s-067_00

施放火球爆炸技能后,火球向四周飞出1600cm,然后飞回玩家的实时位置250cm处发生爆炸,然后销毁火球。

image

Causing FireBall Damage 造成火球伤害

CT_Damage 曲线表添加火球爆炸伤害曲线 Abilities.FireBlast

image

1,15 40,150 自动平滑

GA_FireBlast 配置伤害值

image

抛射物添加函数判断重叠的攻击目标是否有效

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);
        }
    }
}

现在火球爆炸技能可以造成伤害

FireBall Explosive Damage 火球爆炸伤害

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;
}

火球蓝图BP_FireBall设置爆炸伤害参数

事件图表: 用以测试 ExplosionDamageParams break DamageEffectParam

SetIsRadialDamageEffectParam-isRadial-true branch 检查 break DamageEffectParam-isRadialDamage print string-isRadialDamage

image 每个火球爆炸时显示调试信息

删除节点

命中后设置径向伤害参数 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

火球的位置 get actor location

相减得出得到向量长度提升为变量 KnockbackDirection

改变击退方向 KnockbackDirection Make Rot from X make rotator-y-45 get forward vector

get forward vector输出至 SetKnockbackDirection -KnockbackDirection

折叠到函数 OverrideKnockbackPitch 设为纯函数 BPGraphScreenshot_2024Y-02M-24D-15h-43m-09s-930_00

ExplosionDamageParams SetKnockbackDirection SetKnockbackDirection-Magnitude-提升为变量 KnockbackMagnitude 默认800 KnockbackDirection

ExplosionDamageParams SetDeathImpulseDirection SetDeathImpulseDirection-Magnitude-提升为变量 DeathImpulseMagnitude 默认600 KnockbackDirection

ExplosionDamageParams apply damage effect

循环完成后销毁火球

BPGraphScreenshot_2024Y-02M-24D-15h-49m-03s-169_00

现在,火球爆炸技能,飞出后返回,返回后爆炸,如果爆炸半径内有敌人,将再次造成伤害和击退,死亡效果。

Empty Cooldown Texture 空冷却纹理

在技能冷却期间,替换技能操作栏的技能,技能图标替换后,原位置的技能倒计时动画依然在进行。 且冷却完成后,技能图标成为默认的白色纹理。

WBP_SpellGlobe 重设技能按键后,停止技能冷却倒计时

image

ReceiveAbilityInfo 函数图表:

检测收到的技能标签,失败时清除冷却倒计时

CooldownTimerHandle Clear and Invalidate Timer by Handle

清除倒计时文本 Text_Cooldown Set Render opacity

image BPGraphScreenshot_2024Y-02M-24D-16h-03m-41s-897_00

Execute Local Gameplay Cues 执行本地游戏提示

默认的抛射物命中目标播放的粒子和音效都是普通效果,没有使用游戏cue复制。

游戏cue通过多播每帧可复制的限制当前为10个。 但是火球爆炸为12个火球,12个游戏cue效果

使用游戏性cue但是不通过RPC来网络复制,其他客户端将使用预测来显示本客户端的粒子和音效。

这可以极大优化性能。

添加火球爆炸游戏性cue标签

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;
}

AuraProjectile 抛射物基类命中函数设为虚函数 使子类可覆盖,使用本地游戏性cue重写

音效组件设为保护类型用以子类使用

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;
}

基于 GameplayCueNotify_burst (GCN Burst) 创建游戏性显示 蓝图 GC_FireBlast

Content/Blueprints/AbilitySystem/GameplayCueNotifies/GC_FireBlast.uasset

设置标签 image

设置粒子和音效 image

WangShuXian6 commented 8 months ago

Saving Progress 保存进度

1 Saving Progress 保存进度

需要保存和加载的数据列表:

1-技能的主要属性:str,int,dex等。 技能系统组件本身具由技能相关的数据。 通过游戏标签识别技能。所以通过保存技能的游戏标签来保存技能。 也需要保存技能的等级,所以需要数据结构来保存技能的相关信息。 image

2-玩家状态:关卡,技能点,属性点,经验值

image

3-玩家位置,玩家所在关卡,玩家职业,与玩家关联的其他编号,id。

保存数据的方法

1-保存到磁盘,即玩家的实际机器。适合单人游戏,更简单。不需要数据库。 但重要内容需要保存到数据库,例如购买,排行榜。

2-保存到云,数据库,需要通过网络传输数据。适合多人游戏。 可以加密敏感信息。 虽然加密可以破解,但仍具由一定程度的安全性。

现代专用服务器使用数据库保存。 专用服务器没有玩家。 玩家从其他地方连接到专用服务器。服务器连接到数据库,为玩家准备好信息。 为了防止作弊,服务器对数据拥有权限。服务器负责保存数据和加载数据后发送到客户端。 这需要通过api连接到数据库。

但记住密码功能,需要讲用户名和密码保存到本地磁盘。

本课程将数据保存到磁盘

但会设计以替换为保存到数据库。 只需要添加如何连接数据库和专用服务器功能即可。

2 Main Menu 主菜单

保存数据从加载屏幕开始 保存数据之前,需要菜单。

复制Dungeon关卡为MainMenu进行音量后处理设置

打开 MainMenu 删除玩家,敌人,导航NavMeshBoundsVolume,边界BlockingVolume

MainMenu关卡的 gamemode 游戏模式设置为 none,以使用默认游戏模式,生成默认pawn

image

image

但此时可以控制镜头四处移动,该问题需要稍后禁用输入操作解决。

将默认pawn :PlayerStart 移动到自定义的位置

用以决定摄像机默认观看位置 image

菜单关卡展示一个aura角色动画,为加载屏幕制作特殊类型的角色蓝图 BP_Aura_MainMenu

基于 Actor 创建特殊角色蓝图 Content/Blueprints/Character/Aura/BP_Aura_MainMenu.uasset image image

BP_Aura_MainMenu 添加骨骼网格体组件 Skeletal Mesh 改名为 AuraMesh

image

AuraMesh-骨骼网格体资产-SKM_Aura image

AuraMesh-动画模式-使用动画资产 AuraMesh-要播放的动画-AuraPose 动画序列 image

AuraMesh 子级添加 Niagara 组件 改名为 Fireball

image image

Fireball -Niagara 系统资产-NS_Fire_4 Niagara系统 调整Niagara火到人物右手位置 image

将 BP_Aura_MainMenu 放到默认pawn 前方以展示角色

image image

BP_Aura_MainMenu -AuraMesh 子级添加骨骼网格体组件 Skeletal Mesh 改名为 Staff 法杖

image Staff-骨骼网格体资产-SKM_Staff 骨骼网格体 Staff-插槽-父项套接字-WeaponHandSocket 法杖附加到左手

image

BP_Aura_MainMenu -Fireball 火球在每帧做正弦上下摆动运动

通过改变火球的位置。

事件图表: event tick

正选函数需要增量运行时间 Delta Seconds + Time 的值 提升为变量 Time Time 将不断累加增加时间。

正弦函数的周期为 2派,2个圆周率,3.14 * 2 每当时间运行到 6.28318时,将 Time重置为0. 防止Time 不断增加导致数值溢出

6.28318 branch set time=0

折叠为函数 UpdateTime image

image

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 image

用该值设置火球的位置 Z值 event beginplay

Fireball get world location 提升为变量 FireballInitialLocation 存储火球初始位置 image

event tick

然后每一帧为其位置 z 增加 Time FireballInitialLocation
add 正弦值 image

正弦逻辑折叠为纯函数 SinTime image

相加后的值设置为火球世界位置 Fireball set world location

image 现在火球开始上下浮动

调整振幅 Amplitude 改变上下移动的距离。

BP_Aura_MainMenu 添加Decal 魔法阵贴花组件 改名 MagicCircleDecal

image image

复制 MI_MagicCircle_1 为 MI_MagicCircle_MainMenu

贴花材质-MI_MagicCircle_MainMenu image

移动贴花使其可以贴在角色后的墙面 image image

BP_Aura_MainMenu 使贴花旋转

事件图表 event tick 每一帧添加局部旋转

MagicCircleDecal Add Local Rotation

添加变量 MagicCircleRotationRate 浮点 旋转速率 默认-2 获取世界时间增量改变贴花的 x Roll get world delta seconds Multiply MagicCircleRotationRate image

F11 预览

添加 MainMenu 文件夹

image

基于 UserWidget 创建 标题控件 WBP_Title

Content/Blueprints/MainMenu/WBP_Title.uasset

覆层 所需

文本 主标题 image

文本 副标题 下对齐 image

BP_Aura_MainMenu 添加 widget 控件组件 改名 TitleWidget

不在骨骼子级,防止控件随骨骼动画移动 image

TitleWidget-用户界面-控件类-WBP_Title TitleWidget-用户界面-空间-屏幕 TitleWidget-用户界面-以所需大小绘制-启用 防止控件大小错误 image image

运行时才可以看见该控件

TitleWidget-用户界面-空间-场景 使用世界坐标 角色可以阻挡控件 image

旋转调整 TitleWidget image image

TitleWidget-用户界面-几何体模式-圆柱体 控件将弯曲 image image TitleWidget-用户界面-圆柱体弧形角度 可以调整弯曲度

image

3 Play and Quit Buttons 播放和退出按钮

使用 BP_Aura_MainMenu actor 来添加按钮和音效

Music_MainMenu 音波-音效-正在循环-启用

image

BP_Aura_MainMenu

事件图表

event beginplay play sound 2d-Music_MainMenu 音波

image

按钮控件 基于 UserWidget 创建 标题控件 WBP_MainMenu

Content/Blueprints/UI/MainMenu/WBP_MainMenu.uasset 仅作基础控件,所以使用 canvas 画布画板

canvas 画布画板

WBP Wide Button 开始 调整位置 锚点居中下

WBP Wide Button 退出 调整位置 锚点居中下

image

BP_Aura_MainMenu 中将 WBP_MainMenu 添加到视图

事件图表

event beginplay create widget-WBP_MainMenu add to viewport

输入模式设置为仅UI 将WBP_MainMenu设置为焦点 get player controller set input mode UI only 显示鼠标 set show mouse cursor

image

image

新建关卡 LoadMenu 用以点击开始按钮后进入,用以加载数据

复制 MainMenu 关卡为 LoadMenu

LoadMenu 删除 BP_Aura_MainMenu

WBP_MainMenu 开始,退出按钮添加单击事件

image

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 image image

设置默认地图为 MainMenu

项目-地图和模式-默认地图-游戏默认地图-MainMenu image

4 Vacant Load Slot 空置装载槽

为 LoadMenu 创建加载菜单

可以在三种模式之间切换的控件:空插槽,已占用插槽,创建新游戏插槽。

基于 UserWidget 创建 空插槽控件 WBP_LoadSlot_Vacant

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 image

复制 WBP_LoadSlot_Vacant 为 WBP_LoadSlot_EnterName

Content/Blueprints/UI/LoadMenu/WBP_LoadSlot_EnterName.uasset

WBP_LoadSlot_Vacant

image

image

复制 WBP_LoadSlot_Vacant 为 WBP_LoadSlot_Taken

Content/Blueprints/UI/LoadMenu/WBP_LoadSlot_Taken.uasset

5 Enter Name Load Slot 输入名称加载槽

WBP_LoadSlot_EnterName

image

6 Taken Load Slot

点击 空插槽 WBP_LoadSlot_Vacant 进入 输入名称插槽 Enter Name Load Slot

选择 已占用插槽 WBP_LoadSlot_EnterName 进入游戏

WBP_LoadSlot_Taken

image

7 Load Menu 加载菜单

基于 UserWidget 创建 WBP_LoadMenu

Content/Blueprints/UI/LoadMenu/WBP_LoadMenu.uasset

设计器: 控件切换器

WBP_LoadSlot_Taken WBP_LoadSlot_EnterName WBP_LoadSlot_Vacant

image

图表: event construct Button_Quit get button assign on clicked Quit Game open level (by object reference)-level 提升为变量 默认 MainMenu image

使用 LoadMenu 关卡蓝图 将 WBP_LoadMenu 添加到视图

event beginplay create widget-WBP_LoadMenu add to viewport

get player controller set input mode UI only 显示鼠标 set show mouse cursor BPGraphScreenshot_2024Y-03M-05D-19h-50m-10s-450_00 image

8 MVVM

根据选择的插槽的不同,加载对应数据和地图.. image

1-截至目前,项目使用了MVC模型视图控制器架构。即控件+控件控制器+数据。 控件控制器从中获取数据。 控件控制器依赖模型。它引用了这些类【系统,技能系统组件,属性集类】

模型不依赖控件控制器。 控件控制器直接将数据广播到视图。 视图由控件组成。 image

2-但是虚幻新增了更强大的插件 ViewModel 模型视图。 https://russelleast.wordpress.com/2008/08/09/overview-of-the-modelview-viewmodel-mvvm-pattern-and-data-binding/ 模型:系统,技能系统组件,属性集

与MVC相似,模型视图充当模型和视图。 不同点:通常视图模型和视图是1对1的关系,两者直接绑定。【也可以多个视图对应1个视图模型】

image

视图模型类似控件控制器。 视图模型链接到视图,并绑定视图中的变量。 视图具由向用户显示内容的变量。 事件驱动。 视图模型作为容器,保存了视图的数据和功能。

之后将视图模型绑定到加载菜单控件。

9 Changed Needed for 5.3+ ue5.3的变更

手动设置视图模型问题

如果控件绑定的视图模型类型为手动,表示高视图模型是在控件外部的某处创建。 image

每个控件至少要和视图模型上的一个通知变量绑定。否则控件的视图模型无效。

BP_Aura_MainMenu 使用 spawn sound 2d 替代 play sound 2D 。使其可被垃圾收集。

spawn sound 2d return提升为变量 MainMenuMusic后使用音频 image BPGraphScreenshot_2024Y-03M-05D-20h-51m-06s-865_00

WBP_LoadMenu 聚焦问题

由于没有可聚焦控件,删除 set input mode UI only 的 输入参数 in widget to focus 留空

image

10 View Model Class 视图模型类

不再使用系统的控件切换器。

使用自定义切换控件。使其与插槽对应

基于 UserWidget 创建自定义切换器 WBP_LoadSlot_WidgetSwitcher

Content/Blueprints/UI/LoadMenu/WBP_LoadSlot_WidgetSwitcher.uasset

控件切换器 所需 WBP_LoadSlot_Vacant WBP_LoadSlot_EnterName WBP_LoadSlot_Taken

image

WBP_LoadMenu 将空间切换器和子级控件替换为 WBP_LoadSlot_WidgetSwitcher

WBP_LoadSlot_WidgetSwitcher 尺寸 256*300 【因为 WBP_LoadSlot_WidgetSwitcher是所需】 image image

每个 WBP_LoadSlot_WidgetSwitcher 都有自己的ViewModel

LoadMenu 关卡蓝图中,删除全部节点,不再使用常规方式添加控件到视图。改用 C++ 进行精细控制

image 将使用HUD控制。 在C++中创建他的视图模型。控制控件创建时机。 在控件获取自己的视图模型时,确保视图模型已创建好。

需要视图模型。 需要用于加载屏幕的游戏模式和HUD。

基于 AuraGameModeBase C++ 创建游戏模式蓝图 BP_LoadScreenGameMode

Content/Blueprints/Game/BP_LoadScreenGameMode.uasset image

LoadMenu 关卡 使用游戏模式 BP_LoadScreenGameMode

现在游戏模式 BP_LoadScreenGameMode可以控制HUD生成。 HUD可以创建,控制控件。 HUD可以创建,控制视图模型。

这是很好的关注点分离模式。

基于 HUD 创建 C++ LoadScreenHUD

image

Source/Aura/Public/UI/HUD/LoadScreenHUD.h

启用 UMG Viewmodel 视图模型 插件

image

基于 MVVMViewModelBase 创建 C++ MVVM_LoadScreen

作为加载屏幕控件的视图模型

Source/Aura/Public/UI/ViewModel/MVVM_LoadScreen.h

基于 UserWidget 创建 LoadScreenWidget C++ 加载屏幕控件

为其提供功能。 image

Source/Aura/Public/UI/Widget/LoadScreenWidget.h

基于 LoadScreenHUD 创建蓝图 BP_LoadScreenHUD

Content/Blueprints/UI/HUD/BP_LoadScreenHUD.uasset image

BP_LoadScreenGameMode 游戏模式使用 BP_LoadScreenHUD

image

现在进入 LoadMenu 关卡将自动加载 BP_LoadScreenHUD

现在可以在 BP_LoadScreenHUD 执行操作,创建控件,将控件添加到视图。设置变量,模型视图。关联。

11 Constructing a View Model 构造视图模型

在 BP_LoadScreenHUD 中创建控件。 需要控件C++类。

基于 LoadScreenWidget C++ 加载屏幕控件 创建蓝图 WBP_LoadScreenWidget_Base

Content/Blueprints/UI/LoadMenu/WBP_LoadScreenWidget_Base.uasset

这将作为其他控件的基类蓝图,为所有控件提供C++中的工具

将 LoadMenu 文件下的所有控件的父类设置为 WBP_LoadScreenWidget_Base image

包括: WBP_LoadMenu WBP_LoadSlot_EnterName WBP_LoadSlot_Taken WBP_LoadSlot_Vacant WBP_LoadSlot_WidgetSwitcher

LoadScreenHUD C++ 中将控件添加到视图

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();
}

BP_LoadScreenHUD 配置 LoadScreenWidgetClass 为 WBP_LoadMenu 用以将加载屏幕控件添加到视图

image

现在打开关卡 LoadMenu 将显示 WBP_LoadMenu 控件 image

基于 MVVM_LoadScreen c++ 创建视图模型蓝图 BP_LoadScreenViewModel

类似于控件控制器的功能,但有不同之处。 Content/Blueprints/UI/ViewModel/BP_LoadScreenViewModel.uasset image

蓝图版本视图模型也可以添加变量,函数

LoadScreenHUD C++ 中创建视图模型

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();
}

BP_LoadScreenHUD 配置 LoadScreenViewModelClass 视图模型类为 BP_LoadScreenViewModel

image

现在,打开 关卡 LoadMenu ,HUD构建了视图模型,然后构建了控件,添加到视图。

MVVM_LoadScreen 视图模型添加至少一个通知字段,用以绑定到控件,必须。

可以添加仅用于绑定的测试通知字段 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;
}

WBP_LoadMenu 关联视图模型 BP_LoadScreenViewModel

需要像控件控制器一样,可以在控件中,通过技能系统蓝图库,直接获取控件控制器。 视图模型通过 控件-设计器-窗口-视图模型 来关联视图模型 image 关联视图模型后需要重启视图模型窗口使其生效。 image

将视图模型BP_LoadScreenViewModel 的 Bind 通知字段绑定到WBP_LoadMenu 控件的 一个隐藏控件上 例如添加一个透明的文本-Text_Bind

image

窗口-视图绑定-添加控件 Text_Bind image

视图模型类型:

手动 -创建控件后手动为控件创建视图模型。

创建实例-每个控件创建独立的同名视图模型,不与其他控件共享视图模型的数据。

全局-全局唯一

属性路径-添加一个函数名称,控件可以调用该函数来获取模型视图

WBP_LoadMenu 使用 属性路径 FindLoadScreenViewModel 函数 来获取视图模型。 image 控件构造成功后,控件将自动调用控件的 FindLoadScreenViewModel 函数来获取视图模型。

将用于获取视图模型的函数定义到 WBP_LoadScreenWidget_Base 基类蓝图中,非HUD。 因为 WBP_LoadScreenWidget_Base 是加载菜单地图所有控件的基类控件。 这样所有子类都可以使用其中的函数获取视图模型来绑定到自身控件。

WBP_LoadScreenWidget_Base 图表中添加函数 FindLoadScreenViewModel 以获取视图模型 BP_LoadScreenViewModel

图表: 添加函数 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

BPGraphScreenshot_2024Y-03M-06D-00h-46m-36s-186_00

FindLoadScreenViewModel 函数需要设为常量 image 输出名为 ViewModel

现在 WBP_LoadScreenWidget_Base 的子类控件都将自动通过 FindLoadScreenViewModel 获取视图模型 BP_LoadScreenViewModel

图表: get BP_LoadScreenViewModel 已可以使用 image

现在 基于 WBP_LoadScreenWidget_Base 的控件和视图模型BP_LoadScreenViewModel已经关联。

可选的C++ 中获取视图模型

示例代码为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;
}

12 Load Slot View Model 加载槽的视图模型

WBP_LoadMenu 控件通过窗口关联了视图模型。

WBP_LoadMenu 中的 3个插槽控件也需要关联视图模型。 每个插槽控件实例需要获取独立的视图模型。因为他们的数据互相独立。 每个插槽都可以创建,编辑,进入游戏。可以独立保存各自的游戏状态数据。

这需要创建新的视图模型基类

基于 MVVMViewModelBase 创建 C++ MVVM_LoadSlot 用作插槽的视图模型,且各实例数据独立

在创建加载屏幕的视图模型后,创建 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"

加载屏幕 MVVM_LoadScreen 的视图模型将管理3个插槽的视图模型 MVVM_LoadSlot。

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();
}

基于 MVVM_LoadSlot C++ 创建插槽视图模型 蓝图 BP_LoadSlotViewModel

Content/Blueprints/UI/ViewModel/BP_LoadSlotViewModel.uasset

WBP_LoadMenu 控件的视图模型 BP_LoadScreenViewModel 配置 插槽视图模型类 LoadSlotViewModelClass 为 BP_LoadSlotViewModel

image

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"

加载菜单 LoadScreenHUD

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();
}

WBP_LoadScreenWidget_Base 基类控件添加插槽索引整数变量 SlotIndex

插槽控件可以继承 图表: SlotIndex 公开变量 image

WBP_LoadSlot_WidgetSwitcher 插槽控件添加函数InitializeSlot以初始化自身信息

接收参数 InSlot 整数,以设置自身的索引值 set SlotIndex image

为插槽视图模型添加通知字段仅用于绑定到插槽控件,没有业务逻辑功能

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;
}

将3个插槽控件各自绑定到蓝图版本视图模型 BP_LoadSlotViewModel

WBP_LoadSlot_EnterName WBP_LoadSlot_Taken WBP_LoadSlot_Vacant image

绑定类型-手动 将在插槽控件的父级控件插槽切换器中,为每个插槽设置视图模型。 image

将视图模型 BP_LoadSlotViewModel 的 Bind 通知字段绑定到3个插槽控件 控件的 一个隐藏控件上 例如添加一个透明的文本-Text_Bind image image image

窗口-视图绑定-添加控件 Text_Bind 3个插槽控件分别设置 image

插槽切换器控件为每个插槽设置视图模型。

注意:插槽切换器自身没有视图模型,也没有绑定视图模型。 但可以为子插槽设置视图模型。

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个子插槽的分别设置视图模型。 BPGraphScreenshot_2024Y-03M-06D-01h-25m-11s-067_00

WBP_LoadMenu 加载菜单空间实现 BlueprintInitializeWidget ,为插槽设置视图模型

注意,现在提到的索引,均指插槽视图模型的索引,关联到加载菜单中初始化的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

BPGraphScreenshot_2024Y-03M-06D-14h-14m-06s-692_00

13 Switching the Widget Switcher 切换插槽控件的切换

需要一种切换插槽控件的切换工具以使用切换器中的3种控件:新建,编辑,开始游戏。 在C++中定义。

MVVM_LoadSlot 视图模型中定义切换器切换委托 ,委托包含应当显示的插槽控件索引参数。

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);
}

3个插槽控件绑定 BP_LoadScreenViewModel

WBP_LoadSlot_EnterName WBP_LoadSlot_Taken WBP_LoadSlot_Vacant 都绑定 BP_LoadScreenViewModel 类型同样为属性路径 FindLoadScreenViewModel 因为每个插槽修需要访问和修改其他插槽的信息。

MVVM_LoadScreen 中定义切换器的3个控件的按钮事件

Source/Aura/Public/UI/ViewModel/MVVM_LoadScreen.h

Source/Aura/Private/UI/ViewModel/MVVM_LoadScreen.cpp