模块插件与 Subsystem

自动关联目录:模块插件与 Subsystem

模块(Module)是 UE 的编译单元,插件(Plugin)是模块的分发单元,Subsystem 是"生命周期被引擎托管的全局管理器"。三件事解决的是同一个问题:怎么把代码切成可以被别人依赖、可以独立加载、可以自动管理生命周期的块。

一句话定位:C++ 项目里那些"手写的单例",在 UE 里都应该写成 Subsystem。

模块

Build.cs

public class MyModule : ModuleRules
{
    public MyModule(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[]
        {
            "Core", "CoreUObject", "Engine", "InputCore"
        });

        PrivateDependencyModuleNames.AddRange(new string[]
        {
            "Slate", "SlateCore", "UMG"
        });
    }
}
类别含义选择依据
PublicDependencyModuleNames依赖会传递我的头文件里暴露了来自该模块的类型
PrivateDependencyModuleNames依赖不传递只在 .cpp 里用

判断标准很简单:如果 #include 出现在 .h 里,就该放 Public;只在 .cpp 里,放 Private。全塞 Public 会让依赖图变稠,改动一个模块触发大面积重编。

导出宏

class MYMODULE_API FMyClass      // 跨模块访问必须加
{
    ...
};

跨模块使用的类型/函数不带 XXX_API 宏,链接期报"无法解析的外部符号"——这是建新模块时最容易卡住的一步。宏名来自模块名大写 + _API,由 IMPLEMENT_MODULE 自动生成。

入口

// 普通模块
IMPLEMENT_MODULE(FDefaultModuleImpl, MyModule);

// 游戏主模块(一个项目只能有一个)
IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, MyGame, "MyGame");

// 自定义模块的启动/关闭
class FMyModule : public IModuleInterface
{
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
};
IMPLEMENT_MODULE(FMyModule, MyModule);

StartupModule 里只能做与引擎无关的事(注册、分配)。此时很多子系统还没起来,访问 GEngine 可能为 null。

插件

项说明
结构.uplugin 描述文件 + 一个或多个模块
加载时机LoadingPhase(Default / PreDefault / PostDefault / PostEngineInit…)
类型Runtime / Editor / Developer / ThirdParty
启用编辑器插件面板,或 .uproject 里 Plugins 数组

插件与模块的边界:需要跨项目复用、需要独立开关、需要带资源或第三方库 → 插件;只是项目内部分层 → 模块。

加载时机选错的表现

现象原因
编辑器启动时崩在插件的 StartupModuleLoadingPhase 太早,依赖的模块未加载
编辑器 UI 里看不到自己的扩展加载太晚,扩展点已经收集完了
打包后插件不生效插件类型为 Editor,未被打进运行时

Subsystem

Subsystem 是 UE 官方给出的"全局管理器"方案,替代手写单例。好处是生命周期由引擎托管、自动创建销毁、可配置、可继承。

Subsystem 类型生命周期典型用途
UEngineSubsystem引擎进程(编辑器也活)全局配置、跨关卡服务
UEditorSubsystem编辑器编辑器工具与状态
UGameInstanceSubsystem游戏实例(跨关卡存活)存档管理、账号、匹配
UWorldSubsystem单个 World关卡内的系统(刷怪、天气)
ULocalPlayerSubsystem本地玩家输入映射、UI 状态
UCLASS()
class UMyGameSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public:
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;

    void DoSomething();
};

// 使用
UMyGameSubsystem* Sys = GetGameInstance()->GetSubsystem<UMyGameSubsystem>();

Initialize 的参数 Collection 用来声明依赖其它 Subsystem:

void UMyGameSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Collection.InitializeDependency<UOtherSubsystem>();   // 保证对方先初始化
}

不声明依赖而直接 GetSubsystem,可能拿到还没初始化的实例(不会崩,但状态是空的)。

为什么不要手写单例

手写单例Subsystem
生命周期靠自己管,容易泄漏引擎托管,自动创建销毁
编辑器与运行时状态会串实例跟随对应的外层对象,切换即销毁
PIE 多实例会互相污染每个 World/GameInstance 各一份
无法被蓝图/配置系统感知可以

目录约定

Source/
  MyGame/            主模块
  MyGameEditor/      编辑器模块(不打进包)
  MyFeature/         功能模块
Plugins/
  MyPlugin/
    MyPlugin.uplugin
    Source/MyPlugin/

规则:运行时代码不要放进 Editor 模块,否则打包时报一堆找不到符号;反之编辑器工具放运行时模块会白白增大包体。

依赖关系与编译

现象原因
改一个头文件全项目重编依赖放在 Public 且被广泛依赖
Unity Build 报符号冲突两个模块的匿名空间/静态同名
链接慢模块太多或依赖过密
循环依赖编译失败A 的 Public 依赖 B,B 的 Public 依赖 A

解决循环依赖:把公共类型抽到第三个模块;或用接口(IModuleInterface / 纯虚类)反向调用。

常见坑

坑说明
跨模块类型没加 _API链接期找不到符号
Public/Private 全填 Public重编范围失控
StartupModule 里访问 GEngine时机太早,空指针
手写静态单例编辑器与 PIE 状态串味
Subsystem 之间没声明依赖拿到未初始化的实例
运行时代码放在 Editor 模块打包失败
插件 LoadingPhase 选错扩展点收集不到或启动崩溃
模块名与宏名不一致XXX_API 未定义,编译报错