当专案建立的时候,引擎会自动产生一个同名的Game Module在Source资料夹底下。我们当然可以将所有撰写的C++类别全部放在这个Module中,可是当专案越来越大,若还是将所有的功能都放在同个Module下,不仅仅会造成管理上的混乱,而且编译时间也会增加。
试着想像当我们随便改动一个h档或cpp档的参数就要编译几10分钟的情况?由于UE4在一个Module中的cpp数量到达32个的时候就会自动启动编译档案合并的机制(Unity Build),虽然UE4中有参数可以控制这个机制是否启用,但却是舍弃掉了整体的编译时间。因此,如何将功能适当的切到各别Module中,就变成了一件非常重要的程式架构设计问题。
那么实际上UE4的Module系统是怎么运作的呢?
每一个独立的Module基本上是由${MODULE_NAME}.build.cs与其下的C++程式码所组成,而在UE4中,游戏中的Module主要被分成以下几种实作Macro定义:
- IMPLEMENT_PRIMARY_GAME_MODULE
- IMPLEMENT_GAME_MODULE
- IMPLEMENT_MODULE
到底这些实作彼此之间有什么不同?
其实,这些Macro定义在最后都会被展开成IMPLEMENT_MODULE中的实作,它们之间在本质上并没有不同,主要是用提供Module初始化Function的实作给UE4中的ModuleManager呼叫,参照Code 2.4.1:
#define IMPLEMENT_PRIMARY_GAME_MODULE ( ModuleImplClass, ModuleName , GameName ) \ IMPLEMENT_GAME_MODULE ( ModuleImplClass, ModuleName ) #define IMPLEMENT_GAME_MODULE ( ModuleImplClass, ModuleName ) \ IMPLEMENT_MODULE ( ModuleImplClass, ModuleName ) #define IMPLEMENT_MODULE ( ModuleImplClass, ModuleName ) \ extern “C" DLLEXPORT IModuleInterface * InitializeModule () \ { \ return new ModuleImplClass(); \ } \ PER_MODULE_BOILERPLATE \ PER_MODULE_BOILERPLATE_ANYLINK (ModuleImplClass, ModuleName )
Code 2.4.1 Module的定义Macro,最后都会被展开成IMPLEMENT_MODULE中的实作。其中PER_MODULE_BOILERPLATE让引擎有机会提供一些额外的功能给这个module,例如把原本C++中的New跟delete做Overriding,并将功能导向UE4中的记忆体管理机制。最后的PER_MODULE_BOILERPLATE_ANYLINK其实并没有其他特别的功能,就只是用来做标示,用来说明这个Module相关的定义已经完成。
从上面的程式码中我们可以看出,从Primary Game Module、Game Module到Module总共有三个层级的定义。只是,既然这些Module的定义做的事情都差不多,那为何引擎还要个别提供?
这边虽然没办法完全的掌握设计者的意图,但至少我们可以推敲出区分出这些Module定义名称的好处:若以后想要在Primary Game Module或Game Module层级增加功能的时候,就不会影响到最下层的Module层级的实作。
到这里,或许有人会开始思考这几个层级到底应该用在哪些地方。其实UE4在架构上把程式码拆分成以下几个部份:
- Engine:位于${UE4_ENGINE_ROOT}/Engine/Source/下面的Developer、Editor、Runtime以及ThirdParty这几个资料夹。
- Engine Plugin:位于${UE4_ENGINE_ROOT}/Engine/Plugin/。
- Game:位于${PROJECT_NAME}/Source/。
- Game Plugin:位于${PROJECT_NAME}/Plugin/。
- Programs:位于${UE4_ENGINE_ROOT}/Engine/Source/Programs,属于独立运行的工具类程式,里面使用C#或C++分别进行不同的实作。
其中只有位于Game中的Module会使用IMPLEMENT_PRIMARY_GAME_MODULE跟IMPLEMENT_GAME_MODULE这2个实作,从名称中我们可以推测出,作为主要游戏的入口的Primary Game Module只能有一个,其他则都必需设成Game Module。
其他在Engine、Engine Plugin跟Game Plugin底下的Module则都是直接使用IMPLEMENT_MODULE这个定义。
当然,就目前的引擎版本我们是可以全部都用IMPLEMENT_MODULE来实作出上面Module所有效果,但是为了往后更新引擎版本时的兼容性,建议还是照着UE4所定义出来的流程实作。
最后的Programs跟其他的Module不同,里面的C++ Module会使用IMPLEMENT_APPLICATION这个定义,参见Code 2.4.2。
#if IS_MONOLITHIC #define IMPLEMENT_APPLICATION ( ModuleName , GameName ) \ /* For monolithic builds, we must statically define the game's name string (See Core.h) */ \ TCHAR GInternalGameName [64] = TEXT ( GameName ); \ IMPLEMENT_DEBUGGAME () \ IMPLEMENT_FOREIGN_ENGINE_DIR () \ IMPLEMENT_GAME_MODULE ( FDefaultGameModuleImpl , ModuleName ) \ PER_MODULE_BOILERPLATE \ FEngineLoop GEngineLoop ; #else #define IMPLEMENT_APPLICATION ( ModuleName , GameName ) \ /* For non-monolithic programs, we must set the game's name string before main starts (See Core.h) */ \ struct FAutoSet##ModuleName \ { \ FAutoSet##ModuleName() \ { \ FCString :: Strncpy ( GInternalGameName , TEXT ( GameName ), ARRAY_COUNT ( GInternalGameName )); \ } \ } AutoSet##ModuleName; \ PER_MODULE_BOILERPLATE \ PER_MODULE_BOILERPLATE_ANYLINK ( FDefaultGameModuleImpl , ModuleName ) \ FEngineLoop GEngineLoop ; #endif
Code 2.4.2 在UE4中,Module的Link分成Modular跟Monolithic二种方式:Modular指的是将Module编译成各别的dynamic library再做连结;Monolithic则是指将所有的Module全部编译到同一份Library中。其中Editor预设是Modular,而Game预设是Monolithic。
而要不要把该Module放到Plugin或者是当成Game Module,则要看这个Module需不需要用在不同的专案上使用。若是这个Module中的功能需要让其他专案使用的话,则将该Module制作成Plugin会比单纯的当成Game Module会更有弹性。