TomLooman_ActionRoguelike_第二章项目与版本
该专栏用于保存对TomLooman的ActionRoguelike项目的学习笔记,学习过程中的思考与记录不一定准确。
教程参考:https://github.com/tomlooman/ActionRoguelike
基于UE5.0的项目实现:https://github.com/CarolBaggins2023/TomLooman_ActionRoguelike_Tutorial

2023_07_24
C++基础类,添加组件(第三人称摄像机),绑定运动输入,角色设置
建立角色的C++类,继承自Character,

Scharacter.h文件如下,

"CoreMinimal.h"头文件包含一套来自UE的核心编程环境的普遍存在类型(包含FString,FName,TArray等),通常被多数引擎的头文件包含。
因为SCharacter类继承自Character类,基于Character类生成源代码,所以包含"GameFramework/Character.h"头文件。
"SCharacter.generated.h"是所有C++类都会包含的头文件(SCharacter为该类的类名),UE将生成所有反射数据并放入该文件中。您必须将该文件作为声明类型的标头文件中的最后一个包含语句,将其包含进去,他必须在最下方,切记!
Uclass()是类说明符,类声明的语法如下
UCLASS([specifier, specifier, ...], [meta(key=value, key=value, ...)])
class ClassName : public ParentName
{
GENERATED_BODY()
}
Specifier为类说明符,meta为类元数据,两者合称为描述符,被传入CLASS宏。这里的类说明符和类元数据为空。
class ACTIONROGUELIKE_API ASCharacter中class后类名前的ACTIONROGUELIKE_API是一个宏,ACTIONROGUELIKE指代模块名。在虚幻引擎的代码中,MYGAME_API被用来标记一些类以及函数,从而将这些类和函数从模块或者是插件中公布出去,即被MYGAME_API标记的类和函数可以允许被其他模块或者插件访问,也就是控制代码的可见性和模块间的连接。模块的含义参考:https://docs.unrealengine.com/5.2/zh-CN/module-properties-in-unreal-engine/
因为我们创建的SCharacter类继承自Character类,所以属于游戏性类,游戏性类的前缀如下,

我们SCharacter的实例可以直接生成到世界场景中,所以我们代码中的类名不是SCharacter,而是ASCharacter。
GENERATED_BODY()UE4 将这个标记替换为该类型生成的所有必要的样板代码。引擎在背后为我们做了很多工作,生成了很多代码。
结合SCharacter.cpp文件

ASCharacter::ASCharacter()类的构造函数。
PrimaryActorTick.bCanEverTick = true;如果为true,则这个Character在每一帧都调用一次Tick函数。设置为false后可提升性能,默认为false。
virtual void BeginPlay() override;游戏开始或Character出生时被调用。
Super::BeginPlay();先调用ACharacter父类的BeginPlay,其它类成员函数类似。
Tick(float DeltaTime)DeltaTime为两次Tick之间的时间间隔
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;调用该函数,完成该函数内一系列语句,将player的某个input映射到某个函数(控制角色的某种行为)。
而且这里的UInputComponent就是上面说的两种游戏类种的另一类,因为不能直接布置到世界场景中,所以不是AInputComponent,而是UInputComponent。

创建C++的蓝图子类。C++和蓝图应该混合使用,因为一些操作比如添加mesh,在蓝图中更方便。

“终于,到了第4个纪元,UE窥得一丝隔壁平行宇宙Unity的天机。下定决心,让Actor们轻装上阵,只提供一些通用的基本生存能力,而把众多的“技能”抽象成了一个个“Component”并提供组装的接口,让Actor随用随组装,把自己武装成一个个专业能手。”
CapsuleComponent常用来处理碰撞和移动。
ArrowComponent用于指示方向。
这里的mesh(网格)是SkeletonMeshComponent,虚幻引擎中角色的基础资产是 骨骼网格体 资产,这种资产包含角色的一个可视化网格体(即角色的几何模型渲染结果),以及角色的一个包含骨骼数据(用于为角色制作动画)的骨架。
CharacterMovementComponent
能够使人身不使用刚体物理即可行走、跑动、飞行、坠落和游泳。 其为角色特定,无法被任何其他类实现。
每个Actor都有一个RootComponent。一般有一个SceneComponent作为RootComponent,定义Actor在世界中的transform(变换,包括位置location,旋转rotation,缩放scale),其它所有组件都需要attach到RootComponent上。
在SCharacter中添加两个组件

UPROPERTY([specifier, specifier, ...], [meta(key=value, key=value, ...)]) Type VariableName;
UPROPERTY是属性说明符,在声明属性时,属性说明符 可被添加到声明,以控制属性与引擎和编辑器诸多方面的相处方式。这里的VisibleAnywhere指此属性在所有属性窗口中都可见,设置后我们可以在编辑器的蓝图中看到Camera和SpringArm的各种属性,如下

否则红框的部分是不可见的。
UPROPERTY是实现UE中的“反射”的一种方式。
虚幻引擎反射系统 使用宏为提供引擎和编辑器各种功能,封装你的类。也就是
反射就是在运行状态中:
1.对于任意一个类,都能够知道这个类的所有属性和方法;
2.对于任意一个对象,都能够调用它的任意一个属性和方法;
即可以动态的获取信息以及调用对象的方法称之为反射机制。
因为USpringArmComponent和UCameraComponent继承自UObject,不能直接在世界中生成,所以类名的前缀是U。
摄像机(Camera) 代表了玩家的视角,比如玩家如何查看世界。
弹簧臂(SpringArm)组件努力与其对象(在这里是我们的Character)之间保持一个固定距离(TargetArmLength,不存在碰撞时的弹簧臂自然长度)如果发生碰撞,就会使子对象收回,如果没有碰撞,则发生回弹。
如果把SpringArm作为Camera的父项,就可以创建一个第三人称视角,且能保持Camera与SpringArm的对象(我们的Character)之间没有遮挡物。(这里需要调用attach函数,下面会做)




SpringArm始终保持遮挡物(白色方块)不会挡住Camera拍摄Character的画面。
在SCharacter.cpp中实例化SpringArm和Camera,
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>("SpringArmComp");
CameraComp = CreateDefaultSubobject<UCameraComponent>("CameraComp");
形式如下,
CreateDefaultSubobject<类名>(在UE编辑器中显示的名字)
1.CreateDefaultSubobject必须写在Actor的无参构造函数 中,否则crash;
2.CreateDefaultSubobject中的编辑器中显示的名字参数在同一个Actor中不能重复,否则crash;
SpringArmComp->SetupAttachment(RootComponent);
CameraComp->SetupAttachment(SpringArmComp);
将SpringArm附加到角色,将Camera附加到SpringArm。A“附加”到B,指A成为B的子组件。子组件的位置、旋转和缩放是相对于其父组件或Actor。Attach的函数原型如下,
void SetupAttachment(USceneComponent* InParent, FName InSocketName = NAME_None);
如果不进行attach,则会是如下的层级关系,SpringArm不会随Character移动,Camera不会随SpringArm移动。


在SCharacter.h中声明
void MoveForward(float val);
void MoveRight(float val);
在SCharacter.cpp中实现
void ASCharacter::MoveForward(float val) {
FRotator ControlRot = GetControlRotation();
ControlRot.Pitch = 0.0f;
ControlRot.Roll = 0.0f;
AddMovementInput(ControlRot.Vector(), val);
}
void ASCharacter::MoveRight(float val) {
FRotator ControlRot = GetControlRotation();
ControlRot.Pitch = 0.0f;
ControlRot.Roll = 0.0f;
FVector RightVector = FRotationMatrix(ControlRot).GetScaledAxis(EAxis::Y);
AddMovementInput(RightVector, val);
}
并在SetupPlayerInputComponent中调用
PlayerInputComponent->BindAxis("MoveForward", this, &ASCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ASCharacter::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
BindAxis的函数接口为template<class UserClass>
FInputAxisBinding& BindAxis( const FName AxisName, UserClass* Object, typename FInputAxisHandlerSignature::TMethodPtr< UserClass > Func )
第一个参数是在Project Setting里设置的(Axis)轴映射名如下(将外部输入与该输入下要触发的事件绑定),第二个参数是调用此函数的对象?(涉及UE的委托机制),第三个参数是一个函数指针(将触发的事件与成员函数绑定)。

BindAxis后,外部输入会触发类的成员函数,所以要声明、实现该成员函数,并在函数内进行响应。
在MoveForward的实现中,AddMovementInput的函数接口为
UFUNCTION(BlueprintCallable, Category="Pawn|Input", meta=(Keywords="AddInput"))
virtual void AddMovementInput(FVector WorldDirection, float ScaleValue = 1.0f, bool bForce = false);
该函数在所给的WorldDirection上增加一个外部输入的变化量ScaleValue(ScaleValue<0则反向移动)
FRotator(旋转类)是三个欧拉角Pitch、Roll、Yaw的封装,所以Frotator类的ControlRot中有它们三个成员变量。在MoveForward和MoveRight中我们只需要用外部输入控制Character的Yaw,所以把Pitch和Roll设为0(在UE中要显式地用0.0f)。
GetControlRotation()返回Pawn的控制器的旋转向量(相对世界坐标)。
ControlRot.Vector()将FRotator类型的变量转换为Vector类型的变量。
FVector是三维向量结构体。
将Fvector转为FrotationMatrix(FRotationMatrix(ControlRot)),并获取Y轴方向的方向向量(.GetScaledAxis(EAxis::Y)),从而得到指向当前Controller朝向的右方的方向向量。
否则,由于Pawn自身朝向的朝右向量,一旦右转就会一直在变化,导致会原地打转。(参考:https://blog.csdn.net/surkea/article/details/127104682)
我们不把事件"Turn"和"LookUp"绑定到自定义的函数上,而是绑定到Pawn类自带的APawn::AddControllerYawInput和APawn::AddControllerPitchInput上。这两个函数将输入值增加到控制器的Rotation上(Pawn相比Actor,增加了一个Controller,这里的Pawn就是我们的Character,Character是Pawn的子类)。
为了获得正确的Camera与Pawn移动表现,我们还要在构造函数ASCharacter::ASCharacter()中设置,
SpringArmComp->bUsePawnControlRotation = true;(SpringArm的Rotation跟随它所attach的Pawn的Controller(我们的鼠标)的Rotation。若为false,则因为SpringArm实际上控制着Camera(它是Camera的父组件),所以此时Camera固定朝着一个方向,不会随着鼠标的转动而转动)——>控制Camera朝向
GetCharacterMovement()->bOrientRotationToMovement = true;(Character朝向加速方向(Character中有个Arrow组件表示方向)。若为false,则Character在不调用Turn时始终面朝一个方向。该选项与下一项冲突,为下一项为true,则该项无效。)
this->bUseControllerRotationYaw = false;(类似SpringArmComp->bUsePawnControlRotation,Character用Controller的Rotation控制朝向。)
第一句设置Camera朝向,二三句控制Character朝向(所以两者冲突)。
GetCharacterMovement()->bOrientRotationToMovement = true; this->bUseControllerRotationYaw = false; ——>黑魂、老滚5,可看到角色正面(Controller控制Character朝X轴反方向移动时,Character面朝玩家)
GetCharacterMovement()->bOrientRotationToMovement = false; this->bUseControllerRotationYaw = true; ——>光明记忆、一些大逃杀,只能看到角色背面(因为角色始终朝向Controller的X轴方向)
另外,因为bOrientRotationToMovement和bUseControllerRotationYaw都是bool类型的变量,所以它们的名字都是b开头的。

给Character的mesh组件的skeletal mesh一个具体的东西,否则我们看不到角色。
这里还要把导入的角色与碰撞体适配,并且角色朝向与ArrowComponent方向一致。

加入角色动画,否则角色默认摆大字。角色动画会根据角色的状态、速度等因素渲染一个姿势,不同姿势组成的时间序列形成了我们看到的动画效果。