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; // 跟随 GameInstanceUMyWorldSubsystem : public UWorldSubsystem; // 跟随 WorldUMyLocalPlayerSubsystem : public ULocalPlayerSubsystem; // 跟随 LocalPlayerUMyEditorSubsystem : public UEditorSubsystem; // 跟随编辑器(只在 Editor 中存在)1 什么时候用哪种
| Subsystem 类型 | 生命周期 | 适用场景 |
|---|---|---|
| GameInstanceSubsystem | GameInstance 创建 → 销毁 | 全局服务——存档系统、成就系统、匹配系统 |
| WorldSubsystem | World 加载 → 卸载 | 关卡级服务——任务管理器、AI 管理器、环境系统 |
| LocalPlayerSubsystem | LocalPlayer 登录 → 退出 | 玩家本地服务——输入配置、UI 管理器、本地设置 |
| EditorSubsystem | 编辑器启动 → 退出 | 编辑器工具——资产检查、批量处理、自动化工具 |
关键选择标准:你的数据应该跟谁一起死?
任务管理器 → World 换新关卡时就销毁 → WorldSubsystem存档系统 → 跨越所有关卡、贯穿整个游戏进程 → GameInstanceSubsystem2 创建和使用
// 定义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 保证:
- 依赖的 Subsystem 在你之前被创建
- 依赖链中的循环依赖会在启动时报错(而不是静默失败)
这比手写 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/