SuperAPI AI 工具集

subsystems

编程与开发

查看项目

更新于 11-06 16:21

技能介绍

用途

Subsystem 是虚幻引擎中派生自 UObject 的单例,其生命周期与某个引擎构造(引擎进程、GameInstance、World 或 LocalPlayer)绑定。引擎在拥有者创建时自动创建它,在拥有者销毁时自动销毁,无需手动实例化,无需处理 GC,并且可直接暴露给蓝图和 Python。Subsystem 用于替代手写的静态单例和“管理器 Actor”,是现代 Unreal C++ 中实现横切服务的标准模式。

适用场景

  • 需要服务或管理器类(背包、存档系统、对局生命周期、分析、能力注册表、音频协调器),并希望其自动创建和清理。
  • 需要在 UGameInstanceSubsystem、UWorldSubsystem 或管理器 Actor 之间做选择。
  • 正在构建插件,希望插件系统自动初始化,而不给游戏项目增加手动设置代码。
  • 希望获得蓝图可调用的游戏服务,而无需转换到游戏特定类。
  • 希望获得分屏安全的每本地玩家状态。

五种 Subsystem 类型(按生命周期选择)

基类 生命周期 创建/拥有者 访问方式
UEngineSubsystem 引擎进程;跨关卡加载和 PIE 会话存活 UEngine / GEngine GEngine->GetEngineSubsystem<T>()
UGameInstanceSubsystem 游戏会话;跨关卡加载持续存在 UGameInstance GameInstance->GetSubsystem<T>()
UWorldSubsystem 单个 UWorld;每次关卡切换或无缝旅行时重置 UWorld World->GetSubsystem<T>()
UTickableWorldSubsystem 同 UWorldSubsystem,外加自动逐帧 Tick UWorld World->GetSubsystem<T>()
ULocalPlayerSubsystem 单个本地玩家;每个分屏槽一个实例 ULocalPlayer LocalPlayer->GetSubsystem<T>()

UEditorSubsystem(仅编辑器模块)遵循相同模式用于编辑器工具,此技能未覆盖。

编写 Subsystem

一个最小化的 UGameInstanceSubsystem 示例包含:头文件包含 Subsystems/GameInstanceSubsystem.h 和生成的 .generated.h;类声明使用 UCLASS() 和 GENERATED_BODY(),并重写 Initialize、Deinitialize、ShouldCreateSubsystem;提供 BlueprintCallable 的成员函数;用 UPROPERTY() 标记 UObject* 成员。

关键规则:

  • Initialize/Deinitialize 是生命周期钩子,而非 BeginPlay/EndPlay。
  • Subsystem 是 UObject,所有 UObject* 成员需使用 UPROPERTY()。
  • ShouldCreateSubsystem 在任何实例创建前于 CDO 上调用,应保持轻量。
  • 先调用 Super::Initialize(Collection),最后调用 Super::Deinitialize()。

访问 Subsystem

  • 从任意 Actor 访问 GameInstance 子系统:通过 GetGameInstance() 调用 GetSubsystem<T>()。
  • 静态辅助方法:UGameInstance::GetSubsystem<T>(GetGameInstance()),即使 GI 为空也安全。
  • World 子系统:GetWorld()->GetSubsystem<T>(),若 ShouldCreateSubsystem 返回 false 则返回空。
  • 检查式访问器:GetWorld()->GetSubsystemChecked<T>(),为空时断言。
  • 从 PlayerController 访问本地玩家子系统:ULocalPlayer::GetSubsystemFromController<T>(this)。

在蓝图中,Subsystem 以类型化的自动上下文获取节点出现,无需转换。只要函数标记为 BlueprintCallable,任何 UCLASS() 的 Subsystem 都可使用该节点。USubsystemBlueprintLibrary 提供蓝图图表搜索可发现的静态获取器。

依赖排序

在 Initialize 中使用 Collection.InitializeDependency<UOtherSubsystem>()(推荐模板形式,类型安全)。InitializeDependency 仅在同一集合(同一拥有者)内有效,不能声明 UWorldSubsystem 对 UGameInstanceSubsystem 的依赖,因为它们位于不同集合。

Tick — UTickableWorldSubsystem

当世界范围的 Subsystem 需要逐帧工作时,应继承 UTickableWorldSubsystem 而非 UWorldSubsystem。需重写 Initialize、Deinitialize、Tick,并实现纯虚函数 GetStatId(供统计系统使用)。规则:将 Initialize/Deinitialize 转发给 Super 以正确启用/禁用 Tick;未实现 GetStatId 将无法编译;工作不频繁时优先使用定时器而非 Tick。

Subsystem 与替代方案的选择

  • 无状态工具函数:自由函数或 BlueprintFunctionLibrary。
  • 复制状态(HP、分数):AGameState、APlayerState(Subsystem 不复制)。
  • 单个放置在关卡中的世界对象(障碍物生成器):放置在关卡中的管理器 Actor。
  • 每 Actor 行为/数据:Actor 上的组件。
  • 跨关卡会话状态:UGameInstanceSubsystem。
  • 每关卡状态(旅行时重置):UWorldSubsystem。
  • 每玩家 UI/输入状态:ULocalPlayerSubsystem。
  • 插件初始化:UEngineSubsystem 或 UGameInstanceSubsystem。

Subsystem 不做网络复制。权威联网状态应放在 GameState、PlayerState 或复制 Actor 上;Subsystem 用于本地侧的协调、缓存和 UI 逻辑。

常见陷阱

  • GameInstance/Engine 的 Initialize 中无法访问 World:UGameInstanceSubsystem 的 GetWorld() 在 Initialize 期间返回空;应使用 UWorldSubsystem,或将依赖世界的工作推迟到 OnWorldBeginPlay(世界子系统)或定时器。
  • 作用域错误:UWorldSubsystem 中的会话持久数据会在关卡切换时被清除;UGameInstanceSubsystem 中的每关卡数据会跨旅行累积。
  • ShouldCreateSubsystem 返回 false 但调用方未做空检查:GetSubsystem<T> 在跳过时返回 nullptr,调用方应始终空检查。
  • 跨集合依赖:InitializeDependency 仅在同一集合内有效;跨作用域协调需在 Initialize 中调用访问器。
  • 忘记在 Initialize/Deinitialize 中调用 Super:对 UTickableWorldSubsystem,Super::Initialize 启动 Tick、Super::Deinitialize 停止 Tick,跳过会破坏 Tick 生命周期。
  • Initialize 中繁重工作阻塞启动:通过 FStreamableManager 或 AsyncTask 延迟昂贵加载。
  • 循环依赖:InitializeDependency 在运行时检测简单循环并断言;应将共享状态拆分为第三个 Subsystem 以避免相互依赖。

限制

Subsystem 不进行网络复制,因此不适合存放需要同步的权威联网状态。其生命周期严格绑定拥有者,作用域选择错误会导致数据丢失或累积。依赖排序只能在同集合内声明。