
很多刚开始研究 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 资源采集经常遇到的困惑包括选中单位后如何右键让单位去采集单位到达资源点后以什么频率扣减资源玩家持有的资源应该保存在 GameMode、PlayerState 还是某个全局单例中资源节点余量变了UI 怎么感知单位满载后是立刻返回基地还是继续占用资源节点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 之类的接口拿数值。这样在单人演示中没有明显问题但当资源节点由服务端管理、玩家资源由客户端的玩家存档管理时就会混淆。推荐的做法是场景中的ARTSResourceNode只关心“这个节点的剩余量”玩家持有量统一放到ARTSPlayerState采集单位不直接调用 PlayerState 修改资源而是把资源送到提交点后由提交逻辑统一结算。在这个模型下资源节点的数值与玩家的资源数值是两套数据中间通过采集单位的“携带量”作为临时传递而不是一边采集一边直接加到玩家总资源里。这样实现自动施法、科技加速、资源减半等玩法改动时会更灵活。3.3 采集阶段的定义我们把单位要做的事情定义成枚举状态。虽然在小型 Demo 中可以用 bool 标记代替但状态枚举在调试时更有价值。UENUM(BlueprintType) enum class EHarvesterPhase : uint8 { Idle, MovingToNode, Harvesting, MovingToDropOff, Depositing };有了这个枚举单位在 Tick 中只需要根据当前状态做对应判断逻辑会清晰很多后续接入调试菜单时也能一眼看出单位正在做什么。4. 实现资源节点 Actor4.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) TObjectPtrUStaticMeshComponent MeshComponent; UPROPERTY(VisibleAnywhere, Category Components) TObjectPtrUWidgetComponent 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) TSubclassOfUUserWidget 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 CreateDefaultSubobjectUStaticMeshComponent(TEXT(MeshComponent)); SetRootComponent(MeshComponent); InfoWidget CreateDefaultSubobjectUWidgetComponent(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 构造函数不推荐直接硬编码资源路径。正确做法是基于ARTSResourceNode创建蓝图子类在蓝图里把 MeshComponent 的 Static Mesh 指定为立方体或自定义矿脉模型给 MeshComponent 设置合适的碰撞预设默认 BlockAll 即可调整 InfoWidget 的渲染位置。我建议使用 Cube 并缩放成长条形矿脉颜色上用暖色与普通地形区分。这样右键点击时碰撞检测更容易命中。4.3 节点 UI 的更新思路InfoWidget使用的是 WidgetComponent它是一个世界空间 UI。若要显示剩余量可以在ARTSResourceNode蓝图子类中创建一个 UMG Widget让 UI 里的进度条监听OnResourceAmountChanged事件并刷新显示。由于数据变更事件是动态多播蓝图里可以把事件绑定到 UI 的函数节点不需要频繁轮询 Tick。5. 实现采集单位 ARTSHarvester5.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