GameInstance 子系统初始化顺序与「最派生子系统」模式
GameInstance 子系统初始化顺序与「最派生子系统」模式
两个 UGameInstanceSubsystem 之间存在依赖(A 在 Initialize() 里要用到 B),
或者插件提供了一个子系统基类、游戏模块想派生它定制——这两种需求在 UE 里都有坑。
下面这套"最派生子系统 + 初始化依赖"的写法是我在做存档系统时趟出来的,但它和存档无关,
任何子系统场景都能套用。
举的例子是:一个 DataLayer 子系统在 Initialize() 里要向存档系统子系统注册自己
("我是一个需要存读的全局对象")。两个坑连环出现。
坑 1:GI 子系统 Initialize 阶段 GetWorld() 是 null
GameInstance 子系统的初始化发生在任何 World 存在之前。所以在 Initialize() 里写USaveSystem::Get(GetWorld()) 这种以 World 取单例的代码会拿到 null,注册失败。
这个失败往往是静默的:存档照样能存能读,只是少了某块数据,很难一眼看出来。凡是 GI 子系统Initialize 里依赖 World 的逻辑,都要警惕这一点——要么推迟到有 World 的回调,要么改用不依赖
World 的获取方式。
坑 2:两个子系统的初始化先后无法保证
同属 Initialize 阶段的两个子系统,谁先谁后由引擎决定,不保证顺序。如果依赖方(DataLayer
子系统)先跑,它去注册时被依赖方(存档系统)还没准备好,又是注册失败。
解法两件套:
其一,「最派生子系统」保证只实例化一个具体类。
插件提供基类 USaveSystem / UDataLayerCommandsSubsystem,但让基类的ShouldCreateSubsystem() 在存在派生类时返回 DerivedClasses.Num() == 0,
这样引擎只会实例化最派生的那个类,避免基类和派生类被同时创建出两份。
bool UDataLayerCommandsSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
if (!Super::ShouldCreateSubsystem(Outer)) return false;
// 存在派生类时,只让最派生的子类被创建
TArray<UClass*> DerivedClasses;
GetDerivedClasses(GetClass(), DerivedClasses, /*bRecursive=*/false);
return DerivedClasses.Num() == 0;
}
其二,用 InitializeDependency<T>() 强制顺序。
游戏模块各派生一个具体类(UGameSaveSystem / UGameDataLayerCommandsSubsystem),
在依赖方的 Initialize() 里先声明依赖、再走父类初始化:
void UGameDataLayerCommandsSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Collection.InitializeDependency<UGameSaveSystem>(); // 强制存档系统先 Initialize 完成
Super::Initialize(Collection);
}
InitializeDependency<T>() 保证 T 在本子系统之前完成初始化。依赖关系留在游戏模块一侧,
插件不需要反向引用游戏模块,分层干净——这是这套写法最值的地方。
类层次
USaveSystem 一侧是存档全局对象,UDataLayerCommandsSubsystem 一侧通过实现ISaveExtensionInterface 把自己挂进存档流程;两边都各有一个游戏模块的最派生具体类:
这套模式的具体应用场景(DataLayer 状态恢复)见
World Partition 存档恢复时机。