一. 效果与资源准备
1.1 游戏演示效果
1.2 游戏资产素材与源码
1.3 前期准备
- 创建无初学者内容的空项目

- 将素材文件拷贝到项目 Content 文件夹下

- 搭建场景(用 3D 场景模拟 2D 游戏效果)

二. 创建角色类与全局相机
2.1 创建角色类
- 创建C++类(Character),重命名 Cake(Public)

- 创建继承自 Cake 的蓝图(实例化),重命名 BP_Cake,并存放在 Blueprints 文件夹下

- 设置 BP_Cake 模型资产,并调整胶囊体覆盖模型

- 创建 BP_GameModeBase 游戏模式(GameModeBase),并和 BP_Cake 一起挂载到项目设置,那么在场景中放置玩家出生点 Player Start 后,运行时会自动生成 BP_Cake 的玩家出生点位置

2.2 创建全局相机
- 创建C++类(Actor),重命名 Camera(Public)

- 创建继承自 Camera 的蓝图(实例化),重命名 BP_Camera,并存放在 Blueprints 文件夹下

- 指定默认相机

- 调整相机正交模式

三. 移动逻辑
3.1 角色水平方向跟随鼠标移动
- 获取 PlayerController,运行时显示鼠标

- 角色随鼠标移动

- 防止角色出画,添加阻挡体积

- 运行效果:

3.2 角色垂直方向跳跃
- 角色垂直方向跳跃

- 绑定操作映射

- 运行效果:

3.4 设置角色速度
- 设置角色在空中/落地 2种状态下的不同速度

- 设置角色 BP_Cake 蓝图物理细节参数

3.4 全局相机垂直跟随
- 相机左右并不移动,只需调整 Z 轴跟随

- 设置背景板和阻挡体积为可移动,随摄像机移动

- 运行效果:

四. 创建云朵类
4.1 创建云朵类
- 创建C++类(Actor),重命名 Cloud(Public)

- 创建继承自 Cloud 的蓝图(实例化),重命名 BP_Cloud,并存放在 Blueprints 文件夹下

- 添加盒体检测组件和云朵组件

4.2 随机云朵贴图生成
- 通过修改材质中的,随机生成云朵显示


- 设置云朵的显示,并添加材质数组

- 运行效果(拖放多个 BP_Cloud 到场景中运行测试):

- 调整修改云朵的大小

4.3 角色触碰云朵则触发弹跳
- 重写碰撞函数,检测到角色则调用弹跳函数

- 运行效果(小球触碰到云朵就起飞):

4.4 云朵随机下雨
- 添加云朵雨组件并绑定,随机显示云朵雨

- 设置云朵雨的位置,并添加材质

- 运行效果(随机生成云朵雨):

五. 云朵生成器
5.1 创建云朵生成器
- 创建C++类(Actor),重命名 SpawnCloud(Public)

- 创建继承自 SpawnCloud 的蓝图(实例化),重命名 BP_SpawnCloud,存放在 Blueprints 文件夹下

- 云朵生成器的原理:在高处设置一块云朵生成区 SpawnArea,低处设置一块碰撞检测区 TriggerArea,当角色跳跃碰撞到检测区 TriggerArea,则整体上移,并在生成区 SpawnArea 随机位置生成云朵

5.2 随机位置生成云朵
- 添加云朵生成区域组件和检测生成区域组件

- 随机位置生成云朵

5.3 碰撞检测与初始化上升
- 检测是否与角色产生碰撞

- 云朵生成器的初始化与上升

- 在 BP_SpawnCloud 中调整参数

六. 计分系统与云朵销毁
6.1 计分系统
- 在角色中添加分数和计分方法

- 在云朵中添加分数文本,并显示分数

- 调整分数文本的字体位置大小

6.2 TimeLine 云朵销毁
- 结合蓝图中 Timeline 实现云朵淡入淡出效果



- 销毁相机视角外的多余云朵

- 在场景中调整销毁碰撞检测区域的位置

- 运行效果(销毁时显示计数并变化淡出,落下区域无云朵):

七. 游戏结束与重开
7.1 游戏结束的判定
- 结合蓝图中 Timeline (计数 2s )判断角色是否持续下落,如果持续下落则直接降落到地面


- 游戏结束的判定


7.2 游戏重开按钮
- 创建 UMG 蓝图,直接设计游戏重开按钮 UI(不建议在C++里面用代码创建UI按钮,是一件非常费力不讨好的事情)

- 设置 UI 动画

- 结合蓝图,在游戏结束后显示游戏重开按钮


7.3 点击事件游戏重开
- 添加按钮点击事件

- 重开则重新生成云朵

- 点击重开后,移除UI,调用 C++ 中 GameReset 函数

八. 游戏音乐
8.1 背景音乐
- 将音乐文件拖入场景,设置循环播放

8.2 角色接触云朵音乐
- 按云朵按位置播放音乐

- 设置音乐

8.3 云朵雨音乐
- 添加云朵雨的声音组件,并播放

- 创建雨声音频的 Cue 文件并打开,设置雨声衰减


- 在蓝图中设置云朵雨的音效

附录:C++文件提要
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Cake.generated.h"
UCLASS()
class PROJECT_UPUPCAKE_API ACake : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
ACake();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
//声明一个玩家控制器
APlayerController* PlayerController;
//角色在空中的速度
UPROPERTY(EditAnywhere,Category="Speed")
float AirSpeed;
//角色落地的速度
UPROPERTY(EditAnywhere,Category="Speed")
float GroundSpeed;
//计分
int Score;
//判断是否死亡
bool bDeath;
//判断游戏是否开始
bool bGamestart;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
//声明角色跟随鼠标移动的函数
void FollowMouseCursorMoving();
//声明跳跃,游戏启动时跳跃
void BeginJump();
//声明跳跃,碰到云彩时跳跃
void Jump();
//判断角色所在的场景,设置速度
void SetSpeed();
//加分
void IncreaseScore();
//获取分数
int GetScore() const;
//判断角色是否持续下落
void IsFalling();
//刷新时间轴
UFUNCTION(BlueprintImplementableEvent)
void UpdateTimer();
//重置时间轴
UFUNCTION(BlueprintImplementableEvent)
void ResetTimer();
//游戏结束
UFUNCTION(BlueprintCallable)
void GameOver();
//游戏重开
UFUNCTION(BlueprintCallable)
void GameReset();
//显示重开按钮
UFUNCTION(BlueprintImplementableEvent)
void ResetUI();
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "Cake.h"
#include "GameFramework/CharacterMovementComponent.h" //判断角色是否运动
// Sets default values
ACake::ACake()
{
// 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;
//速度初始化
AirSpeed = 3500.0f;
GroundSpeed = 300.0f;
//分数初始化
Score = 0;
//判断是否死亡(初始化)
bDeath = false;
//判断游戏是否开始(初始化)
bGamestart = false;
}
// Called when the game starts or when spawned
void ACake::BeginPlay()
{
Super::BeginPlay();
//获取 PlayerController(因为继承自 Character,可以直接 GetController()获取)
PlayerController = Cast<APlayerController>(GetController());
//获取鼠标显示
PlayerController->bShowMouseCursor = true;
//调用游戏重开
GameReset();
}
// Called every frame
void ACake::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if(!bDeath)
{
FollowMouseCursorMoving(); //每帧判断角色朝鼠标方向移动
SetSpeed(); //每帧判断角色所在的场景,设置速度
IsFalling(); //每帧判断角色是否持续下落
}
}
// Called to bind functionality to input
void ACake::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
//跳跃绑定
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACake::BeginJump);
}
void ACake::FollowMouseCursorMoving()
{
//获取鼠标在世界坐标下的位置(方向暂用不到)
FVector MouseCursorLocation;
FVector MouseCursorDirection;
PlayerController->DeprojectMousePositionToWorld(MouseCursorLocation, MouseCursorDirection);
//角色朝鼠标方向移动
//获取 Y方向(左右方向)的偏移:鼠标位置-小球位置,用 Clamp()函数限定范围为 -1到 1
float YDirection = FMath::Clamp(MouseCursorLocation.Y - GetActorLocation().Y, -1.0f ,1.0f);
FVector Direction = FVector(0,YDirection,0); //转换为向量
AddMovementInput(Direction);
}
void ACake::BeginJump()
{
if(!bGamestart && GetActorLocation().Z != 0)
{
bGamestart = true; //游戏开始
Jump();
}
}
void ACake::Jump()
{
//LaunchCharacter(发射方向,false:XY方向,true:Z方向),弹跳函数只对角色有效
//false/true:是否替换原发射速度(true一直保持 1500)
LaunchCharacter(FVector(0,0,1500),false, true);
}
void ACake::SetSpeed()
{
//判断角色是否下落
if(GetCharacterMovement()->IsFalling())
{
GetCharacterMovement()->MaxWalkSpeed = AirSpeed; //在空中
}
else
{
if(bGamestart)
{
GameOver();
}
GetCharacterMovement()->MaxWalkSpeed = GroundSpeed; //在地面
}
}
void ACake::IncreaseScore()
{
Score++;
}
int ACake::GetScore() const
{
return Score;
}
void ACake::IsFalling()
{
//如果 Z方向小于0,则表示准备开始下落了
if(GetCharacterMovement()->Velocity.Z < 0)
{
//持续性下落,刷新时间轴
UpdateTimer();
}
else
{
//短暂性下落,重置时间轴
ResetTimer();
}
}
void ACake::GameOver()
{
bDeath = true;
SetActorRotation(FRotator::ZeroRotator); //旋转归零
EnableInput(PlayerController); //启用输入
//显示重开按钮
UE_LOG(LogTemp,Warning, TEXT("GameOver"));
ResetUI();
}
void ACake::GameReset()
{
Score = 0;
bDeath = false;
bGamestart = false;
}
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/Actor.h"
#include "Cake.h" //角色
#include "Components/BoxComponent.h" //盒体检测
#include "Camera.generated.h"
UCLASS()
class PROJECT_UPUPCAKE_API ACamera : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ACamera();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
//声明一个相机组件
UPROPERTY(VisibleAnywhere)
UCameraComponent* CameraComponent;
//声明一个玩家控制器
APlayerController* PlayerController;
//声明角色
ACake* Cake;
//销毁相机视角外的多余云朵
UPROPERTY(VisibleAnywhere, Category = "Component")
UBoxComponent* DestroyArea;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
//全局相机垂直跟随
void CameraMoving();
//检测销毁区是否与多余云朵产生碰撞
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; //重写碰撞函数
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "Camera.h"
#include "Camera/CameraComponent.h" //相机组件
#include "Kismet/GameplayStatics.h" //玩家控制器
#include "Cloud.h" //销毁相机视角外的多余云朵
// Sets default values
ACamera::ACamera()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
//定义一个相机组件
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
//将相机组件设置为默认根组件
CameraComponent->SetupAttachment(RootComponent);
//销毁相机视角外的多余云朵
DestroyArea = CreateDefaultSubobject<UBoxComponent>(TEXT("DestroyArea"));
//绑定到根组件
DestroyArea->SetupAttachment(RootComponent);
}
// Called when the game starts or when spawned
void ACamera::BeginPlay()
{
Super::BeginPlay();
//获取玩家控制器
PlayerController = UGameplayStatics::GetPlayerController(this, 0);
//切换视角(立刻对齐)
PlayerController->SetViewTargetWithBlend(this, 0);
//获取角色
Cake = Cast<ACake>(UGameplayStatics::GetPlayerPawn(this,0));
}
// Called every frame
void ACamera::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
CameraMoving(); //每帧调用全局相机垂直跟随
}
void ACamera::CameraMoving()
{
//(相机的 X,相机的 Y,角色的 Z)组成跟随坐标
FVector TargetLoaction = FVector(GetActorLocation().X, GetActorLocation().Y, Cake->GetActorLocation().Z);
//设置相机当前位置
SetActorLocation(TargetLoaction);
}
void ACamera::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
//类型转换
ACloud* Cloud = Cast<ACloud>(OtherActor);
if(Cloud)
{
//检测到多余云朵则调用销毁
Cloud->Destroy();
}
}
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/BoxComponent.h"
#include "GameFramework/Actor.h"
#include "Cake.h" //角色
#include "Components/TextRenderComponent.h" //文本
#include "Sound/SoundWave.h" //播放音乐
#include "Cloud.generated.h"
UCLASS()
class PROJECT_UPUPCAKE_API ACloud : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ACloud();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
//声明盒体检测组件
UPROPERTY(VisibleAnywhere, Category = "Collision")
UBoxComponent* BoxComponent;
//声明云朵组件
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Show")
UStaticMeshComponent* CloudPlane;
//声明云朵雨组件
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Show")
UStaticMeshComponent* RainPlane;
//云朵库
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Show")
TArray<UTexture*> CloudTextures;
//动态材质实例
UPROPERTY(BlueprintReadOnly, Category = "Show")
UMaterialInstanceDynamic* MaterialInstanceDynamic;
UMaterialInterface* MaterialInterface;
//分数文本
UPROPERTY(EditAnywhere, Category = "Show")
UTextRenderComponent* ScoreText;
//检测是否与角色产生碰撞
ACake* Cake;
//角色接触云朵音乐
UPROPERTY(EditAnywhere, Category = "Sound")
USoundWave* CloudSound;
//云朵雨音乐
UPROPERTY(VisibleAnywhere, Category = "Sound")
UAudioComponent* RainSound;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
//随机云朵生成
void SetRandomCloudTextures();
//显示分数
void DisplayScore();
//检测是否与角色产生碰撞
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; //重写碰撞函数
//云朵淡出(蓝图实现)
UFUNCTION(BlueprintImplementableEvent)
void FadeOut();
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "Cloud.h"
#include "Kismet/GameplayStatics.h" //播放音乐
#include "Components/AudioComponent.h" //云朵雨音乐组件
// Sets default values
ACloud::ACloud()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
//定义盒体检测
BoxComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("BoxCollision"));
//设置为默认根组件
RootComponent = BoxComponent;
//定义云朵
CloudPlane = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("CloudMesh"));
//绑定到根组件(盒体检测)
CloudPlane->SetupAttachment(RootComponent);
//定义云朵雨
RainPlane = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("RainMesh"));
//绑定到云朵组件
RainPlane->SetupAttachment(CloudPlane);
//分数文本
ScoreText = CreateDefaultSubobject<UTextRenderComponent>(TEXT("ScoreText"));
//绑定到根组件
ScoreText->SetupAttachment(RootComponent);
//云朵雨音乐
RainSound = CreateDefaultSubobject<UAudioComponent>(TEXT("RainSound"));
//绑定到云朵组件
RainSound->SetupAttachment(CloudPlane);
}
// Called when the game starts or when spawned
void ACloud::BeginPlay()
{
Super::BeginPlay();
SetRandomCloudTextures(); //调用随机云朵生成
//随机显示云朵雨
if(FMath::RandRange(0,2))
{
RainPlane->SetVisibility(true);
//播放云朵雨音乐
RainSound->Activate(true);
}
}
// Called every frame
void ACloud::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void ACloud::SetRandomCloudTextures()
{
//获取 CloudPlane上的材质
MaterialInterface = CloudPlane->GetMaterial(0);
//通过创建动态材质实例来获取指定材质参数的随机材质
MaterialInstanceDynamic = CloudPlane->CreateDynamicMaterialInstance(0,MaterialInterface);
//随机数
int index = FMath::RandRange(0,2);
if(CloudTextures[index])
{
//设置材质参数值(参数节点名Texture,随机云彩库)
MaterialInstanceDynamic->SetTextureParameterValue(FName(TEXT("Texture")), CloudTextures[index]);
//将材质实例赋给模型
CloudPlane->SetMaterial(0, MaterialInstanceDynamic);
}
}
void ACloud::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
//类型转换
Cake = Cast<ACake>(OtherActor);
if(Cake)
{
//按位置播放音乐
UGameplayStatics::PlaySoundAtLocation(this, CloudSound, GetActorLocation());
//检测到角色则调用弹跳
Cake->Jump();
//显示分数
DisplayScore();
//云朵淡出(蓝图实现)
FadeOut();
}
}
void ACloud::DisplayScore()
{
Cake->IncreaseScore();
//设置文本(类型转换 int - FString - FText)
ScoreText->SetText(FText::FromString(FString::FromInt(Cake->GetScore())));
}
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Cloud.h"
#include "GameFramework/Actor.h"
#include "Components/BoxComponent.h" //盒体检测
#include "SpawnCloud.generated.h"
UCLASS()
class PROJECT_UPUPCAKE_API ASpawnCloud : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ASpawnCloud();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
//云朵生成区域
UPROPERTY(VisibleAnywhere, Category = "Component")
UBoxComponent* SpawnArea;
//根据角色位置,检测云朵是否可以继续生成
UPROPERTY(VisibleAnywhere, Category = "Component")
UBoxComponent* TriggerArea;
//场景根组件
USceneComponent* DefaultRootComponent;
//TSubclassOf:限定为 ACloud或其子类
UPROPERTY(EditAnywhere, Category = "Cloud")
TSubclassOf<ACloud> Cloud;
//检测是否与角色产生碰撞
ACake* Cake;
//云朵初始化
UPROPERTY(EditAnywhere, Category = "Cloud")
int32 InitialCloud;
//云朵生成器每次上升距离
UPROPERTY(EditAnywhere, Category = "Cloud")
float SpawnSpacing;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
//随机生成云朵
void SpawnCloud();
//检测是否与角色产生碰撞
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; //重写碰撞函数
//重开则重新生成云朵
UFUNCTION(BlueprintCallable)
void ResetCloud();
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "SpawnCloud.h"
#include "Kismet/KismetMathLibrary.h" //随机生成点
#include "Cloud.h" //生成云朵
#include "Kismet/GameplayStatics.h" //重开销毁之前的所有云朵
// Sets default values
ASpawnCloud::ASpawnCloud()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
//场景根组件
DefaultRootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("DefaultRootComponent"));
RootComponent = DefaultRootComponent;
//云朵生成区域
SpawnArea = CreateDefaultSubobject<UBoxComponent>(TEXT("SpawnArea"));
//绑定到场景根组件
SpawnArea->SetupAttachment(RootComponent);
//根据角色位置,检测云朵是否可以继续生成
TriggerArea = CreateDefaultSubobject<UBoxComponent>(TEXT("TriggerArea"));
//绑定到场景根组件
TriggerArea->SetupAttachment(RootComponent);
//云朵初始化
InitialCloud = 6;
//云朵生成器每次上升距离
SpawnSpacing = 300.0f;
}
// Called when the game starts or when spawned
void ASpawnCloud::BeginPlay()
{
Super::BeginPlay();
ResetCloud();
}
// Called every frame
void ASpawnCloud::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void ASpawnCloud::SpawnCloud()
{
FVector SpawnOrigin = SpawnArea->Bounds.Origin; //生成中心点
FVector SpawnExtent = SpawnArea->Bounds.BoxExtent; //生成范围
//生成的位置
//提供圆心和范围,Y轴随机生成点
float YLocation = UKismetMathLibrary::RandomPointInBoundingBox(SpawnOrigin, SpawnExtent).Y;
FVector SpawnLocation = FVector(SpawnArea->GetComponentLocation().X, YLocation, SpawnArea->GetComponentLocation().Z);
//生成(生成云朵,生成位置,生成旋转)
GetWorld()->SpawnActor<ACloud>(Cloud, SpawnLocation, FRotator::ZeroRotator);
//云朵生成器上升(0,0,300)
AddActorWorldOffset(FVector(0,0,SpawnSpacing));
}
void ASpawnCloud::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
//类型转换
Cake = Cast<ACake>(OtherActor);
if(Cake)
{
//检测到角色则调用随机生成云朵
SpawnCloud();
}
}
void ASpawnCloud::ResetCloud()
{
//云朵初始化
InitialCloud = 6;
//刷新位置为(0,0,0)
SetActorLocation(FVector::ZeroVector);
//重开销毁之前的所有云朵
TArray<AActor*>ResetClouds;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ACloud::StaticClass(), ResetClouds);
for(AActor* TActor : ResetClouds)
{
ACloud* FoundClouds = Cast<ACloud>(TActor); //类型转换
if(FoundClouds!= nullptr)
{
FoundClouds->Destroy();
}
}
//云朵初始化随机生成
while(InitialCloud > 0)
{
SpawnCloud();
InitialCloud--;
}
}
参考资料:
【SiKi学院Unreal视频教程】UE4/5 虚幻4/5初级课程-向上的小松饼_哔哩哔哩_bilibili