斯坦福UE4 + C++课程学习记录 2:移动与相机跟随
目录
1. 创建玩家类
在创建自定义的玩家类前,先简要了解一下UE中的常用class:
Object | 所有UE对象的基类 |
Actor | 可以在世界中放置或生成的object |
Pawn | 可以被玩家或AI控制的Actor |
Character | 实现了双脚行走的Pawn |
PlayerController | 玩家与对应Pawn之间的接口 |
GameModeBase | 定义游戏的基础规则 |
GameStateBase | 记录游戏的状态 |
要创建新的C++类,需要在UE中 -> 文件 -> 新建C++类 -> 选择Character(角色类)-> 设置名称和路径 -> 选择类为公共(public)-> 点击创建。这一步在设置类名称时建议加上前缀用于区分自定义类、UE类或插件的类等。

创建完成后,UE会在相应目录自动创建相应的.h和.cpp文件,并打开VS2019。 关于.h和.cpp文件作用是C++基础知识,可以粗略理解为.h写声明用于编译和共享(public),.cpp写具体实现(private)。

打开.h文件后查看文件内容,在开头会看见类似下面的语句:
class SURKEAUE_API ASurCharacter : public ACharacter
可以发现UE在创建代码时,自动在原本的SurCharacter和Character类前添加了前缀A。这是因为UE遵守一系列代码编写规范,规定通过基类的种类或类的类型来确定前缀,进而可以区分类和实例。具体的规则可参考官方文档,以下是常见的前缀:
基类 | 前缀 | 类型 | 前缀 |
UObject | U | 界面 Interfaces | I |
AAcor | A | 枚举 Enums | E |
SWidget | S | 模板 Templates | T |
2. 相机跟随
在创建相机跟随玩家前,首先需要建立一个关卡。我在content目录下依次创建了/SurkeaUE/Maps文件夹用于保存关卡。进入UE后的某人关卡是未命名(Untitled),我们需要把其中的默认的玩家出身点PlayerStart删除,因为我们要创建自己的玩家。PlayerStart在场景中的图标是一个游戏手柄,也可以在世界大纲视图中找到相应的组件。

随后,文件 -> 关卡另存为 -> 选择建立的/SurkeaUE/Maps并命名为TestLevel,点击保存即可。更改后要及时点击内容浏览器中的“保存所有”按钮,防止各种意外丢失数据的情况,随手保存项目是个好习惯。
然后需要创建玩家。在内容浏览器中右键 -> 蓝图类(Blueprint Class)-> 在下方所有类中选择自定义类SurCharacter -> 命名为Player。

创建完毕后,将角色从内容浏览器拖拽进世界中。此时的角色是个没有任何模型,由数根线条围成的胶囊体。最后在角色的细节面板中搜索Pawn,将“自动控制玩家”设置为玩家0,这样在运行关卡时就可以控制该角色。当然,目前还没有实现控制方法,所以在此处运行时,角色是不会对键盘输入有任何反应的。

下面将转到VS中开始代码的编写,编写代码的总体方法是在.h中声明,在.cpp中实现,完整代码在文末附上。
要实现相机跟随,可以通过代码直接创建UE中的弹簧臂对象和相机对象,并将弹簧臂attach(attach可理解为父子关系,你的确可以在角色的完整蓝图左上角的“组件”看到各个组件的层级关系)到自定义角色上、再把相机attach到弹簧臂上。

这个场景可以想象成角色后面连着一根直线(弹簧臂),直线的另一端是相机。这样做的好处有两个:1. 相机与角色通过弹簧臂相连,实现了相机跟随; 2. 弹簧臂是碰撞体,因此我们的角色在靠近墙壁时,相机不会穿模到墙壁的另一边,从而保证玩家视野中不会因为墙壁遮挡自己的角色。同时,在代码中声明的对象可以利用UPROPERTY宏定义,使得该对象可以在UE中直接修改。本部分编写的代码如下,细节见注释:
// SurCharacter.h
// 为了编译更快,在.h中声明弹簧臂和相机的class,在.cpp引用相应头文件
class USpringArmComponent;
class UCameraComponent;
class SURKEAUE_API ASurCharacter : public ACharacter
{
protected:
// 弹簧臂组件
UPROPERTY(VisibleAnywhere)
USpringArmComponent* SpringArmComp;
// 相机组件
UPROPERTY(VisibleAnywhere)
UCameraComponent* CameraComp;
}
// SurCharacter.cpp
// 为了编译更快,在.h中声明弹簧臂和相机的class,在.cpp引用相应头文件
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
ASurCharacter::ASurCharacter()
{
// 创建相应实例 <创建的class>("UE中显示的该控件的名字")
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>("SpringArmComp");
CameraComp = CreateDefaultSubobject<UCameraComponent>("CameraComp");
// 将相应对象attach,RootComponent是根控件,即角色的胶囊体组件
SpringArmComp->SetupAttachment(RootComponent);
CameraComp->SetupAttachment(SpringArmComp);
}
此时运行关卡(注意是保存在Maps中的关卡,而不是打开UE默认显示的关卡),可以发现角色和相机已经通过弹簧臂连接。

通过在角色和相机的中间插入移除正方体进行阻挡相机的实验,可以更好地理解弹簧臂在其中的作用。如果不添加弹簧臂,正方体就可能从角色和相机中间穿过,从而阻挡玩家视线。

3. 人物移动与转向
人物移动的相关代码需要编写在自动生成的SetupPlayerInputComponent函数中。这个函数中已经传入了PlayerInputComponent组件,所以不需要像上面自己创建新的弹簧臂和相机控件。人物移动对应UE中的轴(Axis)绑定,因为移动的幅度是个连续值,如使用游戏手柄时,不同程度推动摇杆可能对应不同的移动速度。这里需要注意,.cpp中编写的所有函数都需要在.h文件中进行声明。本部分编写的代码如下,细节见注释:
// SurCharacter.cpp
// 实现角色向前移动
void ASurCharacter::MoveForward(float value)
{
// 朝某个方向前进value,GetActorForwardVector指向角色正前方
AddMovementInput(GetActorForwardVector(), value);
}
void ASurCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
// “UE中调用的名称”,this指针表示移动这个角色,&自定义移动方法
PlayerInputComponent->BindAxis("MoveForward", this, &ASurCharacter::MoveForward);
}
随后,需要在UE中设置MoveForward的具体输入。在编辑 -> 项目设置 -> 输入 -> 绑定中添加轴映射 -> 输入MoveForward,即与BindAxis函数中第一个参数一致 -> 添加键盘输入W和S,两者的value分别为1.0和-1.0。这样设置后,在按下S的时候会调用MoveForward函数,传入-1.0,即角色前进负值,等同于后退。

此时运行关卡,就可以发现角色能够进行前进后退,且相机始终保持跟随。同理,利用UE中提供的GetActorRightVector()方法,可以实现角色的左右移动。大家可以自行尝试,注意编写完需要保存编译。

最后,要实现角色通过鼠标转向则更简单。类似平移可以用XYZ轴度量,旋转也可以分解成这三个方向,UE中的Yaw方向表示绕Z轴旋转、即水平旋转,详细解释可参考一位UP主的文章。利用UE提供的AddControllerYawInput()函数可以得到Yaw方向的偏转;AddControllerPitchInput()函数可以得到Pitch方向的旋转。
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
然后,同样需要在UE设置输入如下,鼠标X表示鼠标的横向(horizontal)移动,鼠标Y表示纵向(vertical)移动:

此外,在设置LookUp输入后,还需要在弹簧臂的“摄像机设置”属性中勾选“使用Pawn控制旋转”。最后检查一下运行结果,发现角色可以移动和旋转,相机也始终跟随。

4. 完整代码
SurCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SurCharacter.generated.h"
//为了编译更快,在.h中声明弹簧臂和相机的class,在.cpp引用相应头文件
class USpringArmComponent;
class UCameraComponent;
UCLASS()
class SURKEAUE_API ASurCharacter : public ACharacter
{
GENERATED_BODY()
public:
ASurCharacter();
protected:
//弹簧臂组件
UPROPERTY(VisibleAnywhere)
USpringArmComponent* SpringArmComp;
//相机组件
UPROPERTY(VisibleAnywhere)
UCameraComponent* CameraComp;
// 游戏开始或生成时调用
virtual void BeginPlay() override;
void MoveForward(float value);
void MoveRight(float value);
public:
// 每一帧调用
virtual void Tick(float DeltaTime) override;
// 绑定输入
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};
SurCharacter.cpp
#include "SurCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
ASurCharacter::ASurCharacter()
{
PrimaryActorTick.bCanEverTick = true;
// 创建相应实例 <创建的class>("UE中显示的该控件的名字")
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>("SpringArmComp");
CameraComp = CreateDefaultSubobject<UCameraComponent>("CameraComp");
// 将相应对象进行attach,RootComponent是根控件,即角色这个胶囊体,可以在角色的完整蓝图的左上角“组件”中看到
SpringArmComp->SetupAttachment(RootComponent);
CameraComp->SetupAttachment(SpringArmComp);
}
void ASurCharacter::BeginPlay()
{
Super::BeginPlay();
}
// 实现角色向前移动
void ASurCharacter::MoveForward(float value)
{
// 朝某个方向前进value,GetActorForwardVector指向角色正前方
AddMovementInput(GetActorForwardVector(), value);
}
void ASurCharacter::MoveRight(float value)
{
AddMovementInput(GetActorRightVector(), value);
}
void ASurCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void ASurCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// “UE中调用的名称”,this指针表示移动这个角色,&自定义移动方法
PlayerInputComponent->BindAxis("MoveForward", this, &ASurCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ASurCharacter::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
}