UE5 与 C++ 入门教程·第一课:角色与 Enhanced Input
本文主要围绕 UE5 新的输入系统,手把手从 0 搭建 Unreal 项目,实现角色的基础移动。
重要提示:众所周知,C++ 属于编译型语言,因此动态灵活性不足,不过执行效率高,而蓝图简单灵活,却执行效率低。因此推荐一种开发方式—— 用 C++ 创建基类,蓝图继承 C++ 的基类 ,获得一种折衷的优势。
- 开发工具:Visual Studio 2022
- Unreal 版本:5.2.1
创建项目
- 新建一个基于 C++ 的空项目。
- File -> New Level(或者
Ctrl + N
),建立一个 Basic 关卡,保存到新建的 Levels 文件夹中; - Edit -> Project Settings… -> Project -> Maps & Modes 里将
Editor Startup Map
和Game Default Map
设置为新建的关卡。
角色资产
这里选用的 Epic 商城里的免费资产,Paragon: Lt. Belica。目前用的版本是(写的时候的最新的版本) 5.2.1,目前 Belica 还不支持这个版本,选一个最近的版本 5.1 添加到项目中。
创建一个 Character 类
正如开头所说,我们希望获得一种相对优势,因此会用 C++ 作为父类,项目中实际使用的时候用创建蓝图类继承自 C++。对于简单的参数和功能就可以在蓝图上定制,对于通用或核心的功能或方法则可以在上级 C++ 类中实现。
在 Content Browser
的 C++ Classes
点击右键 New C++ Class...
或者在顶部 Tools -> New C++ Class…
这里并没有放到默认目录,而是放到 Character
目录里。注意,这里生成的 C++ 代码会有一个头文件的问题。
// 由于将类创建到 `Character` 目录下,自动生成的代码是下面的这样
#include "Character/BelicaCharacter.h"
// 去掉 Character/,改成下面这样,否则编译会出错
#include "BelicaCharacter.h"
Spring Arm 与 Camera
角色后面跟着一个相机,叫做 跟随相机(followed camera)。为了控制相机与角色的相对距离,通常还带一个弹簧臂(Spring Arm)。调整弹簧臂就可以调节相机的远近。可以说这是所有角色型游戏的基础。
因此,相机是依附于弹簧臂的。 弹簧臂动,相机就动。
我们需要两个组件,USpringArmComponent
和 UCameraComponent
。前缀 U
代表继承自 UObject
。
在 BelicaCharacter.h
中添加两个组件:
private:
UPROPERTY(VisibleAnywhere, Category = Camera)
class USpringArmComponent* CameraBoom;
UPROPERTY(VisibleAnywhere, Category = Camera)
class UCameraComponent* FollowCamera;
这里使用了前向声明的小技巧,对于指针类型,可以声明类型,而不需要引入头文件,这样可以避免影响编译速度。这就是为什么使用 class
。当然,如果你愿意的话,也可以单独放到文件的开头进行声明,类似下面这样:
class USpringArmComponent;
class UCameraComponent;
UPROPERTY
是 Unreal 定义的宏。某种意义上也是黑魔法,例如你可以让 private
也可以被访问。UPROPERTY
用途非常广,比如变量复制、序列化、蓝图访问、垃圾回收。本文作为入门教程就不展开了。
在 BelicaCharacter.h
中使用了前向声明,但在 BelicaCharacter.cpp
中,我们就需要关注类的具体方法,因此就必须引入头文件。要查询类在哪个头文件里,最简单粗暴但有效的方法就是把类名贴入搜索引擎。如果你使用的是 Jetbrains 家的 Rider 进行 Unreal 开发,IDE 会自动帮你补全头文件,就可以不用操心这个问题。(特此声明,本人没有收取任何 Jetbrains 广告费)
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
在构造函数中初始化这两个组件,并指明层级关系。
ABelicaCharacter::ABelicaCharacter()
{
PrimaryActorTick.bCanEverTick = true;
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(GetMesh());
CameraBoom->TargetArmLength = 500.f;
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
}
- Unreal 有自己的一套内存管理机制,不能简单的进行
new
和delete
。需要使用CreateDefaultSubobject
这样的模板函数,背后也有比较复杂的分配机制。 SetupAttachment
就是用来绑定层级的。可以看到FollowCamera
绑定到了CameraBoom
上。而CameraBoom
绑定的是GetMesh
返回的角色的骨骼。=TargetArmLength
是弹簧臂的长度bUsePawnControlRotation
是使用 Pawn 控制旋转
到了这里,可以进行编译了。
创建蓝图类
可以基于前面的 C++ 类创建一个蓝图类。
可以看到 C++ 代码的层级关系体现在这里,弹簧臂依附与骨骼(Mesh
),跟随相机又依附于弹簧臂。
在 Mesh
设置为我们引入的资产,调整位置和旋转。顺便可以调节一下弹簧臂的高度和长度(不用调跟随相机)。
可以将蓝图类拖入到关卡中查看效果。
动画蓝图
Belica 资产里已经做好了一个动画蓝图,可以直接使用。至于什么是动画蓝图,简单理解就是一系列动画组合而成,能根据某种状态(例如用户输入)自动切换和过渡动画的动画控制逻辑。后面如果有动画的文章会展开讲制作动画蓝图(也许鸽了)。
在蓝图添加 Belica 的动画蓝图:
运行一下,会有激动人心的动画效果出现,应该可以看到 Belica 拿起枪自信又警觉的样子。
Input Action
这里会使用 Unreal 新版本的 Input 功能,相比于 UE4,看起来稍微复杂一点点,但优势也比较明显,就是可定制性更高,层次更加分明。Input Action 可以理解定义了值类型,对于代码,主要关注 Action 的值,不关心怎么出现这个值的。至于 Action 怎么产生,由谁产生(键盘、手柄、触控等等),则是 Input Mapping Context 要定义的,也包括值的处理过程。
创建一个 Inputs 文件夹,然后再创建一个 Action
文件夹,创建 Input Action(点进去修改类型)。
IA_Jump
:处理跳跃,对应Value Type
是Digital(bool)
(一次性触发);IA_Move
:移动,由于这是个有正负方向,而且上下左右,使用Axis2D(Vector2D)
;IA_Look
:鼠标移动,也是Axis2D(Vector2D)
;
Input Action 可以视为 Action 名称(标签)和值类型的映射。
Input Mapping Context
在 Inputs 文件夹中创建 IMC_Belica
。注意 Negate
对值进行取反(负)方向。空格和 D
不需要特别配置。Look 的 Nagate
只作用于 Y
,将 X
和 Z
的勾选去掉。
Enhanced Input
如果使用 5.x 版本以上,Enhanced Input 已经作为默认的 Input 组件(毕竟已经在编辑器里警告说废弃原有 Input 了),但新的 Input 和原有的 Input 是兼容的。如果你看代码的话,新的 EnhancedInputComponent
其实就是 InputComponent
的子类。不放心的话可以去看一眼。
在 .Build.cs
中将 EnhancedInput
添加作为依赖(因为这里是空项目起步,不是第三人称模板,所以需要手动添加)。
PrivateDependencyModuleNames.AddRange(new string[] { "EnhancedInput" });
然后刷新 Visual Studio 项目。可以在编辑器顶部的 Tools -> Refresh Visual Studio 2022 Project。或者在文件夹下,对着 .uproject
右键,生成 Visual Studio project files。
这一步的目的是让依赖的头文件加入到 Visual Studio,否则编译器会找不到头文件。不过,是可以正常编译的。如果你能忍受红色的波浪线,其实也可以不刷新。
角色中加入 IA 和 IMC
有了弹簧臂和跟随相机的铺垫,这一步其实没有太多要讲的。
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
class UInputMappingContext* BelicaMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
class UInputAction* JumpAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
class UInputAction* MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
class UInputAction* LookAction;
但这有个黑魔法,meta = (AllowPrivateAccess = "true")
让蓝图可以访问到私有变量。
初始化和绑定操作
最终的 Action 需要有一个对应的方法去处理这个操作。注意一下,UE4 的 Input 是区分 Axis 和 Action,但现在都已经统一成为 Action 了。所以,都是 BindAction
。
还有一个小点,BelicaMappingContext
和 Action
之间其实并 没有严格的初始化顺序的关系。 本质上,这是两层的抽象,Context 主要是外部输入到 Action。Action 则负责最后绑定到操作上。
在 .cpp 引入头文件:
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
// ...
// Called to bind functionality to input
void ABelicaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(BelicaMappingContext, 0);
}
}
if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
{
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ABelicaCharacter::Move);
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ABelicaCharacter::Look);
}
}
跳跃的处理方法是用 ACharacter
自带的处理方法。所以处理 Action 操作,只需要实现 Move
和 Look
。
处理值的方法
void ABelicaCharacter::Move(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
void ABelicaCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
AddControllerYawInput(LookVector.X);
AddControllerPitchInput(LookVector.Y);
}
}
需要注意,在头文件中声明时,FInputActionValue
是类型的引用,不能使用前向声明。所以需要在 头文件 中(也就是 BelicaCharacter.h
)加入 #include "InputActionValue.h"
GameMode 和 接收输入
将 Action 和 IMC 在角色进行设置,同时接收 Player 0
的输入。
接下去就比较简单了,创建 C++ 项目会自动创建一个 GameMode
,直接继承一个蓝图的 GameMode 类。
将 Pawn 设置为 Belica 的蓝图类,可以删除场景内的 Belica。
在 World Settings 中设置这个 GameMode。
运行游戏,现在 Belica 可以受键盘和鼠标控制移动了。
对了,如果你觉得不希望只能看到 Belica 的背影,可以在 C++ 的构造函数中设置 bUseControllerRotationYaw = false;
,当然更简单的方法是在蓝图直接勾选去掉(这才是蓝图的意义,额,其实本文都可以用蓝图)。
小结
从 0 开始,最终实现 UE5 新版的 EnhancedInput。这是一切的开始。不过,令我意外的是 Belica 提供了动画蓝图(突然手动制作的欲望就下降了)。