第一节 使用代码开发UE设置,以及创建基于类的蓝图
基础设置
首先设置项目的偏好向,方便后续开发。
打开编辑器偏好设置
设置使用的代码编辑器(rider Uproject)
关闭实时代码编写功能。
关闭自动编译新添C++类,我们直接在代码编辑器内编译
在项目设置中修改版权声明
修改打开资产时,资产所在位置
增加调试
开发最重要的是能够调试功能,找到问题出处。
打开选项
下载输入调试用符号,空间需要的比较大,其实也就下载不到20G的内容。
创建角色基础C++类
AuraCharacterBase.cpp
//Source/Aura/Private/Character/AuraCharacterBase.cpp
//所有角色的基类
// 构造函数
AAuraCharacterBase::AAuraCharacterBase()
{
// 基类不需要每一帧都触发事件
PrimaryActorTick.bCanEverTick = false;
//在角色中创建可视化的武器组件
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>("Weapon");
//将武器(Weapon 组件)附加到角色的骨骼系统上,具体附加的位置是角色的 "WeaponHandSocket" 插槽
Weapon->SetupAttachment(GetMesh(),FName("WeaponHandSocket"));
//给武器设置无需碰撞
Weapon->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
// Called when the game starts or when spawned
void AAuraCharacterBase::BeginPlay()
{
Super::BeginPlay();
}
这一行定义了类 AAuraCharacterBase
的构造函数。构造函数是一个特殊的函数,当这个类的对象被实例化时,构造函数会自动执行。
AAuraCharacterBase
是这个类的名称,表示它可能是一个继承自ACharacter
或类似类的角色类(以A
开头的类在 Unreal Engine 中通常代表一个Actor
,即可以在场景中放置的对象)。::AAuraCharacterBase()
表示这是类AAuraCharacterBase
的构造函数定义。构造函数没有返回值,名字与类名相同。
这行代码是设置 PrimaryActorTick
结构体的 bCanEverTick
属性为 false
,其含义是关闭这个角色对象的 Tick 功能。
PrimaryActorTick
是 Unreal Engine 中用于控制Actor
的 Tick 行为的一个成员变量。Tick
是指每帧都会被调用的函数,用于更新对象的状态或行为。bCanEverTick = false
:bCanEverTick
是一个布尔值。将它设置为false
表示关闭Tick
,即这个对象不会在每一帧都调用Tick()
函数。这样可以节省性能,避免不必要的计算,除非该对象确实需要在每帧更新。
CreateDefaultSubobject<USkeletalMeshComponent>("Weapon")
:CreateDefaultSubobject
是 Unreal Engine 中用于创建组件的一个模板函数,它通常在构造函数中被调用,用来初始化类的默认子对象。<USkeletalMeshComponent>
:模板参数,表示你要创建的对象是一个USkeletalMeshComponent
,这是 Unreal Engine 用于表示骨骼网格的组件,可以附加到一个Actor
上。"Weapon"
:这是传递给CreateDefaultSubobject
的参数,指定这个组件的名称。它会被用于标识该组件,以便引擎在场景中管理对象的内部层次结构。
为什么这段代码中不使用文本宏?
在 Unreal Engine 中,文本宏通常是指用于字符串处理的宏,比如 TEXT()
宏,它将字符串转换为 FString
或 FText
类型,确保代码兼容不同的字符编码(如 ANSI 和 Unicode)。TEXT()
宏在许多情况下会用来确保字符串的跨平台支持,尤其是在处理多字节字符集时。
CreateDefaultSubobject()
不要求 TEXT()
宏:
CreateDefaultSubobject
的参数是用来给组件命名的,它接受的是 C++ 字符串字面量,即普通的const char*
类型,不是FString
或FText
。因此,在这种场景下直接传递"Weapon"
即可,而无需使用TEXT()
宏。- Unreal Engine 内部在处理组件名称时通常会自动处理字符串的编码,不需要你手动使用
TEXT()
。
TEXT()
宏的适用场景:
TEXT()
宏主要用于将字符串转换为TCHAR
(兼容宽字符和多字节字符),适用于需要跨平台兼容、支持 Unicode 的场合,比如FString
或FText
类型的字符串。TEXT()
的典型使用场景是输出日志、UI 字符串或者其他需要FString
/FText
类型的文本。在像CreateDefaultSubobject()
这样的函数中,这些地方不需要宽字符或FString
****,因此不需要TEXT()
****。
SetupAttachment
:这是 Unreal Engine 用来将一个组件(子组件)附加到另一个组件(父组件)的方法。在这行代码中,武器被附加到角色的骨骼上。GetMesh()
:这是一个ACharacter
类的函数,返回角色的USkeletalMeshComponent
,即角色的骨骼网格。这通常表示角色本身的骨骼网格(如角色的身体模型)。在这种情况下,武器会附加到这个角色的骨骼上。FName("WeaponHandSocket")
:FName
是 Unreal 中用来表示字符串的高效类。这个参数表示在角色骨骼网格中名为"WeaponHandSocket"
的插槽(Socket)。插槽是一个在骨骼上定义的位置,用来附加其他对象,比如武器、装备等。"WeaponHandSocket"
在UE骨骼可视化编辑器中通过骨骼编辑器在角色的骨骼网格(USkeletalMeshComponent
)中定义的一个 Socket。通常,这个 Socket 是放置在角色的手上,用于装备武器
这段代码是将武器(Weapon
组件)附加到角色的骨骼系统上,具体附加的位置是角色的 "WeaponHandSocket"
插槽
AuraCharacterBase.h
//Source/Aura/Public/Character/AuraCharacterBase.h
//所有角色的基类
//标识为抽象类,不可直接拖拽至场景中
UCLASS(Abstract)
class AURA_API AAuraCharacterBase : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AAuraCharacterBase();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
//角色武器骨骼网格指针
UPROPERTY(EditAnywhere,Category="Combat")//在任意地方可以修改,分类为战斗
TObjectPtr<USkeletalMeshComponent> Weapon;
};
这是Unreal Engine中的一个宏,用来告诉引擎如何管理这个属性。它包含了两个参数:
- EditAnywhere:这意味着这个属性可以在编辑器中的任何地方编辑。无论是关卡蓝图,还是类的默认属性中,开发者都可以直接通过编辑器修改该变量的值。
- Category=“Combat”:这个参数指定了属性在编辑器中的分类。在UE5编辑器的细节面板中,
Weapon
这个属性会出现在一个叫做 “Combat” 的分类下。这样可以更好地组织变量,方便在编辑器中查找。
这一部分定义了一个名为 Weapon
的成员变量,它的类型是 TObjectPtr<USkeletalMeshComponent>
,这是UE5中常用的对象指针类型。
- TObjectPtr:这是UE5的一种智能指针类型,用于管理和引用
UObject
派生类对象的指针。它能够确保对象引用的有效性,同时帮助引擎进行垃圾回收(GC),从而避免指针悬挂或内存泄漏等问题。 - USkeletalMeshComponent:这是Unreal引擎中的一个组件类型,专门用于管理骨骼网格。骨骼网格是由骨骼和蒙皮(Skinning)组成的网格,常用于表示带有骨骼动画的角色或武器等对象。
GENERATED_BODY()
宏的主要作用是为类生成一些必要的代码,以便与 Unreal 引擎的内部系统进行通信。它会自动生成一些代码,如反射系统、属性管理、复制机制等,帮助你处理引擎的底层逻辑,而开发者无需手动编写这些复杂的部分。
具体功能
- 反射系统支持: Unreal 引擎的反射系统允许引擎在运行时查看类和对象的类型、属性等信息。
GENERATED_BODY()
宏生成了必须的代码,以便引擎可以识别你的类,并在编辑器、蓝图、序列化、网络复制等功能中使用。 - 属性和方法的元数据支持: 在 Unreal 引擎中,你可以使用宏(例如
UPROPERTY
、UFUNCTION
)为类的属性和方法添加元数据标签。这些标签决定了它们如何被引擎处理(如暴露给蓝图、网络同步等)。GENERATED_BODY()
负责生成元数据解析代码。 - 序列化支持: 序列化是指对象在内存中的数据如何保存和加载,比如当你保存游戏时,保存了哪些数据。
GENERATED_BODY()
帮助生成必要的序列化代码来处理对象的数据。 - 网络复制: 在多人游戏中,Unreal 的网络复制机制用于同步客户端和服务器之间的对象状态。
GENERATED_BODY()
生成了必要的代码以支持这种数据同步。 - 构造和初始化: 宏还生成一些构造器、析构器以及对象生命周期管理相关的代码。这使得在 Unreal 引擎内部对象可以被正确地创建、初始化、销毁。
与 GENERATED_UCLASS_BODY()
的区别
在早期版本的 Unreal Engine 中(如 UE4 的早期版本),你可能会看到类似 GENERATED_UCLASS_BODY()
的宏。这个宏在功能上与 GENERATED_BODY()
相似,但由于历史原因,GENERATED_BODY()
是现在更为推荐的版本。
GENERATED_BODY()
是一个更新的、更加标准化的方式,适用于更多的场景,而 GENERATED_UCLASS_BODY()
则主要用于旧版本的类声明。
为AuraCharacter创建骨骼蓝图
添加骨骼网格体
添加骨骼插槽
在骨骼插槽中添加预览组件,将骨骼插槽调整至合适的位置
为Weapon骨骼网格添加网格体资产
此时该资产被放置在了之前插槽的位置上
任务:为哥布林敌人创建骨骼蓝图
第二节 创建动画蓝图
添加动画蓝图,添加状态机与默认插槽
在MainStates里,添加状态State,然后命名为IdleWalkRun(站立,行走,跑步)
站立,行走,跑步的动画都在同一个动画资产中,我们由Speed来控制播放哪种动画
此时,我们要在事件图标(Event Graph)中添加变量
在事件图表中添加蓝图初始化动画
事件蓝图
- 尝试获取Pawn拥有者:通过调用
尝试获取Pawn拥有者
节点,获取当前动画蓝图所属的角色实例(即控制该动画蓝图的角色)。 - 类型转换为 BP_AuraCharacter:将获取到的Pawn尝试转换为
BP_AuraCharacter
类型,确保其为正确的自定义角色类。如果转换成功,继续下一步操作。 - 设置BP Aura Character:如果转换成功,将转换后的结果存储在名为
BP Aura Character
的变量中,以便后续使用。 - 设置Character Movement:将
BP Aura Character
中的Character Movement
组件(角色移动组件)提取并存储,以用于控制角色的移动行为或获取相关信息。
- Delta Time X:
事件蓝图更新动画
节点会在每一帧更新动画。Delta Time X
代表这一帧的时间增量,用于控制随时间变化的动画或移动。 - 获取BP Aura Character:这里从上方步骤中缓存的
BP Aura Character
变量中读取数据,确保获取到当前角色的实例。 - Character Movement(角色移动组件):通过获取角色的
Character Movement
组件,获取角色的移动状态。 - 向量长度XY:从
Character Movement
中获取当前角色的速度向量,并通过向量长度XY
节点计算水平面上的移动速度(忽略Z轴的垂直方向)。 - 设置Ground Speed(地面速度):将计算出来的XY平面上的速度存储在
Ground Speed
变量中,通常用于控制动画蓝图中角色的速度,影响角色的行走或跑步动画。
在IdleWalkRun
的State中将事件蓝图中设置的GroundSpeed变量添加进去,随后角色的IdleWalkRun动画的Speed就会与GroundSpeed绑定
动画蓝图模板
对于主角来说,可以添加专属的动画蓝图,而对于许多敌人来说,可以使用动画蓝图模板
我们可以将动画蓝图相同的内容放置到一个动画模板内,将不同的内容通过使用配置项去修改。(尤其是角色资源数量特别多的时候,只需要修改动画模板即可。)实现代码的复用性。
还是像之前一样,设置状态机,IdleWalkRun
在IdleWalkRun中新建BlendSpacePlayer
混合空间播放器,而不是使用动画资产
此时,我们可以创建哥布林的动画蓝图,选择父类为ABP_Enemy
然后在资产覆盖编辑器中,可以找到给混合空间播放器设置资产的选项
任务:为弹弓哥布林创建骨骼蓝图与动画蓝图
第三节 处理输入
创建InputAction
IA_Move
值类型设置为Axis2D,上下左右移动是X和Y轴
创建InputMappingContext
IMC_Aura_Context
增强输入支持来自一维源的输入,例如键盘的方向键或常用的"WASD"键配置;可通过应用正确的输入修饰器来实现此控制方案。具体而言,使用 负值(Negate) 可以将某些键注册为负值,而使用 交换输入轴值(Swizzle Input Axis Values) 可以将某些键注册为Y轴,而不是默认的X轴值:
也就是说,WASD默认都为X轴输入正向的单一值,我们需要通过修饰器来将WASD的输入转化为上下左右的XY轴输入;
D无需设置
A为向左移动,所以修饰器应该是对X取反
W为向上移动,所以我们将X轴输入转化为Y轴输入
使用拌合输入轴值(交换输入轴值)(Swizzle Input Axis Values
)
说人话!
关于拌合输入轴值
和 到世界空间
其实是一个翻译的乌龙
正确一个翻译为**交换输入轴值。**这样就非常好理解了其实 修饰器中拌合输入轴值
和到世界空间
非常相似。比如移动这个输入动作明显是一个 Axis2D 值类型的动作(因为肯定能横向移动和纵向移动嘛),但是键盘这种操作 只能反馈 1 和 0 两种情况 即某一个键按下和未按下。那么我们也可以将 键盘输入 看做 是一个 Axis1D 的输入 即一个 0 ~ 1 的轴。
而如果通过 键盘操作 输入给一个 Axis2D的输入动作 就会出现Axis2D中 Y 始终为 0 ,X 由键盘输入决定的情况。
那么这就导致我无法为Y输入任何值!!!因为引擎将键盘输入的 1D 数据 对应到 2D 数据的X轴 那么为了解决这个问题 我就需要将一个办法 让引擎将 输入的 1D 数据 对应到 2D 数据的Y轴(拌合输入轴值)
所以 在上文,WS 的输入将对应 输入动作Axis2D中的Y轴 即 前后方向 A和D 未添加 拌合输入轴值 修饰 所以对应 Axis2D 中的X轴 即 左右方向
那么同理 到世界空间 无非就是把 Axis2D的输入 映射到 Axis3D的输入动作 类比即可
在C++中处理InputMappingContext
AuraPlayerController.h
// AuraPlayerController.h
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "AuraPlayerController.generated.h"
#include "InputMappingContext.h"
/**
*
*/
UCLASS()
class AURA_API AAuraPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AAuraPlayerController();
protected:
virtual void BeginPlay() override;
private:
UPROPERTY(EditAnywhere,Category = "Input")
//输入映射上下文
TObjectPtr<UInputMappingContext> AuraContext;
};
在 Unreal Engine 中,BeginPlay()
是一个虚函数,属于 AActor
类(所有物体、角色和控制器的基类)。该函数在游戏的运行时或对象被激活时首次被调用,通常用于初始化游戏中的对象或做一些游戏启动时需要完成的任务。
具体用途
- 初始化逻辑:
BeginPlay()
常用于编写对象在游戏开始时的初始化逻辑。虽然构造函数用于在对象被创建时初始化基础数据,但BeginPlay()
更适合那些依赖其他对象、场景状态或需要延迟加载的资源的初始化。
- 游戏开始时触发:
- 当游戏运行或者一个关卡被加载时,所有激活的
AActor
类对象都会触发BeginPlay()
。这意味着任何在关卡中的角色、物体或控制器在游戏开始时会自动执行其BeginPlay()
逻辑。
- 当游戏运行或者一个关卡被加载时,所有激活的
- 延迟初始化:
- 有时候,某些对象需要等到整个场景加载完成后才能正常初始化,例如与其他对象的交互、引用其它角色或物体等。
BeginPlay()
是在场景加载完成后且所有对象已创建时触发的,因此是处理这些任务的合适位置。
- 有时候,某些对象需要等到整个场景加载完成后才能正常初始化,例如与其他对象的交互、引用其它角色或物体等。
- 网络同步:
- 在网络游戏中,
BeginPlay()
可以用于确保服务器和客户端都正确同步了游戏状态。比如,某些逻辑可能需要在BeginPlay()
中检查网络连接、同步数据或初始化网络相关的行为。
- 在网络游戏中,
- 确保父类逻辑执行:
BeginPlay()
通常会调用父类的Super::BeginPlay()
,确保父类中定义的初始化逻辑也能正确执行。这是因为BeginPlay()
可能在多级继承链中都定义了自己的逻辑,而每一级的逻辑都可能是重要的。
调用时机
- 在游戏运行时:当游戏进入
Play
模式,或者你点击 Play in Editor (PIE) 按钮时,所有的AActor
实例的BeginPlay()
会自动被调用。 - 在对象激活时:如果某个对象在游戏运行期间被动态加载或激活,
BeginPlay()
也会在对象变为“活跃”时被调用。
构造函数 vs BeginPlay()
- 构造函数:构造函数在对象被创建时执行,用于初始化数据成员、配置对象默认值等。但此时对象可能还未完全加载(例如关卡中的其他对象或引用的资源可能还未准备好)。
- BeginPlay():
BeginPlay()
在游戏运行时或关卡加载完成后调用,这时所有的对象、资源和引用都已经准备好。因此,适合在BeginPlay()
中进行那些依赖于场景、网络、其他对象或复杂资源的初始化工作。
UInputMappingContext
是UE中增强输入的类
AuraPlayerController.cpp
//AuraPlayerController.cpp
void AAuraPlayerController::BeginPlay()
{
Super::BeginPlay();
//将输入映射上下文绑定到玩家控制器
check(AuraContext)
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
check(Subsystem)
Subsystem->AddMappingContext(AuraContext,0);
//光标设置
bShowMouseCursor = true;
DefaultMouseCursor = EMouseCursor::Default;
FInputModeGameAndUI InputModeData;
InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
InputModeData.SetHideCursorDuringCapture(false);
SetInputMode(InputModeData);
}
AAuraPlayerController::AAuraPlayerController()
{
bReplicates = true;
}
在构造函数中:
bReplicates=true
表示这个控制器的实例会在网络中同步,即可以在服务器和客户端之间复制。通常用于多人游戏中的角色或控制器对象。
check()
宏
这是一个调试宏,用来确保内部指针非空。如果该指针为空,程序会在此处崩溃并抛出错误,帮助开发者在调试过程中快速发现问题。
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
这行代码获取了当前本地玩家的增强输入子系统(Enhanced Input Subsystem
)。具体流程是:
-
GetLocalPlayer()
:获取当前玩家的本地玩家对象(ULocalPlayer
),这是指本地玩家控制的对象。 -
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>()
:调用GetSubsystem
获取该玩家的UEnhancedInputLocalPlayerSubsystem
实例。这是负责处理输入映射和输入绑定的子系统。 -
ULocalPlayer
在 Unreal Engine 中,ULocalPlayer
是一个用于表示本地玩家的类。它主要用于管理与玩家相关的信息,并在多人游戏(包括本地多人或单人模式)中跟踪每个本地玩家的输入、HUD、控制器等。
主要功能
- 本地玩家表示:
ULocalPlayer
表示当前正在运行游戏的一个玩家。对于单人游戏,通常只有一个ULocalPlayer
实例。对于本地多人游戏(例如分屏游戏),每个本地玩家都有自己独立的ULocalPlayer
实例。
- 输入管理:
ULocalPlayer
与输入系统密切相关。通过它,系统可以为每个玩家分配特定的输入设备,确保每个玩家可以独立控制角色。例如,在本地多人模式中,玩家 1 可以使用键盘,而玩家 2 使用手柄。- 通过
ULocalPlayer
可以获取玩家的输入子系统(如UEnhancedInputLocalPlayerSubsystem
),这让开发者能够为每个玩家独立管理输入映射。
- HUD 和视口管理:
ULocalPlayer
管理与该玩家相关的 HUD(Heads-Up Display) 和视口(Viewport)。每个ULocalPlayer
都有一个唯一的视角或屏幕区域(在分屏模式下),并且每个视口可以拥有自己的 HUD。
- 控制器连接:
- 在游戏中,每个
ULocalPlayer
可以与一个玩家控制器(APlayerController
)相关联,这个控制器负责处理与游戏世界中角色的交互以及输入处理。 ULocalPlayer
的主要职责之一是将输入传递到APlayerController
,控制游戏中的角色。
- 在游戏中,每个
- 多人游戏支持:
- 在本地多人游戏(如分屏)或在线多人游戏中,每个玩家都有各自的
ULocalPlayer
实例。每个本地玩家通过ULocalPlayer
类来处理自己的独立游戏会话、控制器和输入设备。
- 在本地多人游戏(如分屏)或在线多人游戏中,每个玩家都有各自的
常用方法
-
获取本地玩家的子系统: 通过
ULocalPlayer
,可以获取并操作与本地玩家相关的输入子系统(如UEnhancedInputLocalPlayerSubsystem
),进而动态调整输入设置。 -
获取本地玩家的控制器: 可以通过
ULocalPlayer
获取该玩家的控制器(APlayerController
),从而与游戏中的角色或相机进行交互。 -
Subsystem
在 增强输入系统(Enhanced Input System)中,Subsystem
(子系统)起到了关键的管理和协调作用。它为玩家、输入设备、以及输入映射上下文(Input Mapping Context
)之间的交互提供了基础架构。
具体来说,Subsystem
是 Unreal Engine
中用于管理和处理特定功能的全局或局部系统。针对增强输入系统,最常用的子系统是:
UEnhancedInputLocalPlayerSubsystem
:用于管理本地玩家的输入系统。UEnhancedInputSubsystem
:负责管理全局的输入系统。
- 这行代码将
PlayerController
类中的 鼠标光标显示属性 设置为true
,表示在游戏中始终显示鼠标光标。通常情况下,鼠标光标在纯游戏模式下是隐藏的,但如果你需要处理游戏中同时包含用户界面(如菜单或交互界面)和游戏内操作时,显示鼠标是必要的。
DefaultMouseCursor = EMouseCursor::Default;
DefaultMouseCursor
设置了鼠标光标的默认类型。EMouseCursor::Default
是 Unreal Engine 提供的一种标准光标类型,表示普通的箭头光标。Unreal Engine 允许使用其他光标类型,如EMouseCursor::Crosshairs
(准星)等,根据需要可以更改。
FInputModeGameAndUI InputModeData;
- 这里创建了一个
FInputModeGameAndUI
类型的对象InputModeData
。FInputModeGameAndUI
:是一种输入模式,允许同时捕捉游戏输入(如角色控制)和用户界面输入(如点击按钮)。这是 Unreal Engine 中提供的三种输入模式之一,另外两种是FInputModeGameOnly
和FInputModeUIOnly
。- 这种模式通常用于游戏中需要让玩家与 UI(如菜单或技能栏)交互的场景,同时玩家还可以控制游戏角色或摄像机。
InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
- 这行代码设置鼠标是否锁定在视口内。
EMouseLockMode::DoNotLock
:指定不锁定鼠标。当鼠标离开游戏视口时,鼠标可以自由移动到视口外的区域。这种设置通常用于需要频繁与外部 UI(例如多窗口环境下的其他窗口)进行交互的情况。- 其他选项包括
EMouseLockMode::LockOnCapture
,表示在捕捉到输入后锁定鼠标,适合专注于游戏操作的场景。
InputModeData.SetHideCursorDuringCapture(false);
- 这行代码设置当玩家点击游戏视口或捕获输入时,是否隐藏鼠标光标。
SetHideCursorDuringCapture(false)
:表示在捕获输入时不隐藏光标。这意味着即使玩家与游戏世界交互,鼠标光标仍然可见。这对于某些交互 UI 的场景(如点击按钮或拖动物体)很重要。- 如果设为
true
,鼠标光标会在捕获输入时自动隐藏,这种行为通常适用于第一人称射击游戏等场景。
SetInputMode(InputModeData);
- 最后一行代码将输入模式设置为前面定义的
InputModeData
,即将输入模式切换为游戏和 UI 共存模式,并且应用上面所定义的光标行为(显示光标、不锁定鼠标、捕获输入时不隐藏光标)。
与输入组件(InputComponent)绑定
AuraPlayerController.h
//...
private:
//...
//InputAction
TObjectPtr<UInputAction> MoveAction;
/**
* 接收输入,控制角色移动
* @param InputActionValue MoveAction的输入
*/
void Move(const FInputActionValue& InputActionValue);
AuraPlayerController.cpp
//...
void AAuraPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent);
EnhancedInputComponent->BindAction(MoveAction,ETriggerEvent::Triggered,this,&AAuraPlayerController::Move);
}
void AAuraPlayerController::Move(const FInputActionValue& InputActionValue)
{
//提取二维向量输入
const FVector2D InputAxisVector = InputActionValue.Get<FVector2D>();
//获取当前相机的方向
const FRotator Rotation = GetControlRotation();
//我们只取玩家视角的 Yaw(即左右旋转),忽略俯仰和滚动
const FRotator YawRotation(0.f,Rotation.Yaw,0.f);
//基于当前的 YawRotation,计算出世界坐标系下面向的“前方”方向的单位向量(即朝向前方的方向)
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
//计算面向“右方”方向的单位向量
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
//获取当前控制的 Pawn,即角色,若有当前角色才执行
if(APawn* ControlledPawn = GetPawn<APawn>())
{
ControlledPawn->AddMovementInput(ForwardDirection,InputAxisVector.Y);
ControlledPawn->AddMovementInput(RightDirection,InputAxisVector.X);
}
/*
*为什么这里GetUnitAxis(EAxis::X),但是 InputAxisVector.Y
* GetUnitAxis(EAxis::X) 表示角色的“前方方向”,而 InputAxisVector.Y 代表玩家输入的前后移动(如按下 W 或 S)
* 所以当玩家想向前或向后移动时,我们将角色的前方方向(即 X 轴方向)与玩家的前后移动输入相结合
*/
}
这个函数用于设置输入组件,并将移动动作与移动函数绑定
Super::SetupInputComponent()
:调用基类APlayerController
的SetupInputComponent()
函数,确保默认的输入绑定和配置正常运行。CastChecked<UEnhancedInputComponent>(InputComponent)
:InputComponent
是默认的输入组件,类型为UInputComponent
。这里使用了CastChecked
,将其强制转换为UEnhancedInputComponent
,这是增强输入系统的组件。如果转换失败,程序会崩溃,提示错误。BindAction()
函数用于绑定输入动作和响应函数。MoveAction
:一个UInputAction
对象,代表移动的输入操作。ETriggerEvent::Triggered
:触发事件类型,表示在输入持续触发时调用响应函数。this
:指向当前对象,AAuraPlayerController
。&AAuraPlayerController::Move
:绑定的回调函数,处理移动逻辑,即Move()
。
这个函数是处理输入逻辑的具体实现,根据玩家的输入移动角色。
FInputActionValue
是增强输入系统中的数据结构,包含输入值。在这里,我们获取了MoveAction
的二维向量输入(如 WASD 或手柄的方向盘输入)。InputActionValue.Get<FVector2D>()
:提取二维向量输入,通常是(X, Y)
,分别对应前后移动(Y)和左右移动(X)。
GetControlRotation()
:获取控制器当前的旋转方向(相机的方向)。
通过构造 FRotator YawRotation(0.f, Rotation.Yaw, 0.f)
,我们只取玩家视角的 Yaw
(即左右旋转),忽略俯仰和滚动。
FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X)
:基于当前的YawRotation
,计算出面向的“前方”方向的单位向量(即朝向前方的方向)。FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y)
:同样计算面向“右方”方向的单位向量。GetPawn<APawn>()
:获取当前控制的Pawn
,即角色。AddMovementInput(ForwardDirection, InputAxisVector.Y)
:- 向前/后方向添加输入,
InputAxisVector.Y
表示输入的 Y 轴分量,即前进或后退。
- 向前/后方向添加输入,
AddMovementInput(RightDirection, InputAxisVector.X)
:- 向左/右方向添加输入,
InputAxisVector.X
表示输入的 X 轴分量,即左移或右移。
- 向左/右方向添加输入,
总结:
- 在
SetupInputComponent()
中,输入的移动操作被绑定到了Move()
函数上。 Move()
函数从输入中提取方向数据,结合角色的朝向,计算出前后和左右的移动方向,最后通过AddMovementInput()
将这些方向施加到控制的角色上,使角色相应地移动。
为什么在Move函数中,ForwardDirection获取 FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X)为X轴
而 ControlledPawn->AddMovementInput中,ForwardDirection的缩放是InputAxisVector.Y?
这是因为
ForwardDirection
对应的是世界空间中角色前进方向,而InputAxisVector.Y
代表玩家输入的前后移动(通常是 W/S 键或手柄的上/下方向)。这两个方向的组合决定了角色在世界空间中的前后移动。让我们仔细分析一下背后的逻辑。1. 角色的朝向(ForwardDirection)
ForwardDirection
是通过FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X)
计算得来的,它表示当前角色在世界空间中的“前进方向”。换句话说,它是基于角色旋转(YawRotation
)的 X 轴方向,即角色在世界空间中朝向的方向。- 当玩家旋转角色时,这个方向向量会动态变化,例如向前、向左、向右、向后等不同的方向。
2. 玩家的输入(InputAxisVector)
InputAxisVector
是一个二维向量,代表玩家的输入方向(例如键盘的 W、A、S、D 或手柄的方向盘)。InputAxisVector.Y
通常代表前后移动(例如 W 和 S 键),InputAxisVector.X
代表左右移动(例如 A 和 D 键)。3. 为什么
GetUnitAxis(EAxis::X)
和InputAxisVector.Y
配合使用?
GetUnitAxis(EAxis::X)
表示角色的“前方方向”,而InputAxisVector.Y
代表玩家输入的前后移动(如按下 W 或 S)。所以当玩家想向前或向后移动时,我们将角色的前方方向(即 X 轴方向)与玩家的前后移动输入相结合。- 举个例子:
- 如果玩家按下 W 键,
InputAxisVector.Y
的值为正(比如 1),这意味着玩家想让角色向前移动。因此,我们将ForwardDirection
(表示角色的前进方向)乘以这个正值,就会让角色沿前方方向移动。- 如果玩家按下 S 键,
InputAxisVector.Y
的值为负(比如 -1),这意味着玩家想让角色向后移动。此时,将ForwardDirection
乘以负值,角色就会沿前进方向的反方向(即向后)移动。4. 左右移动逻辑
类似的,
RightDirection
是通过FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y)
计算的,它表示角色在世界空间中的“右方向”。InputAxisVector.X
表示玩家的左右输入(如按下 A 或 D 键)。当玩家想要左右移动时,将“右方向”(Y 轴方向)与输入值相结合:
- 如果玩家按下 D 键,
InputAxisVector.X
为正值,角色沿右方向移动。- 如果按下 A 键,
InputAxisVector.X
为负值,角色沿左方向移动。
在场景中移动
在UE编辑器中,创建AuraPlayerController的蓝图类
然后设置好AuraContext与MoveActions
创建AuraGameModeBase的C++类
随后创建AuraGameMode蓝图类
随后,打开StarterMap,重载游戏模式为BP_AuraGameMode
人物就可以跟随着wasd动起来了
但是此时,我们没有摄像机,也无法转移视角
如果要操作角色,我们还需要一个查看游戏的视角的相机和一个控制相机的弹簧臂。
只需在BP_AuraCharater中添加弹簧臂和相机即可
然后调整位置
关闭使用Pawn控制旋转,如果这个勾选,那么角色的朝向会跟随相机的朝向。 并且弹簧臂也不继承父节点的Pitch Yaw 和Roll的值
此时就可以以第三人称俯视角来操作角色了
但是角色在左右移动时,没有转身
在AuraCharacter.cpp中设置一些参数即可
//AuraCharacter.cpp
AAuraCharacter::AAuraCharacter()
{
GetCharacterMovement()->bOrientRotationToMovement = true; //设置为true,角色将朝移动的方向旋转
GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f); //旋转速率
GetCharacterMovement()->bConstrainToPlane = true; //约束到平面
GetCharacterMovement()->bSnapToPlaneAtStart = true; //设置了上面一项为true,且此项设置为true,则在开始时与地面对齐
//设置使用控制器选择Pitch,Yaw,Roll的值。
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
bUseControllerRotationYaw = false;
}
---------------------------------------------------------------------
//AuraCharacter.h
UCLASS()
class AURA_API AAuraCharacter : public AAuraCharacterBase
{
GENERATED_BODY()
public:
AAuraCharacter();
};
然后编译过后,人物就可以转向了
每次快要停下来时,头部都会剧烈晃动一下
可以在蓝图里设置一个变量ShouldMove,然后将IdleWalkRun状态改为Running,新建一个状态Idle
在事件图表中设置当GroundSpeed>3时,将SholdMove设为true,反之设为false
在Idle到Running和Running到Idle就是靠ShouldMove
第四节 为敌人创建鼠标移入高亮
创建EnemyInterface
为了让鼠标移入敌人实现高亮,我们必须让敌人身上有某种属性或者实现了某个接口
这个接口由我们自己来定义,由于我们想要让代码有高扩展性,可以让敌人类继承自EnemyInterface
,而不是在AuraEnemy类中添加一个属性
然后PlayerController中检查鼠标是否移入到了实现了EnemyInterface
的类中,如果有就高亮
创建一个Unreal接口类
//EnemyInterface.h
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:
//定义纯虚函数
virtual void HighlightActor() = 0;
virtual void UnHighlightActor() = 0;
//是否高亮
UPROPERTY(BlueprintReadOnly)
bool bHighlighted = false;
};
//AuraEnemy.h
UCLASS()
class AURA_API AAuraEnemy : public AAuraCharacterBase,public IEnemyInterface
{
GENERATED_BODY()
//override EnemyInterface中的纯虚函数
virtual void HighlightActor() override;
virtual void UnHighlightActor() override;
};
实现高亮逻辑
//AuraPlayerController.cpp
//...
void AAuraPlayerController::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
CursorTrace();
}
void AAuraPlayerController::CursorTrace()
{
FHitResult CursorHit;
GetHitResultUnderCursor(ECC_Visibility,false,CursorHit);
//bBlockingHit是否有碰撞
if(!CursorHit.bBlockingHit) return;
//在光标下的对象是否实现了EnemyInterface
//如果实现了EnemyInterface,则高亮
//尝试将触碰到的Actor转换类型为IEnemyInterface(若该Actor继承了IEnemyInterface,则转化成功,反之失败)
//如果转换失败返回null,转换成功返回该Actor
LastActor = ThisActor;
ThisActor = Cast<IEnemyInterface>(CursorHit.GetActor());
/*
*这里有一些情况需要分类讨论
* 1.LastActor不为null ThisActor为null -LastActor取消高亮
* 2.LastActor不为null ThisActor不为null
* A. LastActor==ThisActor -什么都不做
* B. LastActor!=ThisActor -LastActor取消高亮 ThisActor高亮
* 3.LastActor为null ThisActor为null -什么都不做
* 4.LastActor为null ThisActor不为null - ThisActor高亮
*/
if(LastActor == nullptr)
{
//情况4
if(ThisActor != nullptr)
{
//ThisActor高亮
ThisActor->HighlightActor();
}
//else 情况3 什么都不做
}
else{
//情况1
if(ThisActor == nullptr)
{
//LastActor取消高亮
LastActor->UnHighlightActor();
}
else
{
//情况2.B
if(LastActor != ThisActor)
{
//LastActor取消高亮 ThisActor高亮
LastActor->UnHighlightActor();
ThisActor->HighlightActor();
}
//else 情况2.A什么都不做
}
}
}
GetHitResultUnderCursor
获取鼠标与物体的碰撞,将结果放置到第三个参数(引用)中
Tick
函数在每一帧时触发
//AuraEnemy.cpp
//...
void AAuraEnemy::HighlightActor()
{
UE_LOG(LogTemp,Log,TEXT("Highlight"));
bHighlighted = true;
}
void AAuraEnemy::UnHighlightActor()
{
UE_LOG(LogTemp,Log,TEXT("UnHighlight"));
bHighlighted = false;
}
UE_LOG
宏在控制台输出
例如 UE_LOG(LogScript, Warning, TEXT("deleteDic: Not DeleteDic! %s "), *filePath);
在敌人蓝图中添加绘制调试球体
绘制调试球体
然后在碰撞预设中,在碰撞响应中,把Visibility
设为Block
因为在上面代码中GetHitResultUnderCursor(ECC_Visibility,false,CursorHit);
ECC_Visibility
为检测Visibility通道的碰撞,如果不设为Block会检测不到碰撞
随后开启游戏即可看到调试的高亮球体了
为高亮添加后期处理
后期处理配置
在项目设置中将自定义深度-模板通道设置为启用模板
在关卡中,添加后期处理体积
设置无限范围(未限定)
设置后期处理材质
对于pp_highlight
其customdepth的值为250
我们在EnemyBase蓝图中设置渲染自定义深度通道,将值设置为250,即可让敌人添加高亮材质(当然我们要通过C++来控制某个敌人是否该高亮)
EnemyBase蓝图为敌人的蓝图基类配置,EnemyBase继承AuraEnemy类,然后Goblin等敌人可以继承EnemyBase类,这样可以在EnemyBase中编写蓝图配置
编写对后处理的C++逻辑
//AuraEnemy.cpp
AAuraEnemy::AAuraEnemy()
{
//将网格碰撞检测中的Visibility通道打开,
//让AuraPlayerController中的GetHitResultUnderCursor能够在ECC_Visibility通道碰撞到敌人
GetMesh()->SetCollisionResponseToChannel(ECC_Visibility,ECR_Block);
}
void AAuraEnemy::HighlightActor()
{
//获取该实例的网格实例,在网格中设置CustomDepth为后期处理高亮的CustomDepth值
//当该网格的CustomDepth值为高亮的CustomDepth,则后期处理高亮会在这个网格上渲染
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);
}
GetMesh()
:这个函数通常返回角色或对象的USkeletalMeshComponent
,即角色的骨骼网格组件。SetRenderCustomDepth(true)
:这行代码启用了自定义深度渲染。它告诉引擎在渲染网格时,将它的深度信息(即与摄像机的距离)写入一个特殊的缓冲区,称为自定义深度缓冲区(Custom Depth Buffer)。
自定义深度的用途:
- 轮廓描边(Outline):你可以通过使用自定义深度缓冲区来为网格添加轮廓描边效果。这种效果通常用于高亮显示物体,特别是在鼠标悬停或目标选中时。
- 后处理效果:自定义深度还可以用作后期处理效果中的遮罩。通过检测哪些物体写入了自定义深度缓冲区,你可以对这些物体应用特殊的视觉效果,比如模糊、发光等。
通过 SetRenderCustomDepth(true)
,你可以将某些对象标记为需要特殊处理的对象,以便后期处理效果能够检测到它们。
SetCustomDepthStencilValue
:这是设置网格的自定义深度模板值。深度模板缓冲区是一个整数值缓冲区,用来为不同物体分配唯一的标识符或分类。你可以通过这个值来区分哪些物体应该应用某种特定的效果。CUSTOM_DEPTH_RED
:这是一个枚举值或常量(可能在项目中定义为某个整数),通常用来代表某种效果。在这个例子中,CUSTOM_DEPTH_RED
可能表示某种颜色(比如红色)的效果。
深度模板值的用途:
- 区分不同物体:自定义深度模板值可以用来标记不同类别的物体。例如,你可以用不同的模板值来表示红色高亮、绿色高亮等。
- 后期处理:在后期处理(Post-Process)阶段,你可以基于物体的模板值应用不同的视觉效果。比如,使用模板值为
CUSTOM_DEPTH_RED
的物体会显示为红色轮廓。
常见应用场景:
- 物体高亮显示:在游戏中,当你想要对物体(如角色、物品)进行高亮显示时,你可以启用自定义深度渲染并设置不同的模板值,以便通过后期处理效果(如轮廓描边)使它们突出显示。例如,当玩家将鼠标悬停在可交互物品上时,这段代码会将该物品显示为红色轮廓。
- 目标指示:在第一人称射击或其他类型的游戏中,你可以使用这种技术来高亮敌人或目标,使其更易于识别。
这样就可以实现鼠标移入敌人高亮的逻辑了
对于网格的介绍
在 Unreal Engine 5 (UE5) 中,网格 (Mesh) 是构建虚拟世界的核心元素之一。无论是角色、环境、物品,还是其他对象,它们的外形通常都是由网格构成的。网格是一种三维几何形状,由一组顶点、边和面组成。在UE5中,网格大体可以分为两种类型:
- 静态网格 (Static Mesh)
- 骨骼网格 (Skeletal Mesh)
- 静态网格 (Static Mesh)
静态网格是指不具有骨骼结构的三维模型,它通常用于那些不需要动画或者骨骼运动的对象,比如建筑、家具、武器(没有动画时)等。静态网格相对轻量,在性能开销上更小。
静态网格的特性:
- 不可变形:静态网格不能通过骨骼动画进行变形,但可以通过物理、材质或蓝图进行某些运动或变化(如旋转、移动、缩放等)。
- 高效渲染:由于没有骨骼动画等复杂性,静态网格渲染效率高,适合用于大量静态物体(如场景建筑)。
- 碰撞检测:静态网格可以配置简单或复杂的碰撞体积,用于物理交互和碰撞检测。你可以使用引擎自带的工具为静态网格生成简化的碰撞盒或多边形网格。
使用场景:
- 建筑物、墙壁、地面、岩石等环境元素。
- 静态物品,如桌子、椅子、武器架等不需要动画的对象。
创建静态网格:
静态网格通常是由3D建模软件(如 Blender、Maya 或 3ds Max)创建,并导出为 FBX 文件后导入到 Unreal Engine 中。
编辑静态网格:
在UE5中,你可以使用“静态网格编辑器”来调整静态网格的各种属性,如碰撞体积、LOD(细节层次)、材质贴图等。
- 骨骼网格 (Skeletal Mesh)
骨骼网格用于那些需要动画的对象,通常具有一组骨骼(Skeleton)来驱动网格的变形。这种类型的网格用于角色、动物、可变形物体和各种需要动画的对象。
骨骼网格的特性:
- 骨骼与动画:骨骼网格有一个骨架(Skeleton),每个骨骼节点都可以带动网格的不同部分变形。通过骨骼动画,你可以制作角色的行走、奔跑、攻击等动作。
- 支持蒙皮 (Skinning):网格的顶点可以绑定到骨骼,通过骨骼的移动带动网格顶点产生相应的变形。这使得角色和生物等物体能够进行自然的运动。
- 物理模拟:骨骼网格也可以结合物理引擎进行布娃娃模拟、动力学链条等高级效果,比如角色倒地后身体的物理反应。
- 动画蓝图:使用动画蓝图,可以结合角色输入、状态机等逻辑动态地控制骨骼网格的动画表现。
使用场景:
- 角色(如人类、动物等有机物体)。
- 具有可动部件的复杂物体,如车辆的悬挂系统、武器的滑轨等。
- 可变形的物体,如旗帜、绳索、布料等。
骨骼网格的动画系统:
- 骨骼动画:通过预先制作的动画文件驱动骨骼网格的动作,比如角色的行走、跑步、攻击等动画。
- 蒙太奇 (Montage):这是UE中的一种动画工具,允许开发者组合和调整多个动画序列,用于复杂的动作,比如连击或角色动画过渡。
- 物理骨骼模拟:使用物理引擎,可以为角色的骨骼添加物理模拟,创建布娃娃效果或者动力学物体,如角色被击倒后的自然倒地效果。
- 网格的优化与LOD (Level of Detail)
在大型游戏中,优化网格的渲染效率是至关重要的。为了确保高性能,UE5 提供了 LOD (细节层次) 系统来优化网格的渲染。
- LOD 是指根据摄像机与物体的距离,动态切换网格的复杂度。当物体离玩家较远时,使用较低分辨率的网格模型,而近距离时则显示高分辨率的模型。
- 自动生成 LOD:UE5 可以根据需要自动为静态网格生成不同级别的 LOD 模型,也可以手动为骨骼网格和静态网格创建多个版本的 LOD。
- 网格的材质和贴图
网格的外观由 材质 (Material) 和 纹理贴图 (Textures) 决定:
- 材质:是为网格定义外观效果的着色器,控制着物体的表面属性(如颜色、反射、透明度、粗糙度等)。UE5 的材质系统十分强大,支持物理材质渲染(PBR)。
- 纹理贴图:是用来定义材质的图像,比如漫反射贴图(Albedo)、法线贴图(Normal Map)、粗糙度贴图等。通过这些贴图可以让网格表现得更加逼真。
- Nanite 网格
Nanite 是 UE5 引入的革命性虚拟化几何体技术,它允许你在场景中使用数百万甚至数十亿个多边形的高精度网格,而不会对性能产生太大的影响。
- 高细节表现:使用 Nanite,你可以在游戏中导入极高分辨率的网格,甚至是 3D 扫描的模型,细节丰富得令人惊叹。
- 自动 LOD:Nanite 会自动生成多层次的 LOD,并根据摄像机距离无缝切换,消除传统 LOD 切换时的视觉差异。
使用场景:
- 超高精度的建筑、雕像、自然环境。
- 在虚拟生产中用于电影级画面。
- 网格的碰撞检测
在 UE5 中,网格的碰撞是游戏物理和交互的重要部分。你可以为网格配置 简单碰撞 或 复杂碰撞:
- 简单碰撞:使用基本的几何体(如盒子、球体、胶囊体等)包围网格。这种方式计算快速,适用于大多数情况。
- 复杂碰撞:使用网格本身的多边形进行精确的碰撞检测,适合需要精确检测形状的情况,但性能开销较大。
在我们这个敌人高亮中,网格被用来做检测碰撞与渲染效果