UE5 进阶篇第一弹:中期架构升级 —— 组件化开发与 Gameplay 框架实战


观众老爷们大家好 我是邪修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.用事件代替直接调用,用组件代替继承

相关推荐
亚空间仓鼠10 小时前
Docker容器化高可用架构部署方案(六)
docker·容器·架构
RInk7oBjo10 小时前
从零设计生产级 Multi-Agent Harness:架构、评估、记忆、成本与 MCP 工具接入全拆解
架构
张伯毅11 小时前
如何构建一个生产级 AI Agent CLI —— 以 Claude Code 架构探索
人工智能·架构
知识领航员11 小时前
蘑兔AI音乐深度实测:功能拆解、实测表现与适用场景
java·c语言·c++·人工智能·python·算法·github
covco12 小时前
分布式架构实战:全平台矩阵管理系统的技术实现与性能优化
分布式·矩阵·架构
jf加菲猫13 小时前
第21章 Qt WebEngine
开发语言·c++·qt·ui
dhashdoia13 小时前
GPT-5.5 代码开发实战:Codex与Browser Use深度集成与星链4SAPI优化方案
java·数据库·人工智能·gpt·架构
码农-阿杰13 小时前
深入理解 synchronized 底层实现:从 HotSpot C++ 源码看对象锁与 Monitor 机制
开发语言·c++·
Szime14 小时前
深智微IC华润微代理:MCU选型与工业控制方案推荐
c++