UE4中的Subsystem
在Subsystem出現之前的黑暗時代
Subsystem時代
為什么使用Subsystem
Subsystem簡介
USubsystem類
FSubsystemCollectionBase
初始化
Dynamic類型的Subsystem的初始化
銷毀
Dynamic類型Subsystem的銷毀
Engine類型的Subsystem
Editor類型的Subsystem
GameInstance類型的Subsystem
World類型的Subsystem
LocalPlayer類型的Subsystem
Subsystem的使用
參考
在游戲開發過程中我們往往需要創建一系列的工具來輔助我們開發,例如UI管理工具,各類導表工具。在UE4.22之前我們只能夠自己編寫單例,並且自己管理生命周期。或者直接將管理游戲的工具編寫進GameInstance中。但是隨着代碼量的增加,GameInstance將會變得難以維護。在4.22版本發布了之后,我們可以直接將工具寫在Subsystem中,讓引擎幫我們自動管理工具類的生命周期,不再需要自己維護工具的生命周期或者修改引擎的類(如GameInstance)。
在Subsystem出現之前的黑暗時代
我們往往需要一個全局的,生命周期是在整個游戲進行的過程中一直存在的單例,而如果你想要在UE4里面實現一個單例,那么你需要使用以下代碼:
UCLASS()
class HELLO_API UMyScoreManager : public UObject
{
GENERATED_BODY()
public:
// 一些公用的函數或者Property
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Score;
UFUNCTION(BlueprintPure,DisplayName="MyScoreManager")
static UMyScoreManager* Instance()
{
static UMyScoreManager* instance=nullptr;
if (instance==nullptr)
{
instance=NewObject<UMyScoreManager>();
instance->AddToRoot();
}
return instance;
//return GetMutableDefault<UMyScoreManager>();
}
UFUNCTION(BlueprintCallable)
void AddScore(float delta);
};
這就對新人很不友好了(又一個不讓新人碰C++只讓寫Lua的原因),UE4的實現比較難看懂,而且容易出錯。例如很多人會忘記加上instance->AddToRoot();
,如果不記得加上,那么剛剛生成的對象可能會被GC掉,調用的時候會導致崩潰。而且用這種方式創建的單例會在Editor模式下繼續存在,所以運行預覽和停止預覽之后並不會銷毀,下一次預覽的時候里面的數據可能還是上一次運行的數據。如果想要處理這個問題,就需要自己手動加上Initialize()
和Deinitialize()
函數,手動調用,自己管理生命周期。
或者是另一種方法,直接把單例寫進UGameInstance
的子類里面,然后在UGameInstance
的Init
和Shutdown
里面進行創建和銷毀。但是即便是這樣也需要手動為每一個單例類寫一遍,很容易出錯,也不容易維護。
總而言之,不管是什么樣的實現方法,UE4客戶端開發都得要自己管理好自己寫的單例類的生命周期,心智負擔極大。所以官方推出了Subsystem,並自己用在了UE4的部分組件的開發中(如VaRest,官方用Subsystem制作了REST API插件),方便引擎開發、客戶端開發人員對引擎或者游戲做擴展、插件,同時不用自己操心生命周期的問題。
Subsystem時代
為什么使用Subsystem
用Subsystem的好處:
- 不需要自己管理生命周期,引擎自動幫你管理,而且保證和指定的類型(目前只有5種)生命周期一致;
- 官方提供藍圖接口,能夠很方便地在藍圖調用Subsystem;
- 與
UObject
類一樣,可以定義UFUNCTION
和UPROPERTY
; - 容易使用,只需繼承需要的Subsystem類型就能夠正常使用,維護成本低;
- 更模塊化,而且可以遷移某個Subsystem到其他游戲項目使用;
所以為了代碼更加方便維護與移植,還是使用Subsystem編寫需要用到的工具比較好。
Subsystem簡介
傳統美德,先附上官方的介紹:
Subsystems in Unreal Engine 4 (UE4) are automatically instanced classes with managed lifetimes. These classes provide easy to use extension points, where the programmers can get Blueprint and Python exposure right away while avoiding the complexity of modifying or overriding engine classes.
下面簡單翻譯一下。UE4會自動實例化你編寫的Subsystem,並且根據你的Subsystem類型(目前有5種類型)管理Subsystem的生命周期。Subsystem能夠暴露接口給藍圖和Python使用,不需要修改或者繼承引擎的類(如GameInstance)。
目前UE4支持的Subsystem類型有以下5種:
- Engine類:
UEngineSubsystem
- Editor類:
UEditorSubsystem
- GameInstance類:
UGameInstanceSubsystem
- World類:
UWorldSubsystem
- LocalPlayer類:
ULocalPlayerSubsystem
名稱分別對應他們依存的Outer對象(稱之為Outer是因為源代碼里面指向這些對象的指針名為Outer),以及他們對應的生命周期分別是:
- UEngine* GEngine(引擎啟動期間存在,或者說游戲進程期間存在);
- UEditorEngine* GEditor(編輯器啟動期間存在);
- UGameInstance* GameInstance(游戲運行期間存在);
- UWorld* World(關卡運行期間存在,一個游戲可能會有多個關卡,另外要注意的是編輯器下看到的場景其實也是一個World);
- ULocalPlayer* LocalPlayer(本地玩家存在的時候存在,實際上通常和GameInstance生命周期差不多,但是可能有多個本地玩家,而且游戲進行過程中可以隨時添加減少本地玩家,所以生命周期視情況、Outer對象依附於哪個LocalPlayer而定);
他們其實之間沒什么區別,默認的功能都較為相似,目前主要的區別在於不同類型的Subsystem生命周期不同。所有的Subsystem都直接或者間接繼承了USubsystem
類。我們用張圖來大致展示一下各個系統的類的關系:
圖中的UDynamicSubsystem
,前面沒有提到,所以這里簡單介紹下。我們可以看到圖中主要是EditorSubsystem和EngineSubsystem繼承了DynamicSubsystem,這是因為這兩類Subsystem主要是類似模塊,能夠隨時加載和卸載。而DynamicSubsystem就能提供這種功能,讓這類Subsystem只有在需要的時候加載進入編輯器或者游戲引擎中,不需要的時候就可以卸載掉。UDynamicSubsystem
提供的額外功能只有加載和卸載功能。
其他3類Subsystem會在對應的Outer對象生命周期內自動創建,Outer對象生命周期結束的時候才會被自動銷毀。這3類Subsystem相比於沒有繼承DynamicSubsystem的Subsystem少了加載和卸載的功能,其他方面沒什么區別。編寫自定義的Subsystem的時候只需要關注Subsystem用在什么場景和具備什么樣的生命周期和需不需要動態加載卸載即可。
看回到USubsystem,可以注意到旁邊的FSubsystemCollectionBase
,這個類主要用來管理某一類型的Subsystem,負責Subsystem的創建、銷毀、查詢(依靠查詢功能,Subsystem可以相互之間通信)。每個類型的Subsystem模塊都會產生一個相對應的SubsystemCollection
,例如GameInstanceSubsystemCollection
。SubsystemCollection
底層實際會包含一個TMap
變量,用來保存每個特定類型USubsystem
子類的實例(如UGameInstanceSubsystem
子類的實例)。因為TMap會保證每個Key對應的Value唯一,而Key就是子類,所以在這個Outer對象對應的生命周期中只能創建出一個這個子類的對象。最終每個用戶自定義的Subsystem子類生成對象都會是單例(例如用戶編寫了一個UMyGameInstanceSubsystem
類,那么在GameInstance的生命周期中只能創建一個UMyGameInstanceSubsystem
對象)。
另外,FSubsystemCollectionBase
繼承了FGCObject
,所以FSubsystemCollection
內的對象會受到UE4的GC管理。UE4的GC算法在這里不是重點,所以這里不詳細說明。
另另外,上圖中的UMy*Subsystem
都是代表用戶自己創建的Subsystem,用戶編寫自己的Subsystem的時候只需要繼承特定類型的Subsystem即可,引擎會自動管理這些Subsystem的生命周期,保證和Outer對象的生命周期一致。
大概了解了Subsystem的概念、構成之后,下面開始簡單說明各個類型的Subsystem的生命周期、作用等。因為內容比較多所以我會把重點放在比較常用的GameInstanceSubsystem上,畢竟實際上功能差不太多,能夠弄明白GameInstance就基本上可以弄明白其他的Subsystem。如果有機會,其他的Subsystem后面我會詳細說明它們的具體功能與實現。我們先從USubsystem
類開始。
USubsystem
類
首先我們從上述5種Subsystem共同的基類,USubsystem
類說起。USubsystem
也繼承了UObject
,因此和其他UObject
一樣,具有反射、元數據、序列化、被UE4自動GC等功能,可以和UObject
一樣添加各類UFUNCTION
、UPROPERTY
。這里不詳細介紹UObject
,如果感興趣可以另外自己查詢相關資料,或者有機會再另外總結,詳細說明下。
首先看看基類的定義:
注意USubsystem
被標記為了Abstract
,抽象類,所以不要嘗試去實例化它。接下來,我們看看USubsystem
定義了什么接口。
可以看出USubsystem
這個抽象類本身是比較簡單的,接口並不多,我們一個個介紹。ShouldCreateSubsystem
用來控制是否創建Subsystem,可以重寫來自己控制什么時候創建Subsystem,例如我們有部分Subsystem是只在客戶端運行的, 不希望在服務端加載,那么就可以重寫這個接口,保證我們寫的Subsystem只在客戶端被實例化。
Initialize
會在Subsystem實例化的時候調用,我們可以重寫這個接口來初始化我們的Subsystem。注意他的參數是FSubsystemCollectionBase& Collection
,這使得Subsystem可以在實際上創建完成前獲取到外部Outer(這是一個UObject
類型的指針,用來指向外部的對象,這個對象主要取決於Subsystem的類型,例如如果是GameInstance類型的那么就是指向GameInstance),從而獲取到其他的Subsystem對象。
Deinitialize
則是在Subsystem被銷毀的時候執行,我們重寫這個接口可以用來善后,例如釋放掉Subsystem正在占用的資源。
GetFunctionCallspace
主要用來查看網絡狀態,他的默認實現是這樣的:
繼續深入,可以看到GEngine下是這樣描述的:
該函數主要用於判斷當前是不是在遠端調用(即運行這段代碼的時候是在服務端還是在客戶端),Subsystem重寫之后可以用來做網絡相關的功能。
在私有變量中我們可以看到FSubsystemCollectionBase
被聲明為了友元類,這使得FSubsystemCollectionBase
重的函數可以隨意訪問USubsystem
中定義的函數與成員變量(另,FSubsystemCollectionBase
繼承了FGCObject
,不然F開頭的純C++類無法訪問/管理U開頭的UE4的類,如果感興趣的話可以看一下相關的資料,這里不贅述)。
FSubsystemCollectionBase
這部分涉及到的代碼太多,所以這里只是簡單敘述下,不會說得太細(畢竟不是代碼筆記)。后面有機會再詳細解析。我們主要關注的地方是這個類怎么初始化我們的Subsystem,怎么銷毀的。
初始化
我們前面提到了5類Outer對象,這些對象實際上自己有一個變量FSubsystemCollection
,專門用來保存Subsystem,例如GameInstance中:
又或者World中:
(說句題外話,看一下旁邊的行數就知道完全搞懂UE4是不現實的……World這里的截圖,3687行還只是頭文件的代碼量)
而這些Outer對象在初始化的時候會把自己傳入到SubsystemCollection
的Initialize
中:
而SubsystemCollection
繼承了SubsystemCollectionBase
,如下圖:
因此最終執行的其實是SubsystemCollectionBase
的Initialize
:
void FSubsystemCollectionBase::Initialize(UObject* NewOuter)
{
// 省略部分代碼
if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass()))//如果是UDynamicSubsystem的子類
{
// 省略初始化Dynamic類型Subsystem部分的代碼
}
else
{ //普通Subsystem對象的創建
TArray<UClass*> SubsystemClasses;
GetDerivedClasses(BaseType, SubsystemClasses, true);//反射獲得所有子類
for (UClass* SubsystemClass : SubsystemClasses)
{
AddAndInitializeSubsystem(SubsystemClass);//添加初始化Subsystem對象創建
}
}
}
if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass()))
將這部分代碼分成了兩部分,在這里面條件成立的情況是支持UDynamicSubsystem
的Subsystem類型的初始化代碼(Editor和Engine類型的Subsystem),其他的是較為簡單的GameInstance、World、LocalPlayer類型的Subsystem。
我們首先看比較簡單的不是Dynamic的Subsystem部分,這里執行的操作實際上只有2步:
- 通過反射獲取BaseType(5種基本Subsystem類型)的子類;
- 全部每個單獨進行
AddAndInitializeSubsystem
bool FSubsystemCollectionBase::AddAndInitializeSubsystem(UClass* SubsystemClass)
{
//...省略一些判斷語句
const USubsystem* CDO = SubsystemClass->GetDefaultObject<USubsystem>();
if (CDO->ShouldCreateSubsystem(Outer)) //從CDO調用ShouldCreateSubsystem來判斷是否要創建
{
USubsystem*& Subsystem = SubsystemMap.Add(SubsystemClass);//創建且添加到TMap里
Subsystem = NewObject<USubsystem>(Outer, SubsystemClass);//創建對象
Subsystem->InternalOwningSubsystem = this;//保存父指針
Subsystem->Initialize(*this); //調用Initialize
return true;
}
}
簡單的說就是根據你重寫的ShouldCreateSubsystem
,以及依存的Outer
對象,來創建Subsystem對象(並將Subsystem的持有者設定為輸入的Outer對象),並且添加到SubsystemMap里面,最后調用用戶重寫的Initialize進行Subsystem的初始化。而且因為生成的實例保存的地方是TMap
類型的SubsystemMap
,所以最后可以保證每個Subsystem子類只生成一個實例,相當於實現了單例模式。下圖是SubsystemMap的定義:
Dynamic類型的Subsystem較為復雜點,這里只是簡單介紹下大概的初始化過程。
Dynamic類型的Subsystem的初始化
首先看下DynamicSubsystem的聲明:
構造函數的實現:
可以看到,實際上沒有添加功能,只是相當於用來標記一個類別而已。實際動態加載卸載的功能還是通過FSubsystemCollectionBase實現。
讓我們回過頭來看FSubsystemCollectionBase::Initiate
:
void FSubsystemCollectionBase::Initialize(UObject* NewOuter)
{
// 省略部分檢查代碼
if (SubsystemCollections.Num() == 0)// SubsystemCollections實際上是靜態變量,這里通過內容數量判斷是不是第一次創建
{
// 初始化FSubsystemModuleWatcher,監聽模塊的加載與卸載用
FSubsystemModuleWatcher::InitializeModuleWatcher();
}
// 省略
if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass()))// 如果是UDynamicSubsystem的子類
{
// 注意這里的DynamicSystemModuleMap,實際上一部分官方自己寫的Subsystem就在這里面
for (const TPair<FName, TArray<TSubclassOf<UDynamicSubsystem>>>& SubsystemClasses : DynamicSystemModuleMap)
{
for (const TSubclassOf<UDynamicSubsystem>& SubsystemClass : SubsystemClasses.Value)
{
if (SubsystemClass->IsChildOf(BaseType))
{
AddAndInitializeSubsystem(SubsystemClass);
}
}
}
}
else
{ //普通Subsystem對象的創建,省略
}
這段代碼比較簡單,所以只是簡單說一下做了什么。首先我們看到一開始就判斷SubsystemCollections
內容數量是不是為0,這個變量在頭文件SubsystemCollection.h
的定義如下:
可以看到,是一個靜態變量,所以實際上是在判斷是不是第一次創建(因為引擎里有部分組件創建並添加進了這個變量之后就不會再移除,直到引擎關閉,所以可以這么干)。
可以看到原代碼中,第一次創建的時候就會調用FSubsystemModuleWatcher::InitializeModuleWatcher()
來登記每個模塊用到的所有DynamicSystem子類。隨后會把DynamicSystemModuleMap
中記錄的DynamicSubsystem子類模版(原代碼是TArray<TSubclassOf<UDynamicSubsystem>>
)傳入到函數AddAndInitializeSubsystem
中正式開始初始化。
因為AddAndInitializeSubsystem
在上面非動態的Subsystem講解中已經解釋過了,就是簡單地遍歷並且初始化實例。所以這里着重看第一步,即FSubsystemModuleWatcher::InitializeModuleWatcher()
的具體實現:
void FSubsystemModuleWatcher::InitializeModuleWatcher()
{
check(!ModulesChangedHandle.IsValid());
// 這里會獲取所有UDynamicSubsystem的子類
TArray<UClass*> SubsystemClasses;
GetDerivedClasses(UDynamicSubsystem::StaticClass(), SubsystemClasses, true);
for (UClass* SubsystemClass : SubsystemClasses)
{
// 排除抽象類
if (!SubsystemClass->HasAllClassFlags(CLASS_Abstract))
{
// 獲取Subsystem對應的包
UPackage* const ClassPackage = SubsystemClass->GetOuterUPackage();
if (ClassPackage)
{
const FName ModuleName = FPackageName::GetShortFName(ClassPackage->GetFName());
if (FModuleManager::Get().IsModuleLoaded(ModuleName))
{
// 初始化DynamicSubsystem並添加到靜態變量DynamicSystemModuleMap,注意ModuleSubsystemClasses實際上是一個引用
TArray<TSubclassOf<UDynamicSubsystem>>& ModuleSubsystemClasses = FSubsystemCollectionBase::DynamicSystemModuleMap.FindOrAdd(ModuleName);
ModuleSubsystemClasses.Add(SubsystemClass);
}
}
}
}
// 添加監聽事件,這里把函數OnModulesChanged與事件相關聯了,這個事件是在模塊加載和卸載的時候會被觸發的
ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddStatic(&FSubsystemModuleWatcher::OnModulesChanged);
}
上面的DynamicSystemModuleMap(出現在了FSubsystemCollectionBase::Initialize
和FSubsystemModuleWatcher::InitializeModuleWatcher
中),是一個static類型變量:
主要用來記錄當前動態加載的Module和與其對應的所有UDynamicSubsystem類型,與FSubsystemModuleWatcher相關。總之這里只是簡單的創建並按照模塊來添加到DynamicSystemModuleMap
中,后面加載和卸載模塊的時候就要依賴DynamicSystemModuleMap
來創建或者銷毀模塊對應的一系列DynamicSubsystem(不用擔心多個模塊重復用到了某個DynamicSubsystem子類而導致在銷毀的時候刪除某個其他模塊仍要使用的子類對象。因為實際上會有GC系統來管理這些對象,只有所有模塊都不會引用某個DynamicSubsystem對象,這個對象才會發生GC)。
另外,注意這里是TArray<TSubclassOf<UDynamicSubsystem>>
。這里是TArray的原因是我們的模塊可能會依賴多個DynamicSubsystem子類,模塊所有要用到的DynamicSubsystem子類模版類都會保存在TArray中。我們繼續看下去,看看FSubsystemModuleWatcher::OnModulesChanged
的實現:
void FSubsystemModuleWatcher::OnModulesChanged(FName ModuleThatChanged, EModuleChangeReason ReasonForChange)
{
switch (ReasonForChange)
{
case EModuleChangeReason::ModuleLoaded:
// 創建模塊
AddClassesForModule(ModuleThatChanged);
break;
case EModuleChangeReason::ModuleUnloaded:
// 銷毀模塊
RemoveClassesForModule(ModuleThatChanged);
break;
}
}
這個事件在每次加載或者卸載模塊的時候都會觸發,實際上就是依賴這個事件來實現對DynamicSystem子類的動態加載和卸載。
接下來我們看看創建模塊的具體實現:
void FSubsystemModuleWatcher::AddClassesForModule(const FName& InModuleName)
{
// 找到模塊對應的代碼包
const UPackage* const ClassPackage = FindPackage(nullptr, *(FString("/Script/") + InModuleName.ToString()));
TArray<TSubclassOf<UDynamicSubsystem>> SubsystemClasses;
TArray<UObject*> PackageObjects;
// 得到模塊定義的所有對象
GetObjectsWithOuter(ClassPackage, PackageObjects, false);
for (UObject* Object : PackageObjects)
{
// 嘗試把包對象轉成UClass類的對象
UClass* const CurrentClass = Cast<UClass>(Object);
// 確保不是空指針,不是抽象類,是UDynamicSubsystem的子類
if (CurrentClass && !CurrentClass->HasAllClassFlags(CLASS_Abstract) && CurrentClass->IsChildOf(UDynamicSubsystem::StaticClass()))
{
SubsystemClasses.Add(CurrentClass);
// 為這個類創建實例
FSubsystemCollectionBase::AddAllInstances(CurrentClass);
}
}
// 如果其內部有定義Subsystem類,那么就登記
if (SubsystemClasses.Num() > 0)
{
// 登記到DynamicSystemModuleMap靜態變量里面
FSubsystemCollectionBase::DynamicSystemModuleMap.Add(InModuleName, MoveTemp(SubsystemClasses));
}
}
AddClassesForModule
的步驟可以總結為:
- 獲取模塊定義的所有包對象
- 將包對象轉換為UClass類,判斷是不是UDynamicSubsystem的子類,並且不是抽象類(是的,其實你可以繼承UDynamicSubsystem並且聲明為抽象類)
- 第二步的判斷通過,符合條件則開始用轉換成UClass的UDynamicSubsystem類創造實例
- 把
PackageObject
中的所有DynamicSubsystem子類都創建好之后就會添加到靜態變量FSubsystemCollectionBase::DynamicSystemModuleMap
中(代碼包中可能不只是定義/引用了一個DynamicSubsystem子類,所以存放的內容實際上是DynamicSubsystem子類數組)
另外,創建實例的實現如下:
void FSubsystemCollectionBase::AddAllInstances(UClass* SubsystemClass)
{
for (FSubsystemCollectionBase* SubsystemCollection : SubsystemCollections)
{
if (SubsystemClass->IsChildOf(SubsystemCollection->BaseType))
{
// 前面解釋過,用來創建對象
SubsystemCollection->AddAndInitializeSubsystem(SubsystemClass);
}
}
}
可以看到,最終創建實例的過程實際上就是和非動態的Subsystem(GameInstance、LocalPlayer、World)創建實例的過程是一樣的。所以實際上是一開始啟動的時候觸發FSubsystemModuleWatcher::InitializeModuleWatcher
,加載所有用到的UDynamicSubsystem
子類,隨后調用FSubsystemCollectionBase::AddAndInitializeSubsystem
來Initialize所有FSubsystemCollectionBase::DynamicSystemModuleMap
中的UDyanmicSubsystem子類,最后把生成的所有的UDynamicSubsystem
子類實例添加到靜態變量FSubsystemCollectionBase::SubsystemMap
中。
如果是動態加載那么會直接觸發事件,調用FSubsystemCollectionBase::AddAllInstances
,最后還是調用FSubsystemCollectionBase::AddAndInitializeSubsystem
來生成實例。
再提一嘴,FSubsystemCollectionBase::DynamicSystemModuleMap
實際上是以模塊划分,key就是模塊名,value就是模塊依賴的UDynamicSubsystem
子類。單個模塊可能需要用到多個UDynamicSubsystem
,所以value是TArray類型的變量。后面加載或者釋放某個模塊的時候能夠根據DynamicSystemModuleMap
中的記錄,知道該創建和銷毀什么類型的實例。
對於DynamicSubsystem來說實際上多了個FSubsystemModuleWatcher
來管理,因此實際上我們可以把關系圖更新為:
銷毀
實際上每個Outer對象銷毀的時候會調用SubsystemCollection.Deinitialize();
,例如GameInstance的:
Deinitialize
代碼如下:
void FSubsystemCollectionBase::Deinitialize()
{
//...省略一些清除代碼
for (auto Iter = SubsystemMap.CreateIterator(); Iter; ++Iter) //遍歷Map
{
UClass* KeyClass = Iter.Key();
USubsystem* Subsystem = Iter.Value();
if (Subsystem->GetClass() == KeyClass)
{
Subsystem->Deinitialize(); //反初始化
Subsystem->InternalOwningSubsystem = nullptr;
}
}
SubsystemMap.Empty();
Outer = nullptr;
}
可以看出,就是遍歷然后逐個執行用戶重寫的Deinitialize
。但是,此時Subsystem實際上還沒有完全被GC,看到上面的SubsystemMap.Empty()
了嗎?還記得Subsystem實際上是UObject嗎?還記得我們提到過FSubsystemCollectionBase繼承了FGCObject,所以F開頭的純C++類可以引用U開頭的UE4類型對象,從而能夠讓UE4的GC系統管理引用的對象嗎?在FSubsystemCollectionBase中有以下代碼:
在SubsystemMap.Empty()
后,因為保存的Subsystem不再被引用了,所以在下一幀GC系統介入的時候,會將原本保存在Map中的Subsystem對象判定為PendingKill,並且開始GC銷毀這些Subsystem對象(另外提一嘴,實際上UE4也是這么處理創建的Widget的,所以不建議手動銷毀,直接不引用,讓GC系統處理就好了)。
Dynamic類型Subsystem的銷毀
void FSubsystemModuleWatcher::RemoveClassesForModule(const FName& InModuleName)
{
TArray<TSubclassOf<UDynamicSubsystem>>* SubsystemClasses = FSubsystemCollectionBase::DynamicSystemModuleMap.Find(InModuleName);
if (SubsystemClasses)
{
for (TSubclassOf<UDynamicSubsystem>& SubsystemClass : *SubsystemClasses)
{
// 銷毀這個類的所有對象
FSubsystemCollectionBase::RemoveAllInstances(SubsystemClass);
}
// 移除登記
FSubsystemCollectionBase::DynamicSystemModuleMap.Remove(InModuleName);
}
}
DyanamicSubsystem在卸載和被銷毀的時候都會觸發事件OnModulesChanged
,最終調用上面這個函數,比較簡單所以不解釋了。比較疑惑的可能就是FSubsystemCollectionBase::RemoveAllInstances
函數,我們看看它的具體實現:
void FSubsystemCollectionBase::RemoveAllInstances(UClass* SubsystemClass)
{
// 遍歷屬於該類型的實例
ForEachObjectOfClass(SubsystemClass, [](UObject* SubsystemObj)
{
USubsystem* Subsystem = CastChecked<USubsystem>(SubsystemObj);
if (Subsystem->InternalOwningSubsystem)
{
// 釋放掉Subsystem實例
Subsystem->InternalOwningSubsystem->RemoveAndDeinitializeSubsystem(Subsystem);
}
});
}
可以看到實際上還是調用FSubsystemCollectionBase::RemoveAndDeinitializeSubsystem
來遍歷刪除Subsystem子類的實例:
void FSubsystemCollectionBase::RemoveAndDeinitializeSubsystem(USubsystem* Subsystem)
{
check(Subsystem);
USubsystem* SubsystemFound = SubsystemMap.FindAndRemoveChecked(Subsystem->GetClass());
check(Subsystem == SubsystemFound);
Subsystem->Deinitialize();
Subsystem->InternalOwningSubsystem = nullptr;
}
可以看到,調用了用戶重寫的Deinitialize
。
說實話,前面基本上已經說完需要說的了,因為這些不同類型的Subsystem實際上只是定義了一些接口,自帶的功能並不多,所以以下部分都會只是很簡單的介紹下。
Engine類型的Subsystem
UE4里面這種Subsystem的類名為“UEngineSubsystem”,這類Subsystem和引擎一起啟動,在游戲進程啟動開始的時候創建,進程結束銷毀,運行期間一直是全局唯一,適用於開發引擎工具。
Editor類型的Subsystem
和編輯器一起啟動,如果是Runtime的游戲的話那么不會啟動,只會存在編輯器下,且全局唯一。在編輯器啟動的時候開始創建,編輯器退出的時候銷毀。
GameInstance類型的Subsystem
比較常用的Subsystem。和游戲一起啟動,游戲退出的時候銷毀。只會在游戲Runtime或者PIE(Play In Editor,在編輯器中啟動的預覽游戲場景)模式中存在。常常用於編寫各類數據管理工具。例如我們有些時候希望能夠有一個統一的界面管理系統,因為所有的World中都會用到UI,而且有時候切換World也需要顯示一個加載界面的UI,因此不可能是World類型的Subsystem。這時候我們往往會將相關的邏輯寫在一個自己創建的GameInstanceSubsystem子類下,因為GameInstance類型的Subsystem能夠在整個游戲進行期間存在(與GameInstance生命周期一致),獨立於World的加載與切換。
這類Subsystem只是多了一個獲取GameInstance的函數。
World類型的Subsystem
和關卡World一起啟動和銷毀,數量可能大於1(畢竟大多數游戲不止一個關卡)。生命周期和GameMode是一起的。
不過要注意的地方是,UE4編輯器里面預覽的場景其實也是一個World,所以實際上在預覽場景里面可能也會創建World類型的Subsystem,如果不想要你的WorldSubsystem在預覽場景里面創建的話就要在ShouldCreateSubsystem
里面做好判斷。
LocalPlayer類型的Subsystem
和本地玩家一起創建和銷毀,數量可能大於1(例如本地分屏多玩家類型的游戲,在多個玩家的時候就會創建多個LocalPlayer的Subsystem)。每個LocalPlayer會維護自己的LocalPlayer類型的Subsystem,所以可能會有多個ULocalPlayerSubsystem子類實例,但是對於每個LocalPlayer來說都是單例。
Subsystem的使用
Subsystem的調用十分便利,因為官方已經包裝好了相關的藍圖接口,所以在藍圖里面也可以調用Subsystem暴露出來的給藍圖調用函數(或者可以在Subsystem里面定義好BlueprintImplementableEvent
,用Subsystem調用藍圖函數)。對應的C++源碼如下:
在藍圖中的使用:
而如果是在C++中調用的話則是:
//UMyEngineSubsystem獲取
UMyEngineSubsystem* MySubsystem = GEngine->GetEngineSubsystem<UMyEngineSubsystem>();
//UMyEditorSubsystem的獲取
UMyEditorSubsystem* MySubsystem = GEditor->GetEditorSubsystem<UMyEditorSubsystem>();
//UMyGameInstanceSubsystem的獲取
UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(...);
UMyGameInstanceSubsystem* MySubsystem = GameInstance->GetSubsystem<UMyGameInstanceSubsystem>();
//UMyWorldSubsystem的獲取
UWorld* World=MyActor->GetWorld(); //也都可以用其他方式獲取World
UMyWorldSubsystem* MySubsystem=World->GetSubsystem<UMyWorldSubsystem>();
//UMyLocalPlayerSubsystem的獲取
ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(PlayerController->Player)
UMyLocalPlayerSubsystem * MySubsystem = LocalPlayer->GetSubsystem<UMyLocalPlayerSubsystem>();
注意如果使用EditorSubsystem的話就要在工程名.build.cs
里面加上EditorSubsystem模塊的飲用,因為這算是編輯器模塊:
// ... 省略部分內容
if (Target.bBuildEditor)
{
// 最重要的地方
PublicDependencyModuleNames.AddRange(new string[] { "EditorSubsystem" });
}
參考
- 官方Subsystem文檔:建議直接看UE4源碼,官方有部分地方不是特別詳細(而且上面的信息不全),目前網上資料偏少,不如直接看源碼
- 《InsideUE4》GamePlay架構(十一)Subsystems:必看,很詳細,能說的基本都說了
- [英文直播]Programming Subsystems(真實字幕組)
- UE4.22 Subsystem分析:建議一讀,寫得不會涉及太多細節,但是該講的都基本覆蓋到了
- 【UE4 C++】編程子系統 Subsystem
- UE4實驗使用 FGCObject 引用UObject
- 【UE4】TSubclassOf的使用