zifa2003293 发表于 2023-2-6 21:38

【UE5】使用Slate进行UI开发与编辑器拓展(五): 模块

这一篇我们会详细的介绍模块,如果创建自己的模块,解释模块的各个组成部分
一.什么是模块

省流:就是类似于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();
};
如果想让一个函数暴露给其他的模块,可以在前面添加_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();
};
而通过在类前添加_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;
};
省流:

[*]记得在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("FooBar")).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(如果是项目)中声明,其中包含了加载时间,模块的目的以及目标平台。
"Modules": [
        {
                "Name": "FooBar",
                "Type": "Runtime",
        }
]
包含加载时间与支持平台的:
"Modules": [
        {
                "Name": "FooBar",
                "Type": "Runtime",
                "LoadingPhase": "Default",
                “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 = "FooBarPrivatePCH.h";

[*]永远不要手动把他加到其他的.h和.cpp文件中,UBT会自动完成
[*]不要觉得他像OIer常用的万能头文件一样可以随便用,它只是一个优化手段,不万能。
对于Shared PCH:

[*]客户端仔可能基本不会用,只有Engine Module可以使用Shared PCH。
[*]Shared PCH只会编译一次。
在实际使用中,你只需要在那些超大型代码模块中使用Private PCH,比如主游戏模块
太太,你也不想编译一整天吧。
UsageMode

在Build.cs中,有一个PCHUsageMode可以选:

[*]Default
[*]NoSharedPCHs
[*]UseSharedPCHs
[*]UseExplicitOrSharedPCHs
[*]NoPCHs
但是,前三个废弃了,所以要么选第四个,要么就是不用PCH。好好好。
页: [1]
查看完整版本: 【UE5】使用Slate进行UI开发与编辑器拓展(五): 模块