
观众老爷们大家好 我是邪修KING 欢迎来到我的TA->UE游戏引擎博客---进阶篇! C++!UE5精品!精选学习! 系列衔接:入门篇我们完成了 UE 基础操作、蓝图交互、碰撞系统、UMG UI、C++ 与蓝图互操作的学习,已经能实现零散的游戏功能。但你会发现:所有逻辑都堆在一个 Actor 里,代码越来越乱,复用性极差,加一个新功能就要改半天。本篇我们将完成从 "写功能" 到 "做架构" 的关键跨越 ,掌握 UE 工业级开发的两大核心 ------ 组件化设计与 Gameplay 框架,彻底告别面条代码,写出可扩展、可维护的游戏代码。
前提:已掌握 UE 蓝图与 C++ 基础、能实现简单交互功能的开发者,全程沿用前序立方体项目,无需从零搭建。
前置准备
1.沿用前五篇的 UE 项目,已配置好 VS2022 与 UE5 C++ 开发环境
2.已掌握 C++ 类与对象、继承、委托,以及 UE 蓝图函数、事件分发器
3.已实现玩家移动、碰撞扣血、血量条 UI、主菜单等基础功能
4.引擎版本:UE5.0+,全版本通用,无兼容问题
一、为什么我们需要架构升级?
先看你现在的代码痛点:
一个BP_CubeTest写了上千行逻辑:移动、血量、碰撞、UI、特效全堆在一起
想给敌人也加血量系统?只能复制粘贴代码,改一个 bug 要改 N 个地方
想加个计分功能?不知道该写在玩家里还是关卡蓝图里
想换个玩家模型?整个逻辑都要跟着改
这就是单体 Actor 架构 的致命问题:高耦合、低复用、难扩展。当项目超过 10 个功能点后,维护成本会指数级上升。而 UE 作为工业级引擎,早就为我们提供了成熟的解决方案:
组件化开发 :解决 "代码复用" 问题,一个功能做成一个组件,哪里需要就挂在哪里
Gameplay 框架:解决 "职责划分" 问题,明确游戏中各个模块的分工,避免逻辑混乱
二、第一部分:组件化开发实战 ------ 把你的代码拆成积木
核心概念:用 C++ 知识秒懂组件化
组件化的核心思想是 "组合优于继承":不要通过继承来扩展功能,而是通过组合多个独立的组件来实现复杂的对象。
| UE 组件概念 | 对应 C++ 设计原则 | 一句话说明 |
|---|---|---|
| UActorComponent | 组合模式 | 所有组件的基类,一个组件只做一件事 |
| 组件挂载 | 对象组合 | 一个 Actor 可以挂载多个组件,组合出不同的功能 |
| 组件间通信 | 观察者模式 | 组件之间通过事件通信,不直接互相调用 |
两个架构
1.单体架构:Player : public Actor(包含移动、血量、碰撞所有逻辑)
2.组件化架构:Player : public Actor + MovementComponent + HealthComponent + InteractionComponent
分步实操:重构你的玩家类,拆分成 3 个核心组件
我们将把之前堆在AMyPlayerActor里的所有逻辑,拆分成 3 个独立的组件,每个组件只负责一个功能。
步骤 1:创建血量组件UHealthComponent
这是最通用的组件,玩家、敌人、可破坏物体都可以用。
HealthComponent.h
cpp
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "HealthComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class YOUR_PROJECT_API UHealthComponent : public UActorComponent
{
GENERATED_BODY()
public:
UHealthComponent();
protected:
virtual void BeginPlay() override;
public:
// 血量属性
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
float MaxHealth = 100.0f;
UPROPERTY(BlueprintReadOnly, Category = "Health")
float CurrentHealth;
// 事件
UPROPERTY(BlueprintAssignable, Category = "Health|Events")
FOnHealthChanged OnHealthChanged;
UPROPERTY(BlueprintAssignable, Category = "Health|Events")
FOnDeath OnDeath;
// 公共接口
UFUNCTION(BlueprintCallable, Category = "Health")
void TakeDamage(float Damage);
UFUNCTION(BlueprintCallable, Category = "Health")
void Heal(float Amount);
private:
void HandleDeath();
};
HealthComponent.cpp
cpp
#include "HealthComponent.h"
UHealthComponent::UHealthComponent()
{
PrimaryComponentTick.bCanEverTick = false; // 组件不需要Tick,关闭节省性能
}
void UHealthComponent::BeginPlay()
{
Super::BeginPlay();
CurrentHealth = MaxHealth;
}
void UHealthComponent::TakeDamage(float Damage)
{
if (Damage <= 0.0f || CurrentHealth <= 0.0f) return;
CurrentHealth = FMath::Clamp(CurrentHealth - Damage, 0.0f, MaxHealth);
OnHealthChanged.Broadcast(CurrentHealth);
if (CurrentHealth <= 0.0f)
{
HandleDeath();
}
}
void UHealthComponent::Heal(float Amount)
{
if (Amount <= 0.0f || CurrentHealth >= MaxHealth) return;
CurrentHealth = FMath::Clamp(CurrentHealth + Amount, 0.0f, MaxHealth);
OnHealthChanged.Broadcast(CurrentHealth);
}
void UHealthComponent::HandleDeath()
{
OnDeath.Broadcast();
}
步骤 2:创建移动组件UMovementComponent
负责所有移动相关的逻辑,玩家、敌人、NPC 都可以复用。
MovementComponent.h
cpp
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "MovementComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class YOUR_PROJECT_API UMovementComponent : public UActorComponent
{
GENERATED_BODY()
public:
UMovementComponent();
protected:
virtual void BeginPlay() override;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
// 移动属性
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MoveSpeed = 500.0f;
// 移动输入
UFUNCTION(BlueprintCallable, Category = "Movement")
void MoveForward(float Value);
UFUNCTION(BlueprintCallable, Category = "Movement")
void MoveRight(float Value);
UFUNCTION(BlueprintCallable, Category = "Movement")
void MoveUp(float Value);
private:
FVector CurrentVelocity;
};
MovementComponent.cpp
cpp
#include "MovementComponent.h"
#include "GameFramework/Actor.h"
UMovementComponent::UMovementComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UMovementComponent::BeginPlay()
{
Super::BeginPlay();
}
void UMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (CurrentVelocity.IsZero()) return;
GetOwner()->AddActorWorldOffset(CurrentVelocity * DeltaTime);
CurrentVelocity = FVector::ZeroVector;
}
void UMovementComponent::MoveForward(float Value)
{
if (Value == 0.0f) return;
CurrentVelocity += GetOwner()->GetActorForwardVector() * Value * MoveSpeed;
}
void UMovementComponent::MoveRight(float Value)
{
if (Value == 0.0f) return;
CurrentVelocity += GetOwner()->GetActorRightVector() * Value * MoveSpeed;
}
void UMovementComponent::MoveUp(float Value)
{
if (Value == 0.0f) return;
CurrentVelocity += FVector::UpVector * Value * MoveSpeed;
}
步骤 3:重构玩家类,挂载组件
现在玩家类变得极其简洁,只负责组件的初始化和输入绑定。
MyPlayerActor.h
cpp
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyPlayerActor.generated.h"
class UHealthComponent;
class UMovementComponent;
UCLASS()
class YOUR_PROJECT_API AMyPlayerActor : public AActor
{
GENERATED_BODY()
public:
AMyPlayerActor();
protected:
virtual void BeginPlay() override;
public:
virtual void SetupInputComponent(class UInputComponent* PlayerInputComponent) override;
// 组件引用
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UStaticMeshComponent* MeshComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UHealthComponent* HealthComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UMovementComponent* MovementComp;
};
MyPlayerActor.cpp
cpp
#include "MyPlayerActor.h"
#include "HealthComponent.h"
#include "MovementComponent.h"
#include "Components/StaticMeshComponent.h"
AMyPlayerActor::AMyPlayerActor()
{
PrimaryActorTick.bCanEverTick = false; // 玩家本身不需要Tick,Tick交给移动组件
// 创建网格体组件
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
RootComponent = MeshComp;
// 创建并挂载组件
HealthComp = CreateDefaultSubobject<UHealthComponent>(TEXT("HealthComp"));
MovementComp = CreateDefaultSubobject<UMovementComponent>(TEXT("MovementComp"));
}
void AMyPlayerActor::BeginPlay()
{
Super::BeginPlay();
}
void AMyPlayerActor::SetupInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupInputComponent(PlayerInputComponent);
// 绑定输入到移动组件
PlayerInputComponent->BindAxis("MoveForward", MovementComp, &UMovementComponent::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", MovementComp, &UMovementComponent::MoveRight);
PlayerInputComponent->BindAxis("MoveUp", MovementComp, &UMovementComponent::MoveUp);
}
步骤 4:引擎内操作,验证效果
1.编译 C++ 代码,打开BP_CubeTest蓝图
2.你会看到蓝图中已经自动添加了HealthComp和MovementComp两个组件
3.删除之前写在玩家蓝图中的所有移动和血量逻辑
4.重新绑定 UI 事件:在玩家蓝图的BeginPlay中,绑定HealthComp的OnHealthChanged事件到 UI 的更新函数
5.运行游戏,WASD 移动、碰撞扣血、血量条更新所有功能都正常工作!
组件化的巨大优势
1.极致复用 :现在你想给敌人加血量和移动,只需要给敌人 Actor 挂载HealthComponent和MovementComponent即可,一行代码都不用写
2.解耦维护 :修改血量逻辑只需要改HealthComponent,所有挂载了该组件的对象都会自动更新
3.性能优化 :不需要的组件可以直接关闭,比如静态物体不需要移动组件,关闭后节省性能
4.团队协作:不同的人可以同时开发不同的组件,互不干扰
三、第二部分:Gameplay 框架实战 ------ 明确游戏的职责划分
组件化解决了单个 Actor 的代码复用问题,而 Gameplay 框架解决了整个游戏的职责划分问题。很多新手会把所有逻辑都写在玩家或关卡蓝图里,导致游戏逻辑混乱不堪。
核心概念:Gameplay 框架的 5 个核心类
UE 的 Gameplay 框架是一套成熟的游戏架构,明确规定了各个类的职责,你只需要把逻辑写到对应的类里即可。
| 类名 | 职责 | 对应你的项目 | 生命周期 |
|---|---|---|---|
| AGameMode | 游戏规则管理 | 游戏开始、结束、胜负判定 | 整个游戏过程 |
| AGameState | 游戏全局状态管理 | 全局分数、游戏时间、剩余敌人数量 | 整个游戏过程 |
| APlayerController | 玩家输入与控制 | 输入处理、摄像机控制 | 玩家存在期间 |
| APlayerState | 玩家个人状态管理 | 玩家分数、等级、击杀数 | 玩家存在期间 |
| AHUD | 玩家 UI 管理 | 血量条、分数显示 | 玩家存在期间 |
核心原则:谁拥有数据,谁就负责管理数据。比如分数是全局状态,就应该放在GameState里,而不是玩家或 UI 里。
分步实操:实现全局计分系统
我们将实现一个全局计分系统:玩家每销毁一个障碍物得 10 分,分数显示在 UI 上,游戏结束时显示最终得分。
步骤 1:创建AMyGameState类,管理全局分数
MyGameState.h
cpp
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "MyGameState.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnScoreChanged, int32, NewScore);
UCLASS()
class YOUR_PROJECT_API AMyGameState : public AGameStateBase
{
GENERATED_BODY()
public:
AMyGameState();
protected:
virtual void BeginPlay() override;
public:
// 全局分数
UPROPERTY(BlueprintReadOnly, Category = "Score")
int32 CurrentScore;
// 分数变化事件
UPROPERTY(BlueprintAssignable, Category = "Score|Events")
FOnScoreChanged OnScoreChanged;
// 加分接口
UFUNCTION(BlueprintCallable, Category = "Score")
void AddScore(int32 Amount);
};
MyGameState.cpp
cpp
#include "MyGameState.h"
AMyGameState::AMyGameState()
{
CurrentScore = 0;
}
void AMyGameState::BeginPlay()
{
Super::BeginPlay();
}
void AMyGameState::AddScore(int32 Amount)
{
if (Amount <= 0) return;
CurrentScore += Amount;
OnScoreChanged.Broadcast(CurrentScore);
}
步骤 2:修改障碍物逻辑,碰撞时加分
1.打开BP_Obstacle蓝图,找到碰撞销毁逻辑,在销毁障碍物之前添加:
2.右键搜索Get Game State,获取全局游戏状态
3.转换为MyGameState类型
4.调用Add Score函数,传入 10 分
步骤 3:修改 UI,显示分数
1.打开WBP_PlayerHUD蓝图,添加一个文本控件,命名为ScoreText
2.在BeginPlay中,获取MyGameState,绑定OnScoreChanged事件
3.事件触发时,更新ScoreText的文本为 "分数:{CurrentScore}"
步骤 4:设置项目默认 GameState
1.点击顶部菜单栏编辑→项目设置→地图与模式
2.在Game State Class下拉菜单中,选择MyGameState
3.运行游戏,每销毁一个障碍物,分数就会自动增加!
Gameplay 框架的核心价值
1.职责清晰 :每个类只做自己该做的事,找 bug 时一眼就能知道该去哪里找
2.数据安全 :全局数据统一管理,避免多个地方同时修改导致的数据不一致
3.易于扩展 :想加个游戏时间功能?直接在GameState里加变量和事件即可
4.网络友好:UE 的 Gameplay 框架天生支持网络同步,后续做多人游戏时几乎不用改架构
四、新手必踩的架构坑与避坑指南
1.过度组件化 :不要把一个简单的功能拆成 10 个组件,一个组件只做一件事,但也不要拆得太细
2.组件间直接调用 :永远不要在一个组件里直接获取另一个组件的指针并调用函数,应该通过事件通信
3.逻辑放错地方 :不要把全局逻辑写在玩家里,不要把玩家逻辑写在 GameMode 里,严格遵循职责划分
4.滥用关卡蓝图 :关卡蓝图只应该写关卡专属的逻辑,比如关卡初始化、剧情触发,不要写通用游戏逻辑
5.不使用 Gameplay 框架:很多新手觉得 Gameplay 框架复杂,自己写一套管理逻辑,最后只会越写越乱
五、本篇总结
本篇我们完成了从 "写功能" 到 "做架构" 的关键跨越,核心收获有 4 点:
1.掌握了组件化开发的核心思想,将玩家逻辑拆分成了独立的组件,实现了代码的极致复用
2.理解了 UE Gameplay 框架的 5 个核心类的职责,学会了用 GameState 管理全局状态
3.实现了全局计分系统,完成了组件化与 Gameplay 框架的结合应用
4.避开了新手最容易踩的架构坑,养成了良好的代码架构习惯
5.用事件代替直接调用,用组件代替继承
