UE5之横版2D游戏(一)

也算是一边看文档和各种教程,一边用蓝图做了一个简单的demo出来,不过还是感觉很多东西的云里雾里的,正好打算把部分蓝图内容转换为C++实现,所以趁这个机会记录一下

PaperCharacter

地图的制作感觉可以先放一放,因为整个流程还是比较简单的,主要就是素材的导入,给必要的Tile添加碰撞,然后自由组合即可,等后续需要和角色产生交互再统一记录吧,所以这里就先记录一下角色制作的一些关键流程

Enhanced Input

以前处理输入主要依赖于轴映射输入,但现在UE5更推荐使用增强输入来处理,个人的理解是增强输入可以解耦输入操作的逻辑,以组合的形式提高灵活性和复用性。不过我自己也没有详细了解过增强输入系统,所以就不展开介绍原理了,只简单记录一下如何使用

横板2D游戏的移动比较简单,最基础的就是左右移动和跳跃,所以就先从这两个最基础的Action开始,首先从InputMappingContext和InputAction创建IMC_Default、IA_Move和IA_Jump

然后在IMC_Default中完成配置

接下来创建一个C++类用于表示游戏中的角色,这里暂时使用默认的Character,因为2D角色主要就是把骨骼组件改为成了Sprite,动画组件变为FlipBook,其余地方没有太多区别,而正式创建2D角色会用到PaperZD这个插件,所以就先用默认的Character展示

我们需要实现Move和Jump的逻辑,然后把相应的资源和逻辑载入到输入增强系统中,所以我们首先在头文件CharacterBase.h中声明相关变量和函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "CharacterBase.generated.h"

class UInputMappingContext;
class UInputAction;
struct FInputActionValue;

UCLASS()
class TEST_API ACharacterBase : public ACharacter
{
GENERATED_BODY()

// 添加输入映射上下文
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputMappingContext* InputMappingContext;

// 添加Move动作
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* MoveAction;

// 添加Jump动作
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* JumpAction;

public:
// Sets default values for this character's properties
ACharacterBase();

protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;

public:
// Called every frame
virtual void Tick(float DeltaTime) override;

// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

void Move(const FInputActionValue& Value);
};

然后在CharacterBase.cpp中完成相关逻辑(这里已经在构造函数中调整了胶囊组件和骨骼组件的部分配置,也可以延迟到在蓝图子类中设置),简单来说

  • 对于Move函数,由于是2D游戏,我们只需要控制角色在水平方向移动即可,假设我们的角色在X轴上移动,那么我们就可以不用关心Y轴和Z轴
  • 对于Jump函数,由于没有什么特殊需求,所以直接使用了默认的实现
  • 最后,我们需要把Action和相应逻辑绑定,并加入到增强输入系统中

下面是具体的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Fill out your copyright notice in the Description page of Project Settings.


#include "CharacterBase.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"

// Sets default values
ACharacterBase::ACharacterBase()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;

// 初始化胶囊组件
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

// 设置骨骼网格组件并加载Asset
GetMesh()->SetRelativeLocation(FVector(0.0f, 0.0f, -90.0f));
GetMesh()->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
GetMesh()->SetSkeletalMeshAsset(LoadObject<USkeletalMesh>(nullptr, TEXT("/Script/Engine.SkeletalMesh'/Game/Characters/Mannequin_UE4/Meshes/SK_Mannequin.SK_Mannequin'")));
}

// Called when the game starts or when spawned
void ACharacterBase::BeginPlay()
{
Super::BeginPlay();
}

// Called every frame
void ACharacterBase::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}

// Called to bind functionality to input
void ACharacterBase::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);

// 输入映射上下文
if (APlayerController* PC = Cast<APlayerController>(Controller)) {
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer())) {
Subsystem->AddMappingContext(InputMappingContext, 0);
}
}

// 绑定输入动作
if (UEnhancedInputComponent* EIComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
EIComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ACharacterBase::Move);
EIComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacterBase::Jump);
EIComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
}
}

// 移动逻辑
void ACharacterBase::Move(const FInputActionValue& Value) {
FVector ForwardDirection = FVector(1.0f, 0.0f, 0.0f);
FVector2D MovementValue = Value.Get<FVector2D>();

AddMovementInput(ForwardDirection, MovementValue.X);
}

之后创建一个基于CharacterBase的蓝图类BP_CharacterBase,并添加之前制作好的IMC_Default、IA_Move和IA_Jump,因为我们没有用代码加载它们,所以需要手动设置一下(如果你之前没有用代码调整胶囊组件和骨骼组件的部分配置,那么也可以在这里设置)

然后在GameMode中指定BP_CharacterBase为默认的Pawn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// AtestGameMode.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "testGameMode.generated.h"

UCLASS(minimalapi)
class AtestGameMode : public AGameModeBase
{
GENERATED_BODY()


public:
AtestGameMode();
};

// AtestGameMode.cpp

#include "testGameMode.h"
#include "testCharacter.h"
#include "UObject/ConstructorHelpers.h"

AtestGameMode::AtestGameMode()
{
// set default pawn class to our Blueprinted character
static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("/Game/ThirdPerson/Blueprints/BP_CharacterBase"));
if (PlayerPawnBPClass.Class != NULL)
{
DefaultPawnClass = PlayerPawnBPClass.Class;
}
}

然后完成相关配置

现在我们运行游戏,并选中角色以便观察位置的变化(此时还没有添加摄像机,所以看不到人物模型),然后按D试试能否移动

可以观察到,只有X发生了变化,所以移动的逻辑暂时没有太大的问题,现在我们处理一下摄像机,以便我们可以更好的观察

Camera

首先需要确定的是,我们的游戏是一个类似于冒险岛空洞骑士的横版本2D游戏,所以摄像机应该从侧视角拍摄我们的角色,那么这里会用到两个关键的组件SpringArm和Camera

简单解释一下SpringArm是什么,大致可以把SpringArm看作成一个扛着摄像机的人,控制摄像机从什么位置,什么角度进行拍摄。这也能很好的解释为什么后续代码中,我们实现侧视角拍摄旋转的是SpringArm,而不是Camera本身

首先在CharacterBase.h中添加相应的组件

1
2
3
4
5
6
7
// 添加SpringArm组件
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
USpringArmComponent* SpringArm;

// 添加Camera组件
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UCameraComponent* Camera;

然后在构造函数中完成初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Sets default values
ACharacterBase::ACharacterBase()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;

// 初始化胶囊组件
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

// 设置骨骼网格组件并加载Asset
GetMesh()->SetRelativeLocation(FVector(0.0f, 0.0f, -90.0f));
GetMesh()->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
GetMesh()->SetSkeletalMeshAsset(LoadObject<USkeletalMesh>(nullptr, TEXT("/Script/Engine.SkeletalMesh'/Game/Characters/Mannequin_UE4/Meshes/SK_Mannequin.SK_Mannequin'")));

// 创建SpringArm和Camera组件,并加入到RootComponent中
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(RootComponent);
SpringArm->TargetArmLength = 300.0f;

// 调整拍摄的角度,侧视角,并添加一定的俯视角
SpringArm->SetRelativeRotation(FRotator(-45, -90, 0));

// 创建Camera组件,添加到SpringArm组件之下
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
Camera->ProjectionMode = ECameraProjectionMode::Orthographic;
Camera->OrthoWidth = 720.0f;
}

现在基础的移动功能就完成了(跳跃其实也是正常的,不过视角问题,而且没有添加动画,所以不太明显)

最终实现

由于使用的是第三人称模板,所以出于方便都是用的3D角色做的实验,现在正式使用PaperZD来实现2D角色(第三方插件),创建一个继承自PaperZDCharacter的类PaperCharacterBase

代码方面变动不大,只需要把骨骼相关的内容删除即可,下面给出完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#pragma once

#include "CoreMinimal.h"
#include "PaperZDCharacter.h"
#include "PaperCharacterBase.generated.h"

class UInputMappingContext;
class UInputAction;
class USpringArmComponent;
class UCameraComponent;
struct FInputActionValue;

UCLASS()
class TEST_API APaperCharacterBase : public APaperZDCharacter
{
GENERATED_BODY()

// 添加输入映射上下文
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputMappingContext* InputMappingContext;

// 添加Move动作
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* MoveAction;

// 添加Jump动作
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* JumpAction;

// 添加SpringArm组件
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
USpringArmComponent* SpringArm;

// 添加Camera组件
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UCameraComponent* Camera;

public:
APaperCharacterBase();

protected:
virtual void BeginPlay() override;

public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

void Move(const FInputActionValue& Value);
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include "PaperCharacterBase.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "Components/CapsuleComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "PaperFlipbookComponent.h"

APaperCharacterBase::APaperCharacterBase()
{
PrimaryActorTick.bCanEverTick = true;

// 初始化胶囊组件
GetCapsuleComponent()->InitCapsuleSize(22.f, 40.0f);

// 创建SpringArm和Camera组件,并加入到RootComponent中
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(RootComponent);
SpringArm->TargetArmLength = 300.0f;
SpringArm->SetRelativeRotation(FRotator(0, -90, 0));

// 创建Camera组件,添加到SpringArm组件之下
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
Camera->ProjectionMode = ECameraProjectionMode::Orthographic;
Camera->OrthoWidth = 720.0f;
}

// Called when the game starts or when spawned
void APaperCharacterBase::BeginPlay()
{
Super::BeginPlay();
}

// Called every frame
void APaperCharacterBase::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void APaperCharacterBase::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);

// 输入映射上下文
if (APlayerController* PC = Cast<APlayerController>(Controller)) {
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer())) {
Subsystem->AddMappingContext(InputMappingContext, 0);
}
}

// 绑定输入动作
if (UEnhancedInputComponent* EIComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
EIComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APaperCharacterBase::Move);
EIComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &APaperCharacterBase::Jump);
EIComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &APaperCharacterBase::StopJumping);
}
}


// 移动逻辑
void APaperCharacterBase::Move(const FInputActionValue& Value) {
FVector ForwardDirection = FVector(1.0f, 0.0f, 0.0f);
FVector2D MovementValue = Value.Get<FVector2D>();

AddMovementInput(ForwardDirection, MovementValue.X);
}

同样的,需要创建一个继承自PaperCharacterBase的蓝图类BP_PaperCharacterBase,并在GameMode中把默认Pawn修改过来

最后看一下效果(这里我只设置了侧视角,没有设置俯视角)

一些有意思的设定

回到默认的第三人称模板(把GameMode中默认Pawn改回去就行),在调设置的时候发现了几个有意思的设定,分别是角色移动组件里的使用控制器所需的旋转(bUseControllerDesiredRotation)将旋转朝向运动(bOrientRotationToMovement)、Self里的使用控制器旋转YAW(bUseControllerRotationYaw)

如果以上3个选项都不勾选,那么角色的朝向是永远是初始化到游戏场景中的朝向(如果你可以通过某种方式改变控制器正方向的朝向,那么此时,控制器的朝向和角色的朝向是分离的)

例如最开始移动时,角色朝向和控制器正方向是一致的,按W可以往前走。由于第三人称模板提供了IA_Look,可以用鼠标改变控制器的正方向的朝向,所以当我移动鼠标,角色就开始朝不同的方向移动,即使整个过程都只按了W

将控制器和角色朝向关联起来的关键就是这三个设置,其中使用控制器所需的旋转使用控制器旋转YAW二者的功能是一致的,当勾选之后,角色朝向会和控制器的正方向绑定

当我移动鼠标时,角色朝向总是与控制器的正方向一致,这就保证了,按下W时总是往角色朝向的方位运动

那么使用控制器所需的旋转使用控制器旋转YAW有什么区别呢,在使用控制器所需的旋转的描述中有提到

如果该选项为true,那么会应用RotationRate以平滑的旋转,会被将旋转朝向运动这一选项覆盖,并且需要取消勾选使用控制器旋转YAW

我们勾选使用控制器所需的旋转,然后把旋转速率降低

可以看到旋转的速度是比较缓慢的,尽管我早就已经用鼠标完成了转动的操作

由于勾选使用控制器旋转YAW会覆盖掉这些设置,且功能一致,所以就不单独测试了,接下来直接看将旋转朝向运动的功能即可

这个功能并不会将角色的朝向和控制器的正方向绑定(即移动鼠标角色的朝向不会改变),但它会让角色运动时旋转到运动的方向,可以看到,在我不改变控制器朝向时,分别按下WDA,可以观察到角色的朝向改变了(注意这个选项会覆盖掉使用控制器所需的旋转的功能,即控制器的正方向可以改变,但人物朝向不变)

同样的,如果想使用这个功能,应该取消勾选使用控制器旋转YAW

简单总结一下

  • 使用控制器所需的旋转使用控制器旋转YAW是把角色朝向和控制器正方向绑定
  • 将旋转朝向运动是在运动时改变角色朝向,将其和实际运动的方向绑定

你可能会好奇,第三人称模板不应该是移动鼠标然后旋转摄像机的视角吗,怎么我这里移动鼠标只能改变控制器在水平方向上的旋转,摄像机总是拍摄角色的背部,这就是最后一个设定,SpringArm中的使用Pawn控制旋转(bUsePawnControlRotation)

我们先把之前的几个设定取消勾选,然后开启使用Pawn控制旋转注意,虽然这里写的是摄像机设置,但实际是SpringArm中的选项,而不是摄像机中的(摄像机也有相同的选项)

当然,由于关闭了之前的三个选项,所以控制器的正方向和角色朝向不是一致的

所以要实现正确的第三人称视角就有两种组合,使用Pawn控制旋转 + 使用控制器所需的旋转使用Pawn控制旋转 + 将旋转朝向运动,它们的区别就在于朝不同的方向移动时,角色是否会面向这个方向

简单看一下效果,首先是Pawn控制旋转 + 使用控制器所需的旋转组合,可以看到,角色朝向会随着我移动鼠标变化,但当我按AD时,角色并没有转向运动的方向,但如果我只按W,用鼠标改变控制器正方向时,一切都很正常,因为此时角色的朝向与控制器的正方向绑定,所以OK

现在我们看看Pawn控制旋转 + 将旋转朝向运动的组合,最大的区别就在于,不产生运动时,角色的朝向并不会改变,但我按AD时,角色会转向运动的方向,并且最后我只按W,用鼠标改变控制器正方向时,也是正常的,所以这个组合可以看作一个满血的第三人称控制,那么Pawn控制旋转 + 使用控制器所需的旋转组合有什么用途呢,假设我们的游戏提供了一个AutoRun的功能,此时玩家便无需按W、A、S、D控制角色运动,只需要移动鼠标控制方向即可