当专案建立的时候,引擎会自动产生一个同名的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会更有弹性。
- 在UE项目添加一个新的Module
本文只简单介绍下创建自定义模块的方法
1. 新建项目并在Source文件夹下建立我们要添加的新模块
2. 并在新建的NewModule文件夹下创建.h .cpp .Build.cs 后缀的3个文件
3. 进入VS建立NewModule(筛选器)并把刚才文件夹里新创建的3个文件拖入NewModule下
4.为NewModule.Build.cs添加内容(直接从我们项目里的TestModule.Build.cs拷贝即可)
using UnrealBuildTool;
public class NewModule: ModuleRules
{
public NewModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
PrivateDependencyModuleNames.AddRange(new string[] { });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
5. 修改NewModule.h为
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "Engine.h"
#include "ModuleManager.h"
class FNewModuleModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
6.修改NewModule.cpp为
// Copyright Sigurdur Gunnarsson. All Rights Reserved.
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
#include "NewModule.h"
void FNewModuleModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
}
void FNewModuleModule::ShutdownModule()
{
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
// we call this function before unloading the module.
}
IMPLEMENT_MODULE(FNewModuleModule, NewModule)
6. 进入项目的uproject内添加我们的新模块
7. 最后在项目里的.Target.cs里添加新的模块名
2个.Target.cs后缀的都需要添加模块名
8. 使用Editor在NewModule下创建Actor,重新编译成功后,在C++ Class下就能看到新建的NewModule