☰
UE5 C++中GameInstanceSubsystem创建机制完全解析
2026/10/7 3:06:58 网站建设 项目流程

玩UE5的C++时间久了,你会发现项目里最难管的不是某一个Actor,也不是某一个UI,而是散落在各种地方的"全局数据":当前网络状态、玩家会话信息、全局设置、跨关卡要保留的临时数据……以前我习惯写单例,或者挂在GameInstance上,结果要么生命周期要自己蹚,要么和蓝图交互麻烦。后来认认真真把GameInstanceSubsystem摸了一遍,才体会到UE的Subsystem这套框架是真的"声明式":你只需要写好一个类,剩下什么时候创建、什么时候销毁、怎么取出来用,引擎全帮你管了。

这篇文章想把GameInstanceSubsystem在C++里的创建方式完整讲透。注意我说的是"创建方式",不是"怎么new一个对象"——在Subsystem体系里,你几乎不应该手动去实例化它。真正值得搞明白的,是声明类、触发创建、获取实例、依赖初始化这些核心环节,以及在蓝图、模块加载、编辑器PIE这些不同场景下它到底怎么被创建出来的。适合刚接触UE5 C++想优化项目结构的人,也适合已经用了Subsystem但偶尔遇到空指针、重复初始化问题的人。

1. 先搞清楚GameInstanceSubsystem是什么,再谈创建

1.1 它不是单例,也不是随便new出来的对象

很多从传统C++转过来的开发者,第一反应是写一个这样的东西:

class AMyManager { static AMyManager* Get(); };

然后自己维护静态指针,自己处理析构,遇到跨关卡保留、多人联机、编辑器PIE切换的时候,各种状态错乱。GameInstanceSubsystem想解决的就是这件事:它是由引擎统一管理的“有状态服务类”,你不需要关心它在内存里何时出现,只要知道它一定会随着GameInstance的整个生命周期存在,并且在GameInstance销毁时被框架清理。

它和普通单例最大的区别在于作用域。单例在进程里只有一份,但UE的项目里完全可能出现多个GameInstance(比如编辑器PIE同时跑多个客户端、服务器和客户端各自有独立GameInstance)。用普通静态单例,两个客户端之间的数据会互相串;用GameInstanceSubsystem,每个GameInstance都有自己独立的一套Subsystem实例,天然隔离。这个特性在做网络联机、多开预览的时候非常宝贵。

1.2 Subsystem家族有哪几种,GameInstanceSubsystem处在什么位置

UE的Subsystem不是只有一个类,而是一个家族。可以把它想象成一套分层的服务框架,不同层次的Subsystem跟不同对象共存亡:

Subsystem类型生命周期作用域典型使用场景
UEngineSubsystem整个引擎进程引擎级插件配置、全局渲染设置缓存
UEditorSubsystem编辑器模块生命周期编辑器工具面板、资产操作Hook
UWorldSubsystem单个UWorld从加载到卸载关卡内全局管理器,进入新关卡自动重建
UGameInstanceSubsystemGameInstance从创建到销毁跨关卡保持的全局数据、网络服务、玩家会话
ULocalPlayerSubsystem某个本地玩家登录到登出本地玩家输入配置、成就状态

GameInstanceSubsystem的关键优势是:它比WorldSubsystem活得久,比EngineSubsystem活得“个性化”。你从关卡A切到关卡B,WorldSubsystem销毁重建,但GameInstanceSubsystem还在。你在编辑器里按了编译热重载,GameInstance只要没销毁,TheseSubsystem也还在。这就让跨关卡携带数据——比如玩家背包、当前大厅房间号、网络连接句柄——变成了它的核心主场。

搞清楚了这个定位,再谈创建方式就会很清晰:它不是一个需要你手动组装的功能类,而是引擎在你启动游戏时帮你搭好的一间“独立办公室”,你只需要告诉引擎办公室的类型。

2. C++里最基础的创建方式:声明一个UCLASS子类就够了

2.1 最小代码模板,照着写就能跑

在UE5的C++环境里,创建GameInstanceSubsystem的第一步,也是最关键的一步,是声明一个继承自UGameInstanceSubsystem的UCLASS。拿一个实际项目举例,比如我需要管理全局的比赛配置:

// MyGameSubsystem.h #pragma once #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" #include "MyGameSubsystem.generated.h" UCLASS(BlueprintType, Blueprintable) class MYGAME_API UMyGameSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: // 引擎会在Subsystem创建并注册后调用,相当于初始化入口 virtual void Initialize(FSubsystemCollectionBase& Collection) override; // 引擎在GameInstance销毁前调用,负责清理 virtual void Deinitialize() override; UFUNCTION(BlueprintCallable, Category = "MyGame|Subsystem") void SetCurrentLevelName(const FString& InLevelName); UFUNCTION(BlueprintPure, Category = "MyGame|Subsystem") FString GetCurrentLevelName() const; private: FString CurrentLevelName; };

对应的cpp:

// MyGameSubsystem.cpp #include "MyGameSubsystem.h" #include "Subsystems/SubsystemCollection.h" void UMyGameSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); // 这里做初始化,比如读取配置文件、向网络模块注册回调 CurrentLevelName = TEXT("Default"); } void UMyGameSubsystem::Deinitialize() { // 断开网络连接、写缓存、清理委托绑定等 CurrentLevelName.Empty(); Super::Deinitialize(); } void UMyGameSubsystem::SetCurrentLevelName(const FString& InLevelName) { CurrentLevelName = InLevelName; } FString UMyGameSubsystem::GetCurrentLevelName() const { return CurrentLevelName; }

这段代码就是“最标准”的创建样板。你没有写任何new,没有在GameInstance里声明成员变量,也没有在某个Module的StartupModule里注册它。引擎看到UCLASS()宏和继承关系,就已经知道这是一个游戏实例子系统,会自动处理后面的一切。

2.2 声明之后,引擎什么时候真正创建它

这里我想把时间线讲清楚,因为很多新手卡在“我明明写了类,为什么这里取不到”这个问题上。

UE引擎启动流程中,和GameInstance相关的关键节点是:

  1. 引擎启动,找到默认的GameInstance类并创建实例。
  2. GameInstance的构造和Init过程里,SubsystemCollection开始工作。
  3. SubsystemCollection会扫描当前已经加载的模块里所有继承自UGameInstanceSubsystem的类。
  4. 对每一个类,框架实例化出一个UObject,并调用它的Initialize。
  5. 之后你在任何地方通过GetSubsystem拿到的,都是这个已经创建的实例。

所以正常情况下,你的UMyGameSubsystem在GameInstance初始化阶段就已经被引擎创建好了。这个“创建”发生在引擎打开游戏之后、你第一个关卡蓝图开始跑之前。换句话说,哪怕你的关卡蓝图里躺着几百个Actor,它们还没BeginPlay时,Subsystem已经活蹦乱跳了。

蓝图侧也一样。当你在蓝图里拖出“Get Game Instance Subsystem”节点,并指定Class UMyGameSubsystem时,它本质是调用了一次C++层面的GetSubsystem ()。如果实例已经存在就直接返回;如果因为某些原因还不存在(比如这个类来自一个后来动态加载的模块),引擎也会尝试实时创建一个出来。

2.3 千万别做的事情:构造函数里做业务初始化,也不要手动NewObject

有两个非常容易踩的坑,我想提前挂在最前面。

第一,不要在构造函数里做依赖环境的初始化。不要以为构造函数被调用就等于可以访问一切了。Subsystem的构造函数执行时机可能非常早,你依赖的其它子系统、模块、PlayerController都还没就绪。所以在构造函数里访问外部对象,轻则拿到nullptr,重则直接崩掉。正确的做法是把逻辑放到Initialize里,因为Initialize是框架给系统注入依赖的阶段。

第二,不要尝试手动NewObject或者new一个Subsystem出来。Subsystem的生命周期归SubsystemCollection管,你手动创建出来的对象既不会进入框架的管理列表,也不会收到Deinitialize清理,还会跟框架创建的真实例形成两个对象,使用方一看,哦豁,GetSubsystem拿到的是A,你手上操作的是B,数据全对不上。

注意:如果你看到某个教程让你在UCLASS声明里加UPROPERTY()反射标记,那是为了让属性暴露给蓝图/编辑器,跟“创建方式”没有关系。创建这件事,始终是引擎在背后做的。

3. 获取与访问:四种常见入口,本质都绕不开GetSubsystem

3.1 C++里通过GetGameInstance()->GetSubsystem ()访问

你写好了类,也确认引擎启动时会创建它,但怎么在项目代码里拿到它?最常见的入口是:

UGameInstance* GameInstance = GetGameInstance(); if (UMyGameSubsystem* MySubsystem = GameInstance->GetSubsystem<UMyGameSubsystem>()) { MySubsystem->SetCurrentLevelName(TEXT("Level02")); }

这里的GetSubsystem ()是整个体系的唯一正统入口。它做两件事:在实例存在时返回实例,在实例不存在但有创建条件时尝试创建。所以哪怕你是在游戏运行过程中、模块后加载的情况下调用它,通常也能拿到一个可用实例。

如果你在GameInstance子类里,可以直接写:

GetSubsystem<UMyGameSubsystem>()->SetCurrentLevelName(...);

因为在UGameInstance内部,这个函数的封装会直接拿到当前SubsystemCollection。

在Actor里,可以通过UGameplayStatics::GetGameInstance(WorldContextObject)替代,不过能直接用GetGameInstance()的话更简洁。在UMG的Widget里,也可以在BeginConstruction时缓存一个:

UGameInstance* GI = GetOwningPlayer()->GetGameInstance(); UMyGameSubsystem* MySubsystem = GI ? GI->GetSubsystem<UMyGameSubsystem>() : nullptr;

这里我要多说一句:Subsystem的获取开销并不高,但不建议在一帧内重复调用上百次。框架内部维护一个HashMap,查询很快,但再做别的事也要花点时间。个人习惯是进入一个系统时把指针缓存成成员变量,只在生命周期相关的地方重新获取。

3.2 蓝图中访问GameInstanceSubsystem的节点

很多人以为蓝图只能访问“纯蓝图”的子系统,其实完整流程是:先在C++里写好UGameInstanceSubsystem子类,然后加上Blueprintable/BlueprintType,再在蓝图中使用“Get Game Instance Subsystem”节点。

具体操作:

  • 在关卡蓝图、组件蓝图或UMG控件蓝图中,右键搜索“Get Game Instance Subsystem”。
  • 有一个Class下拉框,点开后搜索你的C++类名。
  • 选中后,节点输出引脚就是你那个类的实例,之后就能直接调用标了UFUNCTION(BlueprintCallable)的函数。

这个节点的本质,就是帮你封装了一次C++的GetSubsystem (),所以它跟C++侧拿到的对象是同一个。我见过有人误以为蓝图里创建了一个“蓝图版子系统”,然后在C++里又写了一份,两个类之间靠全局静态变量通消息,那完全是自找麻烦。

如果你想用纯蓝图做一个“GameInstanceSubsystem”,也完全可行:新建Blueprint Class,继承GameInstanceSubsystem。C++代码不认识它,但蓝图里的其它蓝图节点可以通过Get Game Instance Subsystem节点拿到并调用它。核心创建机制不变,只不过类来源是蓝图资产而不是C++源码。

3.3 在游戏启动早期主动触发创建,调整初始化顺序

常规情况下,所有在加载模块里注册的GameInstanceSubsystem都会在GameInstance::Init的时候由框架批量创建。但如果你某个Subsystem依赖另一个Subsystem,并且希望通过“早期主动调用”来保证顺序,可以重写GameInstance的Init:

void UMyGameInstance::Init() { Super::Init(); // 这里可以强制触发某些子系统的创建/初始化 UMyGameSubsystem* MySubsystem = GetSubsystem<UMyGameSubsystem>(); if (MySubsystem) { MySubsystem->LoadConfigData(); } }

需要注意的是:即便你不写这一段,引擎也会在你第一条游戏逻辑之前创建MySubsystem。这个阶段主动GetSubsystem,更多是让你能提前执行一些顺序敏感的操作,而不是为了“创建”而创建。

3.4 一次性获取多个同类型Subsystem:GetAllSubsystems

GameInstanceSubsystem大多数时候是按单个类获取的。但有时你定义了一个基类,又派生了多个实现,想一口气枚举出当前GameInstance下所有实现了这个基类的Subsystem,可以用FSubsystemCollection::GetAllSubsystems:

TArray<UBaseAbilitySubsystem*> AbilitySubsystems; GetGameInstance()->GetSubsystemCollection().GetAllSubsystems<UBaseAbilitySubsystem>(AbilitySubsystems);

这在做插件式架构时很实用:你不需要知道具体有哪些实现,只要声明了接口/基类,框架把你挂载到GameInstance上的全部实现都一次性列出来。相比手动维护一个TArray,这种方式几乎零成本。

4. 引擎背后是怎么“创建”的:SubsystemCollection与反射注册机制

4.1 从GameInstance::Init到CreateSubsystem的调用链

能够“声明即创建”的背后,是UE的SubsystemCollection在工作。大致链路如下:

  1. UGameInstance::Init是GameInstance开始工作的入口,内部会调用FSubsystemCollectionBase::Initialize。
  2. Initialize会遍历当前已注册到引擎的所有UGameInstanceSubsystem子类。
  3. 对于每个子类,框架调用CreateSubsystem完成对象实例化,并把实例存进内部Map。
  4. 实例化后,框架调用该Subsystem的Initialize(FSubsystemCollectionBase& Collection)。
  5. 后续所有GetSubsystem ()调用,都从Map中按类的类型查询。

这期间你不需要在Build.cs里加什么“创建器”,也不需要把类注册进某个Init数组。UCLASS宏配合UHT生成的代码已经把类的信息注册到了反射系统,SubsystemCollection只要通过反射就能发现所有子类。

模块加载的时机会影响“扫描范围”:如果你的模块在GameInstance::Init之后才被动态加载并注册到引擎,那SubsystemCollection首次扫描时看不到你的类。这也是为什么模块加载早期会取不到Subsystem,但稍后再次GetSubsystem又能拿到——框架在GetSubsystem阶段做了动态补救。

4.2 UCLASS标记、模块依赖与Build.cs注意事项

写C++的GameInstanceSubsystem,UCLASS()是必须的。如果没有这个宏,UHT不会生成反射代码,类就不会被SubsystemCollection识别,整个类形同虚设。所以“创建方式”的第一个硬性条件就是确保头文件里有:

#include "MyGameSubsystem.generated.h"

放在最后,且类声明用了GENERATED_BODY()。

另一个容易被忽略的地方是模块依赖。GameInstanceSubsystem类在引擎的GameInstanceSubsystem模块里。如果你的模块只依赖了Core和Engine,某些版本下编译能过,但为了稳妥,建议在模块的Build.cs里明确添加:

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

如果你还在用旧版本引擎里常见的“Subsystems”头文件路径,注意新版本推荐的路径是"Subsystems/GameInstanceSubsystem.h"。遇到编译器说找不到头文件,多半是模块依赖没加全。

4.3 依赖注入与初始化顺序:Initialize里的Collection参数

多个GameInstanceSubsystem之间经常互相调用。比如网络子系统要通知UI子系统,UI子系统要读取玩家信息子系统。这些依赖关系最理想的处理方式是在Initialize阶段通过Collection参数来解析:

void UMyGameSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); // 确保OrderSubsystem先完成初始化,当前系统再继续 UOrderSubsystem* OrderSubsystem = Collection.InitializeDependency<UOrderSubsystem>(); if (OrderSubsystem) { OrderSubsystem->RegisterDependent(this); } }

InitializeDependency ()做的事情,是告诉SubsystemCollection:请先初始化/确认T这个子系统可用,再把控制权交给当前类。它比直接在构造函数里强制访问更安全,也是官方推荐做法。

我再强调一下这个阶段的意义:初始化顺序本质上是依赖图。SubsystemCollection初始化每个实例时,会检查这个实例在Initialize里声明了哪些依赖,先把依赖初始化了。如果两个子系统互相依赖,框架一般能检测出循环依赖并给出报错,而不是让你死锁。

5. 我在实际项目里踩过的坑:创建/初始化阶段常见问题

5.1 取到的Subsystem是空指针,到底哪些原因

这是出现频率最高的问题。我按可能性从高到低列一个排查清单:

  • 类实际没有继承UGameInstanceSubsystem,而是继承成了UObject或者UActorComponent。
  • 头文件里少了UCLASS,或者忘了GENERATED_BODY,UHT没有生成正确的注册信息。
  • 你在GameInstance创建之前、比如某个全局静态初始化函数里取Subsystem,那必然拿不到。
  • 模块尚未加载。特别是动态加载插件里的Subsystem,在模块加载完成前查询就是空。
  • 编辑器里没有真正运行游戏,只是打开了关卡预览,GameInstance可能没有进入正常的初始化流程。
  • 蓝图里的Class Filter填错了,选了父类或者无关类。

我最常犯的是第五种:在编辑器里直接点Play之前,先打开某个编辑器工具窗口,工具窗口里想用Subsystem,结果拿了个空。不是代码写错,是生命周期还没到。解决方式是提前判空,或者把逻辑挪到真正需要运行时再执行。

5.2 构造函数里访问别的子系统导致崩溃

你可能会想,反正引擎启动后会创建Subsystem,那我在构造函数里调一下另一个Subsystem不行吗?真不行。构造函数执行的时候,当前GameInstance可能还没完全初始化,SubsystemCollection也可能还在建设过程中。试图从Collection里GetSubsystem,轻则返回null,重则触发assert直接卡进程。

正确姿势就是前面说的:把初始化放在Initialize里,把跨系统依赖用InitializeDependency声明。这样框架能保证你依赖的子系统已经在内存中了,而不是碰运气。

还有一个变体:在Deinitialize里访问已经被销毁的系统。Deinitialize的调用顺序是反过来的,越晚初始化的越早清理,你的依赖如果已经Deinitialize了,再去访问同样容易出错。所以Deinitialize里尽量只清理自己持有的资源,别指望还能稳妥地调用其它子系统。

5.3 编辑器PIE与热重载:重复创建的惊吓现场

在编辑器里反复Point In Editor(PIE)时,每次点击Play,引擎都会创建一个新的GameInstance,自然也会创建一批新的Subsystem实例。退出Play,这些实例销毁。这个过程看起来是自动的,但如果你在Subsystem里用了静态变量或全局指针,就会出现这种情况:上一个PIE会话的旧数据被静态指针留住,新PIE会话的Subsystem读到一个残留状态。

解决方案很简单:Subsystem内不要用静态变量跨会话保存重要数据。需要跨编辑器会话保留的数据,放进UGameInstanceSubsystem之外的DeveloperSettings、SaveGame,或者独立的配置文件。让Subsystem只保存“当前GameInstance生命周期内的状态”,它的创建和销毁才会真正安心。

热重载是另一个容易迷惑的地方。你在编辑器运行中改了C++代码,点了编译,引擎会尝试热重载。Subsystem作为一个UObject,会在重载过程里被替换掉,但新的实例会保留哪些字段取决于UPROPERTY的标记。没有标记的属性在重载后可能全部回到默认值。我后来得出的经验是:Subsystem里的字段该标UPROPERTY就标上,尤其你想跨热重载保留的缓存数据,不然编译完状态丢光,排查起来特别隐蔽。

5.4 编译报错MSB3073、找不到GameInstanceSubsystem模块这类问题

热词里有人搜"UE5 msb3073",这个错误本身和Subsystem没直接关系,但它出现的场景往往是你改完Build.cs或新增模块后,VS和UE的构建流程没有正确同步。常见表现是:子进程返回错误码,项目编不过。

我的建议是:新增Subsystem类以后,如果出现与类注册、反射生成相关的怪异编译错误,先执行一次UE菜单里的“Tools -> Refresh Visual Studio Project”,让UHT重新生成项目文件。然后再重新编译。很多时候所谓“创建不了”不是代码逻辑问题,而是构建系统压根没把你的新类纳入编译范围。

另外,如果你加了Build.cs依赖后编不过,检查模块名是否在引擎版本里存在。GameInstanceSubsystem模块在4.26之后的常见版本里都有,旧版本某些分支可能没有独立模块,那时头文件路径可能在Engine模块里。以你引擎版本实际能搜到的路径为准。

5.5 常见问题速查表

现象最可能原因处理方案
GetSubsystem返回nullptr模块没有加载或类没写UCLASS等待模块加载完,确认反射标记
构造函数里访问别的系统崩溃初始化顺序未到把逻辑搬到Initialize
PIE结束后再次Play数据残留用了静态变量用UPROPERTY成员代替静态变量
热重载后字段全丢了字段没有UPROPERTY给需要保留的字段加反射标记
蓝图里找不到你的类类没加BlueprintableUCLASS里加上Blueprintable/BlueprintType
构建报MSB3073VS工程文件和UHT未同步刷新Visual Studio项目再编译

6. 创建之外:什么时候该用、什么时候别用、以及组合玩法

6.1 适合GameInstanceSubsystem的任务与示例

一句话概括:凡是“从进游戏到退游戏都活着、并且跨关卡不丢的全局服务”,都适合塞进GameInstanceSubsystem。举几个我实际项目里的例子:

  • 在线服务客户端:登录、房间列表、队伍信息缓存。登录成功后从服务器拉的玩家昵称、头像URL,放在Subsystem里,切关卡不丢。
  • 全局输入配置:比如移动端双指触摸状态、屏幕边缘手势识别。这个数据需要随时被UI和玩法系统读取。
  • 音频总线管理:跨关卡保持BGM播放进度、全局音量设置。
  • 本地化配置缓存:当前语言、首选的地区设置,任何Widget生成时都能快速访问。
  • 比赛流程控制:大厅进房间、房间进对局、对局结束回大厅,这种状态机放在GameInstanceSubsystem里非常合适,因为关卡切换过程中WorldSubsystem会重建,而它不会。

RTS类项目也常见这种用法:全局经济系统、可选的建造队列、玩家科技树状态,都不是某一关单独拥有的,而是整个对局会话共享。你总不能每次换图都读取存档重建一遍,这些天然该放Subsystem。

6.2 不适合的场景:别把啥都往里塞

GameInstanceSubsystem不是万能收纳箱。以下场景用它反而难受:

  • 需要随关卡重置的数据,请用WorldSubsystem。比如每个关卡不同的敌人刷新管理器,关卡卸载时它就应该跟着消失,否则上一关的敌人列表会污染下一关。
  • 需要写盘持久化的玩家存档,请用SaveGame + USaveGameSystem。Subsystem只保留运行期内存数据,退出游戏一概不管。
  • 纯静态配置、不需要运行时逻辑的数据,请用UDeveloperSettings。编辑器里就能改,改完生成默认值,比在Subsystem里写死JSON方便。
  • 需要服务器权威同步的玩法数据,不要图省事直接塞Subsystem。GameInstanceSubsystem不会帮你处理网络复制,它只是本地运行的容器;真正需要同步的数值应该放在GameState或PlayerController身上,Subsystem可以作为本地缓存和逻辑层。

关于网络同步多说一句:Subsystem本身没有复制属性,但在联机架构中经常作为“本地服务层”出现:客户端Subsystem向服务器发RPC,服务器Subsystem处理逻辑并把结果通过GameState同步回来,客户端Subsystem再修改本地缓存。它不替代网络层,而是配合网络层。

6.3 多Subsystem协作的设计模型

与其写一个巨大的“God Subsystem”,不如拆成多个小Subsystem,让它们通过依赖和消息互相调用。比如一个移动端双指触摸项目:

  • UInputGestureSubsystem:负责收集触摸输入、判断手势。
  • UPlayerSessionSubsystem:负责会话、玩家状态。
  • UUIStateSubsystem:负责跨关卡UI栈。

在UUIStateSubsystem的Initialize里:

UInputGestureSubsystem* InputSubsystem = Collection.InitializeDependency<UInputGestureSubsystem>(); UPlayerSessionSubsystem* SessionSubsystem = Collection.InitializeDependency<UPlayerSessionSubsystem>();

这样初始化的顺序就会变成:输入系统先就绪,会话系统其次,最后UI系统开始运行。一旦代码量变大,这套依赖声明比在GameInstance::Init里顺序调用一堆函数要清晰得多。

我个人在实际项目里的做法是:每个Subsystem只管一个业务域,对外只暴露UFUNCTION/公开方法,内部细节封装。不同Subsystem之间通过依赖或直接方法调用协作,不再另外写一个全局事件总线。实际测下来,代码定位快、调试清晰,也不太会出现“全局状态改了但不知道谁改的”这种问题。

最后再分享一个小技巧:如果你在同一个项目里既有C++又有蓝图,C++写核心逻辑,蓝图做表现层扩展,建议把Subsystem类都加上Blueprintable,并把关键方法都设为BlueprintCallable或BlueprintPure。这样你在蓝图层也能舒服地取到同一个实例。UE这套机制让我最舒服的点就在这里:创建方式一旦想通,剩下就是组织代码风格的问题。你不用再纠结全局管理器放在哪个类里,写一个UGameInstanceSubsystem子类,剩下的事交给引擎就好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询