|
这一篇我们会详细的介绍模块,如果创建自己的模块,解释模块的各个组成部分
一. 什么是模块
省流:就是类似于C++的动态加载库(DLL),是一些方法和类的集合,UE本身就是由1000多个模块组成的。
使用模块有许多好处:
- 更好的代码封装
- 更好的代码重用
- 发布时只需要发布需要的模块
- 更快的编译与链接速度
- 更容易控制加载时间
二. 创建一个模块
视频中的大佬把模块创建分为B.U.I.L.D.,即Build Use Implement Load Depend,在这里,我们可以直接跑到Rider,用Rider的模板先创建一个出来。
Build
我们会得到一个Build.cs文件,它实现了:
在Unreal中,项目是由你的Target.cs文件与Build.cs文件描述构建的,而不是Solution文件,UBT(Unreal Build Tool)会忽略这些Solution文件。
就像我在第一篇中讲述如果构建一个Slate学习空项目一样,UE构建一个项目,做法是:运行GenerateProjectFile.bat,然后对uproject右键生成sln。
一个最简单的Build.cs,可以只含有如下内容:
public class Foobar : ModuleRules
{
public Foobar(ReadOnlyTargetRules Target) : base(Target)
{
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Core"
}
);
}
}
Core模块是必须引入的,因为FModuleManager在这里,Core是一个模块依赖的最小集合。
Use
向其中添加一个.h与.cpp文件,注意这里的Public和Private文件夹,我们一会儿会解释这些文件夹。
- 代码中所有的函数和类,默认都不会暴露给其他模块,如果需要暴露出来,你需要将其标记
- 并不是所有的.h文件都在Public,所有的.cpp文件都在Private,如果你的.h文件不希望被其他模块使用,那就可以将其放进Public文件夹
- 如果你的模块不需要暴露给其他模块,那就不需要Public,Private文件夹
#pragma once
#include "GameFramework/Actor.h"
#include "CoreMinimal.h"
#include "NicknamedActor.generated.h"
UCLASS(Blueprintable)
class ANicknamedActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString Nickname;
UFUNCTION(BlueprintCallable)
void SayNickname();
};
这里的UCLASS(),UPROPERTY(),UFUNCTION()是给UHT(Unreal Head Tool)看的,他会注册反射,暴露给引擎和编辑器,但是,其他模块仍然无法使用这些。
在这里我不再解释UBT与UHT,想要了解的朋友们可以看:
我们可以给UCLASS中加入一个属性说明符Minimal,这可以实现:
- 允许其他模块继承自这个类
- 允许其他模块使用这个类的内联函数
- 允许其他模块类型转换到这个类
#pragma once
#include "GameFramework/Actor.h"
#include "CoreMinimal.h"
#include "NicknamedActor.generated.h"
UCLASS(Blueprintable, MinimalAPI)
class ANicknamedActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString Nickname;
UFUNCTION(BlueprintCallable)
void SayNickname();
};
如果想让一个函数暴露给其他的模块,可以在前面添加[YourModuleName]_API
#pragma once
#include "GameFramework/Actor.h"
#include "CoreMinimal.h"
#include "NicknamedActor.generated.h"
UCLASS(Blueprintable, MinimalAPI)
class ANicknamedActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString Nickname;
UFUNCTION(BlueprintCallable)
FOOBAR_API void SayNickname();
};
而通过在类前添加[YourModuleName]_API,可以将整个类都暴露给其他模块
#pragma once
#include "GameFramework/Actor.h"
#include "CoreMinimal.h"
#include "NicknamedActor.generated.h"
UCLASS(Blueprintable)
class FOOBAR_API ANicknamedActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString Nickname;
UFUNCTION(BlueprintCallable)
void SayNickname();
};
接下来,我们聊一聊模块之间的依赖。
一般的,模块之间的依赖有四种:
PublicDependencyModuleNames.Add();
PrivateDependencyModuleNames.Add();
PublicIncludePathModuleNames.Add();
PrivateIncludePathModuleNames.Add();这一方面,
讲的非常详细,方便起见我也将其贴过来:
二者都能使本模块可以包含其它模块Public目录下的头文件,从而调用函数.
但IncludePathModuleNames只能调用定义全部在头文件里的函数. 因为在编译时会将包含的头文件的内容都拷贝一份到.cpp里,所以定义全部在头文件里的函数能正常工作.
而这带来的问题是:同一个函数,被编译多次(根据它被包含的次数),在大型项目里将拖慢编译速度.
所以我们为了让函数只编译一次,才会把函数定义放在cpp文件里.
使用DependencyModuleNames时,会进行对两个模块进行链接.故不存在此问题. IncludePathModuleNames是过时的用法,现在一般只用DependencyModuleNames,下文统一按照DependencyModuleNames来处理。
然后我们在讨论Public和Private,简单来说,Public与Private描述的是依赖在多个模块间的传递性问题。
如果只存在两个模块,则Public与Private毫无意义。
在只有两个模块的时候,无论选择Public还是Private,依赖模块(Your Module)都将包含被依赖模块(Child Module)的Public文件夹中的头文件,并链接Child Module中的暴露出来的函数,变量等
但是,如果再一个三个模块的依赖关系中,如果存在Private与Public混用,情况就会变得复杂,此时情况如下:
如果Parent Module依赖你的模块(无论Public还是Private),但你的模块Private依赖Child Module,那么Parent Module不会含有Child Module的头文件,也不会链接里面的Cpp符号。
值得一提的是,就算你都是Public依赖,Cpp符号也不会被链接。
省流:
- 在三个模块的传递中,Private会隐藏信息,导致本模块的依赖的模块的头文件不会传递给依赖本模块的模块。
- 在三个模块的传递中,Public会传递头文件依赖
- 链接永不传递。
一些小Tips:
- 多用PrivateDependency,首先不容易混淆,其次,他可以减少编译时间。
- 方便的话,就用Forward Declaration
- 出现下面的Error,大概率就是依赖有问题
Error!
SomeActor.cpp.obj : error LNK2019: unresolved external symbol
"public: void __cdecl ANicknamedActor::SayNickname(void)"
(?SayNickname@ANicknamedActor@@QEAAXXZ)
referenced in function
"public: void __cdecl ASomeActor::Test(void)"
(?Test@ASomeActor@@QEAAXXZ)Implement
#include "Modules/ModuleManager.h"
IMPLEMENT_MODULE(FDefaultModuleImpl, FooBar)
class FFooBarModule : public IModuleInterface
{
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
省流:
- 记得在[YouModuleName]Module.cpp调用IMPLEMENT_MODULE,但是,理论上讲,他其实可以出现任何.cpp文件里,这只是一种约定俗成,或者说规范/方便。
- 把他暴露给你的模块的其他类
我们这里引用了ModuleManager,他在Core模块中,再次解释为什么模块依赖的最小集是Core模块。
我们的实现类应该继承自IModuleInterface,通常情况下,实现StartupModule与ShutdownModule两个方法。
- 其实,你也不一定在IMPLEMENT_MODULE中使用FDefaultModuleImpl,你可以用自己的类。
- 模块实现类的生命周期与模块本身保持一致。
- 可以使用IMPLEMENT_GAME_MODULE来声明一个游戏模块。IMPLEMENT_PRIMARY_GAME_MODULE声明主游戏模块(可以开一个UE空游戏项目EmptyProgram,看看Program.h与Program.cpp)
- 可以通过如下方法获得类实例,使用类方法:
FModuleManager::Get().LoadModuleChecked<FFooBarModule>(TEXT(&#34;FooBar&#34;)).DoFoo();
我们再聊一下模块的加载顺序,简单来说:他是栈式的,很容易理解,如果模块A加载了模块B,那么销毁的时候就先销毁B后销毁A。
接下来,我们谈一下游戏模块
通常来说,游戏模块只有一个,即Primary Game Module,原因其实也很简单:原封不动的把Gameplay模块移到另一个项目里去,这种情况基本不存在。但如果你一定要使用Gameplay Module,那么使用这个原则:只有需要依赖别的Gameplay Module时,他才需要是Gameplay Module。
当你需要指定一个模块是Gameplay Module时:
- 令virtual boolIsGameModule() const 返回true。
- 使用IMPLEMENT_GAME_MODULE代替IMPLEMENT_MODULE
Load
你的模块需要在uplugin(如果是插件)或者uproject(如果是项目)中声明,其中包含了加载时间,模块的目的以及目标平台。
&#34;Modules&#34;: [
{
&#34;Name&#34;: &#34;FooBar&#34;,
&#34;Type&#34;: &#34;Runtime&#34;,
}
]
包含加载时间与支持平台的:
&#34;Modules&#34;: [
{
&#34;Name&#34;: &#34;FooBar&#34;,
&#34;Type&#34;: &#34;Runtime&#34;,
&#34;LoadingPhase&#34;: &#34;Default&#34;,
“WhitelistPlatforms”: [“Win64”]
}
]
对于这些枚举的值,可以参见:
Load
省流:只有被加入依赖链的模块会被编译。
如何将模块加入依赖链:
- 放到其他模块的Dependency中
- 如果实在没有别的模块使用它,你可以在.target.cs的ExtraModuleNames中添加他。
三. PCH
PCH简述
PCH即Precompiled head,PCH基于这样的思想:头文件不是自己编译的,他们会被放到.cpp文件中编译,那么如果多个.cpp文件包含同一个.h文件,就容易造成冗余编译。那么我们干脆先把一部分常用的.h文件编译好,避免多次重复编译。
所以PCH:
- 要求定义一个头文件,包含了绝大多数你的常用头文件。
- 比其他文件编译的更早。
- 除非里面的头文件发生改变,不会再编译。
- 如果重新编译了,那这模块里所有的.cpp文件都会要求重新编译。所以一般用于引擎文件或者那些几乎不会发生变化的文件。
PCH有Private PCH与Shared PCH之分,简单来说,Private PCH是给自己的模块看的,Shared PCH给依赖本模块的其他模块看。
对于Private PCH:
- 需要你自己在模块中创建
- 需要你在.Build.cs文件中指定:
PrivatePCHHeaderFile = &#34;FooBarPrivatePCH.h&#34;;
- 永远不要手动把他加到其他的.h和.cpp文件中,UBT会自动完成
- 不要觉得他像OIer常用的万能头文件一样可以随便用,它只是一个优化手段,不万能。
对于Shared PCH:
- 客户端仔可能基本不会用,只有Engine Module可以使用Shared PCH。
- Shared PCH只会编译一次。
在实际使用中,你只需要在那些超大型代码模块中使用Private PCH,比如主游戏模块
太太,你也不想编译一整天吧。
UsageMode
在Build.cs中,有一个PCHUsageMode可以选:
- Default
- NoSharedPCHs
- UseSharedPCHs
- UseExplicitOrSharedPCHs
- NoPCHs
但是,前三个废弃了,所以要么选第四个,要么就是不用PCH。好好好。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|