UE5游戏开发实战:构建RTS资源采集闭环系统
2026/9/17 8:24:07 网站建设 项目流程

很多刚开始研究 UE5 RTS 的同学,通常会把第一个 Demo 放在“单位移动”和“兵种战斗”上,结果做到中期才发现:真正把整个玩法串起来的,其实是资源采集这一条非常基础又非常容易被小看的链路。单位走到矿脉、采集、带回基地、更新 HUD、节点资源扣减、满载调度……任何一个环节没想清楚,后面做大系统时都会回来返工。

本文将围绕“虚幻引擎5 游戏开发实战:打造 RTS 资源采集系统”这条主线,拆开一个最小可用但结构清晰的资源采集原型。文章会覆盖 UE5 的 Gameplay 框架、Actor 数据设计、C++ 类与蓝图配合方式、右键命令与自动提交资源等关键内容。适合有一定 UE 基础、想从零搭建 RTS 单局玩法的开发者阅读;如果你已经有 Unity 或 Godot 项目经验,也可以当作跨引擎思路对照。

1. 为什么先把 RTS 资源采集系统单独拿出来做

1.1 RTS 资源采集到底在做什么

RTS 游戏的资源采集可以抽象成一句话:让某个单位把场景中的资源点数据,搬运到属于玩家的存储容器中。看起来只有“采集”和“提交”两个动作,但实际展开之后会涉及:

  • 资源种类划分,例如 Gold、Mineral、Food;
  • 资源节点的当前余量与总量显示;
  • 采集单位的状态切换;
  • 单位寻路到达资源点;
  • 到达后按节奏抽取资源;
  • 采集单位携带容量满后寻找提交点;
  • 提交后把资源加到玩家或队伍的总量中;
  • HUD 实时刷新资源数值。

如果只是临时写死一串数值,Demo 当然很快。但一旦加入第二个资源类型、多个阵营、多个采集单位,你会发现代码结构中“资源数值存在哪里”“谁有权添加资源”“节点剩余量由谁扣减”会成为主要矛盾。

这也是 UE5 游戏开发中资源管理话题一直讨论度很高的原因。很多人会把大量精力放在资产和打包资源上,但从玩法设计角度看,运行时资源数值的归属与流转同样重要。

1.2 UE5 落地时容易卡住的点

使用 UE5 实现 RTS 资源采集,经常遇到的困惑包括:

  1. 选中单位后,如何右键让单位去采集;
  2. 单位到达资源点后,以什么频率扣减资源;
  3. 玩家持有的资源应该保存在 GameMode、PlayerState 还是某个全局单例中;
  4. 资源节点余量变了,UI 怎么感知;
  5. 单位满载后,是立刻返回基地还是继续占用资源节点;
  6. C++ 写的数值变更,蓝图界面如何订阅。

这些问题如果从一开始就明确了,后面再接多单位框选、行为树 AI、联网同步都会顺很多。

1.3 本文实现的功能边界

本文不会强行实现完整的 RTS 框架,而是聚焦下面这条可以跑通的闭环:

左键选中采集单位 → 右键点击资源节点 → 单位寻路到节点附近 → 开始按时间间隔采集 → 背包满或节点枯竭后回到最近提交点 → 资源写入玩家 PlayerState → HUD 刷新

这套结构足够支撑一个可演示的 UE5 RTS 原型,同时也方便后续扩展。

2. 技术方案与演示环境

2.1 引擎与工程

本文使用的核心引擎为 Unreal Engine 5 系列。UE5.0 之后 Gameplay 框架整体变化不大,后续给的代码在 UE5.1、UE5.2、UE5.3 或更高版本中思路通用。

由于不同版本默认输入系统差异较大,文章在输入响应部分会同时说明传统 BindKey 与 Enhanced Input 的对接思路。你可以根据自己工程创建时的模板选择合适方式。

2.2 准备一个第三人称或空白工程

如果你已经有一个 UE5 C++ 工程,可以直接在现有 Module 中添加代码。如果是从零开始,建议在启动器中选择“游戏”分类下的 Blank 或 Third Person 模板,并勾选“包含初学者内容包”。

初学者内容包里的基础几何体和材质可以用来快速搭场景。

2.3 Gameplay 框架的类划分

在这个资源采集 Demo 中,我建议按 UE5 的标准 Gameplay 框架划分职责:

职责
ARTSResourceNode场景中的资源节点,负责扣减自身余量
ARTSHarvester玩家控制的采集单位,负责移动、采集、提交
ARTSResourceDropOffPoint基地提交点,提交后资源进入玩家账户
ARTSPlayerState保存当前玩家的资源数量
ARTSPlayerController处理鼠标点击选中与右键命令
HUD/UMG显示玩家资源数值

这里有一个很重要的设计观点:不要把所有逻辑都塞进 GameMode。GameMode 更适合控制规则流程,不适合保存某个玩家的个人资源数据。多人模式下每个客户端对应不同 PlayerState,把资源数量放在 PlayerState 中是最符合 UE 习惯的做法。

3. 第一步:先设计资源数据,而不是先摆场景

3.1 创建基础资源类型

在 UE5 中,资源类型最简单的表达方式是枚举。为了能暴露给蓝图,我们使用 UENUM。

文件路径:Source/RTSSystem/RTSResourceTypes.h

#pragma once #include "CoreMinimal.h" #include "RTSResourceTypes.generated.h" UENUM(BlueprintType) enum class ERTSResourceType : uint8 { None UMETA(DisplayName = "None"), Gold UMETA(DisplayName = "Gold"), Mineral UMETA(DisplayName = "Mineral"), Energy UMETA(DisplayName = "Energy") };

这样设计的好处是:

  • 枚举名称可读;
  • 蓝图下拉框能直接选择;
  • 后续需要扩展资源类型时,只需要增加枚举项;
  • 可以像普通整数一样用 TMap 做映射存储。

3.2 把资源数值放在哪一层

很多 RTS 新手会把“资源数量”直接写死在资源节点 Actor 上,同时在 HUD 中通过 GetActorLocation 之类的接口拿数值。这样在单人演示中没有明显问题,但当资源节点由服务端管理、玩家资源由客户端的玩家存档管理时,就会混淆。

推荐的做法是:

  1. 场景中的ARTSResourceNode只关心“这个节点的剩余量”;
  2. 玩家持有量统一放到ARTSPlayerState
  3. 采集单位不直接调用 PlayerState 修改资源,而是把资源送到提交点后,由提交逻辑统一结算。

在这个模型下,资源节点的数值与玩家的资源数值是两套数据,中间通过采集单位的“携带量”作为临时传递,而不是一边采集一边直接加到玩家总资源里。这样实现自动施法、科技加速、资源减半等玩法改动时会更灵活。

3.3 采集阶段的定义

我们把单位要做的事情定义成枚举状态。虽然在小型 Demo 中可以用 bool 标记代替,但状态枚举在调试时更有价值。

UENUM(BlueprintType) enum class EHarvesterPhase : uint8 { Idle, MovingToNode, Harvesting, MovingToDropOff, Depositing };

有了这个枚举,单位在 Tick 中只需要根据当前状态做对应判断,逻辑会清晰很多,后续接入调试菜单时也能一眼看出单位正在做什么。

4. 实现资源节点 Actor

4.1 创建 ARTSResourceNode 类

资源节点是整个资源采集的源头。它需要暴露:

  • 资源类型;
  • 初始总量;
  • 当前余量;
  • 被采集时可以扣减的接口;
  • 资源变化的事件回调。

文件路径:Source/RTSSystem/RTSResourceNode.h

#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "RTSResourceTypes.h" #include "RTSResourceNode.generated.h" class UStaticMeshComponent; class UWidgetComponent; DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams( FOnResourceAmountChanged, ARTSResourceNode*, Node, float, NewAmount ); UCLASS(Blueprintable) class ARTSResourceNode : public AActor { GENERATED_BODY() public: ARTSResourceNode(); virtual void BeginPlay() override; UPROPERTY(VisibleAnywhere, Category = "Components") TObjectPtr<UStaticMeshComponent> MeshComponent; UPROPERTY(VisibleAnywhere, Category = "Components") TObjectPtr<UWidgetComponent> InfoWidget; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS|Resource") ERTSResourceType ResourceType = ERTSResourceType::Gold; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS|Resource") float TotalAmount = 1000.0f; UPROPERTY(BlueprintReadOnly, Category = "RTS|Resource") float CurrentAmount = 0.0f; UPROPERTY(BlueprintAssignable, Category = "RTS|Event") FOnResourceAmountChanged OnResourceAmountChanged; UFUNCTION(BlueprintCallable, Category = "RTS|Resource") float Extract(float RequestedAmount); UFUNCTION(BlueprintPure, Category = "RTS|Resource") bool IsDepleted() const; protected: void UpdateWidget(); UPROPERTY(EditDefaultsOnly, Category = "RTS|UI") TSubclassOf<UUserWidget> InfoWidgetClass; };

有一个细节要提前说清楚:CurrentAmount不能在构造函数里直接复制TotalAmount,因为蓝图放置到场景后,设计者可能修改TotalAmount的默认值。更稳妥的方式是在BeginPlay中重置。

对应实现文件:

文件路径:Source/RTSSystem/RTSResourceNode.cpp

#include "RTSResourceNode.h" #include "Blueprint/UserWidget.h" #include "Components/StaticMeshComponent.h" #include "Components/WidgetComponent.h" ARTSResourceNode::ARTSResourceNode() { PrimaryActorTick.bCanEverTick = false; MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent")); SetRootComponent(MeshComponent); InfoWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("InfoWidget")); InfoWidget->SetupAttachment(MeshComponent); InfoWidget->SetWidgetSpace(EWidgetSpace::World); InfoWidget->SetDrawSize(FVector2D(220.0f, 80.0f)); InfoWidget->SetRelativeLocation(FVector(0.0f, 0.0f, 160.0f)); } void ARTSResourceNode::BeginPlay() { Super::BeginPlay(); CurrentAmount = TotalAmount; UpdateWidget(); } float ARTSResourceNode::Extract(float RequestedAmount) { if (IsDepleted() || RequestedAmount <= 0.0f) { return 0.0f; } const float ActualAmount = FMath::Min(RequestedAmount, CurrentAmount); CurrentAmount -= ActualAmount; UpdateWidget(); OnResourceAmountChanged.Broadcast(this, CurrentAmount); return ActualAmount; } bool ARTSResourceNode::IsDepleted() const { return CurrentAmount <= 0.0f; } void ARTSResourceNode::UpdateWidget() { if (!InfoWidget || !InfoWidgetClass) { return; } if (!InfoWidget->GetWidget()) { InfoWidget->SetWidgetClass(InfoWidgetClass); } }

4.2 给资源节点添加可见 Mesh

代码中只创建了UStaticMeshComponent,并没有指定具体的 Static Mesh,因为引擎的 C++ 构造函数不推荐直接硬编码资源路径。正确做法是:

  1. 基于ARTSResourceNode创建蓝图子类;
  2. 在蓝图里把 MeshComponent 的 Static Mesh 指定为立方体或自定义矿脉模型;
  3. 给 MeshComponent 设置合适的碰撞预设,默认 BlockAll 即可;
  4. 调整 InfoWidget 的渲染位置。

我建议使用 Cube 并缩放成长条形矿脉,颜色上用暖色与普通地形区分。这样右键点击时碰撞检测更容易命中。

4.3 节点 UI 的更新思路

InfoWidget使用的是 WidgetComponent,它是一个世界空间 UI。若要显示剩余量,可以在ARTSResourceNode蓝图子类中创建一个 UMG Widget,让 UI 里的进度条监听OnResourceAmountChanged事件并刷新显示。

由于数据变更事件是动态多播,蓝图里可以把事件绑定到 UI 的函数节点,不需要频繁轮询 Tick。

5. 实现采集单位 ARTSHarvester

5.1 Harvester 类声明

采集单位继承 ACharacter 比较方便,因为不需要自己实现角色移动组件,可以直接使用 CharacterMovement 在 NavMesh 上移动。

文件路径:Source/RTSSystem/RTSHarvester.h

#pragma once #include "CoreMinimal.h" #include "GameFramework/Character.h" #include "RTSResourceTypes.h" #include "RTSHarvester.generated.h" class ARTSResourceNode; class ARTSResourceDropOffPoint; class UStaticMeshComponent; UENUM(Blueprint

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询