最近因为ACM省赛以及参加笔试面试等原因,许久没有更新博客,由于实习公司的原因,最近开始看虚幻四引擎的官方文档,这篇博客是对官方文档中C++编程部分第一人称FPS部分的代码分析和总结。
一、GameMode
GameMode又称游戏模式(其实就是翻译过来),它存在的意义是制定游戏的规则,比如龟兔赛跑,其规则就是谁先到达终点谁就获胜,它仅存在于服务器上。
在官方文档的第一人称FPS教程部分,有稍微提到过GameMode,代码如下:
FPSProjectGameMode.h
#pragma once
#include "GameFramework/GameMode.h"
#include "FPSProjectGameMode.generated.h"
/**
* GameMode类用于构建游戏规则
*/
UCLASS()
class FPSPROJECT_API AFPSProjectGameMode : public AGameMode
{
GENERATED_BODY()
virtual void StartPlay() override;//重写AGameMode基类的虚函数StartPlay
};
FPSProjectGameMode.cpp
#include "FPSProject.h"
#include "FPSProjectGameMode.h"
void AFPSProjectGameMode::StartPlay()
{
Super::StartPlay();//Super类即为AGameMode类
if (GEngine)
{
// 显示调试信息五秒。
// -1“键”值(首个参数)说明我们无需更新或刷新此消息。
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("Hello World, this is FPSGameMode!"));
}
}
二、Pawn与Character
Pawn可由玩家或 AI 控制的所有 Actors 的基础类。而Actor可以简单认为是游戏世界中的任何可以动作的物体(我暂时这么理解)。Character是类人的Pawn,它自带许多组件,比如CharacterMovementComponent(移动组件)、CapsuleComponent(胶囊体组件)和SkeletalMesh(骨骼网格组件)。
官方教程在这里给出了FPS基本的动作比如:移动、跳跃、开火)
代码如下:
FPSCharacter.h
#pragma once
#include "GameFramework/Character.h"
#include "FPSCharacter.generated.h"
UCLASS()
class FPSPROJECT_API AFPSCharacter : public ACharacter
{
GENERATED_BODY()
public:
// 设置该角色属性的默认值。
AFPSCharacter();
// 游戏开始时或生成时调用。
virtual void BeginPlay() override;//重写Actor的BeginPlay函数
// 每帧调用。
virtual void Tick(float DeltaSeconds) override;//重写Actor的Tick函数
// 调用后将功能绑定到输入。
virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;//重写Character的SetupPlayerInputComponent函数
// 处理前后移动的输入。UFUNCTION表示将该函数注册到蓝图中
UFUNCTION()
void MoveForward(float Value);
// 处理左右移动的输入。
UFUNCTION()
void MoveRight(float Value);
// 按下按键时设置跳跃标记。
UFUNCTION()
void StartJump();
// 松开按键时清除跳跃标记。
UFUNCTION()
void StopJump();
// 处理开火的函数。
UFUNCTION()
void Fire();
// FPS 摄像机。UPROPERTY表示将一个类成员注册到蓝图中
UPROPERTY(VisibleAnywhere)
UCameraComponent* FPSCameraComponent;//定义一个UCameraComponent——相机组件
// 第一人称模型(手臂),仅对拥有玩家可见。
UPROPERTY(VisibleDefaultsOnly, Category = Mesh)
USkeletalMeshComponent* FPSMesh;//定义一个USkeletalMeshComponent——骨骼网格组件
// 从摄像机位置的枪口偏移。
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
FVector MuzzleOffset;
// 生成的发射物类。
UPROPERTY(EditDefaultsOnly, Category = Projectile)
TSubclassOf<class AFPSProjectile> ProjectileClass;//AFPSProjectile类及其子类的类型引用,在蓝图中会使其可以通过下拉菜单来详细指定其类型
};
FPSCharacter.cpp
#include "FPSProject.h"
#include "FPSCharacter.h"
#include "FPSProjectile.h"
// 设置默认值
AFPSCharacter::AFPSCharacter()
{
// 设置此角色每帧调用 Tick()。不需要时可将此关闭,以提高性能。
PrimaryActorTick.bCanEverTick = true;
// 创建一个第一人称摄像机组件。
//CreateDefaultSubobject创建一个组件或者子对象
FPSCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
// 将摄像机组件附加到胶囊体组件。
//Character类在Pawn基础上含有CharacterMovementComponent、CapsuleComponent和SkeletalMesh组件
//GetCapsuleComponent得到character类中胶囊提组件的指针
FPSCameraComponent->AttachTo(GetCapsuleComponent());
// 将摄像机放置在眼睛上方不远处。
//BaseEyeHeight是Pawn类中的成员
//SetRelativeLocation设置局部空间中的位置
FPSCameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 50.0f + BaseEyeHeight));
// 用 pawn 控制摄像机旋转。
FPSCameraComponent->bUsePawnControlRotation = true;
// 为拥有玩家创建一个第一人称模型组件。
FPSMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("FirstPersonMesh"));
// 该模型仅对拥有玩家可见。
FPSMesh->SetOnlyOwnerSee(true);
// 将 FPS 模型添加到 FPS 摄像机。
FPSMesh->AttachTo(FPSCameraComponent);
// 禁用部分环境阴影,保留单一模型存在的假象。
FPSMesh->bCastDynamicShadow = false;
FPSMesh->CastShadow = false;
// 拥有玩家无法看到普通(第三人称)身体模型。
//GetMesh是character类中的函数,返回绑定在character类(基类)中的骨骼模型
GetMesh()->SetOwnerNoSee(true);
}
// 游戏开始时或生成时调用。
void AFPSCharacter::BeginPlay()
{
Super::BeginPlay();//调用基类的BeginPlay函数??
if (GEngine)
{
// 显示调试信息五秒。-1“键”值(首个参数)说明我们无需更新或刷新此消息。
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("We are using FPSCharacter."));
}
}
// 每帧调用。
void AFPSCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// 调用后将功能绑定到输入。
void AFPSCharacter::SetupPlayerInputComponent(UInputComponent* InputComponent)
{
Super::SetupPlayerInputComponent(InputComponent);
// 设置“移动”绑定。
InputComponent->BindAxis("MoveForward", this, &AFPSCharacter::MoveForward);
InputComponent->BindAxis("MoveRight", this, &AFPSCharacter::MoveRight);
// 设置“查看”绑定。
InputComponent->BindAxis("Turn", this, &AFPSCharacter::AddControllerYawInput);
InputComponent->BindAxis("LookUp", this, &AFPSCharacter::AddControllerPitchInput);
// 设置“动作”绑定。
InputComponent->BindAction("Jump", IE_Pressed, this, &AFPSCharacter::StartJump);
InputComponent->BindAction("Jump", IE_Released, this, &AFPSCharacter::StopJump);
InputComponent->BindAction("Fire", IE_Pressed, this, &AFPSCharacter::Fire);
}
void AFPSCharacter::MoveForward(float Value)
{
// 明确哪个方向是“前进”,并记录玩家试图向此方向移动。
//Controller是Pawn定义的成员即为控制该Pawn的控制器,GetControlRotation获得控制器的旋转角,GetScaledAxis的到某一个轴的值(这里是矩阵的第一列即X轴)
FVector Direction = FRotationMatrix(Controller->GetControlRotation()).GetScaledAxis(EAxis::X);
AddMovementInput(Direction, Value);
}
void AFPSCharacter::MoveRight(float Value)
{
// 明确哪个方向是“向右”,并记录玩家试图向此方向移动。
FVector Direction = FRotationMatrix(Controller->GetControlRotation()).GetScaledAxis(EAxis::Y);
AddMovementInput(Direction, Value);
}
void AFPSCharacter::StartJump()
{
bPressedJump = true;
}
void AFPSCharacter::StopJump()
{
bPressedJump = false;
}
void AFPSCharacter::Fire()
{
// 尝试发射物体。
if (ProjectileClass)
{
// 获取摄像机变换。
FVector CameraLocation;
FRotator CameraRotation;
//如果存在Controller或它的Pawn,这个函数则返回Controller或他的Pawn的视角。基本上,它会返回玩家从哪个位置和方向观看。
GetActorEyesViewPoint(CameraLocation, CameraRotation);
// 将 MuzzleOffset 从摄像机空间变换到世界空间。
FVector MuzzleLocation = CameraLocation + FTransform(CameraRotation).TransformVector(MuzzleOffset);
FRotator MuzzleRotation = CameraRotation;
// 将准星稍微上抬。
MuzzleRotation.Pitch += 10.0f;
UWorld* World = GetWorld();
if (World)
{
//FActorSpawnParameters是world类中的结构体,表示游戏世界中的一个pawn。。。(大概是这样)
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
//Instigator是可以对pawn造成伤害的actor
SpawnParams.Instigator = Instigator;
// 在枪口处生成发射物。
//SpawnActor用来创建Actor类型的物体(物体类型,生成地点,生成方向,和世界中的一个物体??)
AFPSProjectile* Projectile = World->SpawnActor<AFPSProjectile>(ProjectileClass, MuzzleLocation, MuzzleRotation, SpawnParams);
if (Projectile)
{
// 设置发射物的初始轨道。
FVector LaunchDirection = MuzzleRotation.Vector();
Projectile->FireInDirection(LaunchDirection);
}
}
}
}
三、发射物的实现
这里,因为发射物是一个不需要AI或玩家控制的物体,但是其也会进行运动,所以该类继承自Actor类即可。
我们只需计算它的运动状态(碰撞啊之类的)即可。
FPSProjectile.h
#pragma once
#include "GameFramework/Actor.h"
#include "FPSProjectile.generated.h"
UCLASS()
class FPSPROJECT_API AFPSProjectile : public AActor
{
GENERATED_BODY()
public:
// 设置该 actor 属性的默认值。
AFPSProjectile();
// 游戏开始时或生成时调用。
virtual void BeginPlay() override;
// 每帧调用。
virtual void Tick(float DeltaSeconds) override;
// 球体碰撞组件。
UPROPERTY(VisibleDefaultsOnly, Category = Projectile)
USphereComponent* CollisionComponent;
// 发射物运动组件。
UPROPERTY(VisibleAnywhere, Category = Movement)
UProjectileMovementComponent* ProjectileMovementComponent;
// 在发射方向上设置发射物初速度的函数。
void FireInDirection(const FVector& ShootDirection);
// 发射物命中物体时调用的函数。
void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit);
};
FPSProjectile.cpp
#include "FPSProject.h"
#include "FPSProjectile.h"
// 设置默认值
AFPSProjectile::AFPSProjectile()
{
// 将此 actor 设为每帧调用 Tick()。不需要时可将此关闭,以提高性能。
PrimaryActorTick.bCanEverTick = true;
// 使用球体代表简单碰撞。
CollisionComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
//BodyInstance存储这个组件的物理信息,并持有一个刚体
CollisionComponent->BodyInstance.SetCollisionProfileName(TEXT("Projectile"));
//OnComponentHit检测碰撞,当发生碰撞后调用某个函数
CollisionComponent->OnComponentHit.AddDynamic(this, &AFPSProjectile::OnHit);
// 设置球体的碰撞半径。
CollisionComponent->InitSphereRadius(15.0f);
// 将碰撞组件设为根组件。
RootComponent = CollisionComponent;
// 使用此组件驱动此发射物的运动。
ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovementComponent"));
//设置作为碰撞物的组件
ProjectileMovementComponent->SetUpdatedComponent(CollisionComponent);
//初始速度
ProjectileMovementComponent->InitialSpeed = 3000.0f;
//最大速度
ProjectileMovementComponent->MaxSpeed = 3000.0f;
//方向是否受速度影响
ProjectileMovementComponent->bRotationFollowsVelocity = true;
//是否反弹
ProjectileMovementComponent->bShouldBounce = true;
ProjectileMovementComponent->Bounciness = 0.3f;
// 3 秒后消亡。
InitialLifeSpan = 10.0f;
}
// 游戏开始时或生成时调用。
void AFPSProjectile::BeginPlay()
{
Super::BeginPlay();
}
// 每帧调用。
void AFPSProjectile::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// 在发射方向上设置发射物初速度的函数。
void AFPSProjectile::FireInDirection(const FVector& ShootDirection)
{
ProjectileMovementComponent->Velocity = ShootDirection * ProjectileMovementComponent->InitialSpeed;
}
// 发射物命中物体时调用的函数。
//UPrimitiveComponent具有某种形式的表现,如网格,粒子等可见的东西;
//HitComponent撞击物体
//OtherActor被撞物体
//OtherComponent被撞物体的组件
//FHitResult存储碰撞结果(发出射线进行碰撞检测然后存储结果)
void AFPSProjectile::OnHit(UPrimitiveComponent* HitComponent,AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit)
{
//IsSimulatingPhysics判断是否遵循物理现象
//注意父组件与子组件是两个实体,如果有二者满足碰撞条件,注意不要让二者重叠,
//否则一旦游戏中产生这个Actor,两个组件就会以一定的速度相互排斥开来。
//所以不要随意开启组件的simulate physics并且注意各个组件之间的关系。
//而这里添加OtherActor != this这句话也可以防止该实体自身的组件间的发生碰撞
if (OtherActor != this && OtherComponent->IsSimulatingPhysics())
{
//ImpactPoint碰撞点
//AddImpulseAtLocation(冲量,位置)——在某一位置增加一个冲量
OtherComponent->AddImpulseAtLocation(ProjectileMovementComponent->Velocity * 1000000.0f, Hit.ImpactPoint);
}
}
四、HUD准星的实现
FPSHUD.h
#pragma once
#include "GameFramework/HUD.h"
#include "FPSHUD.generated.h"
/**
*
*/
UCLASS()
class FPSPROJECT_API AFPSHUD : public AHUD
{
GENERATED_BODY()
protected:
// 这将在屏幕中央绘制。
//UTexture2D——2D纹理类型
UPROPERTY(EditDefaultsOnly)
UTexture2D* CrosshairTexture;
public:
// HUD 的主绘制调用。
virtual void DrawHUD() override;
};
FPSHUD.cpp
#include "FPSProject.h"
#include "FPSHUD.h"
void AFPSHUD::DrawHUD()
{
Super::DrawHUD();
if (CrosshairTexture)
{
// 找到画布中心。
FVector2D Center(Canvas->ClipX*0.5f, Canvas->ClipY*0.5f);
// 纹理维度一半偏移,使纹理中心和画布中心对齐。
FVector2D CrossHairDrawPosition(Center.X - (CrosshairTexture->GetSurfaceWidth() * 0.5f), Center.Y - (CrosshairTexture->GetSurfaceHeight() * 0.5f));
// 在中心点绘制准星。
FCanvasTileItem TileItem(CrossHairDrawPosition, CrosshairTexture->Resource, FLinearColor::White);
TileItem.BlendMode = SE_BLEND_Translucent;
Canvas->DrawItem(TileItem);
}
}