997 字
5 分钟
UE Subsystem——不依赖 Actor 的服务架构

一、Singleton 在 UE 里的问题#

你很可能写过或者见过这样的代码:

// 经典 Singleton——全局单例
class UQuestManager {
public:
static UQuestManager* Get() { return Instance; }
void StartQuest(FName QuestId);
void CompleteObjective(int32 PlayerId, FName ObjectiveId);
private:
static UQuestManager* Instance;
};

问题:

  • 生命周期不可控——谁创建它?什么时候销毁?GC 能不能碰到它?
  • 多人游戏下混乱——如果是 PIE(Play In Editor)多窗口,每个窗口运行独立的服务器——一个全局单例到底属于哪个 World?
  • 测试困难——无法为测试环境替换不同的实例
  • 依赖隐式——任何代码都可以 Get() 然后调用——谁在用、谁依赖谁完全不可见

UE 的 Subsystem 体系就是来解决这些问题的。


二、Subsystem 的四种生命周期#

UMyGameInstanceSubsystem : public UGameInstanceSubsystem; // 跟随 GameInstance
UMyWorldSubsystem : public UWorldSubsystem; // 跟随 World
UMyLocalPlayerSubsystem : public ULocalPlayerSubsystem; // 跟随 LocalPlayer
UMyEditorSubsystem : public UEditorSubsystem; // 跟随编辑器(只在 Editor 中存在)

1 什么时候用哪种#

Subsystem 类型生命周期适用场景
GameInstanceSubsystemGameInstance 创建 → 销毁全局服务——存档系统、成就系统、匹配系统
WorldSubsystemWorld 加载 → 卸载关卡级服务——任务管理器、AI 管理器、环境系统
LocalPlayerSubsystemLocalPlayer 登录 → 退出玩家本地服务——输入配置、UI 管理器、本地设置
EditorSubsystem编辑器启动 → 退出编辑器工具——资产检查、批量处理、自动化工具

关键选择标准:你的数据应该跟谁一起死?

任务管理器 → World 换新关卡时就销毁 → WorldSubsystem
存档系统 → 跨越所有关卡、贯穿整个游戏进程 → GameInstanceSubsystem

2 创建和使用#

// 定义
UCLASS()
class UQuestSubsystem : public UWorldSubsystem {
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
void StartQuest(FName QuestId);
void CompleteObjective(int32 PlayerId, FName ObjectiveId);
};
// 使用——在任何地方
UQuestSubsystem* QuestSys = GetWorld()->GetSubsystem<UQuestSubsystem>();
QuestSys->CompleteObjective(PlayerId, ObjectiveId);

不需要手动 Create / Destroy——引擎根据生命周期自动管理。 World 加载时自动 Initialize,World 卸载时自动 Deinitialize


三、Subsystem 间的依赖#

void UQuestSubsystem::Initialize(FSubsystemCollectionBase& Collection) {
// 声明依赖——在 Initialize 之前确保这些 Subsystem 已经存在
Collection.InitializeDependency<UInventorySubsystem>();
Collection.InitializeDependency<UNetworkSubsystem>();
}

InitializeDependency 保证:

  1. 依赖的 Subsystem 在你之前被创建
  2. 依赖链中的循环依赖会在启动时报错(而不是静默失败)

这比手写 Singleton 里 if (!InventorySys) InventorySys = new ... 强太多了。


四、用 Subsystem 改进 RPG 任务系统#

之前的架构(可能存在):

GameMode 里直接写任务逻辑
→ 换一个 GameMode(剧情变合作),任务逻辑全丢
→ 测试任务逻辑必须启动完整 GameMode

改进后:

UQuestSubsystem : UWorldSubsystem
→ 不依赖 GameMode——任何 World 里都能用
→ 单人模式:GameMode_Story + QuestSubsystem
→ 合作模式:GameMode_Coop + QuestSubsystem(完全同一段任务逻辑)
→ 单元测试:创建 World → 获取 QuestSubsystem → 直接测任务规则

Subsystem 让”业务逻辑”从 GameMode 的附庸变成了独立的可测试单元。


五、Subsystem 不是万能药#

不适合 Subsystem 的场景为什么替代方案
只有特定 Actor 需要的数据不是全局服务Actor 上的 Component
数据需要被 GC 追踪(UObject 引用)Subsystem 不被 GC 扫描放在 GameState 或其他 UObject 上
只有特定玩家需要(不是所有玩家共享)WorldSubsystem 是所有玩家共享的GameInstanceSubsystem + PlayerState
纯函数、无状态不需要生命周期管理Blueprint Function Library 或静态函数

六、总结#

概念一句话
GameInstanceSubsystem全局服务——整个游戏进程生命周期
WorldSubsystem关卡级服务——跟 World/Session 同生共死
InitializeDependency声明依赖链——保证初始化顺序,防止循环依赖
和 Singleton 的区别Subsystem 有明确的生命周期 Owner、自动创建销毁、可测试
什么时候不适合数据有 GC 依赖、只有特定 Actor 需要、纯无状态函数

Subsystem 是 UE 给你的一把”不用就亏”的好刀。如果你的项目里还有手动管理的 Singleton,把它换成 Subsystem——代码量更少、bug 更少、测试更容易。

UE Subsystem——不依赖 Actor 的服务架构
https://www.m4doka.xyz/posts/ue/ue-7-subsystem/
作者
m4doka
发布于
2026-08-14
许可协议
CC BY-NC-SA 4.0