ue4的代碼是模塊的形式來組織
在源碼層面,一個包含*.build.cs的目錄就是一個模塊
這個目錄里的文件在編譯后都會被鏈接在一起,比如一個靜態庫lib,或者一個動態庫dll。
不管是哪種形式,都需要提供一個給外部操作的接口,也就是一個IModuleInterface指針。
*注意這里並不是說調用模塊內任何函數(或類)都要通過該指針來進行,實際上外部代碼只要include了相應的頭文件,就能直接調用對應的功能了(比如new一個類,調一個全局函數等),因為實現代碼要么做為lib被鏈接進exe,或是做為dll被動態加載了。
這個IModuleInterface指針是用來操作做為整體的模塊本身的,比如說模塊的加載、初始化和卸載,以及訪問模塊內的一些全局變量(向下轉成具體的模塊類型后)

外部獲取這個指針,只有一個辦法,就是通過FModuleManager上的LoadModule/GetModule。
而FModuleManager去哪里找需要的模塊呢?
1、在動態鏈接情況下,根據名字直接找對應的dll就行了,做為合法ue4模塊的dll,必定要導出一些約定的函數,來返回自身IModuleInterface指針。
2、在靜態鏈接時,去一個叫StaticallyLinkedModuleInitializers的Map里找,這就要求所有模塊已把自己注冊到這個Map里。
以上2點基本就是FModuleManager::LoadModule的內容。
而每個模塊為滿足以上約定,也需要插入一些例程代碼,這就是IMPLEMENT_MODULE干的事。
在靜態鏈接時:
// If we're linking monolithically we assume all modules are linked in with the main binary. #define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \ /** Global registrant object for this module when linked statically */ \ static FStaticallyLinkedModuleRegistrant< ModuleImplClass > ModuleRegistrant##ModuleName( #ModuleName ); \ /** Implement an empty function so that if this module is built as a statically linked lib, */ \ /** static initialization for this lib can be forced by referencing this symbol */ \ void EmptyLinkFunctionForStaticInitialization##ModuleName(){} \ PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)
template< class ModuleClass > class FStaticallyLinkedModuleRegistrant { public: FStaticallyLinkedModuleRegistrant( const ANSICHAR* InModuleName ) { // Create a delegate to our InitializeModule method FModuleManager::FInitializeStaticallyLinkedModule InitializerDelegate = FModuleManager::FInitializeStaticallyLinkedModule::CreateRaw( this, &FStaticallyLinkedModuleRegistrant<ModuleClass>::InitializeModule ); // Register this module FModuleManager::Get().RegisterStaticallyLinkedModule( FName( InModuleName ), // Module name InitializerDelegate ); // Initializer delegate } IModuleInterface* InitializeModule( ) { return new ModuleClass(); } };
FStaticallyLinkedModuleRegistrant是一個注冊輔助類,它利用全局變量的構造函數被crt自動調用的特性,實現自動注冊邏輯。
它在構造函數里把一個“注冊器”添到前面說的StaticallyLinkedModuleInitializers里,而這個注冊器的內容就是創建並返回相應模塊。
在動態鏈接時:
#define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \ \ /**/ \ /* InitializeModule function, called by module manager after this module's DLL has been loaded */ \ /**/ \ /* @return Returns an instance of this module */ \ /**/ \ extern "C" DLLEXPORT IModuleInterface* InitializeModule() \ { \ return new ModuleImplClass(); \ } \ PER_MODULE_BOILERPLATE \ PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)
聲明了一個dllexport函數,功能就是創建並返回相應模塊。
實際的應用:
1、如果模塊本身實在沒什么特別的,那么就:
IMPLEMENT_MODULE(FDefaultModuleImpl, ModuleName)
FDefaultModuleImpl是一個IModuleInterface的空實現,什么都沒干,ModuleName則是模塊名字,很重要,其它地方要加載模塊,就靠這個名字了。
2、如果模塊有自己的初始化邏輯,那么應該實現自己的IModuleInterface子類,比如說MyUtilModule,然后:
IMPLEMENT_MODULE(MyUtilModule,MyUtil)
這里類名帶Module后綴,而模塊名不帶,是ue4的慣例。
3、如果模塊是一個包含游戲邏輯的模塊(相對於通用功能型模塊),那么可以用一個特殊的宏IMPLEMENT_GAME_MODULE,不過目前看來它和前者沒啥差別,可能未來有所擴展
4、更特別的是,如果模塊是表示當前游戲項目的“主模塊”,那么應該用一個更特殊的宏IMPLEMENT_PRIMARY_GAME_MODULE,而且在構建一個UEBuildGame類型的Target時,必須有一個這樣的模塊。
當IS_MONOLITHIC,構建成單個exe時:
#define IMPLEMENT_PRIMARY_GAME_MODULE( ModuleImplClass, ModuleName, DEPRECATED_GameName ) \ /* For monolithic builds, we must statically define the game's name string (See Core.h) */ \ TCHAR GInternalGameName[64] = TEXT( PREPROCESSOR_TO_STRING(UE_PROJECT_NAME) ); \ /* Implement the GIsGameAgnosticExe variable (See Core.h). */ \ bool GIsGameAgnosticExe = false; \ IMPLEMENT_DEBUGGAME() \ IMPLEMENT_FOREIGN_ENGINE_DIR() \ IMPLEMENT_GAME_MODULE( ModuleImplClass, ModuleName ) \ PER_MODULE_BOILERPLATE \ void UELinkerFixupCheat() \ { \ extern void UELinkerFixups(); \ UELinkerFixups(); \ }
非單體構建時:
#define IMPLEMENT_PRIMARY_GAME_MODULE( ModuleImplClass, ModuleName, GameName ) \ /* Nothing special to do for modular builds. The game name will be set via the command-line */ \ IMPLEMENT_GAME_MODULE( ModuleImplClass, ModuleName )
統一來看,其實也就比普通模塊多做了三件事,一是設置游戲名字GInternalGameName,二是設置了GIsGameAgnosticExe=false,三是多了個UELinkerFixupCheat暫且不究。
GIsGameAgnosticExe是一個有趣的變量,它表示當前這個exe是特定於某游戲的?還是一個通用的加載器?
如果整體編成一個exe文件,即IS_MONOLITHIC,那肯定是特定於某游戲,這時游戲名GInternalGameName必定是已知固定的,所以以宏參數的形式直接硬編碼在exe里了。
與之相反,當使用模塊化構建時(通過給ubt傳入-modular參數),將會生成一個exe和一堆dll,並且能加載任何其它以dll形式存的游戲模塊,游戲名是未知的,是通過命令行參數來決定要加載哪一個游戲,所以也就用不着GInternalGameName變量了。
5、當構建一個工具程序(TargetType.Program)時,可以使用專門定制的IMPLEMENT_APPLICATION:
最特別的是它竟然提供了一個FEngineLoop GEngineLoop,而這個變量存在於一般的Game/Editor構建時都會鏈接的【Launch模塊】中。
不過這也正常,因為在工具程序中一般是要自己寫main函數的,所以就用不着重量級的Launch模塊了。
