上一節展示了Module相關的基本概念,本節就讓我們試著為專案加入一個Game Module吧。首先,我們可以把專案下面預設的Primary Game Module複制一份出來,並重新命名成希望的Module名稱,見Figure 2.5.1。
Figure 2.5.1 為HorizonTutorialGame專案增加一個HorizonTutorialGameEditor作為額外的Game Module以擴充遊戲內Editor相關功能。這個Editor Game Module我們可以在之後用來客製化UPROPERTY在editor中detail pnael的顯示方式,或者是追加新的編輯模式……等等。
然後必須更改新Module下面build.cs中的類別名稱,見Code 2.5.1。
HorizonTutorialGameEditor.Build.cs |
using UnrealBuildTool;
public class HorizonTutorialGameEditor : ModuleRules { public HorizonTutorialGameEditor(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { “Core”, “CoreUObject”, “Engine”, “InputCore” }); PrivateDependencyModuleNames.AddRange(new string[] { }); } } |
Code 2.5.1 將Build.cs中的ModuleRules類別名稱改成HorizonTutorialGameEditor。
接著我們必須要定義要給這個Module的介面,見Code 2.5.2。
HorizonTutorialGameEditor.h |
#pragma once
#include “Engine.h” #include “ModuleManager.h” class FHorizonTutorialGameEditorModule : public IModuleInterface { public: virtual void StartupModule() override; virtual void ShutdownModule() override; };
|
Code 2.5.2 每個Module都必須要擁有一個IModuleInterface的實作才行,其中在Module載入時會呼叫StartupModule、結束時會呼叫ShutdownModule。
HorizonTutorialGameEditor.cpp |
#include “HorizonTutorialGameEditor.h”
#include “Modules/ModuleManager.h” //IMPLEMENT_PRIMARY_GAME_MODULE( FHorizonTutorialGameEditor, HorizonTutorialGameEditor, “HorizonTutorialGameEditor” ); //使用 IMPLEMENT_GAME_MODULE 代替 IMPLEMENT_PRIMARY_GAME_MODULE IMPLEMENT_GAME_MODULE(FHorizonTutorialGameEditorModule, HorizonTutorialGameEditor); void FHorizonTutorialGameEditorModule::StartupModule() { UE_LOG(LogTemp, Log, TEXT(“FHorizonTutorialGameEditorModule::StartupModule()”)); } void FHorizonTutorialGameEditorModule::ShutdownModule() { UE_LOG(LogTemp, Log, TEXT(“FHorizonTutorialGameEditorModule::ShutdownModule()”)); }
|
Code 2.5.3 使用IMPLEMENT_GAME_MODULE來完成我們的Module實作,其中第一個參數表示要使用的Module實作類別,第2個參數則傳入這個Module的名稱。
可能有些人會困惑為什麼在Primary Game Module中並沒有看見類似的IModuleInterface程式碼,其實並不是沒有,而是專案的預設模版所使用的Module實作所使用的是引擎中的內建實作『FDefaultGameModuleImpl』,。
HorizonTutorialGame.cpp |
#include “HorizonTutorialGame.h”
#include “Modules/ModuleManager.h” IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, HorizonTutorialGame, “HorizonTutorialGame” ); |
Code 2.5.4 Primary Game Module使用引擎內建的IModuleInterface實作「FDefaultGameModuleImpl」。
到底FDefaultGameModuleImpl是什麼?從原始碼中我們可以看出它繼承了IModuleInterface,並override了IsGameModule這個功能,見Code 2.5.5。
FDefaultGameModuleImpl實作 |
class FDefaultGameModuleImpl
: public FDefaultModuleImpl { /** * Returns true if this module hosts gameplay code * * @return True for “gameplay modules”, or false for engine code modules, plug-ins, etc. */ virtual bool IsGameModule() const override { return true; } }; |
Code 2.5.5 從注釋中我們可以看到,當IsGameModule回傳true的時候,代表的是這個Module裡面存在的都是跟GamePlay相關的Code。
到底什麼是GameModule?
從前面章節中的討論中我們知道,專案source底下的Module我們會將它當成Game Module來宣告。這邊比較讓人無法理解的是,當我們的使用IMPLEMENT_PRIMARY_GAME_MODULE或IMPLEMENT_GAME_MODULE來定義Module時,並沒有真的把這個Module真的設成GameModule,反而是我們必須要去override IsGameModule這個功能並回傳true之後,才能真正的讓引擎有所認知。
這邊的流程可以明顯的看出一些缺陷,或許在引擎以後的版本會將這部份進行優化,但目前我們能做的,就是盡可能的照著它的流程來進行,以減少往後升級引擎版本的困擾。
或許有人會開始思考,實際上當引擎知道這個Module是屬於GameModule時會做什麼事?試著在引擎的源始碼中搜尋IsGameModule這個關鍵字之後,會發現,其大部份都是在處理C++ HotReload相關的邏輯。
在實作完Module Interface之後,接著來便是讓我們的Target知道在編譯的時候,需要將新增加的Module編譯進到最後的執行檔中。
HorizonTutorialGameEditor.Target.cs |
using UnrealBuildTool;
using System.Collections.Generic; public class HorizonTutorialGameEditorTarget : TargetRules { public HorizonTutorialGameEditorTarget(TargetInfo Target) : base(Target) { Type = TargetType.Editor; ExtraModuleNames.AddRange( new string[] { “HorizonTutorialGame” } ); ExtraModuleNames.Add(“HorizonTutorialGameEditor”); } }
|
Code 2.5.6當TargetType為Editor時,直接將該Module加入編譯。
HorizonTutorialGame.Target.cs |
using UnrealBuildTool;
using System.Collections.Generic; public class HorizonTutorialGameTarget : TargetRules { public HorizonTutorialGameTarget(TargetInfo Target) : base(Target) { Type = TargetType.Game; ExtraModuleNames.AddRange( new string[] { “HorizonTutorialGame” } ); if (UEBuildConfiguration.bBuildEditor) { ExtraModuleNames.Add(“HorizonTutorialGameEditor”); } } }
|
Code 2.5.7 當TargetType為Game時,只有帶有Editor時才將該Module加入編譯(執行GenerateProject指令時會觸發這個特殊條件)。
在將Module編譯進Target之後,我們還必須要告訴引擎,Runtime時要什麼時機點才將該Module載入,見Code 2.5.8。這邊要注意的是,若我們沒有在Target中加入需要的Module,則會跳出編譯失敗的錯誤訊息。
HorizonTutorialGame.uproject |
{
“FileVersion”: 3, “EngineAssociation”: “4.16”, “Category”: “”, “Description”: “”, “Modules”: [ { “Name”: “HorizonTutorialGame”, “Type”: “Runtime”, “LoadingPhase”: “Default” }, { “Name”: “HorizonTutorialGameEditor”, “Type”: “Editor”, “LoadingPhase”: “PostEngineInit” } ] } |
Code 2.5.8 在uproject中指定要載入的Host Type為Editor,時機點在PostEngineInit。
從Code 2.5.8中我們可以看出,uproject整個檔案是用json格式來表示,在Module中的Name表示要載入的Game Module名稱、Type為Module所載入的環境、LoadingPhase則是載入的時機點。下面的表格將所有的可用的參數做了整理:
EHostType | |
Runtime | Runtime代表遊戲中任何時機點都可以使用,在shipping或editor中都可以存取到該Module中的類別。 |
RuntimeNoCommandlet | 基本上同Runtime,但只有在build沒有加入Commandlet時才能存取。 |
RuntimeAndProgram | Runtime跟Program都能夠存取 |
CookedOnly | 只有在做Cook時才能存取。 |
Developer | 只有在Development這個config時才會將Module載入(editor跟runtime),Shipping時就無法存取。 |
Editor | 只有在Editor中能夠存取。 |
EditorNoCommandlet | 基本上同Editor,但只有在build沒有加入Commandlet時才能存取。 |
Program | 只有Program能夠存取。 |
ServerOnly | 只有Server Build才能存取。 |
ClientOnly | 只有Client Build才能存取。 |
Table 2.5.1 Module在載入時,所有可用的目標環境,用來指定該Module適合哪些類型的應用使用。
Table 2.5.2 Module在載入時,所有可用的時機點。
最後,讓我們將上面的步驟做個總整理:
-
建立{MODULE_NAME}.build.cs。
-
繼承IModuleInterface並依需求對裡面的function進行override實作。
-
呼叫IMPLEMENT_GAME_MODULE,並將把在第二步實作的類別當作參數傳入。
-
在Target.cs中將該新增的Module加入編譯
-
在${PROJECT_NAME} .uproject中決定該Module所要載入的環境與時機點。
[置頂] Unreal Engine 4哲學與實務:從Blueprint到C++ - 地平線上的多貢
[…] [UE4]為專案增加一個Game Module […]