【UE4】C++编程

一、工程目录

  • .vs:VS的缓存文件夹;
  • Binaries:存放UE的工程的可执行文件,以及编译的中间文件;
  • Biuld:存放一些编辑器相关的日志;
  • Config:存放游戏的默认配置文件;
  • Content:存放项目资产;
  • DerivedDataCache:主要存放DivX Descriptor File文件,应该是UE为制作影视视频准备的;
  • Plugins:存放项目中使用的插件;
  • Intermediate:存放UBT生成的文件,如:.generated.h文件;
  • Saved:缓存一些临时配置文件,在PIE模式下运行项目产生的日志以及Cook产生的数据文件;
  • Script:用来存放脚本语言,如python脚本等;
  • Source:存放项目的C++文件。

.uproject

右键.uproject会出现几个选项:

  • Open:使用默认的UE编辑器打开.uproject文件;
  • Launch game:以打包后的exe的形式直接运行项目;
  • Generate Visual Studio project files:生成VS相关文件;
  • Switch Unreal Engine version:选择默认的UE版本用以打开.uproject文件。

.uproject是UE的项目描述文件,采用json的格式来描述一个项目的版本信息、模块信息、插件信息等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"FileVersion": 3,
"EngineAssociation": "4.26",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "MyProject",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine"
]
}
],
"Plugins": [
{
"Name": "WebBrowserWidget",
"Enabled": true
}
]
}

.uproject参数描述文件—ProjectDescriptor.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
struct PROJECTS_API FProjectDescriptor
{
/** Descriptor version number. */
EProjectDescriptorVersion::Type FileVersion;
/**
* The engine to open this project with. Set this value using IDesktopPlatform::SetEngineIdentifierForProject to ensure that
* the most portable value for this field is used.
* This field allows us to open the right version of the engine when you double-click on a .uproject file, and to detect when you
* open a project with a different version of the editor and need the upgrade/downgrade UI flow. The normal engine
* version doesn't work for those purposes, because you can have multiple 4.x branches in various states on one machine.
* For Launcher users, this field gets set to something stable like "4.7" or "4.8", so you can swap projects and game binaries
* between users, and it'll automatically work on any platform or machine regardless of where the engine is installed. You
* can only have one binary release of each major engine version installed at once.
* For Perforce or Git users that branch the engine along with their games, this field is left blank. You can sync the repository
* down on any platform and machine, and it can figure out which engine a project should use by looking up the directory
* hierarchy until it finds one.
* For other cases, where you have a source build of the engine but are working with a foreign project, we use a random identifier
* for each local engine installation and use the registry to map it back to the engine directory. All bets are off as to which
* engine you should use to open it on a different machine, and using a random GUID ensures that every new machine triggers the
* engine selection UI when you open or attempt to generate project files for it.
* For users which mount the engine through a Git submodule (where the engine is in a subdirectory of the project), this field
* can be manually edited to be a relative path.
* @see IDesktopPlatform::GetEngineIdentifierForProject
* @see IDesktopPlatform::SetEngineIdentifierForProject
* @see IDesktopPlatform::GetEngineRootDirFromIdentifier
* @see IDesktopPlatform::GetEngineIdentifierFromRootDir
*/
FString EngineAssociation;
/** Category to show under the project browser */
FString Category;
/** Description to show in the project browser */
FString Description;
/** List of all modules associated with this project */
TArray<FModuleDescriptor> Modules;
/** List of plugins for this project (may be enabled/disabled) */
TArray<FPluginReferenceDescriptor> Plugins;
/** Array of platforms that this project is targeting */
TArray<FName> TargetPlatforms;
/** A hash that is used to determine if the project was forked from a sample */
uint32 EpicSampleNameHash;
/** Custom steps to execute before building targets in this project */
FCustomBuildSteps PreBuildSteps;
/** Custom steps to execute after building targets in this project */
FCustomBuildSteps PostBuildSteps;
/** Indicates if this project is an Enterprise project */
bool bIsEnterpriseProject;
/** Indicates that enabled by default engine plugins should not be enabled unless explicitly enabled by the project or target files. */
bool bDisableEnginePluginsByDefault;
...
}
  • FileVersion:描述项目版本;

  • EngineAssociation:引擎版本;

  • Category:这个分类着实是不知道有什么卵用;

  • Description:项目描述;

  • Modules:模块信息,是一个FModuleDescriptor类型的数组,数组中一个元素代表一个模块,FModuleDescriptor的具体描述在ModuleDescriptor.h

    里面涉及很多模块的描述,最常使用的基本只有三个:

    Name:模块名称;

    Type:模块的使用类型,描述模块在什么时候能够使用;

    Type是一个EHostType::Type类型,描述文件也在ModuleDescriptor.h中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    namespace EHostType
    {
    enum Type
    {
    // Loads on all targets, except programs.
    Runtime,
    // Loads on all targets, except programs and the editor running commandlets.
    RuntimeNoCommandlet,
    // Loads on all targets, including supported programs.
    RuntimeAndProgram,
    // Loads only in cooked games.
    CookedOnly,
    // Only loads in uncooked games.
    UncookedOnly,
    // Deprecated due to ambiguities. Only loads in editor and program targets, but loads in any editor mode (eg. -game, -server).
    // Use UncookedOnly for the same behavior (eg. for editor blueprint nodes needed in uncooked games), or DeveloperTool for modules
    // that can also be loaded in cooked games but should not be shipped (eg. debugging utilities).
    Developer,
    // Loads on any targets where bBuildDeveloperTools is enabled.
    DeveloperTool,
    // Loads only when the editor is starting up.
    Editor,
    // Loads only when the editor is starting up, but not in commandlet mode.
    EditorNoCommandlet,
    // Loads only on editor and program targets
    EditorAndProgram,
    // Only loads on program targets.
    Program,
    // Loads on all targets except dedicated clients.
    ServerOnly,
    // Loads on all targets except dedicated servers.
    ClientOnly,
    // Loads in editor and client but not in commandlets.
    ClientOnlyNoCommandlet,
    //~ NOTE: If you add a new value, make sure to update the ToString() method below!
    Max
    };

    LoadingPhase:模块的加载策略,控制模块的加载时机;

    LoadingPhase是一个ELoadingPhase::Type类型,描述文件也在ModuleDescriptor.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    namespace ELoadingPhase
    {
    enum Type
    {
    /** As soon as possible - in other words, uplugin files are loadable from a pak file (as well as right after PlatformFile is set up in case pak files aren't used) Used for plugins needed to read files (compression formats, etc) */
    EarliestPossible,
    /** Loaded before the engine is fully initialized, immediately after the config system has been initialized. Necessary only for very low-level hooks */
    PostConfigInit,
    /** The first screen to be rendered after system splash screen */
    PostSplashScreen,
    /** Loaded before coreUObject for setting up manual loading screens, used for our chunk patching system */
    PreEarlyLoadingScreen,
    /** Loaded before the engine is fully initialized for modules that need to hook into the loading screen before it triggers */
    PreLoadingScreen,
    /** Right before the default phase */
    PreDefault,
    /** Loaded at the default loading point during startup (during engine init, after game modules are loaded.) */
    Default,
    /** Right after the default phase */
    PostDefault,
    /** After the engine has been initialized */
    PostEngineInit,
    /** Do not automatically load this module */
    None,
    // NOTE: If you add a new value, make sure to update the ToString() method below!
    Max
    };
  • Plugins:插件信息,是一个FPluginReferenceDescriptor数组,数组中一个元素代表一个插件,描述文件在PluginDescriptor.h和PluginReferenceDescriptor.h文件中

    其中也包含大量的描述参数,最常用的基本也只有Name:插件名称,Enabled:是否启用插件。

  • TargetPlatforms:描述项目的目标平台;

  • EpicSampleNameHash:也没有研究出是干什么用的;

  • PreBuildSteps和PostBuildSteps:根据源码描述是用来在当前项目构建的前后执行一些自定义操作用的,具体怎么使用着实是找不到案例,查看了FCustomBuildSteps是一个很简单的结构体,里面只有一个叫HostPlatformToCommands的与命令行相关的TMap,和几个简单函数。

  • bIsEnterpriseProject:描述当前项目是否为企业项目;

  • bDisableEnginePluginsByDefault:是否启用引擎默认启用的插件,这是在FProjectDescriptor结构体中的变量,所以默认值为0。

二、类

1.UE4中的预定义类

UE的预定义类,祥见GamePlay架构

2.C++类的创建

使用Unreal Editor创建C++类

当我们创建一个C++编程模板时,在内容浏览器中会生成一个C++类文件夹,同时目录下还会生成一个项目名称文件夹,我们可以在对应的文件夹下右键创建一个C++类,选择新类需要继承的父类和存储位置后确定,UE4会自动打开VS,并生成一个.cpp文件和一个.h文件

勾选Show All Classes可以看到引擎支持的所有可继承的类。

在Path处可以直接添加新的文件夹名称,UE会自动创建新的文件夹来存放新建的类。

我们在创建类时可以选择后面的公有与私有,或者都不选

  • 选择公有,UE4会在C++Class/ProjectName文件夹下创建一个Public文件夹存放我们创建的类,而在VS中则会创建一个public文件夹存放.h文件,创建一个private文件夹存放.cpp文件;
  • 选择私有,UE4会在C++Class/ProjectName文件夹下创建一个Public文件夹存放我们创建的类,而在VS中则将.h和.cpp都存放在private文件下;
  • 如果都不选,则我们创建的类直接存放在C++Class/ProjectName文件夹下,而VS中.cpp和.h文件都存放在Source文件夹下。
  • 如下图Unkown处是一个用于选择模块的下拉列表,可以选择当前创建的类应该放在哪个模块下,这个一般涉及到多模块时才使用,一般情况下都是直接选择当前项目,在UE的视角当前项目也是一个模块。

创建的C++类的.h文件的结构,以一个UObject为例

1
2
3
4
5
6
7
8
9
10
11
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "PakExpand.generated.h"

UCLASS()
class UNKOWN_API UPakExpand : public UObject
{
GENERATED_BODY()
}
  • CoreMinimal.h头文件中包含了一些UE预定义需要包含的头文件;

  • NoExportTypes.h头文件中包含了大量的引擎需要的状态量;

  • .generated.h文件则是由UBT生成的用于反射的代码;

  • UCLASS()表示这个类加入UE4的反射系统,使用UCLASS()就必须包含.generated.h头文件;

  • XXX_API这个宏由UBT生成,XXX会被模块名代替,如我当前的项目名为Unkown,则生成UNKOWN_API,对于引擎来说,整个项目就是一个模块,UNKOWN_API标识这个类属于UNKOWN模块。、;

  • GENERATED_BODY():

    这里GENERATED_BODY()宏处有两种情况,我们可以使用GENERATED_UCLASS_BODY()宏和GENERATED_BODY()宏,二者的区别是:

    使用了GENERATED_BODY()宏,我们的类中就不能直接使用父类中的声明,如果我们要去实现,我就必须在本类中声明。使用GENERATED_BODY()宏,我们必须手动实现一个无参构造函数。

    使用GENERATED_UCLASS_BODY()宏,我们就可以使用父类声明的构造函数,在本类中不需要再声明,而可以直接实现即可,且实现的构造函数必须带const FObjectInitializer&参数。

在VS中手动创建类

VS中的工程目录的Source目录下的目录结构有两种:

  • 一堆的.cpp、.h和.build.cs文件。
  • .h文件在public目录下,.cpp文件在private目录下,.biuld.cs文件在Source目录下.。

对于第一种目录结构,直接在Source文件夹下使用VS添加.cpp和.h文件即可。

对于第二种目录结构,我们需要在public下添加.h文件,在private下添加.cpp文件。

在VS中手动创建的类如果继承UObject,我们需要手动添加UCLASS()宏和GENERATED_BODY()或GENERATED_UCLASS_BODY()宏。

 但是要注意的是手动创建的类系统不会自动为类名加前缀,所以手动创建的类定义类名时应该合乎UE4C++类的命名规范。

2.C++类的删除

UE4引擎自身不提供C++的删除功能,但是有时候我们需要删除一些类的时候怎么办呢?

唯一的办法就是建立在文件操作上了,步骤如下:

  • 删除项目目录下Source文件夹下需要删除类的.cpp.h文件;
  • 重新Generate Visual Studio project files,生成sln文件;
  • 双击.uproject文件,启动项目让引擎重新加载配置。

3.UE4类的命名规则

UE4为一些常用类的命名添加了一些命名前缀, 如果我们不写这些前缀,UE4会编译错误

前缀 说明
F 纯c++类
U 继承自UObject,但不继承自Actor的类
A 继承自Actor的类
S Slate控件相关类
H HitResult相关类

4.C++类的实例化

在UE4中实例化C++类稍显复杂,分为如下几种情况:

  • 如果是一个纯C++类型的类,即按UE4的命名规则F开头的类,符合C++的实例化条件,可以直接使用new运算符来实例化,或者直接使用构造函数在栈区中实例化;

  • 如果是一个继承自UObject的类,那么我们需要使用NewObject<T>()函数来实例化类对象;

  • 如果是一个继承自Actor的类,那么我们需要使用UWorld对象中的SpawnActor函数来实例化,调用方式为: GetWorld()->SpawnActor<T>()GetWorld()->SpawnActor<T>()不可以在构造函数中使用,如果直接在构造函数中使用UE4在编译时会直接崩溃。

  • 如果我们需要产出一个Slate类,那么我们需要使用 SNew()函数来实例化。

5.类的使用

继承自UObject类的C++类

UE中UObject类可以直接在C++中使用但是不能直接在蓝图中使用,如果想要在蓝图中使用一个继承自UObject的类,那么我们就可以使用UCLASS(Blueprintable)在类前说明,编译后就可以直接在蓝图中使用这个C++类,同时可以创建继承这个类的蓝图类,也可以在对应的类对象上右键创建蓝图类,否则创建蓝图类的按钮是非激活状态。

当然如果我们只需要在蓝图中使用,而不需要创建对应的蓝图类,我们可以使用UCLASS(BlueprintType)来描述。

如:

1
2
3
4
5
6
UCLASS(BlueprintType)
class ARP_04_API UTest : public UObject
{
GENERATED_BODY()

};

除此之外,我们若想要让类里面的变量和函数也能被蓝图类使用,我同时还需要在变量前指定UPROPERTY(BlueprintReadWrite),在函数前指定UFUNCTION(BlueprintCallable),就如上面的实例代码一样。

UPROPERTY(BlueprintReadWrite)里的参数不是唯一的, BlueprintReadWrite表示成员变量在蓝图类里可读写,BlueprintReadOnly表示成员变量在蓝图类里只读,BlueprintWriteOnly表示成员变量在蓝图类里只写

经过以上步骤我们的继承自UObject类的类便可以通过对应的蓝图类在关卡蓝图中使用了,使用BeginPlay节点开始程序,使用Construct节点来实例化我们的蓝图类,通过实例化出来的对象便可调用类中的资源了。

继承自AActor类的C++类

由于AActor在UE中就是一个场景中的基本实体,所以继承自AActor类的C++类默认是可以直接被蓝图使用的。

纯C++类

纯C++类完全脱离UE的反射系统,无法被UCLASS,UPROPERTY等宏修饰,所以无法被蓝图使用。由于纯C++类无法使用UE的反射系统,所以也就不支持UE的GC系统,所以UE提供了共享指针来管理纯C++类。

6.抽象类

UE对C++的抽象类也进行了魔改,UE使用UCLASS(abstract)宏来标识一个类是抽象类,对于UObject类和AActor类的抽象类在实例化上又有所区别。

继承自UObject的抽象类

使用UCLASS(abstract)宏标识一个UObject类,那么这个UObject类就是一个于C++抽象类基本一致的抽象类了,由于在C++中抽象类是不可以实例化的,所以如果代码中去实例化了一个抽象类,编译阶段就会报错,但是由于编译器不完全支持UE魔改后的C++标准,所以即使我们在代码里去实例化了一个抽象类,编译也不会报错,只有在编辑器运行时,真正跑到这行代码时才会报错。

1
2
3
4
5
6
7
UCLASS(abstract)
class MYPROJECT_API UAbstractObject : public UObject
{
GENERATED_BODY()
public:
void Log();
};

之所以编译阶段不会报错是因为,UE是通过UCLASS(abstract)宏来标识抽象类的,所以即使一个类没有纯虚函数UE也可以识别它为抽象类,但这个类在C++层面其实不是抽象类,所以编译阶段不会出现错误,当然我们也可以给这个类创建一个纯虚函数,是这类成为一个真正意义上的抽象类。

继承自AActor的抽象类

继承自AActor的抽象类在实例化上和UObject有一些区别,一个不包含纯虚函数的AActor抽象类在运行时是可以被实例化的,UE编辑器仅仅是报出一个警告,标识为抽象类的Actor不可以被放入场景中,但是却可以在C++中使用SapwnActor函数实例化,并且实例是有效的,只是Actor不会被spawn到场景中,只能像UObject一样驻留在内存中。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UCLASS(abstract)
class MYPROJECT_API AAbstractBase : public AActor
{
GENERATED_BODY()
public:
AAbstractBase();

protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
void Log()
{
UE_LOG(LogTemp, Error, TEXT("AAbstractBase::Log()"));
}
};

然后在另一个Actor—AAbstractOpreater的BeginPlay中去实例化:

1
2
3
4
5
6
void AAbstractOpreater::BeginPlay()
{
Super::BeginPlay();
AAbstractBase* AbstractBase = GWorld->SpawnActor<AAbstractBase>();
AbstractBase->Log();
}

然后将AAbstractOpreater丢进场景中运行,看看结果:

可以看到AAbstractBase虽然被实例化了,并且Log函数也调用成功了,但是场景中并没有AAbstractBase这个Actor。

这估计是UE的bug吧,在使用时抽象类最好还是创建一个纯虚函数,以符合C++的标准。

7.接口

与C++的接口不同,UE对C++的接口进行了魔改,我们先创建一个UE接口来看一下,UE的接口长什么样。

我们要创建一个UE的接口,那么我们就需要使自己的接口继承自UInterface,在编辑器里则是选择:

创建出来的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "UnkInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UUnkInterface : public UInterface
{
GENERATED_BODY()
};

class MYPROJECT_API IUnkInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:

};

可以看到.h文件里面有两个类,一个是继承自UInterface,一个为纯C++类,UE这样做的目的就是为了使接口也接入UE的对象系统,因为UInterface的顶层基类也是UObject,但是这就出现了另一个问题,即继承自UObject的其他类如果想要使用UE接口,就会出现菱形继承,为了使开发者避免自己使用虚继承来避开菱形继承问题,UE就直接魔改了C++的接口。

其中两部分 UInterface负责对象系统,即接口也能使用UE的反射系统和GC系统,而纯C++类部分则负责具体的接口内容,使用方式完全和C++一样,同时由于接口接入UE的反射系统,所以接口行数可以使用UFUNCTION来暴漏给蓝图。

C++使用接口

C++使用接口,和普通C++一样,直接继承并实现所有接口的纯虚函数,然后使用即可。

蓝图使用接口

虽然UE提供了纯蓝图的接口,但是纯蓝图接口限制比较多,不能设置成员变量,也不能在接口中对函数进行实现,所以很多时候还是会需要用到C++接口,比如要写一些通用接口的时候,但是C++接口可以声明成员变量却不能暴漏给蓝图,估计是UE接口的特殊实现方式决定的。

C++接口默认暴漏给蓝图,所以不需要再使用UCLASS(BlueprintCallable)或UCLASS(BlueprintType)去暴漏,但是蓝图只能看到接口暴漏给蓝图的函数和变量,蓝图类继承一个包含纯虚函数的接口,可以不实现,因为在UE中纯虚函数是不能暴漏给蓝图的。

所以如果蓝图要继承一个C++接口,那么C++接口中的函数就需要暴漏给蓝图,函数需要使用UFUNCTION(BlueprintNativeEvent)和UFUNCTION(BlueprintNativeEvent)暴漏给蓝图。

  • BlueprintNativeEvent:BlueprintNativeEvent可以使一个函数在C++中声明并在C++中实现,然后提供给蓝图可重写的能力,如果蓝图不重写,那么蓝图调用时就使用C++的实现,如果蓝图重写了,那么蓝图调用时就是用蓝图重写的实现。在UE C++接口中被BlueprintNativeEvent标识函数需要在C++中实现一个[FunctionName]_Implementation的函数体,并且必须要配合一个virtual [FunctionName]_Implementation函数声明,二者缺一都会导致程序编译不过,如:

    .h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #pragma once
    #include "CoreMinimal.h"
    #include "UObject/Interface.h"
    #include "UnkInterface.generated.h"

    UINTERFACE(MinimalAPI)
    class UUnkInterface : public UInterface
    {
    GENERATED_BODY()
    };

    class MYPROJECT_API IUnkInterface
    {
    GENERATED_BODY()
    public:
    UFUNCTION(BlueprintNativeEvent)
    void Log(const FString& msg);
    virtual void Log_Implementation(const FString& msg);
    UFUNCTION(BlueprintImplementableEvent)
    void Log2(const FString& msg);
    };

    .cpp

    1
    2
    3
    4
    5
    6
    #include "UnkInterface.h"

    void IUnkInterface::Log_Implementation(const FString& msg)
    {
    UE_LOG(LogTemp, Error, TEXT("IUnkInterface::Log_Implementation(%s)"), *msg);
    }

    当然如果BlueprintNativeEvent标识的函数去掉virtual [FunctionName]_Implementation函数体声明和[FunctionName]_Implementation函数体定义也是可以编译过的,只是这样就失去了BlueprintNativeEvent的意义。如:

    .h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #pragma once
    #include "CoreMinimal.h"
    #include "UObject/Interface.h"
    #include "UnkInterface.generated.h"

    UINTERFACE(MinimalAPI)
    class UUnkInterface : public UInterface
    {
    GENERATED_BODY()
    };

    class MYPROJECT_API IUnkInterface
    {
    GENERATED_BODY()
    public:
    UFUNCTION(BlueprintNativeEvent)
    void Log(const FString& msg);
    UFUNCTION(BlueprintImplementableEvent)
    void Log2(const FString& msg);
    };

    .cpp

    1
    #include "UnkInterface.h"

    这里有一点是需要注意的,C++接口中使用BlueprintNativeEvent和在普通的类中使用BlueprintNativeEvent是有所不同的,如前面所说,接口中使用BlueprintNativeEvent必须配合一个virtual [FunctionName]_Implementation声明并且必须实现[FunctionName]_Implementation函数体,而在普通的UE类中使用BlueprintNativeEvent则不需要带virtual [FunctionName]_Implementation声明,只需要实现[FunctionName]_Implementation函数体即可。

    这可能是因为C++接口既要考虑蓝图实现,又要考虑C++实现的原因,virtual [FunctionName]_Implementation声明就是为了给C++实现这个函数留接口。

  • BlueprintImplementableEvent:BlueprintImplementableEvent标识的函数不能有C++实现,必须由蓝图来重写,BlueprintImplementableEvent标识的函数可以看成是相对于蓝图的纯虚函数。如上面代码中Log2函数,C++中是没有函数体实现的。

BlueprintNativeEvent和BlueprintImplementableEvent函数的蓝图实现

  • 参数中使用了FString类的时候有一个坑,即所有的FString参数都必须使用引用—FString&,否则编译时会报没有找到重载的成员函数,但是使用引用又会引出另一个问题,就是引用类型参数暴漏给蓝图是作为蓝图函数返回值来使用的,如果想要FString&作为输入参数,那么就必须使用const FString&。

  • BlueprintNativeEvent和BlueprintImplementableEvent标识的函数暴漏给蓝图,根据是否有返回值在蓝图中的表现是不一样的,不带返回值的函数在蓝图中以事件的形式存在,需要通过右键菜单调出重写,在接口栏表现为一个黄色的函数标识;带返回值的函数在蓝图中表现为普通的函数,可以直接双击接口栏的函数名重写,在接口栏表现为一个蓝色的函数标识。

C++调用蓝图的接口实现

UE接口的实现原理使得同一个UE接口既可以被C++继承也可以被蓝图继承,于是就会涉及到C++来调用蓝图的接口实现的情况,事实上C++调用蓝图的接口实现依旧是通过蓝图子类重写父类函数的方式实现的,只是说通过接口的方式调用可以做到比重写子类的方式更加灵活的多态。

比如我们现在创建6个类,分别是动物:Animal,狗狗:Dog,飞禽:Bird,母鸡:Chicken,动物园:Zoo以及接口IMove

Animal.h

1
2
3
4
5
6
7
8
9
10
11
UCLASS()
class MYPROJECT_API AAnimal : public AActor,public IMove
{
GENERATED_BODY()
public:
AAnimal();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
};

Dog.h

1
2
3
4
5
6
7
8
9
10
UCLASS()
class MYPROJECT_API ADog : public AAnimal
{
GENERATED_BODY()
public:
void Move_Implementation();
{
UE_LOG(LogTemp, Error, TEXT("Dog can creep"));
}
};

Bird.h

1
2
3
4
5
6
7
8
9
10
UCLASS()
class MYPROJECT_API ABird : public AAnimal
{
GENERATED_BODY()
public:
void Move_Implementation()
{
UE_LOG(LogTemp, Error, TEXT("Bird can fly"));
}
};

Move.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UINTERFACE(MinimalAPI)
class UMove : public UInterface
{
GENERATED_BODY()
};
class MYPROJECT_API IMove
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent,Category="IMove")
void Move();
virtual void Move_Implementation();
UFUNCTION(BlueprintImplementableEvent, Category = "IMove")
void Shout(const FString& Cry);
};

Move.cpp

1
2
3
4
5
#include "Interface/Move.h"
void IMove::Move_Implementation()
{
UE_LOG(LogTemp, Error, TEXT("Animal can move"));
}

Zoo.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UCLASS()
class MYPROJECT_API AZoo : public AActor
{
GENERATED_BODY()
public:
AZoo();

protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
void AnimalObservationDiary();
void AnimalAction(AAnimal* Animal);
};

Zoo.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "Interface/Zoo.h"
//...
void AZoo::AnimalObservationDiary()
{
AAnimal* Animal;
UE_LOG(LogTemp, Error, TEXT("Animal Action:"));
Animal = GWorld->SpawnActor<AAnimal>();
AnimalAction(Animal);
UE_LOG(LogTemp, Error, TEXT("Dog Action:"));
Animal = GWorld->SpawnActor<ADog>();
AnimalAction(Animal);
UE_LOG(LogTemp, Error, TEXT("Bird Action:"));
Animal = GWorld->SpawnActor<ABird>();
AnimalAction(Animal);
UE_LOG(LogTemp, Error, TEXT("Bird Action:"));
UClass* ChickenClass = StaticLoadClass(ABird::StaticClass(), nullptr, TEXT("Blueprint'/Game/CPlus/Chicken.Chicken_C'"));
if (ChickenClass != nullptr)
{
Animal = GWorld->SpawnActor <ABird>(ChickenClass);
AnimalAction(Animal);
}
}
void AZoo::AnimalAction(AAnimal* Animal)
{
UClass* AnimalClass = Animal->GetClass();
if (AnimalClass->ImplementsInterface(UMove::StaticClass()))
{
IMove* Move = CastChecked<IMove>(Animal);
IMove::Execute_Move(Animal);
IMove::Execute_Shout(Animal, " ");
}
}

以及继承自Bird的蓝图类Chicken

  • ImplementsInterface:检测一个类是否实现了接口;
  • IMove* Move = CastChecked<IMove>(Animal):通过接口指针指向子类,实现多态;
  • IMove::Execute_Shout(Animal, “ “):UE4C++调用蓝图的接口实现,由于蓝图属于C++的上层,所以UE使用了Execute_XX的反射调用方式,第一个参数必须为要调用的对象,后面用空格隔开要传入的参数。

全部实现之后我们把Zoo类丢进场景里,运行下看看:

可以看到C++通过接口调用到了蓝图函数。

这里面有一个坑,就是如果蓝图类是一个纯蓝图类直接去继承IMove接口,而不是像上面一样通过一个Animal父类去继承IMove接口,那么Zoo::AnimalAction在调用蓝图的接口实现的时候将会产生断言。

三、字符串

1.FString

FString是UE4C++编程中极其常用的一个UE4字符串封装类型,是UE4自带字符串类型中唯一可以进行各种字符串操作的字符串类型,同时FString的资源消耗也是最大的。FString的底层事实上是一个TCHAR的数组。

FString初始化

  • 方法1:
1
FString fStr = FString(TEXT("str")); 
  • 方法2:
1
FString fStr = FString("str"); 
  • 方法3:
1
FString fStr = "str"; 
  • 方法4:
1
FString fStr = TEXT("str"); 

FString的中文乱码

默认情况下我们用FString存储一个中文字符串,在UE里使用,如使用UE_LOG打印是会出现乱码的情况的,这是因为VS在国区默认使用的文件编码为GB2313和UE使用的UTF-8编码不一致导的,我们需要把cpp文件的编码格式换成UTF-8即可。

默认情况下VS把设置文件编码的入口隐藏了起来,所以要想设置文件编码我们还需要把设置入口给显示出来。

然后高级保存选项就出来了,然后设置文件编码格式。

然后我们来测试一下

1
2
3
4
5
void AAbstractOpreater::FStringConvert()
{
FString fStr = TEXT("中文");
UE_LOG(LogTemp, Error, TEXT("fStr:%s"),*fStr);
}

结果:

2.FName

FName也是UE4自带的字符串类型,FName是不区分大小写的,赋予FName的字符串会被存放到UE4的数据表中,多个FName赋予相同的字符串时都会指向同一个数据表地址。FName被赋值之后不可改变也不能被操作,FName的不可改变的性质和C++的string类很相似,std::string在修改时事实上是创建了一个新的字符串,而不是修改原字符串,因为FName的这些性质使得FName的查找和访问非常快。

  • FName的初始化
1
2
3
FName fn1 = FName(TEXT("str"));
FName fn2 = FName("str");
FName fn3 = "str";

3.FText

FText是一个FString的升级版字符串,存储容量比FString要大很多,主要用于UE4的文本存储与处理。FText主要涉及UE的文本本地化处理。

FText不可以像FString和FName一样从构造函数通过TCHAR构建,FText从构造函数只能构建一个空FText,要想构建一个有内容的FText,则需要使用FText::FromString,FText::FromName,FText::AsCultureInvariant。

1
2
3
4
5
FString fStr = TEXT("fStr");
FName fn = FName(TEXT("fn"));
FText ft1 = FText();
FText ft2 = FText::FromString(fStr);
FText ft3 = FText::AsCultureInvariant(fStr);

4.TCHAR

TCHAR就是UE层面的char类型了,TCHAR是UE对C++的char和wchar_t的封装,C++的char和wchar_t适用于不同的平台,而TCHAR则将二者的操作进行了统一,使得TCHAR具备可以移植性。

事实上TCHAR不是对char和wchar_t的直接封装,UE将char封装成了ANSICHAR,将wchar_t封装成了WIDECHAR,而TCHAR则是对ANSICHAR和WIDECHAR的再封装。

UE对C++类型的封装都在GenericPlatform.h文件里。

7.TChar

TChar是一个针对ASCII编码的字符串操作的封装泛型结构体,提供一系列方法操作字符串。

其中UE还对专门将TChar<TCHAR>封装成了FChar,将TChar<WIDECHAR>封装成了FCharWide,将TChar<ANSICHAR>封装成了FCharAnsi

8.TCString

TCString和TChar类似,是UE专门封装的用于处理C字符串的泛型结构体,其中对ANSICHAR和WIDECHAR字符做了专门实现,提供一系列方法操作C字符串。

和TChar一样,TCString也对一些常用的类型做了封装,TCString<TCHAR>封装成FCStringTCString<ANSICHAR>封装成FCStringAnsiTCString<WIDECHAR>封装成FCStringWide

9.TStringView

TStringView和FString用法基本上是一样的,底层也是对TCHAR的封装,只是TStringView有着自己的特殊的使用场景,按照源码的说明就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* String View
*
* A string view is implicitly constructible from const char* style strings and from compatible character ranges such as FString and TStringBuilderBase.
*
* A string view does not own any data nor does it attempt to control any lifetimes, it merely points at a subrange of characters in some other string. It's up to the user to ensure the underlying string stays valid for the lifetime of the string view.
*
* A string view is cheap to copy and is intended to be passed by value.
*
* A string view does not represent a NUL terminated string and therefore you should never pass in the pointer returned by GetData() into a C-string API accepting only a pointer. You must either use a string builder to make a properly terminated string, or use an API that accepts a length argument in addition to the C-string.
*
* String views are a good fit for arguments to functions which don't wish to care which style of string construction is used by the caller. If you accept strings via string views then the caller is free to use FString, FStringBuilder, raw C strings, or any other type which can be converted into a string view.
**/

谷歌翻译过来就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 字符串视图
*
* 字符串视图可以从 const char* 样式字符串和兼容的字符范围(例如 FString 和 TStringBuilderBase)隐式构造。
*
* 字符串视图不拥有任何数据,也不尝试控制任何生命周期,它仅指向其他字符串中的字符子范围。由用户确保底层字符串在字符串视图的生命周期内保持有效。
*
* 字符串视图复制起来很便宜,并且旨在按值传递。
*
* 字符串视图不代表 NUL 终止的字符串,因此您永远不应将 GetData() 返回的指针传递给仅接受指针的 C 字符串 API。您必须使用字符串生成器来生成正确终止的字符串,或者使用除了 C 字符串之外还接受长度参数的 API。
*
* 字符串视图非常适合不希望关心调用者使用哪种字符串构造方式的函数的参数。如果您通过字符串视图接受字符串,则调用者可以自由使用 FString、FStringBuilder、原始 C 字符串或任何其他可以转换为字符串视图的类型。
**/

和TChar,TCString一样,TStringView也将TStringView<TCHAR>封装成了FStringViewTStringView<ANSICHAR>封装成了FAnsiStringViewTStringView<WIDECHAR>封装成了FWideStringView

不过TStringView实际使用过程中好像也没什么用😂。

10.TEXT()

TEXT宏又是对TCHAR的一层封装,使得TCHAR更易于使用,TEXT宏可以使其包裹的字符串根据设备自行选择最适合的编码格式,所以在UE里使用字符串时最好使用TEXT宏包裹,使代码的兼容性更好。

需要注意的是TEXT()宏不支持中文,如果转码中文UE4在打印时会变成乱码。

11.各字符串之间的转换

FString->FName

1
2
FString fStr = TEXT("fStr");
FName fn = FName(*fStr);

FString->FText

1
2
FString fStr = TEXT("fStr");
FText ft = FText::FromString(fStr)

FString->bool

1
2
FString fStr = TEXT("fStr");
bool b = fStr.ToBool();

FString->int

1
2
FString fStr = TEXT("fStr");
int i = FCString::Atoi(*fStr);

FString->float

1
2
FString fStr = TEXT("fStr");
float f = FCString::Atof(*fStr);

FString->char*

1
2
FString fStr = TEXT("fStr");
char* chr = TCHAR_TO_ANSI(*fStr);

FString->std::string

1
2
FString fStr = TEXT("fStr");
std::string str = TCHAR_TO_UTF8(*fStr);

char* ->FString

1
2
char* chr = "chr";
FString fStr = ANSI_TO_TCHAR(chr)

std::string->FString

1
2
std::string str = "str"
FString fStr = FString(str.c_str());

FName->FStirng

1
2
FName fn = FName("fn");
FString fstr = fn.ToString();

FText->FString

1
2
FText ft = FText("ft");
FString fstr = ft.ToString();

float->FString

1
2
float FloatValue = 0.1f;
FString fstr = FString::SanitizeFloat(FloatValue);

int->FString

1
2
int IntValue = 1;
FString fstr = FString::FromInt(IntValue);

bool->FString

1
2
bool InBool = true
FString fstr = InBool?TEXT("true"):TEXT("false");

FVector->FString

1
2
FVector Vec = FVector(0,0,0);
FString fstr = Vec.ToString();

FVector2D->FString

1
2
FVector2D Vec2D = FVector2D(0,0)
FString fstr = Vec2D.ToString();

FRotator->FString

1
2
FRotator Rot = FRotator(0,0,0)
FString fstr = Rot.ToString();

UObject->FString

1
2
UObject* InObj = NewObject<UObject>();
FString fstr = (InObj != NULL)?InObj->GetName():FString(TEXT("None"));

12.TCHAR与char,wchar_t的转换

C++支持两种字符集:ANSI和Unicode,ANSI字符集对应char,Unicode字符集对应wchar_t,其中Unicode字符集又分为三种编码:UTF-8,UTF-16,UTF-32,不同的字符集适应不用的平台,而TCHAR是UE对C++的char和wchat_t的封装,统一了char和wchar_t的操作,是UE的字符具有可移植性。

  • TCHAR_TO_ANSI:使用ANSI字符集将TCHAR*转换成char*

  • ANSI_TO_TCAHR:将ANSI字符集的char*转换成TCHAR*

  • TCHAR_TO_UTF8:使用Unicode字符集UTF-8编码将TCHAR*转换成char*

  • UTF8_TO_TCAHR:将Unicode字符集UTF-8编码的char*转换成TCHAR*

    同理还有TCAHR_TO_UTF16,UFT16_TO_TCAHR,TCHAR_TO_UTF32,UTF32_TO_TCHAR。

  • TCHAR_TO_WCHAR:将TCAHR*转换成wchar_t*

  • WCHAR_TO_TCAHR:将wchat_t*转换成TCAHR*,或者使用强转,如:

    1
    2
    wchar_t str = L"str";
    TCAHR* tchr = (TCAHR*)str;

    四、枚举与结构体

1.枚举

UE遵循C++11标准,所以枚举的构造有两种形式:

C++11以前的标准形式

1
2
3
4
5
6
7
8
9
UENUM()
namespace EnumSpace
{
enum NEnum
{
NE_A,
NE_B
};
}

这种形式的枚举不能通过:去指定枚举使用的索引类型,也不能使用UENUM(BlueprintType)宏暴露给蓝图,尽管加了这个宏编译不会报错,但是蓝图里面识别不到这个枚举,所以也没法使用UMETA()宏去设置枚举成员,同时也不支持反射。

C++11的标准形式

1
2
3
4
5
6
UENUM(BlueprintType)
enum class MEnum : uint8
{
ME_A UMETA(DisplayName="MEnumA"),
ME_B UMETA(DisplayName="MEnumB")
};

这种形式可以暴露给蓝图,也可以使用:指定枚举的索引类型,还可以使用UMETA宏描述枚举成员,同时可以使用UE提供的枚举反射函数。如:

2.UMETA

UMETA宏是枚举成员专用的元数据说明宏,和UFUNCTION,UCALSS,UPROPERTY以及UINTERFACE中使用的meta效果上是一样的,注意这里说的是枚举成员而不是枚举类型,枚举类型的元数据也使用meta。

UMETA可以使用的元数据说明符详情参见第八章宏第4节meta

3.结构体

根据官方文档的说法,UE结构体通过USTRUCT宏支持了UE的反射系统,支持UPROPERTY属性反射,但是由于结构体不继承自UObject所以不支持GC,也不支持UFUNCTION,所以结构体中的函数无法暴露给蓝图。

通过UPROPERTY(BlueprintReadWrite)可以将结构体的属性暴露给蓝图,如果不暴露的话,蓝图将无法识别到结构体中的属性。

结构体的命名必须以F开头,否则编译不通过。

1
2
3
4
5
6
7
8
9
10
11
12
USTRUCT(BlueprintType)
struct FStru
{
GENERATED_BODY()

UPROPERTY()
FString tchr = TEXT("tchr");
FString GetTCHR()
{
return tchr;
}
};

结构体有两种写法,上面是一种,另种是:

1
2
3
4
5
6
7
8
9
10
11
12
USTRUCT(BlueprintType)
struct FStru
{
GENERATED_USTRUCT_BODY()

UPROPERTY()
FString tchr = TEXT("tchr");
FString GetTCHR()
{
return tchr;
}
};

GENERATED_BODY和GENERARED_USTRUCT_BODY没有什么区别,在结构体中GENERATED_BODY就是GENERARED_USTRUCT_BODY,这是因为结构体不存在父类构造函数这一说,UE在ObjectMacros.h中做了预定义:

1
#define GENERATED_USTRUCT_BODY(...) GENERATED_BODY()

五、蓝图函数库

C++蓝图函数库是一个UBlueprintFunctionLibrary的派生类,C++蓝图函数库存在的目的就是把C++的函数暴露给蓝图使用,所以函数库里的函数均需要使用static标识,且使用UFUNCTION(BlueprintCallable)暴露给蓝图,二者缺一,蓝图都无法识别到函数库里的函数。

由于静态函数是属于类的,所以无论函数是private、protected还是public都可以被蓝图识别到,但是函数库里的属性无论是否暴露给蓝图,蓝图都识别不到。

1
2
3
4
5
6
7
8
9
10
11
12
UCLASS()
class MYPROJECT_API UFunLib : public UBlueprintFunctionLibrary
{
GENERATED_BODY()

public:
UPROPERTY(BlueprintReadWrite, Category="FunLib")
int a = 0;
private:
UFUNCTION(BlueprintCallable, Category = "FunLib")
static void Log();
};

六、代理

C++自身不支持委托,所以UE自己用宏实现了委托,在UE里又叫代理。由于UE的代理是用宏来实现声明的,所以UE的代理没有办法像C#一样声明任意数量任意类型参数的代理,所以UE的代理就出现了一个很诡异的现象,即不同参数个数的代理需要使用不同的宏来声明,虽然很二,但是UE没法像底层语言一样自己去创建一个关键字来标识代理,这也许是在限制条件下的最优解了。

1.代理的分类

根据可绑定的响应函数的数量分

单播代理

单播代理只能绑定一个响应函数,形式如:

1
2
3
4
5
6
7
8
//无返回值,无参数
DECLARE_DELEGATE(FDelegateOne);
//无返回值,两个参数
DECLARE_DELEGATE_TwoParams(FDelegateTwo, int, bool);
//以此类推到
DECLARE_DELEGATE_NineParams(FDelegateNine,int,int,int,int,int,int,int,int,int);
//返回bool类型,两个参数
DECLARE_DELEGATE_RetVal_TwoParams(bool, FDelegateTwo, int, int);

多播代理

多播代理可以绑定多个响应函数,形式如:

1
2
3
4
5
//无返回值,无参数
DECLARE_MULTICAST_DELEGATE(FMulticastDelegateNone);
//无返回值,一个参数
DECLARE_MULTICAST_DELEGATE_OneParam(FMulticastDelegateOne, FString);
//同理类推到九个参数

需要注意,所有的多播代理都没有返回值。

根据是否可蓝图调用分

静态/动态,单播/多播,有/无返回值,参数个数,四个维度除了动态多播不能有返回值外其他的可以自由的互相组合。

静态代理

静态代理只能在C++中使用,前面单播代理和多播代理的例子都属于静态代理。

如:

1
DECLARE_DELEGATE_RetVal_OneParam(bool,FDynamicDelegateOne, FString,Str);

动态代理

动态代理支持序列化,可以直接使用函数名字符串来创建委托,而无需传递函数入口地址,只有动态多播可以暴露给蓝图使用,使用UPROPERTY(BlueprintAssignable)标识即可暴露。

1
2
error : 'BlueprintAssignable' is only allowed on multicast delegate properties

1
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParam(FDynamicMulticastDelegateOne, FString,param1,int,param2);

动态代理在声明的时候和其他代理有所不同,不像其他代理声明含参代理时只需要注明参数类型,动态代理声明含参代理的时候必须注明参数类型和参数名,并且用,隔开。动态多播不能有返回值。

需要注意的时,代理的名称必须以F开头。

2.代理的声明

代理可以声明在类内也可以声明在类外,声明在类的内外在实际效果上没有区别,只是在声明代理对象时有所区别,如:

在类内声明代理对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Announcer.generated.h"
//在类外声明代理
DECLARE_DELEGATE_RetVal_OneParam(FString, FMagazineNumberFive, int);
DECLARE_MULTICAST_DELEGATE_OneParam(FMagazineNumberSix, int);
DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(FString, FMagazineNumberSeven, int, ReleaseNumber);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMagazineNumberEight, int, ReleaseNumber);

UCLASS()
class MYPROJECT_API AAnnouncer : public AActor
{
GENERATED_BODY()
public:
//在类内声明代理
DECLARE_DELEGATE_RetVal_OneParam(FString, FMagazineNumberOne, int);
DECLARE_MULTICAST_DELEGATE_OneParam(FMagazineNumberTwo, int);
DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(FString, FMagazineNumberThree, int, ReleaseNumber);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMagazineNumberFour, int, ReleaseNumber);

//声明代理对象
FMagazineNumberOne MagazineNumberOne;
FMagazineNumberTwo MagazineNumberTwo;
FMagazineNumberThree MagazineNumberThree;
FMagazineNumberFour MagazineNumberFour;

FMagazineNumberFive MagazineNumberFive;
FMagazineNumberSix MagazineNumberSix;
FMagazineNumberSeven MagazineNumberSeven;
FMagazineNumberEight MagazineNumberEight;
private:
static AAnnouncer* Announcer;
public:
AAnnouncer();
static AAnnouncer* GetAnnouncerInstance();
void OnPublicate();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;

};

在类外声明代理对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once
#include "Delegate/Announcer.h"
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SubscripterNumberOne.generated.h"

UCLASS()
class MYPROJECT_API ASubscripterNumberOne : public AActor
{
GENERATED_BODY()
public:
ASubscripterNumberOne();
//声明在类内的代理声明代理对象
AAnnouncer::FMagazineNumberOne MagazineNumberOne;
//声明在类外的代理声明代理对象
FMagazineNumberFive MagazineNumberFive;
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
};

3.代理的绑定

声明了代理对象之后就可以往对象身上绑定执行函数了,根据代理和执行函数的不同,绑定的方式也有所不同。

静态单播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void ASubscripterNumberOne::BeginPlay()
{
Super::BeginPlay();
//BindUObject-绑定UObject类的函数到单播代理,可以是UFUNCTION函数也可以不是
//AAnnouncer::GetAnnouncerInstance()->MagazineNumberOne.BindUObject(this, &ASubscripterNumberOne::SubscriptMagazineOne);

//BindUFunction-通过名字绑定UObject类的UFUNCTION函数到单播代理
//AAnnouncer::GetAnnouncerInstance()->MagazineNumberOne.BindUFunction(this, "SubscriptMagazineOne");

//BindUFunction-通过名字绑定UObject类的静态UFUNCTION函数
//AAnnouncer::GetAnnouncerInstance()->MagazineNumberOne.BindUFunction(this, STATIC_FUNCTION_FNAME(TEXT("ASubscripterNumberOne::SubscriptMagazineOneStatic")));

//BindRaw-绑定指向的C++类的函数的共享指针到单播代理
//TSharedPtr<FSubscripterNumberThree> SubscripterNumberThree = MakeShareable(new FSubscripterNumberThree());
//MagazineNumberOne.BindRaw(SubscripterNumberThree.Get(), &FSubscripterNumberThree::SubscriptMagazineOne);

//BindSP-绑定指向的C++类的函数的共享引用到单播代理
//TSharedRef<FSubscripterNumberThree> SubscripterNumberThree = MakeShareable(new FSubscripterNumberThree());
//AAnnouncer::GetAnnouncerInstance()->MagazineNumberOne.BindSP(SubscripterNumberThree, &FSubscripterNumberThree::SubscriptMagazineOne);

//BindThreadSafeSP-绑定指向的C++类的函数的线程安全的共享引用到单播代理
//TSharedRef<FSubscripterNumberThree,ESPMode::ThreadSafe> SubscripterNumberThree = MakeShareable(new FSubscripterNumberThree());
//AAnnouncer::GetAnnouncerInstance()->MagazineNumberOne.BindThreadSafeSP(SubscripterNumberThree, &FSubscripterNumberThree::SubscriptMagazineOne);

//BindLambda-绑定Lambda表达式到单播代理
//MagazineNumberOne.BindLambda([](int ReleaseNumber)->FString {
// TArray<FStringFormatArg> Arguments;
// Arguments.Add(FStringFormatArg((int32)ReleaseNumber));
// Arguments.Add(FStringFormatArg((int32)ReleaseNumber));
// FString PublicateContentOne = FString::Format(TEXT("发布者发布了{0}号杂志给订阅者3号,发布号:{1}"), Arguments);
// UE_LOG(LogTemp, Error, TEXT("%s"), *PublicateContentOne);
// return PublicateContentOne;
// });

//BindWeakLambda-将Lambda表达式与指定对象进行弱关联,再绑定这个对象关联的Lambda表达式到单播代理
//AAnnouncer::GetAnnouncerInstance()->MagazineNumberOne.BindWeakLambda(this, [](int ReleaseNumber) {
// TArray<FStringFormatArg> Arguments;
// Arguments.Add(FStringFormatArg((int32)ReleaseNumber));
// Arguments.Add(FStringFormatArg((int32)ReleaseNumber));
// FString PublicateContentOne = FString::Format(TEXT("发布者发布了{0}号杂志给订阅者1号,发布号:{1}"), Arguments);
// UE_LOG(LogTemp, Error, TEXT("%s"), *PublicateContentOne);
// return PublicateContentOne;
// });

//BindStatic-绑定静态函数到单播代理,在类内外都可以使用,函数可以UFUNCTION函数也可以不是
//AAnnouncer::GetAnnouncerInstance()->MagazineNumberOne.BindStatic(ASubscripterNumberOne::SubscriptMagazineOneStatic);

//BindStatic-绑定静态函数到单播代理,只能在类内使用,函数可以UFUNCTION函数也可以不是
AAnnouncer::GetAnnouncerInstance()->MagazineNumberOne.BindStatic(SubscriptMagazineOneStatic);
}

单播对象被销毁时会自动在析构函数中调用UnBind解绑

静态多播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void ASubscripterNumberOne::BeginPlay()
{
Super::BeginPlay();
//AddUObject-添加一个UObject类的函数到多播代理
AAnnouncer::GetAnnouncerInstance()->MagazineNumberTwo.AddUObject(this, &ASubscripterNumberOne::SubscriptMagazineTwo);
//AddUFunction-通过名字添加UObject类的UFUNCTION函数到多播代理
AAnnouncer::GetAnnouncerInstance()->MagazineNumberTwo.AddUFunction(this, TEXT("SubscriptMagazineTwo"));
//Add-添加一个指向UObject类的函数的FDelegate进入多播代理,FDelegate是代理的内部类
DelegateTwo = AAnnouncer::FMagazineNumberTwo::FDelegate::CreateUObject(this, &ASubscripterNumberOne::SubscriptMagazineTwo);
AAnnouncer::GetAnnouncerInstance()->MagazineNumberTwo.Add(DelegateTwo);

//AddLambda-添加一个Lambda表达式到多播代理
AAnnouncer::GetAnnouncerInstance()->MagazineNumberTwo.AddLambda([](int ReleaseNumber)->void {
TArray<FStringFormatArg> Arguments;
Arguments.Add(FStringFormatArg((int32)ReleaseNumber));
Arguments.Add(FStringFormatArg((int32)ReleaseNumber));
FString PublicateContentOne = FString::Format(TEXT("发布者发布了{0}号杂志给订阅者2号,发布号:{1},Lambda"), Arguments);
UE_LOG(LogTemp, Error, TEXT("%s"), *PublicateContentOne);
});
//AddWeakLambda-将Lambda表达式与指定对象进行弱关联,再添加这个对象关联的Lambda表达式到多播代理
AAnnouncer::GetAnnouncerInstance()->MagazineNumberTwo.AddWeakLambda(this, [](int ReleaseNumber)->void {
TArray<FStringFormatArg> Arguments;
Arguments.Add(FStringFormatArg((int32)ReleaseNumber));
Arguments.Add(FStringFormatArg((int32)ReleaseNumber));
FString PublicateContentOne = FString::Format(TEXT("发布者发布了{0}号杂志给订阅者2号,发布号:{1},WeakLambda"), Arguments);
UE_LOG(LogTemp, Error, TEXT("%s"), *PublicateContentOne);
});

//AddRaw-添加指向的C++类的函数的共享指针到多播代理
TSharedPtr<FSubscripterNumberThree> SubscripterNumberThree = MakeShareable(new FSubscripterNumberThree());
AAnnouncer::GetAnnouncerInstance()->MagazineNumberTwo.AddRaw(SubscripterNumberThree.Get(), &FSubscripterNumberThree::SubscriptMagazineTwo);
//AddSP-添加指向的C++类的函数的共享引用到多播代理
TSharedRef<FSubscripterNumberThree> SubscripterNumberThree2 = MakeShareable(new FSubscripterNumberThree());
AAnnouncer::GetAnnouncerInstance()->MagazineNumberTwo.AddSP(SubscripterNumberThree2, &FSubscripterNumberThree::SubscriptMagazineTwo);
//AddThreadSafeSP-添加指向的C++类的函数的线程安全的共享引用到多播代理
TSharedRef<FSubscripterNumberThree,ESPMode::ThreadSafe> SubscripterNumberThree3 = MakeShareable(new FSubscripterNumberThree());
AAnnouncer::GetAnnouncerInstance()->MagazineNumberTwo.AddThreadSafeSP(SubscripterNumberThree3, &FSubscripterNumberThree::SubscriptMagazineTwo);

//AddStatic-添加静态函数到多播代理,在类内外都可以使用,函数可以UFUNCTION函数也可以不是
AAnnouncer::GetAnnouncerInstance()->MagazineNumberTwo.AddStatic(ASubscripterNumberOne::SubscriptMagazineTwoStatic);
//AddStatic-添加静态函数到多播代理,只能在类内使用,函数可以UFUNCTION函数也可以不是
AAnnouncer::GetAnnouncerInstance()->MagazineNumberTwo.AddStatic(SubscriptMagazineTwoStatic);
}

AddSP和AddThreadSafeSP有一个坑,就是像上面的代码一样在一个函数里面去创建一个共享引用,并往多播代理添加这个共享引用,到代理触发执行函数时,函数不会执行,可能时因为共享引用在函数栈退出时就已经置空了,所以才无法触发,这里还没看到共享引用,暂时先当一个坑。

动态单播

动态代理就没那么多花里胡哨了,动态单播只有一个BindUFunction函数来绑定UFUNCTION函数,即动态单播代理只能用于绑定UObject类的UFUNCTION函数。

1
2
3
4
5
void ASubscripterNumberOne::BeginPlay()
{
Super::BeginPlay();
AAnnouncer::GetAnnouncerInstance()->MagazineNumberThree.BindUFunction(this, TEXT("SubscriptMagazineThree"));
}

动态多播

动态多播也相当简洁,只有Add和AddUnique函数,只能用来添加FScriptDelegate指向的函数,而TScriptDelegate也只提供一个BindUFunction函数来绑定UObject类的UFUNCTION函数,所以动态多播只能添加UObject的UFUNCTION函数,其中AddUnique会对已添加到多播的委托做检查,如果该函数已经存在在多播中了,就不在添加重复的函数。

1
2
3
4
5
6
7
8
9
void ASubscripterNumberOne::BeginPlay()
{
Super::BeginPlay();
TScriptDelegate<FWeakObjectPtr> SubscriptMagazineFourDelegate;
SubscriptMagazineFourDelegate.BindUFunction(this, TEXT("SubscriptMagazineFour"));
AAnnouncer::GetAnnouncerInstance()->MagazineNumberFour.Add(SubscriptMagazineFourDelegate);

AAnnouncer::GetAnnouncerInstance()->MagazineNumberFour.AddUnique(SubscriptMagazineFourDelegate);
}

注意事项

  • AddRaw和BindRaw使用于C++函数,如果在代理执行之前C++对象已经被销毁,则会导致内存操作违规引起的崩溃。

  • AddLambda和BindLamb如果Lambda表达式有捕获外部变量,且在代理执行之前外部变量已经被销毁,则会导致内存操作违规引起的崩溃。

  • AddWeakLambda/BindWeakLambda、AddUObject/BindUObject、AddUFunction/BindUFunction、AddSP/BindSP、AddThreadSafeSP/BindThreadSafeSP,如果相关对象在代理执行之前被GC回收了,而在执行Execute函数时没有做有效性判断,则会导致内存操作违规引起的崩溃。建议在执行Execute函数之前使用IsBound做有效性判断或直接使用ExecuteIfBound函数。

疑问

在网上看别人的博客和官方的4.27的文档动态多播的绑定都是使用的AddDybamic和AddUniquueDynamic,而我自己在4.26中使用时并没有这两个函数,取而代之的是__Internal_AddDynamic__Internal_AddUniqueDymanic,不知道是引擎代码已经改了,而官方文档没更新还是怎么着。

在查询资料的过程还发现了一个宏FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE,看起来像是一个委托,但是实在是查不到资料,只能有事件自己再去摸索了。

4.代理的执行

单播代理

UE提供Execute函数和ExectueIfBound两个函数供单播代理触发执行函数,其中Execute支持有返回值函数和无返回值函数,但不会做执行函数所属对象的有效性检查,需要手动使用IsBound函数做有效性检查;而ExecuteIfBound只支持无返回值的函数,会在触发执行函数时自动检查执行函数所属对象的有效性,有效的才执行。

1
MagazineNumberOne.Execute(1); 

多播代理

多播代理只有一个Broadcast函数用于触发执行函数,且不会进行一执行函数所属对象的有效性检查,也需要使用IsBound函数手动进行检查。

1
MagazineNumberTwo.Broadcast(2);

5.代理的解绑

单播代理

单播代理在对象被析构时会自动调用UnBind函数进行解绑,或者重新绑定新的函数会自动解绑当前绑定函数,所以UE4.26可能就没有再暴露Unbind函数供外部调用了(看网上的博客是可以直接调用Unbind函数的,我在4.26中已经不能调用了,不知道作者用的是那个版本的引擎)。

不过对于动态单播还是可以使用Unbind和Clear函数来进行函数解绑。

多播代理

多播代理无论是静态多播还是动态多播均提供Clear、Remove和RemoveAll三个函数来做函数解绑,其中Clear是解绑多播中的所有函数;Remove则是解绑FDelegateHandle指定的函数,FDelegateHandle是代理绑定行数的返回值类型;RemoveAll是解绑指定对象的所有函数,不影响已经绑定的其他对象的函数。

6.代理的创建

一个代理的使用流程就是声明代理、声明代理对象、代理对象绑定函数、执行代理、代理解绑。除此之外对于静态单播UE还提供CreateRaw、CreateLambda、CreateWeakLambda、CreateUObject、CreateUFunction、CreateSP、CreateThreadSafeSP等来直接创建一个已经已经绑定了对应函数的静态单播,虽然感觉也没什么用😂。

7.事件

UE的事件效果上几乎和C#的事件一模一样,理解C#的事件对UE的事件就是秒懂,来一张C#的事件说明:

事件就是一个被再次封装的静态多播,声明上,关键字只是把DELEGATE关键字改为EVENT,参数则需要指定一个类名,以便UE在生成反射代码是保证这个事件只能在指定类的内部使用,如:

1
DECLARE_EVENT_OneParam(AAnnouncer,FMagazineEvent, int);

事件的绑定、使用、解绑基本和静态多播一样,可以直接参考静态多播。

8.预制代理

UE预制了大量的代理给我使用,由于量实在是巨大,这里就只列出所在文件了,需要用的时候再直接进源码去看吧。

  • 系统与引擎下相关的预制代理:FCoreDelegates 、FCoreUObjectDelegates,同时UEngine中也几个相关代理;
  • 编辑器相关预制代理:FEditorDelegates、FGameDelegates,同时UWorld中也有几个相关代理;
  • World相关预制代理:FWorldDelegates
  • 引擎子系统相关预制代理:FNetDelegates,子系统的代理基本都分布在各个子系统的类中了;
  • GamePlay相关预制代理:FGameModeEvents及各个GamePlay的类中;

网上有以为大佬可可西整理一篇挺全的博客。

七、指针与引用

UE4的共享指针和C++的智能指针原理和作用基本一致,UE4的共享指针存在的主要目的就是作用于UE4的跨平台。

UE4共享指针的特性:

  • 防止内存溢出;
  • 有线程安全机制;
  • 可虚拟化任何对象;
  • 负载大,内存占用为原生C++指针的两倍。

UE4共享指针共有TSharedPtr、TSharedRef、TWeakPtr、TAutoPtr四种。

在UE中共享指针的作用主要是用来管理C++类对象,由于C++类不在UObject体系中,所以如果C++对象管理不善用于出现内存泄漏、野指针和程序崩溃等问题,共享指针就是为了解决这些问题而存在的。

除了共享指针外UE还提供了很多其他类型的引用。

1.TSharedPtr

  •  TSharedPtr不能指向UObject类,因为UObject类有自己的一套垃圾回收机制,而TSharedPtr也有自己的一套垃圾回收机制,如果使用TSharedPtr指向一个UObject类,当UObject自己回收后,会出现指针不为空,但是对象却被销毁了,所以在指针销毁对象的时候就会出现销毁不存在对象,而导致引擎崩溃。

声明

1
TSharedPtr<FMyClass> emptyPtr;//单独声明出来的共享指针为空指针

初始化

1
2
3
4
5
6
TSharedPtr<FMyClass> onePtr(new FMyClass());
TSharedPtr<FMyClass> twoPtr = MakeShareable<FMyClass>(new FMyClass());
//TSharedPtr不支持直接赋值的初始化,如:
//TharedPtr<MyClass> onePtr;
//onePtr = new Class();
//这样的初始化是错误的

复制

1
TSahredPtr<FMyClass> twoPtr = onePtr;//指针复制,引用计数器+1

查看一块内存的引用计数

1
TSharedPtr<T>::GetSharedReferenceCount()

 一块内存只能被一个TSharedPtr指针初始化,如果一块内存被多个SharedPtr引用,UE4会在编译时直接崩溃,比如下面代码:

1
2
3
UMyObject *obj = NewObject<UMyObject>();
TSharedPtr<UMyObject> onePtr(obj);
TSharedPtr<UMyObject> twoPtr(obj);

 如果想要多个TSharedPtr指针指向一块内存地址,则需要使用TSharedPtr复制。

获取原始指针

1
2
3
TSharedPtr<FMyClass> twoPtr;
twoPtr = MakeShareable<FMyClass>(new FMyClass());
twoPtr.Get();

2.TSharedRef

TSharedRef又称共享引用,相对于共享指针,共享引用由于不能为空,也无法置空,所以使用上比共享指针更安全,和引用一样,共享引用的初始化不能和声明分开,即声明的时候就必须做初始化了,共享引用的初始化可以使用MakeShareable函数或使用共享指针转换。

1
2
3
TSharedRef<FMyClass> oneRef = MakeShareable<FMyClass>(new FClass());
TSharedPtr<FMyClass> Ptr = MakeShareable<FMyClass>(new FClass());
TSharedRef<FMyClass> twoRef = Ptr.toSharedRef();

TSharedPtr和TSharedRef的互相转换

1
2
3
4
5
6
TSharedRef<FMyClass> oneRef = MakeShareable<FMyClass>(new FClass());
TSharedPtr<FMyClass> onePtr = MakeShareable<FMyClass>(new FClass());
//共享指针转共享引用
TSharedRef<FMyClass> twoRef = onePtr.toSharedRef();
//共享引用转共享指针
TSharedPtr<FMyClass> twoPtr = oneRef

3.TWeakPtr

TWeakPtr是对标的STL的weak_ptr的,TweakPtr不对增加对象的引用计数,所以不会影响对象的释放,也就避免了互相引用的死锁而导致的内存溢出,但这也导致了TWeakPtr随时可能被置空的问题,所在使用都需要做有效性判断。

TWeakPtr只能通过TSharedPtr或TSharedRef来初始化。

1
2
3
4
TSharedRef<FCpp> SharedRef = MakeShareable<FCpp>(new FCpp());
TSharedPtr<FCpp> SharedPtr = MakeShareable<FCpp>(new FCpp());
WeakPtr = SharedPtr;
WeakPtr = SharedRef;

需要注意的是,TWeakPtr并没有重写->,所以TWeakPtr不能直接使用指向对象的成员,要想要使用指向对象的成员,则需要先通过Pin()函数将TWeakPtr转换成TSharedPtr再使用。

4.TUniquePtr

TUniquePtr可以保证指向一个对象只能被一个唯一的TUniquePtr指针指向,TUniquePtr不能赋值给其他的指针。

初始化

1
TUniquePtr<FCpp> UniquePtr = MakeUnique<FCpp>(new FCpp());

TUniquePtr使用专门的MakeUnique函数进行初始化。

在使用的过程想到一个疑问,如果使用TUniquePtr指针指向一个对象,然后用另一个TSharedPtr也指向这个对象,直接绕开TUniquePtr而不是使用TUniquePtr赋值TSharedPtr,这样指向这个对象的指针就不止一个了。

事实证明这样子是行不通的,因为共享指针不能直接使用C++指针初始化,也不能指向C++指针。

5.TAutoPtr,TScopedPointer

这两个指针在网上的很多文章里都有出现,然后我自己使用的时候发现根本没有这两个指针,然后查阅官方文档才发向这两个指针在4.15就已经废弃了删掉了。

6.TWeakObjectPtr

TWeakObjectPtr是UE专门用来指向UObject的指针,由于UObject的GC与共享指针的GC是两套系统,我们使用TSharedPtr来指向一个UObject可能会导致一个UObject已经被系统GC了,而TSharedPtr依旧是有效的,所指向的内容却已经没了,而引起的系统崩溃。但有的时候又有需要使用一个指针来引用UObject而又要不影响UObject的正常GC,于是TWeakObjectPtr就运行而生了,当UObject被GC后TWeakObjectPtr也会被自动置为nullptr。

TWeakObjectPtr可以直接指向一个UObject对象,也可以指向一个UObject指针。

1
TWeakObjectPtr<AAnnouncer> WeakObjectPtr = GWorld->SpawnActor<AAnnouncer>();
1
2
AAnnouncer* Announcer = GWorld->SpawnActor<AAnnouncer>();
TWeakObjectPtr<AAnnouncer> WeakObjectPtr = Announcer;

7.TAutoWeakObjectPtr

TAutoWeakObjectPtr是TWeakObjectPtr的TAutoPtr版本,在对象声明周期结束后,指针会自动释放其引用对象。使用方式和TWeakObjectPtr一致。

8.TSharedFromThis

TSharedFromThis不是一个指针而是一个模板类,可供我们使用C++类去继承来创建一个自定义的共享指针。

TSharedFromThis类提供Get方法将共享指针转换成普通指针,提供AsShred方法将普通指针转成共享指针。

1
2
3
4
5
6
7
8
9
#include "CoreMinimal.h"
#include "Templates/SharedPointer.h"

class MYPROJECT_API FCppPtr : public TSharedFromThis<FCppPtr>
{
public:
FCppPtr();
~FCppPtr();
};

9.FSoftObjectPath

FSoftObjectPath是一个记录资源路径的结构体,用于C++暴露给编辑器,然后可以在编辑器中方便快捷的设置C++类需要使用的资源,C++加载蓝图资源需要使用UE给定的路径格式,FSoftObjectPath就可以通过直接选择的方式来提供C++所需要的资源路径。

这里直接使用一个例子来说明可能会更具象点。

创建一个C++类的Actor

.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "OpreateActor.generated.h"

UCLASS()
class MYPROJECT_API AOpreateActor : public AActor
{
GENERATED_BODY()
public:
AOpreateActor();
void Do();
public:
UStaticMeshComponent* StaticMeshComponent;
UPROPERTY(EditAnywhere,meta=(AllowedClasses="Material"))
TArray<FSoftObjectPath> MatArr;
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;

};

.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "TSharedPtr/OpreateActor.h"
#include "Runtime/Engine/Classes/Kismet/GameplayStatics.h"
#include "Delegate/Announcer.h"
#include "Templates/SharedPointerInternals.h"
AOpreateActor::AOpreateActor()
{
PrimaryActorTick.bCanEverTick = true;
ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMesh(TEXT("StaticMesh'/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere'"));
StaticMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
StaticMeshComponent->SetupAttachment(GetRootComponent());
StaticMeshComponent->SetStaticMesh(StaticMesh.Object);
}

void AOpreateActor::Do()
{
int index = FMath::RandHelper(MatArr.Num());
FString MatPath = TEXT("Material'") + MatArr[index].GetAssetPathString() + TEXT("'");
UMaterial* Mat = LoadObject<UMaterial>(nullptr, *MatPath);
UMaterialInstanceDynamic* MatIns = UMaterialInstanceDynamic::Create(Mat, StaticMeshComponent);
StaticMeshComponent->SetMaterial(0,MatIns);
}
void AOpreateActor::BeginPlay()
{
Super::BeginPlay();
EnableInput(UGameplayStatics::GetPlayerController(GWorld, 0));
InputComponent->BindAction("ActorInputP", EInputEvent::IE_Pressed, this, &AOpreateActor::Do);
}
void AOpreateActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
  • 创建一个FSoftObjectPath数组用于存储设置的材质路径,使用meta限定选择时只显示材质对象;
  • 在构造对象时为这个Actor附加一个SceneComponent和StaticMeshComponent,材质需要附着在mesh组件上;
  • 在BeginPlay函数中启动Actor接收输入,并为输入按键绑定执行函数Do();
  • 在Do函数中为Actor设置随机Material。
  • 然后把Actor放入场景。

来看一下效果:

10.TSoftObjectPtr

TSoftObjectPtr是FSoftObjectPath和TWeakObjectPtr的模板类封装,非模板的封装则是FSoftObjectPtr,这两个指针功能上基本一致。按照官方文档的描述,TSoftObjectPtr可以在其保存的弱引用指向的对象不存在时快速的通过FSoftObjectPath保存的路径加载对象到内存中,效果上和tSoftObjectPath差不多,只是省去了我们手动加载资源的步骤。

这里写个例子:

这里通过TSoftObjectPtr来加载两个内存中不存在Actor蓝图类对象Truck和Motorcycle,他们分别继承自C++Actor类ACar,ACar继承自接口IBPInterface,IBPInterface提供一个由蓝图实现的函数接口Log,然后在AOpreateActor的Do函数中来进行这些操作。

关于C++通过接口调用蓝图函数,请移步置接口

使用C++加载蓝图类并调用蓝图类中函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "OpreateActor.generated.h"

UCLASS()
class MYPROJECT_API AOpreateActor : public AActor
{
GENERATED_BODY()
public:
AOpreateActor();
void Do();
public:
UPROPERTY(EditAnywhere)
TArray<TSoftObjectPtr<UObject>> SoftObjectPtrArr;
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void AOpreateActor::Do()
{
for (TSoftObjectPtr<UObject> ObjPtr : SoftObjectPtrArr)
{
FString ActorPath = TEXT("Blueprint'") + ObjPtr.ToSoftObjectPath().GetAssetPathString() + TEXT("'");
UObject* LoadObj = StaticLoadObject(UBlueprint::StaticClass(), NULL, *ActorPath);
if (LoadObj)
{
UBlueprint* Blueprint = CastChecked<UBlueprint>(LoadObj);
FActorSpawnParameters SpawnParams;
ACar* ActorBase = GWorld->SpawnActor<ACar>(Blueprint->GeneratedClass, FVector(0, 0, 0), FRotator(0, 0, 0), SpawnParams);
UClass* ActorClass = ActorBase->GetClass();
if (ActorClass->ImplementsInterface(UBPInterface::StaticClass()))
{
IBPInterface* BPInterface = CastChecked<IBPInterface>(ActorBase);
IBPInterface::Execute_Log(ActorBase);
}
}
}
}

Truck的Log接口实现:

Motorcycle的Log接口实现:

将AOpreateActor放入Level并配置TSoftObjectPtr数组

运行效果:

开始时Level中没有Truck和Motorcycle;

运行后:

11.FSoftClassPath

FSoftClassPath是FSoftObjectPath的子类,使用方式于效果和FSoftObjectPath一模一样,FSoftClassPath解决的是FSoftObjectPath只能引用资产而不能引用蓝图类的问题。FSoftObjectPath如果引用蓝图类会导致编辑器发生中断,而FSoftClassPath引用资产也会造成编辑器中断,并且二者对类型的筛选也是不一样的,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "OpreateActor.generated.h"
UCLASS()
class MYPROJECT_API AOpreateActor : public AActor
{
GENERATED_BODY()
public:
AOpreateActor();
public:
UPROPERTY(EditAnywhere,meta=(AllowedClasses="Material"))
FSoftObjectPath SoftObjectPath;
UPROPERTY(EditAnywhere,meta=(MetaClass = "Car"))
FSoftClassPath SoftClassPath;
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
};

SoftClassPath就可以引用所有继承自ACar的蓝图类了。

12.TSoftClassPtr

TSoftClassPtr就是TSoftObjectPtr的减配版,只能提供FSoftObjectPath不能提供TWeakObjectPtr,除此之外使用和功能都和TSoftObjectPtr一样。

13.TSubclassOf

TSubClassOf基本等于UClass,只是增加了类型安全和类型筛选。

14.TAssestPtr

TAssestPtr是一个配合FStreamableManager进行资产异步加载使用的指向资产的指针,是一个弱指针,指向的资产可以已经被加载了也可以还未被加载。

八、容器

UE的容器主要有TArray,TMap,TSet,TQueue基本上和其他语言里的Array,Map,Set,Queue都没多大区别。

1.TArray

TArray是UE中使用最广泛的容器,一个可扩容动态数组,TArray的定义在Engine\Source\Runtime\Core\Public\Containers\Array.h,是一个模板类,纵观TArray类的代码近3000行,但是变量却只有3个;

  • AllocatorInstance是数组的内存分配器;
  • ArrayNum是数组元素的实际个数;
  • ArrayMax是数组最大可容纳的元素数量;

所以一个空数组的大小就是这三个成员变量的大小,由于分配器也是一个模板类,所以根据分配器的类型不同,空数组的大小也会有所变化,最小是一个指针加两个整型,即16字节。

而一个数组的内存大小就是ArrayMax*sizeof(ElementType)+AllocatorInstance所占用的大小。

TArray的扩容原理

TArray的扩容原理和C++ STL的vector差不多,当数组容量满了之后就会再分配一个更大的内存空间,和vector一样TArray也是分配的一个连续的内存空间,所以在重新分配了内存之后TArray会把旧地址上的内容拷贝到新的内存地址中并释放掉旧内存;在网上没有查到TArray的扩容机制,既然TArray在原理和vctor差不多,那么我想TArray的扩容机制应该也是两倍扩容。

TArray的移动构造函数

与vector不同的是TArray使用了C++11提供的新特性—移动构造函数,我们知道在使用一个数组来初始化另一个数组时会执行TArray的拷贝构造函数,如果数组元素中存在指针引用,那么此时的执行的拷贝构造函数就是深拷贝,即不仅要拷贝指针还要把指针指向的内容也一并拷贝一份,TArray在提供了普通的拷贝构造函数的同时还提供给了移动构造函数,在使用一个数组初始化另一个数组时,移动构造函数只会拷贝旧数组的分配器和数组容量,数组的内容则是直接从旧数组移交给新数组,然后把旧数组恢复到无份配的初始状态,这样旧数组就成了一个空数组。

我们写一段代码来测试一下:

1
2
3
4
5
6
7
TArray<int> arr1 = { 1,2,3 };
TArray<int> arr2;
UE_LOG(LogTemp, Warning, TEXT("Arr1:%d,Arr2:%d"), arr1.Num(), arr2.Num());
arr2 = MoveTemp(arr1);
UE_LOG(LogTemp, Warning, TEXT("Arr1:%d,Arr2:%d"), arr1.Num(), arr2.Num());
TArray<int> arr3(arr2);
UE_LOG(LogTemp, Warning, TEXT("Arr2:%d,Arr3:%d"), arr2.Num(), arr3.Num());

输出:

这里有一个一点是需要注意的,就是使用移动构造的时候必须使用MoveTemp显示调用,否则调用不成功,反而去执行了拷贝构造函数,用例对比就是arr1初始化arr2和arr2初始化arr3。

2.TMap

TMap也是一个经常使用的UE4容器,和TArray不同的是TMap是一个散列结构,TMap的存储空间可能不是一个连续的内存片段,而是通过hash映射来存储的,所以相对于TArray来说TMap的元素查找非常快速。

TMap是通过TSet来实现的,TMap的底层实现原理就是HashTable–哈希表,这与C++STL的Map不一样,STL的Map底层实现原理选择的是Red-Black Tree–红黑树,哈希表于红黑树在数据结构上又有区别,哈希表是基于数组的数据结构,而红黑树则是基于链表的数据结构,这是由TMap在UE4中主要面向的是渲染系统决定的,在渲染系统中高平率的数据操作是查找,在这方面数组相较于链表是有优势的。

TMap的元素都是以TPair<KeyType,ValueType>的形式存储,其中KeyType和ValueType是任意TMap可支持的类型,之所以说是可支持的,是因为不是所有的类型都是可以作为TMap的键的,按照官方文档的说法,只有支持GetTypeHash函数并提供==运算符重载来比较键值是否相等的类型才能作为TMap的键类型,而值就可以是任意类型了。

TMap的内存分配原理

TMap是支持自选内存分配器来控制内存分配行为的,当然一般情况下我们都是使用默认的内存分配器—FDefaultSetAllocator ,这里TMap和TSet是一致的,顺带一提,TArray使用默认内存分配器是FDefaultAllocator ,UE4的内存分配器的篇幅也挺长的,详情可查看:https://www.cnblogs.com/kekec/p/12012537.html。

当我们创建一个TMap时:

1
TMap<int FString> StrMap

此时StrMap是一个空Map,没有被分别任何内存。

和TArray一样,当我们往TMap中添加第一个元素时,TMap会使用配置的内存分配来申请一块内存空间,当这块内存空间被使用完之后,也会根据内存分配器的算法来进行扩容。

自定义TMap键值

前面有提到过,一个类型无论是结构体或是自定义类,只要满足重载==的自身类型比较和提供Hash值的GetTypeHash函数,则这个类型就可以作为TMap的键,所以自定键值的方式就很明确了。

结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
USTRUCT()
struct FCusKey
{
GENERATED_BODY()
UObject* Obj;
friend inline bool operator==(const FCusKey& A, const FCusKey& B)
{
return A.Obj == B.Obj;
}
friend inline uint32 GetTypeHash(const FCusKey& Key)
{
uint32 Hash = 0;
Hash = HashCombine(Hash, GetTypeHash(Key.Obj));
return Hash;
}
};

应用:

1
2
3
4
5
6
7
void AContainerctor::CusTMapFun()
{
TMap<FCusKey, FString> CusMap;
FCusKey CusKey;
CusKey.Obj = NewObject<UObject>();
CusMap.Add(CusKey, TEXT("FirstObj"));
}

自定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UE4CPP_API FCusClassKey
{
public:
FCusClassKey();
~FCusClassKey();

friend bool operator==(FCusClassKey& A,FCusClassKey& B)
{
return A == B;
}

friend int GetTypeHash(FCusClassKey& Key)
{
int Hash = 0;
Hash = HashCombine(Hash, GetTypeHash(Key));
return Hash;
}
};

应用:

1
2
3
4
5
void AContainerctor::CusTMapFun()
{
TMap<FCusClassKey*, int> CusClassMap;
CusClassMap.Add(new FCusClassKey(), 1);
}

需要一提的是,两个函数中friend关键字是必须的否则编译就会报错,而inlineconst都不是必须的,甚至GetTypeHash的返回值也不一定必须是uint32。

3.TSet

UE4的TSet是通过TSparseArray实现的,本质上是一个特殊的数组,特殊之处在于TSet虽然是一个数组,但是TSet是散列的,内部存储元素不是连续的,并且TSet使用Hash值来映射index。

TSet和TMap极其相似,毕竟TMap就是使用TSet实现的,TSet也通过键值来操作元素,只不过TSet的键值和元素是同一个,同时TSet也能像TMap一样通过配置内存分配器来控制内存分配行为,也可以通过实现 重载==和GetTypeHash函数来自定义键值。

TSet与其他容器不同的是,TSet是一个自排序容器,添加进入TSet的元素,会按照TSet的默认排序规则自动排序,因为这个特性是的TSet对于元素的查找,删除,添加等操作都极为迅速,时间复杂度都是常数级的。

4.TTuple

看到TTuple时我是吃惊的,因为我一直以为元组是脚本类语言特有的容器,没想到UE4也实现了,更深入了解后才发现,原来C++中也有实现std::tuple。

不过UE4中元组的创建就不像脚本语言那样可以直接使用[]来得方便了,而是使用了专门的创建函数MakeTuple,这一点和智能指针有点类似。

另外UE4元组的元素数量和类型在声明元组时就已经确定了,不可以在运行时额外添加,如:

1
TTuple<int, bool, float, FString> tuple1(1, false, 1.0f, TEXT("aaa"));

即使我们使用auto类型,也必须配合static使用,而静态类型又要求设置初始值,所以实际上,元组的类型依然是在声明时就定死的,无法在运行时添加声明以外的元素类型和数量。

另外UE4的代理就是使用元组来实现的。

5.UE4容器的注意事项

理论上说,由于TArray的扩容机制,在数组存储空间用完时UE4会重新分配一个更大的存储空间并把数组迁移到新的空间去,这就会导致在扩容前保存的数组元素的引用出现引用失效的可能,而TSet,TMap的底层都是TArray所以都会有这样的问题。但是在我的实际测试中,把我笔记本16G内存榨干了似乎也没有出现引用失效的情况。

九、宏

UE4中提供大量的宏于开发者使用,这些宏的作用就类似库函数一般。只是需要注意的是,有的宏末尾没有“;”而有的宏末尾需要加“;”,在下面的书写中需要“;”的宏我就直接在末尾加“;”,不需要的则不加。UE4不人性化的一点是很多宏里面的参数在VS中没有提示,也没有颜色变化,更不会提示错误,所以在给宏添加参数时尤其要注意书写正确。

1.UE_LOG();

作用

UE_LOG();宏用于向UE4控制台输出内容。

使用

UE4提供三个级别的UE_LOG;宏,

1
2
3
UE_LOG(LogTemp, Log, TEXT("Hello World!"));
UE_LOG(LogTemp, Warning, TEXT("Hello World!"));
UE_LOG(LogTemp, Error, TEXT("Hello World!"));
  • LogTemp:是UE4提供给我们的一个临时UE_LOG();宏标签,这个标签是可以自定义的,如此我们便可以在不同的模块中使用不同标签的UE_LOG();宏,极大的便利了调试,这也是UE_LOG();宏的强大之处。
  • Log/Warning/Error:这是UE_LOG();宏的三个级别,分别是日志级别–在控制台中输出白色字体;警告级别–在控制台中输出黄色字体,并有Warning提示;错误级别–在控制台中输出红色字体,并有Error提示。
  • **TEXT()**:TEXT()也是一个宏用于将我们自定义的额字符串输出到控制台。

输出变量

UE_LOG()宏可以输出FString字符串,且字符串类型中只能输出FString。

1
UE_LOG(LogTemp, Error, TEXT("%s"),*str);

 使用*str是因为FString是UE4的封装类,定义的FString对象都是指针。

日志等级

UE对日志的打印分了很多个级别,不同的级别打印的内容有所差异,这里就直接摘抄一段狼图腾_094d的内容了。

  • Fatal

Fatal等级日志总是会打印到控制台和日志文件以及crash文件中,甚至logging功能被禁用。

使用Fatal等级的日志时,当代码运行到这个日志出会直接触发代码中断,是严重等级最高的日志等级。

  • Error

Error等级日志被打印到控制台和日志文件中,默认以红色显示。

  • Warning

Warning等级日志被打印到控制台和日志文件中,默认以黄色显示。

  • Display

Display等级日志被打印到控制台和日志文件中。

  • Log

Log等级日志打印到日志文件中但是不出现在game模式的控制台中,但是通过编辑器的日志输出窗口还是能够被看到。

  • Verbose

Verbose等级日志打印到日志文件中但是不出现在game模式的控制台中,这个通常被用来作为详细日志和调试使用。

  • VeryVerbose

VeryVerbose等级日志被打印到日志文件中但是不出现在game模式的控制台中,这通常用来打印非常详细的日志记录否则会产生很多不必要的垃圾输出。

严重级别从上往下一次递减,详细程度从上往下一次递增,日志类别的详细定义在LogVerbosity.h这个头文件中。

自定义UE_LOG输出标签

UE提供DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity)宏来让开发者自定日志标签,其中:

  • CategoryName:日志类别的名称;
  • DefaultVerbosity:这个类别的日志等级,主要用于在控制台或ini文件里进行等级覆盖。
  • CompileTimeVerbosity:这个类别的日志可用的最大等级,如,设置成Warning,那么这个类别的日志就只能使用Warning、Error、Fatal三个等级,如果设置成All,那么这个类别就可以使用所有的日志等级,如果使用时传入的等级大于CompileTimeVerbosity规定的等级,那么这个日志行将不会被编译进程序里,运行时这一行代码不运行,这可以很好地进行批量地日志屏蔽。

在.h文件中声明

日志等级的覆盖

在代码里写好了的日志如果想要修改打印等级,除了直接修改代码外,UE还提供了更方便的方法,通过修改DefaultEngine.ini文件或输入命令行来修改日志等级。

  • 通过运行时命令行来动态地修改日志等级

    运行通过按~键通过控制台输入Log [CategoryName] [Level],(CategoryName:日志类别的名称,Level:等级)可以达到临时的动态的修改日志等级

  • 通过修改Engine.ini来修改日志等级:我们在Engine.ini中添加一个

    1
    2
    3
    [Core.Log]
    global=Log
    CategoryName=Error

    global:对全局生效,覆盖所有日志的等级;CategoryName:日志类别的名称。

  • 通过在Cmd启动exe在命令行后面接入指令或在exe快捷方式后面接入指令的方式修改日志等级

    这里以快捷方式为例(cmd的效果是一样的),首先打包一个windwos包,为exe创建一个快捷方式,打开快捷方式的属性,在快捷方式后面接入-LogCmds="CategoryName Error,bar off"(CategoryName :日志类别的名称),启动游戏后就会按CompileTimeVerbosity的规则屏蔽或显示日志。

3.UCLASS()/UPROPERTY()/UFUNCTION()

作用

这三个宏主要用于为类(UCLASS()),成员变量(UPROPERTY())和成员函数(UFUNCTION())指定一些特性, 他们用来指定被修饰对象可以被复制、被序列化,并可从蓝图中进行访问。垃圾回收器还使用它们来追踪对 UObject 的引用数。

UCLASS()宏的参数

参数 作用
Abstract 抽象类说明符将类声明为 “抽象基类”, 防止用户在虚幻编辑器中向世界添加此类的参与者, 或在游戏中创建此类的实例。这对于那些本身没有意义的类很有用。例如, ATriggerBase 基类是抽象的, 而 ATriggerBox 子类别不是抽象的, 您可以在世界上放置一个 ATriggerBox 类的实例, 并且它是有用的, 而 ATriggerBase 的实例本身并不有用
AdvancedClassDisplay AdvancedClassDisplay 类说明符强制类的所有属性只显示在 “详细信息” 面板的 “高级” 部分中, 默认情况下隐藏在 “视图” 中。若要在单个属性上重写此项, 请在该属性上使用 SimpleDisplay 说明符
AutoCollapseCategories=(Category1, Category2, …) 类说明符取消对父类AutoExpandCategories说明符的列出类别的影响
DontAutoCollapseCategories=(Category, Category, …) 否定从父类继承的列出类别的AutoCollapseCategories说明符
AutoExpandCategories=(Category1, Category2, …) 指定应在该类的对象的 “虚幻编辑器” 属性窗口中自动展开的一个或多个类别。若要自动声明为无类别的变量, 请使用声明变量的类的名称
Blueprintable / NotBlueprintable 将此类公开为创建蓝图的可接受基类。默认是NotBlueprintable,除非继承,否则该说明符由子类继承
BlueprintType 将此类公开为可用于蓝图中的变量的类型
ClassGroup=GroupName 表示在Actor浏览器中启用Group View后,虚幻编辑器的Actor浏览器应该在指定的GroupName中包含此类以及此类的任何子类 CollapseCategories / DontCollapseCategories
CollapseCategories / DontCollapseCategories 表示不应将此类的属性分组到虚幻编辑器属性窗口中的类别中。这个说明符被传播给子类;但是,子类可以使用DontCollapseCategories说明符来覆盖它
Config=ConfigName 表示允许此类在配置文件 (. ini) 中存储数据。如果有使用 config 或 globalconfig 说明符声明的任何类属性, 则此指定符将导致这些属性存储在命名的配置文件中。此说明符被传播到所有子类, 不能被否定, 但是子类可以通过 re-declaring 配置说明符并提供不同的 ConfigName 来更改配置文件。常见的 ConfigName 值是 “Engine”、”Editor”、”Input” 和 “Game”
Const 该类中的所有属性和函数都是const的,并以const的形式导出。该说明符由子类继承
ConversionRoot Root转换将一个子类限制为仅能够转换为第一个Root类的子类,并上升到层次结构
CustomConstructor 阻止自动生成构造函数声明
DefaultToInstanced 这个类的所有实例都被认为是“实例化”的。实例化的类(组件)在构建时被复制。该说明符由子类继承
DependsOn=(ClassName1, ClassName2, …) 列出的所有类将在该类之前编译。类必须在同一个 (或上一个) 包中指定一个类。可以使用由逗号分隔的单个取决于行指定多个依赖项类, 也可以为每个类使用单独的取决于行指定。当类使用在另一个类中声明的结构或枚举时, 这一点很重要, 因为编译器只知道它已编译的类中的内容
Deprecated 这个类不推荐使用,而且这个类的对象在序列化的时候不会被保存。该说明符由子类继承
EditInlineNew / NotEditInlineNew 表示可以从 “虚幻编辑器” 属性窗口创建此类的对象, 而不是从现有资产引用。默认行为是只有对现有对象的引用才可以通过属性窗口分配。此说明符被传播到所有子类;子类可以使用 NotEditInlineNew 说明符来重写此说明符
HideCategories=(Category1, Category2, …) 列出应隐藏在此类对象的 “虚幻编辑器” 属性窗口中的一个或多个类别。若要隐藏声明为 “no” 类别的属性, 请使用声明该变量的类的名称。此说明符被传播到子类
ShowCategories=(Category1, Category2, …) 为所列出的类别取消一个HideCategories说明符(从基类继承)
HideDropdown 防止在虚幻编辑器属性窗口组合框中显示此类
HideFunctions=(Category1, Category2, …) 从属性查看器隐藏指定类别中的所有函数
ShowFunctions=(Category1, Category2, …) 在属性查看器中显示列出的类别中的所有功能
HideFunctions=FunctionName 隐藏属性查看器中的指定名称的函数
ShowFunctions=FunctionName 显示属性查看器中的指定名称的函数
Intrinsic 这表明这个类是直接用C ++声明的,并且没有虚幻头文件工具生成的样板文件。不要在新类上使用这个说明符
MinimalAPI 仅导致要导出的类的类型信息供其他模块使用。类可以被强制转换, 但不能调用类的函数 (内联方法除外)。这提高了编译时间, 因为不为不需要在其他模块中访问所有函数的类导出所有内容
NoExport 表示此类的声明不应包含在由标头生成器自动生成的 c++ 头文件中。必须在单独的头文件中手动定义 c++ 类声明。仅对本机类有效。不要将此用于新类
Placeable/NotPlaceable 表示可以在编辑器中创建此类, 并将其置于关卡、UI 场景或蓝图中 (具体取决于类类型)。此标志将传播到所有子类;子类可以使用 NotPlaceable 说明符来重写此标志
Transient/NonTransient 属于此类的对象将永远不会保存到磁盘。这与某些非永久性的自然类 (如播放器或窗口) 结合使用非常有用。此说明符被传播到子类, 但可以由 NonTransient 说明符重写
Within=OuterClassName 这个类的对象不能存在于一个OuterClassName对象的实例之外。这意味着创建这个类的Object需要提供一个OuterClassName的实例作为其外部对象

参考博客: https://blog.csdn.net/u012793104/article/details/78547655

UPROPERTY()宏的参数

参数 作用
AdvancedDisplay 属性在“详细信息”面板的高级下拉列表中
AssetRegistrySearchable 表明此属性及其值将会为任意将其作为成员变量而包含的资源类示例被自动添加到资源注册中。不可用于结构体属性或参数
BlueprintAssignable 仅能用于Multicast代理。应显示该属性,以供在蓝图中分配
BlueprintCallable 仅能用于Multicast代理。应显示该属性,以在蓝图代码中调用
BlueprintReadOnly 设置属性为蓝图只读。会在蓝图脚本中为被修饰的变量提供 Get 方法,没有 Set 方法
BlueprintReadWrite 设置属性为蓝图读写。会在蓝图脚本中为被修饰的变量提供 Get 和 Set 方法
BlueprintGetter 指定一个自定义存取器函数。如果这个属性没有被标记为BlueprintSetter或者BlueprintReadWrite,那么它就是隐式的BlueprintReadOnly。用法:BlueprintGetter = GetterFunctionName()
BlueprintSetter BlueprintSetter属性有一个自定义的mutator函数,并用BlueprintReadWrite隐式标记。注意,必须指定mutator函数,用法BlueprintSetter = SetterFunctionName
Category = “TopCategory` SubCategory
Config 这个变量将被配置。当前值可以保存到与类关联的.ini文件中,并在创建时加载。无法在默认属性中给出值。意味着ReadOnly
Const 这个变量是常量,应该导出为常量。在编辑器中const属性将是不可修改的
DuplicateTransient 表示变量值应在任意类型的重复过程中(复制/粘贴, 二进制文件复制等)被重置为类默认值
EditAnywhere 此属性可以通过属性窗口,原型和实例进行编辑(原型指的是类模板,实例指的是具体的对象实例),这里要注意,不要把指针变量设置成EditAnywhere
VisibleAnywhere 此属性在所有属性窗口中都可见,但无法编辑。这个标签与“Edit”标签不兼容
EditDefaultsOnly 该属性可以由属性窗口编辑,但只能在原型上编辑。该操作符与“Visible”标签不兼容
VisibleDefaultOnly 此属性仅在原型的属性窗口中可见,并且不能被编辑。这个标签与“Edit”标签不兼容
EditInstanceOnly 这个属性可以被属性窗口编辑,但只能在实例上编辑,而不能在原型上编辑。该操作符与“Visible”标签不兼容
VisibleInstanceOnly 此属性仅在实例的属性窗口中可见,而不在原型中显示,且无法编辑。这个标签与“编辑”标签不兼容
EditFixedSize 只对动态数组有用。这将防止用户通过虚幻编辑器属性窗口更改数组的长度
EditInline 允许用户编辑虚幻编辑器属性检查器中由该变量引用的对象的属性(仅用于对象引用,包括Object引用数组)
Export 仅适用于对象属性 (或对象数组)。指示在复制对象 (用于复制/粘贴) 或导出到 T3D 时, 应将分配给此属性的对象整体导出为对象块, 而不是仅输出对象引用本身
GlobleConfig 像Config一样工作,只是你不能在子类中覆盖它。无法在默认属性中给出值。意味着ReadOnly
Instanced 对象 (UCLASS) 属性。创建此类的实例时, 将为默认情况下分配给该变量的对象提供一个唯一的副本。用于在类默认属性中定义的实例子。类似 EditInline 和 Export 修饰符
Interp 表示该值可由Matinee的浮点或向量属性轨迹来随时间驱动
Localized 此变量的值将定义本地值。最常用于字符串。ReadOnly
Native C++代码负责对其序列化并显示给GC
NoClear 防止该对象引用在编辑器中被设置为None.隐藏编辑器的清除(以及浏览)按钮
NoExport 仅对native类有效。此变量不应被包含在自动生成的类声明中
NonPIEDuplicateTeansient 在复制过程中,该属性将被重置为默认值,除非复制PIE会话
NonTransactional 表示对此变量值所做的更改将不会包含在编辑器的撤销/重做历史记录中
NotReplicated 跳过复制。这只适用于服务请求函数中的结构成员和参数
Ref 该值在函数调用后被复制出来。仅在函数参数声明中有效
Replicated 变量应通过网络进行复制
ReplicatedUsing = FunctionName ReplicatedUsing标签指定了一个回调函数,当通过网络更新变量时执行回调函数
RepRetry 仅用于结构体属性。如无法被完全发送,请重试复制此属性(例如,对象引用尚无法通过节点网络来进行序列化)。对于简单引用来说,这是一个默认值,但对结构体来说,由于带宽消耗,很多情况下我们不需要。所以除非此标识被定义,否则其会被禁用
SaveGame 此说明符是一种简单的方法,可以在属性级别为检查点/保存系统显式包含字段。该标志应设置在所有意图成为已保存游戏一部分的字段上,然后可使用代理归档程序对其进行读取/写入
SerializeText 应将Native属性序列化为文本(ImportText,ExportText)
SkipSerialization 该属性不会被序列化,但仍然可以导出为文本格式(例如复制/粘贴)
SimpleDisplay “Visible”或“Edit”属性显示在“详细信息”面板中,不显示“高级”部分即可见
TextExportTransient 此属性不会被导出为文本格式(例如复制/粘贴)
Transient 属性是暂时的,这意味着它不会被保存或加载。以这种方式标记的属性在加载时将被填满

参考博客: https://blog.csdn.net/u012793104/article/details/78480085

UPROPERTY()宏提供了一些元数据说明符,元数据说明符可以对变量做一些限制,比如输入值的大小限定在某一个范围内,如:

1
UPROPRETY(meta=(ClampMin=-5.0f,ClampMax=5.0f,UIMin=-5.0f,UIMax=5.0f))

详细的说明参官方文档: https://docs.unrealengine.com/zh-CN/Programming/UnrealArchitecture/Reference/Metadata/index.html

UFUNCTION()宏的参数

参数 作用
BlueprintAuthorityOnly 如果在具有网络权限的计算机(服务器,专用服务器或单人游戏)上运行,此功能只能从Blueprint代码执行,如无网络权限,则该函数将不会从蓝图代码中执行
BlueprintCallable 该函数可以在蓝图或关卡蓝图图表中执行
BlueprintCosmetic 此函数为修饰函数而且无法运行在专属服务器上
BlueprintGetter 修饰自定义的Getter函数专用,该函数将用作Blueprint暴露属性的访问器。这个说明符意味着BlueprintPure和BlueprintCallable。参考:https://blog.csdn.net/u012793104/article/details/78480085
BlueprintSetter 修饰自定义的Setter函数专用,此函数将用作Blueprint暴露属性的增变器。这个说明符意味着BlueprintCallable。参考:https://blog.csdn.net/u012793104/article/details/78480085
BlueprintImplementableEvent 此函数可以在蓝图或关卡蓝图图表内进行重载不能修饰private级别的函数,函数在C++代码中不需要实现定义
BlueprintInternalUseOnly 表示该函数不应该暴露给最终用户
BlueprintNativeEvent 此函数将由蓝图进行重载,但同时也包含native类的执行。提供一个名称为[FunctionName]_Implementation的函数本体而非[FunctionName];自动生成的代码将包含转换程序,此程序在需要时会调用实施方式
BlueprintPure 该函数不会以任何方式影响拥有对象,并且可以在蓝图或级别蓝图图表中执行
CallInEditor 该函数可以在编辑器中通过详细信息面板中的按钮在选定实例中调用
Category = “TopCategory` `SubCategory|…”
Client 此函数仅在该函数从属对象所从属的客户端上执行。提供一个名称为[FunctionName]_Implementation的函数主体,而不是[FunctionName]; 自动生成的代码将包含一个转换程序来在需要时调用实现方法
CustomThunk UnrealHeaderTool(虚幻头文件工具)的代码生成器将不会为此函数生成execFoo转换程序; 可由用户来提供
Exec 此函数可从游戏中的控制台中执行。Exec命令仅在特定类中声明时才产生作用,此标记修饰的函数应在可以接受输入的类中,才能正常接受命令
NetMilticast 无论角色的NetOwner如何,该函数都在服务器上本地执行并复制到所有客户端
Reliable Reliable函数在网络间进行复制,并会忽略带宽或网络错误而被确保送达。仅在与客户端或服务器共同使用时可用
UnReliable UnReliable函数在网络间复制,但可能会由于带宽限制或网络错误而传送失败。仅在与客户端或服务器一起使用时有效
SealeEvent 这个函数不能在子类中重写。 SealedEvent关键字只能用于事件。对于非事件函数,声明它们是static的还是final的来封闭它们
ServiceRequest ServiceRequest函数是一个RPC服务请求
ServiceResponse ServiceResponse函数是一个RPC服务响应
Server 此函数仅在服务器上执行。提供一个名称为[FunctionName]_Implementation的函数主体,而不是[FunctionName]; 自动生成的代码将包含一个转换程序来在需要时调用实现方法
WithValidation 声明一个名为与main函数相同的附加函数,但将_Validation添加到最后。该函数采用相同的参数,并返回一个布尔值来指示是否应该继续调用主函数

UFUNCTION()宏也提供了元数据说明符,元数据说明符可以对参数做一些限制,这里不再列出,详细的说明参官方文档: https://docs.unrealengine.com/zh-CN/Programming/UnrealArchitecture/Reference/Metadata/index.html

4.meta

meta是UCLASS、UPROPERTY、UFUNCTION、UINTERFACE、USTRUCT、UENUM宏使用的元数据说明,用以描述类、属性、函数、接口、结构体和枚举,对于不同的类型meta有不同的元数据说明符。

这里就直接摘抄UE官网的内容了。

类元数据说明符

说明符 描述
BlueprintSpawnableComponent 如果存在,组件类可以由蓝图生成。
BlueprintThreadSafe 仅对蓝图函数库有效。该说明符将此类中的函数标记为可在动画蓝图中的非游戏线程上调用。
ChildCannotTick 用于 Actor 和 Component 类。如果原生类无法勾选,则基于此 Actor 或组件的蓝图生成的类将永远无法勾选,即使bCanBlueprintsTickByDefault为真。
ChildCanTick 用于 Actor 和 Component 类。如果原生类无法勾选,则基于此 Actor 或组件的蓝图生成的类可以bCanEverTick覆盖该标志,即使该标志bCanBlueprintsTickByDefault为 false。
DeprecatedNode 对于行为树节点,表示该类已弃用,编译时将显示警告。
DeprecationMessage="Message Text" 具有此元数据的已弃用类将包含此文本以及蓝图脚本在编译期间生成的标准弃用警告。
DisplayName="Blueprint Node Name" 蓝图脚本中此节点的名称将替换为此处提供的值,而不是代码生成的名称。
DontUseGenericSpawnObject 不要使用蓝图脚本中的通用创建对象节点生成类的对象;此说明符仅适用于既不是 Actor 也不是 Actor 组件的蓝图类型类。
ExposedAsyncProxy 在异步任务节点中公开此类的代理对象。
IgnoreCategoryKeywordsInSubclasses 用于使类的第一个子类忽略所有继承的ShowCategories和说明HideCategories符。
IsBlueprintBase="true/false" 说明此类是(或不是)可接受的用于创建蓝图的基类,类似于Blueprintable或“NotBlueprintable”说明符。
KismetHideOverrides="Event1, Event2, .." 不允许被覆盖的蓝图事件列表。
ProhibitedInterfaces="Interface1, Interface2, .." 列出与类不兼容的接口。
RestrictedToClasses="Class1, Class2, .." 蓝图函数库类可以使用它来限制对列表中命名的类的使用。
ShortToolTip="Short tooltip" 一个简短的工具提示,在某些情况下使用完整的工具提示可能会让人不知所措,例如父类选择器对话框。
ShowWorldContextPin 指示放置在此类拥有的图表中的蓝图节点必须显示其世界上下文引脚,即使它们通常是隐藏的,因为此类的对象不能用作世界上下文。
UsesHierarchy 指示类使用分层数据。用于实例化详细信息面板中的分层编辑功能。
ToolTip="Hand-written tooltip" 覆盖从代码注释自动生成的工具提示。

属性元数据说明符

说明符 描述
AllowAbstract="true/false" 用于SubclassSoftClass属性。指示抽象类类型是否应显示在类选择器中。
AllowedClasses="Class1, Class2, .." 用于FSoftObjectPath属性。逗号分隔列表,指示要在资产选择器中显示的资产的类类型。
AllowPreserveRatio 用于FVector属性。在详细信息面板中显示此属性时,它会导致添加比率锁定。
ArrayClamp="ArrayProperty" 用于整数属性。将可在 UI 中输入的有效值限制在 0 和命名数组属性的长度之间。
AssetBundles 用于SoftObjectPtrSoftObjectPath属性。主数据资产中使用的 Bundle 名称列表,用于指定此引用属于哪些 Bundle。
BlueprintBaseOnly 用于SubclassSoftClass属性。指示类选择器中是否应仅显示蓝图类。
BlueprintCompilerGeneratedDefaults CopyPropertiesForUnrelatedObjects属性默认值由蓝图编译器生成,在编译后调用函数时不会被复制。
ClampMin="N" 用于浮点和整数属性。N指定可以为属性输入的最小值。
ClampMax="N" 用于浮点和整数属性。N指定可以为属性输入的最大值。
ConfigHierarchyEditable 此属性被序列化为 config ( .ini) 文件,并且可以在配置层次结构中的任何位置设置。
ContentDir FDirectoryPath属性使用。表示将使用Content文件夹内的 Slate 样式目录选择器选择路径。
DisplayAfter="PropertyName" PropertyName只要两个属性属于同一类别,该属性就会在名为 的属性之后立即显示在蓝图编辑器中,无论其在源代码中的顺序如何。如果多个属性具有相同的DisplayAfter值和相同的DisplayPriority值,它们将按照在头文件中声明的顺序出现在命名属性之后。
DisplayName="Property Name" 为此属性显示的名称,而不是代码生成的名称。
DisplayPriority="N" 如果两个属性具有相同的DisplayAfter值,或者属于同一类别且没有DisplayAfter元标记,则此属性将确定它们的排序顺序。最高优先级值为 1,表示DisplayPriority值为 1 的属性将出现在DisplayProirity值为 2 的属性之上。如果多个属性具有相同的DisplayAfter值,它们将按照它们在标题中声明的顺序出现文件。
DisplayThumbnail="true" 表示该属性是一个资产类型,它应该显示所选资产的缩略图。
EditCondition="BooleanPropertyName" 命名一个布尔属性,用于指示是否禁用编辑此属性。放“!” 在属性名称反转测试之前。EditCondition 元标记不再局限于单个布尔属性。它现在使用成熟的表达式解析器进行评估,这意味着您可以包含完整的 C++ 表达式。
EditFixedOrder 防止通过拖动对数组元素进行重新排序。
ExactClass="true" 用于FSoftObjectPath与 结合使用的属性AllowedClasses。指示是否只能使用中指定的确切类AllowedClasses,或者子类是否也有效。
ExposeFunctionCategories="Category1, Category2, .." 指定在蓝图编辑器中构建功能列表时应公开其功能的类别列表。
ExposeOnSpawn="true" 指定该属性是否应在此 Class 类型的 Spawn Actor 节点上公开。
FilePathFilter="FileType" FFilePath属性使用。指示要在文件选择器中显示的路径过滤器。常用值包括“uasset”和“umap”,但这些不是唯一可能的值。
GetByRef 使该属性的“获取”蓝图节点返回对该属性的 const 引用,而不是其值的副本。仅可用于稀疏类数据,并且仅在NoGetter不存在时使用。
HideAlphaChannel 用于FColorFLinearColor属性。指示Alpha在详细信息中显示属性小部件时应隐藏该属性。
HideViewOptions 用于SubclassSoftClass属性。隐藏在类选择器中更改视图选项的能力。
InlineEditConditionToggle 表示布尔属性仅在其他属性中作为编辑条件切换内联显示,不应显示在其自己的行上。
LongPackageName FDirectoryPath属性使用。将路径转换为长包名。
MakeEditWidget 用于变换或旋转器属性,或变换或旋转器数组。指示该属性应在视口中作为可移动小部件公开。
NoGetter 导致蓝图生成不为此属性生成“获取”节点。仅可用于稀疏类数据。

函数元数据说明符

说明符 描述
AdvancedDisplay="Parameter1, Parameter2, .." 逗号分隔的参数列表将显示为高级引脚(需要 UI 扩展)。
AdvancedDisplay=N 替换N为数字,第N个之后的所有参数都会显示为高级引脚(需要UI扩展)。例如,“AdvancedDisplay=2”会将除前两个参数之外的所有参数都标记为高级)。
ArrayParm="Parameter1, Parameter2, .." 指示BlueprintCallable函数应使用调用数组函数节点,并且列出的参数应视为通配符数组属性。
ArrayTypeDependentParams="Parameter" 使用时ArrayParm,该说明符表示一个参数,它将确定ArrayParm列表中所有参数的类型。
AutoCreateRefTerm="Parameter1, Parameter2, .." 列出的参数虽然通过引用传递,但如果它们的引脚断开连接,将具有自动创建的默认值。这是蓝图的一项便利功能,通常用于阵列引脚。
BlueprintAutocast BlueprintPure仅由蓝图函数库中的静态函数使用。将自动为函数的返回类型和第一个参数的类型添加一个强制转换节点。
BlueprintInternalUseOnly 该函数是一个内部实现细节,用于实现另一个函数或节点。它永远不会直接暴露在蓝图图表中。
BlueprintProtected 此函数只能在蓝图中的所属对象上调用。它不能在另一个实例上调用。
CallableWithoutWorldContext 用于BlueprintCallable具有WorldContextpin 的函数,以指示即使其 Class 未实现该函数也可以调用该GetWorld函数。
CommutativeAssociativeBinaryOperator 指示BlueprintCallable函数应使用 Commutative Associative Binary 节点。该节点没有引脚名称,但有一个添加引脚按钮,可以创建额外的输入引脚。
CompactNodeTitle="Name" 指示BlueprintCallable函数应以紧凑显示模式显示,并提供在该模式下显示的名称。
CustomStructureParam="Parameter1, Parameter2, .." 列出的参数都被视为通配符。此说明符需要UFUNCTION-level 说明符CustomThunk,这将要求用户提供自定义exec函数。在这个函数中,可以检查参数类型,并根据这些参数类型进行适当的函数调用。UFUNCTION永远不应该调用 基础,如果是,则应该断言或记录错误。要声明自定义函数,请使用原始函数名称的exec语法。DECLARE_FUNCTION(execMyFunctionName)``MyFunctionName
DefaultToSelf 对于BlueprintCallable函数,这表明 Object 属性的命名默认值应该是节点的自身上下文。
DeprecatedFunction 对该函数的任何蓝图引用都会导致编译警告,告知用户该函数已被弃用。您可以使用元数据说明符添加到弃用警告消息(例如,提供有关替换弃用函数的说明)DeprecationMessage
DeprecationMessage=”消息文本” 如果该函数已弃用,则在尝试编译使用它的蓝图时,此消息将添加到标准弃用警告中。
DeterminesOutputType="Parameter" 函数的返回类型将动态更改以匹配连接到命名参数引脚的输入。参数应该是模板类型,例如TSubClassOf<X>or TSoftObjectPtr<X>,其中函数的原始返回类型是X*或具有值类型的容器X*,例如TArray<X*>.
DevelopmentOnly 标记为的函数DevelopmentOnly只会在开发模式下运行。这对于调试输出等功能很有用,预计交付的产品中不存在该功能。
DisplayName="Blueprint Node Name" 蓝图中此节点的名称将替换为此处提供的值,而不是代码生成的名称。
ExpandEnumAsExecs="Parameter" 对于BlueprintCallable函数,这表示应该为enum参数使用的每个条目创建一个输入执行引脚。参数必须是具有UENUM标记的枚举类型。
HidePin="Parameter" 对于BlueprintCallable函数,这表明参数引脚应该从用户的视图中隐藏。以这种方式,每个功能只能隐藏一个引脚。
HideSelfPin 隐藏“self”引脚,它指示正在调用函数的对象。“self”引脚在BlueprintPure与调用蓝图的类兼容的函数上自动隐藏。HideSelfPin经常使用元标记的函数也使用说明DefaultToSelf符。
InternalUseParam="Parameter" 与 类似HidePin,这从用户的视图中隐藏了命名参数的引脚,并且每个函数只能用于一个参数。
KeyWords="Set Of Keywords" 指定搜索此函数时可以使用的一组关键字,例如在蓝图图表中放置节点以调用函数时。
Latent 表示潜在动作。潜在动作有一个类型参数FLatentActionInfo,该参数由说明LatentInfo符命名。
LatentInfo="Parameter" 对于 LatentBlueprintCallable函数,指示哪个参数是 LatentInfo 参数。
MaterialParameterCollectionFunction 对于BlueprintCallable函数,指示应使用材质覆盖节点。
NativeBreakFunc 对于BlueprintCallable函数,表示该函数应以与标准 Break Struct 节点相同的方式显示。
NotBlueprintThreadSafe 仅在蓝图函数库中有效。此函数将被视为拥有类的一般BlueprintThreadSafe元数据的异常。
ShortToolTip="Short tooltip" 一个简短的工具提示,在某些情况下使用完整的工具提示可能会让人不知所措,例如父类选择器对话框。
ToolTip="Hand-written tooltip" 覆盖从代码注释自动生成的工具提示。
UnsafeDuringActorConstruction 在 Actor 构造期间调用此函数是不安全的。
WorldContext="Parameter" BlueprintCallable函数用来指示哪个参数确定操作发生的世界。

接口元数据说明符

说明符 描述
CannotImplementInterfaceInBlueprint 此接口可能不包含BlueprintImplementableEventBlueprintNativeEvent函数,除了内部函数。如果它包含未定义蓝图的蓝图可调用函数,则必须在本机代码中实现这些函数。

结构体元数据说明符

说明符 描述
HasNativeBreak="Module.Class.Function" 指示此结构具有自定义 Break Struct 节点。必须提供模块、类和函数名称。
HasNativeMake="Module.Class.Function" 指示此结构具有自定义 Make Struct 节点。必须提供模块、类和函数名称。
HiddenByDefault Make Struct 和 Break Struct 节点中的引脚默认隐藏。
ShortToolTip="Short tooltip" 一个简短的工具提示,在某些情况下使用完整的工具提示可能会让人不知所措,例如父类选择器对话框。
ToolTip="Hand-written tooltip 覆盖从代码注释自动生成的工具提示。

枚举元数据说明符

说明符 描述
Bitflags 指示此枚举类型可以用作使用元数据说明符UPROPERTY设置的整数变量的标志。Bitmask,关于位掩码的使用参见张悟基大佬的文章
Experimental 将此类型标记为实验性且不受支持。
ScriptName="Display Name" 带引号的字符串将在编辑器中用作此枚举类型的名称,而不是 Unreal Header Tool 生成的默认名称。
ToolTip="Hand-written tooltip" 覆盖从代码注释自动生成的工具提示。

UMATE

UMATE是枚举成员专用的元数据说明宏,可以使用如下说明符:

元数据说明符 描述
DisplayName=”Enumerated Value Name” 该值的名称将是此处提供的文本,而不是代码生成的名称。
Hidden 该值不会出现在编辑器中。
ToolTip=”Hand-written tooltip.” 覆盖从代码注释自动生成的工具提示。

5.GENERATED_BODY()

GENERATED_BODY()宏标识的类表示,此类不可以使用父类的声明,最常见的就是GENERATED_BODY标识的类必须要自己声明和实现无参构造函数,否则编译将无法通过。

6.GENERATED_UCLASS_BODY()

GENERATED_UCLASS_BODY()宏标识的类表示此类继承父类的声明,最常见的就是GENERATED_UCLASS_BODY()标识的类不需要声明构造函数,如果需要重写构造函数,则必须为构造函数传递FObjectInitializer类的常量引用,这也是为什么我们经常在UE4编程中看见如下代码的缘故

1
2
3
4
5
UMySQLDatabase::UMySQLDatabase(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
//重写块
}

其中Super()用于给父类传递参数

 GENERATED_BODY()和GENERATED_UCLASS_BODY()宏都会为其标识的类生成一些成员函数,只是二者在使用权限上有一些区别,具体生成了什么成员函数及其区别由于目前自己搜索到的资料过少,暂时无法弄明白,需要以后慢慢研究

十、C++加载蓝图资源

C++加载蓝图资源分两类,普通文件资源(如:mesh,material等)和蓝图类资源。

1.加载文件资源

UE4的文件资源一般都是继承自UObject,如:UStaticMesh、UMaterial、UUserWidget、UComponent等,这些继承自UObject的资源都可以通过LoadObject函数加载,如:

1
2
3
4
5
6
7
8
void AContainerctor::BeginPlay()
{
Super::BeginPlay();
UStaticMesh* Mesh = LoadObject<UStaticMesh>(nullptr, TEXT("StaticMesh'/Game/Cube.Cube'"));
UStaticMeshComponent* MeshComp = NewObject<UStaticMeshComponent>(this, TEXT("Cube"));
MeshComp->RegisterComponent();
MeshComp->SetStaticMesh(Mesh);
}

除此之外UE4还提供一个StaticLoadObject函数,效果上和LoadObject函数一致,区别在于LoadObject是对StaticLOadObject的封装,LoadObject会自动转换UObject类型,而StaticLoadObject需要自己手动转换。

虽然LoadObject在构造函数内外都可以使用,不过UE4还是提供了一个专门给构造函数使用的资源加载函数ConstructorHelpers::FObjectFinder

1
2
3
4
5
6
7
8
AContainerctor::AContainerctor()
{
PrimaryActorTick.bCanEverTick = true;
ConstructorHelpers::FObjectFinder<UStaticMesh> Mesh(TEXT("StaticMesh'/Game/Cube.Cube'"));
UStaticMeshComponent* MeshComp = CreateDefaultSubobject<UStaticMeshComponent>("Cube");
MeshComp->SetupAttachment(GetRootComponent());
MeshComp->SetStaticMesh(Mesh.Object);
}

我们注意到直接通过编辑器拷贝的资源路径都有一个对应资源的前缀,在这里是StaticMesh,实际上这个前缀要不要效果是一样的。

上面的加载方式想要把资源路径做得更灵活,可以把资源路径以FString变量的形式传递,并可以把变量公开到蓝图,但是在输入上我们依然要输入一个资源路径,UE4封装了一个更便捷的封装方式—FSoftObjectPath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UCLASS()
class UE4CPP_API AContainerctor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
FSoftObjectPath softObj;
AContainerctor();
protected:
virtual void BeginPlay() override;

public:
virtual void Tick(float DeltaTime) override;
};
1
2
3
4
5
6
7
8
void AContainerctor::BeginPlay()
{
Super::BeginPlay();
UStaticMesh* Mesh = CastChecked<UStaticMesh>(softObj.TryLoad());
UStaticMeshComponent* MeshComp = NewObject<UStaticMeshComponent>(this, TEXT("Cube"));
MeshComp->RegisterComponent();
MeshComp->SetStaticMesh(Mesh);
}

这一顿操作下来就可以形成这样的资源的路径选择。

2.加载蓝图资源

加载蓝图类使用的是LoadClass,相对的也存在一个StaticLoadClass

1
2
3
4
5
6
void AContainerctor::BeginPlay()
{
Super::BeginPlay();
UClass* uclass = LoadClass<AActor>(nullptr,TEXT("Blueprint'/Game/Act.Act_C'"));
AActor* Act = GetWorld()->SpawnActor<AActor>(uclass);
}

与加载资源不同的是,加载蓝图类的路径需要手动加载_C后缀,否则UE4会找不到对应的蓝图类。

同上,在构造函数中加载,UE4也提供了ConstructorHelpers::FClassFinder函数。

1
ConstructorHelpers::FClassFinder<AActor> Act(TEXT("Blueprint'/Game/Act.Act_C'"));

但是由于UE4是不允许在构造函数中SpawnActor的,所以即使我们在构造函数中加载了类资源,也必须在构造函数外加载蓝图类。

同FSoftObjectPath一样,UE4提供FSoftClassPath。

最终的效果就是这样的:

十一、反射和垃圾回收

UE4使用C++语言进行开发,但是C++并不支持反射和垃圾回收机制,所以UE4便自己实现了反射和垃圾回收。

反射

UE4使用一系列的宏来实现反射,在反射中用的宏主要有

对应的反射对象
UCLASS C++类
UFUNCTION 函数
UPROPERTY 成员变量
USTRUCT 结构体

要使用这些宏,必须包含头文件#include "MyActor.generated.h",并且这个头文件还必须放在左后一位

UE4是如何实现反射的呢?

我们要想要让某一块代码块可以被反射,我们就必须在这个代码块中使用上面的宏,如:我们想要某个类可以被反射,那么就必须在类前添加宏UCLASS(),并且面的函数,成员变量,结构体前也必须添加相应的宏。当我们添加了宏后,UE4在编译时会调用中头文件.generate.h中相应宏定义有关反射的方法,并通过Unreal Build Tool(UBT)和Unreal Header Tool(UHT)两个工具生成一个.generate.cpp文件,.generate.h文件则是一个包含了反射数据的C++代码。如此UE4便可以通过.generate.cpp来获取元数据。

垃圾回收

UE4的垃圾回收的使用有如下几种方式:

继承自UObject类的类对象

我们可以直接在成员变量前引入宏UPROPERTY(),这个宏不仅可以标记反射还可以为垃圾回收做标记。

我们也可以是使用TWeakObjectPtr指针,TWeakObjectPtr是一个弱指针,通常定义在类的内部用来操纵堆区中的对象。TWeakObjectPtr是一个泛型指针,使用时需要指定类型参数,如:

1
TWeakObjectPtr<ClassName> tw;

局部的UObject类对象

有时我们可能在函数中定义一个局部的UObject对象,为了防止对象被UObject的回收机制回收,我们应当使用AddToRoot()来锁定对象,用完后使用RemoveFromRoot()来移除锁定。

不继承自UObject和UStruct的结构体和类

这种结构体我们使用TSharedPtr指针来引用堆区的对象,TSharedPtr也是一个泛型指针,使用时也需要指定类型参数。

如果我们想要使用引用而不是指针则使用TSharedRef,如:

1
TSharedRef<FMyCustom> MyCustom = MakeShared<FMyCustom>(); 

此时M有Custom就是MakeShared<FMyCustom>()返回对象的引用。

我们也可以使用TWeakPtr指针,TWeakPtr指针的效果和TWeakObjectPtr指针的效果是一致的,只是TweakPtr用于非UObject类对象。

当一个不继承自UObject的结构体中出现了UObject对象时

这种情况下,结构体可以正常访问,但是结构体里的UObject对象会由于UObject的回收机制,在过一段时间后被销毁,从而导致这个对象无法访问和出现野指针的情况,UE4则使用FGCObject类来解决这种情况,我们只需让这种情况下的结构体继承自FGCObject类积即可。

十二、线程

由于不同的平台线程的调度有所差异,所以和UE4的其他模块一样,为了实现跨平台的特性,UE4为线程封装了FRunnable框架,并把线程封装进了FRunnableThread类,FRunnableThread类也提供一系列我们对线程的常规操作,所以我们要开启一个自己的线程就需要一个继承FRunnable类并重载相应的函数,然后通过FRunnableThread::Create在FRunnable的框架内完成线程的创建。

FRunnable和FRunnableThread的关系,这里盗用一张Jerish大佬的图来说明:

1.FRunnable和FRunnableThread

FRunnbale是不对编辑器暴露的,所以我们无法直接从编辑器创建一个继承自FRunnable的类,因为我们需要自己创建一个C++类然后手动继承FRunnable类。

FRunnable是一个抽象类,其中FRunnable::Run()函数是一个纯虚函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once
#include "HAL/Runnable.h"
#include "CoreMinimal.h"
class UE4CPP_API FMyRunnable : public FRunnable
{
public:
FMyRunnable(int InCount);
~FMyRunnable();
virtual bool Init() override;
virtual uint32 Run() override;
virtual void Stop() override;
virtual void Exit() override;
bool Create(FString Name);
public:
FRunnableThread* RunnableThread;
int Count;
FString ThreadName;
};

这里要使用FRunnable需要包含HAL/Runnable.h头文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "FMyRunnable.h"
FMyRunnable::FMyRunnable(int InCount):Count(InCount)
{
}
FMyRunnable::~FMyRunnable()
{
}
bool FMyRunnable::Init()
{
UE_LOG(LogTemp, Log, TEXT("Thread %s inited successfully"),*ThreadName);
return true;
}
uint32 FMyRunnable::Run()
{
for (int i = 0; i < Count; i++)
{
UE_LOG(LogTemp, Log, TEXT("i=%d"),i);
}
return 0;
}
void FMyRunnable::Stop()
{
UE_LOG(LogTemp, Log, TEXT("Stoped thread %s"),*ThreadName);
}
void FMyRunnable::Exit()
{
UE_LOG(LogTemp, Log, TEXT("Exit thread %s"), *ThreadName);
}
bool FMyRunnable::Create(FString Name)
{
ThreadName = Name;
RunnableThread = FRunnableThread::Create(this, *Name);
if (RunnableThread)
{
UE_LOG(LogTemp, Log, TEXT("Created a thread %s"), *RunnableThread->GetThreadName());
return true;
}
else
{
UE_LOG(LogTemp, Log, TEXT("Failed to create a thread %s"), *RunnableThread->GetThreadName());
return false;
}
}

然后就可以在外部使用这个线程了。

1
2
3
4
5
6
void AContainerctor::BeginPlay()
{
Super::BeginPlay();
FMyRunnable* MyRunnable = new FMyRunnable(Count);
MyRunnable->Create(TEXT("MyFirstThread"));
}

我这里是直接把线程的操作FRunnableThread也一并封装到FMyRunnbale中了,以方便管理。如果不封装在一起就是这么使用的:

1
2
3
4
5
6
void AContainerctor::BeginPlay()
{
Super::BeginPlay();
FMyRunnable* MyRunnable = new FMyRunnable(Count);
FRunnableThread* RunnableThread = FRunnableThread::Create(MyRunnable,TEXT("MyFirstThread"));
}

这样的化就要管理两个对象,MyRunnable和RunnableThread。

  • Init():在线程被创建后自动调用,会初始化线程的一些数据,比如名字与ID,此时是不可以直接通过FRunnableThread对象去获取线程名字和ID的,否则会导致程序中断;
  • Run():在线程初始化成功后自动调用,我们需要线程干的事情就写在这个函数里;
  • Stop():主动结束线程;
  • Exit():在Run()函数跑完之后自动调用,退出线程。

FRunnable没有提供挂起线程的函数重写,如果需要挂起线程的操作,则需要我们主动调用FRunnableThread::Suspend( bool bShouldPause = true )函数,bShouldPause=true表示挂起,bShouldPause=false表示恢复。

这里在测试时碰到一个坑,即按照源码的说明Run函数是只在Init函数初始化成功之后才会调用,而事实情况则是Run函数与Init函数存在并行的情况,我在Run函数中调用线程挂起时,如果不做延迟处理会出现线程不可用的情况而导致程序中断,这说明Run函数运行初期,Init函数还未完成。

正常情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
uint32 FMyRunnable::Run()
{
for (int i = 0; i < Count; i++)
{
UE_LOG(LogTemp, Log, TEXT("i=%d"),i);
FPlatformProcess::Sleep(1.0f);
if (i == 5)
{
RunnableThread->Suspend(true);
}
}
return 0;
}

中断情况:

1
2
3
4
5
6
7
8
9
10
11
12
uint32 FMyRunnable::Run()
{
for (int i = 0; i < Count; i++)
{
UE_LOG(LogTemp, Log, TEXT("i=%d"),i);
if (i == 5)
{
RunnableThread->Suspend(true);
}
}
return 0;
}

2.FAsyncTask和FAutoDeleteAsyncTask

FAsyncTask是UE使用FRunnable实现的,基于线程池的一套异步任务处理系统,经过封装的FAsyncTask在使用上就比FRunnable要方便得多了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma once
#include "Async/AsyncWork.h"
#include "CoreMinimal.h"

class UE4CPP_API FMyAsyncTask : public FNonAbandonableTask
{
public:
FMyAsyncTask(){};
~FMyAsyncTask(){};
public:
friend class FAsyncTask<FMyAsyncTask>;
void DoWork()
{
for (int i = 0; i < 10; ++i)
{
UE_LOG(LogTemp, Log, TEXT("Doing work,i=%d"), i);
FPlatformProcess::Sleep(1);
}
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FMyAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
}
};

这里声明的友元类friend class FAsyncTask<FMyAsyncTask>,目的就是让我们的自定义类可以访问FAsyncTask类的成员。

DoWorkTStatId是两个必须实现的函数,DoWork负责任务要实现的逻辑,GetStatId是给UE底层使用的,用于统计任务用时,所以实现上基本是固定的。

FNonAbandonableTask是一个空类,应该是UE为了实现多态而使用的,而要继承这个类则需要包含头文件#include "Async/AsyncWork.h"

创建好类之后就可以使用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//.h
FAsyncTask<FMyAsyncTask>* MyTask;
//...
void AContainerctor::CreateThread()
{
MyTask = new FAsyncTask<FMyAsyncTask>();
UE_LOG(LogTemp, Log, TEXT("Created MyTask"));
MyTask->StartBackgroundTask();
//MyTask->EnsureCompletion();
}
void AContainerctor::Tick(float DeltaTime)
{
if (MyTask && MyTask->IsDone())
{
delete MyTask;
MyTask = nullptr;
UE_LOG(LogTemp, Log, TEXT("Completed MyTask"));
}
Super::Tick(DeltaTime);
}

需要注意的是我们自定的FMyAsyncTask类必须以FAsyncTask<FMyAsyncTask>类型来接收和创建,如上面的代码所示,否则我们的FMyAsyncTask类对象就不能访问FAsyncTask类的成员了。

还有一点因为FAsyncTask是需要手动销毁的,所以我们需要自行判断任务是否完成,UE提供了FAsyncTask::EnsureCompletion函数等待任务完成后再执行后续代码,但这会阻塞当前执行的线程,所以我上面使用了在Tick函数中判断的方式来销毁MyTask。

正式因为这样,UE又提供了FAutoDeleteAsyncTask类,FAutoDeleteAsyncTask提供任务完成后自动销毁的能力,DoWork函数退出后会自动调用析构函数析构对象。

实现上FAutoDeleteAsyncTask和FAsyncTask基本没什么区别,只是将友元类从FAsyncTask换成了FAutoDeleteAsyncTask。

FAsyncTask和FAutoDeleteAsyncTask有两种启动任务的方式即StartSynchronousTaskStartBackgroundTask,前者是在当前线程启动任务,这会导致当前线程阻塞,后者是将任务丢入UE预制的线程池,从线程池中获取线程执行任务。

3.线程池

前面一节我们有提到过UE的预制线程池,这个线程池是在引擎与初始化时的FEngineLoop::PreInit函数中的PreInitPreStartupScreen函数创建的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int32 FEngineLoop::PreInit(const TCHAR* CmdLine)
{
const int32 rv1 = PreInitPreStartupScreen(CmdLine);
if (rv1 != 0)
{
PreInitContext.Cleanup();
return rv1;
}

const int32 rv2 = PreInitPostStartupScreen(CmdLine);
if (rv2 != 0)
{
PreInitContext.Cleanup();
return rv2;
}

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//FEngineLoop::PreInitPostStartupScreen
//...
if (FPlatformProcess::SupportsMultithreading())
{
{
SCOPED_BOOT_TIMING("GIOThreadPool->Create");
GIOThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfIOWorkerThreadsToSpawn();
if (FPlatformProperties::IsServerOnly())
{
NumThreadsInThreadPool = 2;
}
verify(GIOThreadPool->Create(NumThreadsInThreadPool, 96 * 1024, TPri_AboveNormal, TEXT("IOThreadPool")));
}
}

UE预制的线程是FQueuedThread类型的线程对象,FQueuedThread也是继承自FRunnable的子类,而FQueuedThread被存储在一个FQueuedThreadPool维护的队列里,说是队列其实在源码里就是一个FQueuedThread类型的TArray。

1
2
3
4
5
6
7
8
class FQueuedThreadPoolBase : public FQueuedThreadPool
{
protected:
//...
/** The thread pool to dole work out to. */
TArray<FQueuedThread*> QueuedThreads;
//...
};

FQueuedThread里面会维护一个FEvent事件,用于任务的事件触发和线程控制。

预制的线程池被UE保存在一个GThreadPool指针中,和GEngine一样GThreadPool也是一个全局指针。

FAysncTask和FAutoDeleteAsyncTask的StartBackgroundTask函数默认情况下就是使用的线程池里的线程,当然我们也可以传自定义的线程进去,让任务使用自定义线程执行。

1
2
3
4
5
6
7
8
//AsyncWork.h
/**
* Run this task on the lo priority thread pool. It is not safe to use this object after this call.
**/
void StartBackgroundTask(FQueuedThreadPool* InQueuedPool = GThreadPool)
{
Start(false, InQueuedPool);
}

创建自定义线程池

创建自定的线程池其实也比较简单,使用的方式就是和UE预制线程池是一样的。

首先我们需要创建自己的任务对象—一个实现了IQueuedWork的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma once
#include "Misc/IQueuedWork.h"
#include "CoreMinimal.h"
class UE4CPP_API FMyWorker : public IQueuedWork
{
public:
FMyWorker(FString Name):WorkName(Name){}
~FMyWorker(){}
public:
virtual void DoThreadedWork() override
{
UE_LOG(LogTemp, Log, TEXT("Doing work %s"), *WorkName);
FPlatformProcess::Sleep(1);
}
virtual void Abandon() override
{
UE_LOG(LogTemp, Log, TEXT("Abandoned work %s"), *WorkName);
delete this;
}
private:
FString WorkName;
};

这里有两个函数是必须重写的,DoThreadedWorkAbandon,DoThreadedWork会在任务对象加入线程池之后自动调用,用于处理自己的任务逻辑;Abandon则是放弃当前任务。

创建好了任务我们就可以创建线程池了:

1
2
3
4
5
6
7
8
9
10
void AContainerctor::CreateThread()
{
FQueuedThreadPool* Pool = FQueuedThreadPool::Allocate();
Pool->Create(5, 12 * 1024, TPri_Normal, TEXT("MyThreadPool"));
for (int i = 0; i < 10; ++i)
{
FString WorkName = TEXT("MyWork") + FString::FromInt(i);
Pool->AddQueuedWork(new FMyWorker(WorkName));
}
}

FQueuedThreadPool::Allocate()创建线程池对象,Create函数往线程池中加入线程,5为线程数量,12*1024为线程大小,TPri_Normal为线程类型,最后的字符串为线程名字。AddQueuedWork将任务对象加入线程池,任务即可开始执行。

4.线程锁

提到线程就一定逃避不了锁,为了保证线程安全锁是必要的,UE也提供了自己的一套锁机制,UE提供四种线程锁:

  • FScopeLock:区域锁;
  • FCriticalSection:临界区;
  • FScopeRWLock:读写锁。
  • FSystemWideCriticalSection

我们一个个来看。

FScopeLock

我们先来看看没有锁的情况以便和有锁的情况做对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class UE4CPP_API FMyRunnable : public FRunnable
{
public:
FMyRunnable(int InCount);
~FMyRunnable();
virtual bool Init() override;
virtual uint32 Run() override;
virtual void Stop() override;
virtual void Exit() override;
bool Create(FString Name);
void ChangeCount();//供外部修改Count值
public:
FRunnableThread* RunnableThread;
int Count;//Count值在Run运行时不应该被修改
FString ThreadName;
FCriticalSection CountLock;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint32 FMyRunnable::Run()
{
for (int i = 0; i < Count; i++)
{
UE_LOG(LogTemp, Log, TEXT("i=%d"), i);
FPlatformProcess::Sleep(0.001f);
}
return 0;
}

void FMyRunnable::ChangeCount()
{
Count += 5;
UE_LOG(LogTemp, Log, TEXT("Set Count=%d"), Count);
}

线程外部:

1
2
3
4
5
6
7
8
9
10
void AContainerctor::CreateThread()
{
MyRunnable = new FMyRunnable(1000);
MyRunnable->Create(TEXT("MyRunnable"));
}

void AContainerctor::ChangeValue()
{
MyRunnable->ChangeCount();
}

然后在蓝图中通过按键触发CreateThread和ChangeCount函数,然后跑一下看看

可以看到在Run运行的过程中我们修改了Count的值到505,而线程结束时Count的值确实被修改了,这个结果明显不是我们先要的。

下面来使用锁对比效果。

FScopeLock和FCriticalSection是一起使用的,ScopeLock的使用方法有两种:

方法一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//.h
FCriticalSection CountLock;
//.cpp
void FMyRunnable::Run()
{
{
FScopeLock ScopeLock(&CountLock);//对ScopeLock所在的作用域中的变量上锁
for (int i = 0; i < Count; i++)
{
UE_LOG(LogTemp, Log, TEXT("i=%d"), i);
FPlatformProcess::Sleep(0.001f);
}
}
}
void FMyRunnable::ChangeCount()
{
FScopeLock ScopeLock(&CountLock);
Count += 5;
UE_LOG(LogTemp, Log, TEXT("Set Count=%d"), Count);
}

需要注意的是这种方法需要在读和写的地方都要上锁,ScopeLock会把作用域中使用的外部变量都锁住,即使有的变量没有在写的时候上锁,我们把上面的例子改一下,上个锁再看一下。

可以看到,Count的值是在线程退出之后才被设置到505

方法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void FMyRunnable::Run()
{
FScopeLock* ScopeLock = new FScopeLock(&CountLock);//上锁
for (int i = 0; i < Count; i++)
{
UE_LOG(LogTemp, Log, TEXT("i=%d"), i);
FPlatformProcess::Sleep(0.001f);
}
delete ScopeLock;//解锁
}
void FMyRunnable::ChangeCount()
{
FScopeLock* ScopeLock = new FScopeLock(&CountLock);//上锁
Count += 5;
delete ScopeLock;//解锁
}

和方法一一样,方法二也需要在读写的地方都要上锁。

在实际测试中发现ScopeLock似乎有一个bug,即当在ScopeLock的作用域中存在多个变量,有的需要上锁而有的不需要上锁时,会因为修改的时序导致结果有所差异,举个例子:

我们在上面的例子中在增加一个变量Num。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UE4CPP_API FMyRunnable : public FRunnable
{
public:
FMyRunnable(int InCount);
~FMyRunnable();
virtual bool Init() override;
virtual uint32 Run() override;
virtual void Stop() override;
virtual void Exit() override;
bool Create(FString Name);
void ChangeCount();
void ChangeNum();
public:
FRunnableThread* RunnableThread;
int Count;
int Num = 10;
FString ThreadName;
FCriticalSection CountLock;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
uint32 FMyRunnable::Run()
{
FScopeLock* ScopeLock = new FScopeLock(&CountLock);
for (int i = 0; i < Count; i++)
{
UE_LOG(LogTemp, Log, TEXT("i=%d"), i);
FPlatformProcess::Sleep(0.001f);
}
UE_LOG(LogTemp, Log, TEXT("Num=%d"), Num);
delete ScopeLock;
return 0;
}
void FMyRunnable::ChangeCount()
{
FScopeLock* ScopeLock = new FScopeLock(&CountLock);
Count += 5;
UE_LOG(LogTemp, Log, TEXT("Set Count=%d"), Count);
delete ScopeLock;
}

void FMyRunnable::ChangeNum()
{
Num += 10;
UE_LOG(LogTemp, Log, TEXT("Set Num=%d"), Num);
}

如果ChangeNum在ChangeCount之前调用那么在Run的运行期间,Num值会被修改,如果ChangeNum在ChangeCount之后调用,那么Num值会被锁住只能在线程结束后才会被修改。

还是一样我们来运行下看看。

情况一ChangeNum在ChangeCount之前运行:

1
2
3
4
5
6
7
8
9
10
void AContainerctor::CreateThread()
{
MyRunnable = new FMyRunnable(Count);
MyRunnable->Create(TEXT("MyRunnable"));
}
void AContainerctor::ChangeValue()
{
MyRunnable->ChangeNum();
MyRunnable->ChangeCount();
}

可以看到Num的值在线程结束前已经被修改了。

情况二ChangeNum在ChangeCount之后运行:

1
2
3
4
5
6
7
8
9
10
void AContainerctor::CreateThread()
{
MyRunnable = new FMyRunnable(Count);
MyRunnable->Create(TEXT("MyRunnable"));
}
void AContainerctor::ChangeValue()
{
MyRunnable->ChangeCount();
MyRunnable->ChangeNum();
}

可以看到Num值被锁住了,在线程结束之后才被设置到20。

FCriticalSection

FCriticalSection除了配合FScopeLock使用,自己单独也可以当作锁来用,用法上和FScopeLock的方法二类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FCriticalSection CountLock;

uint32 FMyRunnable::Run()
{
CountLock.Lock();
for (int i = 0; i < Count; i++)
{
UE_LOG(LogTemp, Log, TEXT("i=%d"), i);
FPlatformProcess::Sleep(0.001f);
}
CountLock.Unlock();
return 0;
}
void FMyRunnable::ChangeCount()
{
CountLock.Lock();
Count += 5;
UE_LOG(LogTemp, Log, TEXT("Set Count=%d"), Count);
CountLock.Unlock();
}

和FScopeLock一样,当锁的代码段出现了多个变量有的值需要保持不变而有的值需要改变时,FCriticalSection会出现和FScopeLock一样的问题。

FScopeRWLock

关于FScopeRWLock我在内外网几乎没有找到任何代码示例,官方文档的描述甚至只有短短的一句话,在内网中几乎找不到FScopeRWLock相关的关键字,只在一些博客里有看到FRWLock,而这个类我在引擎源码里搜索发现这只是一个类别名,写在各个平台相关的文件里,在源码里只找到了一个FScopeRWLock类,类也比较简单,具体怎么用只能等日后有时间再自己摸索了。

FSystemWideCriticalSection

FSystemWideCriticalSection和FCriticalSection的用法是一样的,他们的区别只在于FCriticalSection是用户模式下的临界区,线程进入临界区时不需要从用户态切换到内核态,所以效率上比FSystemWideCriticalSection高,但是也是由于没有进入内核态所以无法进行进程之间的同步,只能用于线程;FSystemWideCriticalSection则是基于内核对象Mutex实现的,线程进入临界区是会从用户态切换进入内核态,所以效率上不如FCriticalSection,但是可以进行线程进程的同步。

5.TGraphTask

TGraphTask是UE4基于多线程抽象出来的一个异步任务处理系统,一整个任务由一个个任务节点组成,个节点之间可以进行单向的依赖,用一张图来说明可能会更形象。

我们以这个流程来做一个计算加速度的例子(v2-v1)/t来说明TaskGraph的用法。用任务1计算v1,任务2计算v2,用任务3来计算v2-v1,用任务4来计算加速度。

创建自定义FGraphTask

和FRunnable一样,我们也需要创建一个C++类来实现自己的TGraphTask,这里我创建四个任务节点,FGraphTask1、FGraphTask2、FGraphTask3、FGraphTask4,和一个UObject—UDataObj用于在各个任务节点间传递数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "../Common/DataObj.h"
#include "CoreMinimal.h"
class UE4CPP_API FGraphTask1
{
public:
FGraphTask1(FString Name,UDataObj* Obj) : TaskName(Name),DataObj(Obj) {}
~FGraphTask1() {};
public:
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FMyTaskGraph, STATGROUP_TaskGraphTasks);
}
static ENamedThreads::Type GetDesiredThread()
{
return ENamedThreads::AnyThread;
}
static ESubsequentsMode::Type GetSubsequentsMode()
{
return ESubsequentsMode::TrackSubsequents;
}
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
UE_LOG(LogTemp, Log, TEXT("Doing %s"), *TaskName);
FPlatformProcess::Sleep(3);
DataObj->V1 = 15;
UE_LOG(LogTemp, Log, TEXT("Done %s and setted V1=%f"), *TaskName,DataObj->V1);
}
private:
FString TaskName;
UDataObj* DataObj;
};

剩下的三个任务节点实现方式都一样样,只是DoTask函数体有所区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void FGraphTask2::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
UE_LOG(LogTemp, Log, TEXT("Doing %s"), *TaskName);
FPlatformProcess::Sleep(3);
DataObj->V2 = 25;
UE_LOG(LogTemp, Log, TEXT("Done %s and setted V2=%f"), *TaskName, DataObj->V2);
}
//------------------------
void FGraphTask3::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
UE_LOG(LogTemp, Log, TEXT("Doing %s"), *TaskName);
FPlatformProcess::Sleep(3);
DataObj->Difference = DataObj->V2 - DataObj->V1;
UE_LOG(LogTemp, Log, TEXT("Done %s and setted Difference=%f"), *TaskName, DataObj->Difference);
}
//------------------------
void FGraphTask4::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
UE_LOG(LogTemp, Log, TEXT("Doing %s"), *TaskName);
FPlatformProcess::Sleep(3);
DataObj->A = DataObj->Difference/DataObj->T;
UE_LOG(LogTemp, Log, TEXT("Done %s and setted A=%f"), *TaskName, DataObj->A);
}

其中GetStatIdGetDesiredThreadGetSubsequentsModeDoTask这四个函数是必须要实现的。

  • GetStatId用于统计任务用时;
  • GetDesiredThread函数用于告诉系统这个任务使用什么类型的线程执行,有4种类型可选,ENamedThreads::AnyThread,ENamedThreads::GameThread,ENamedThreads::RHIThread,ENamedThreads::AudioThread,一般我们都使用AnyThread表示使用UE4专门为TGraphTask系统预制的线程;
  • GetSubsequentsMode函数用于告诉系统当前任务完成后的后续执行模式,因为一个任务节点完成后可以接下一个任务节点,有两种模式ESubsequentsMode::TrackSubsequents后续有任务,ESubsequentsMode::FireAndForget后续无任务,我这里的例子中只有任务4没有后续任务。
  • DoTask函数用于处理自己的任务逻辑。

UDataObj就只用来传递数据:

1
2
3
4
5
6
7
8
9
10
11
UCLASS()
class UE4CPP_API UDataObj : public UObject
{
GENERATED_BODY()
public:
float V1;
float V2;
float A;
float T = 10;
float Difference;
};

然后创建任务节点并处理各个任务节点之间的依赖关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void AContainerctor::CreateTask()
{
FGraphEventRef Task1, Task2, Task3, Task4;
UDataObj* DataObj = NewObject<UDataObj>();
Task1 = TGraphTask<FGraphTask1>::CreateTask().ConstructAndDispatchWhenReady(TEXT("Task1"), DataObj);
Task2 = TGraphTask<FGraphTask2>::CreateTask().ConstructAndDispatchWhenReady(TEXT("Task2"), DataObj);
FGraphEventArray Prerequistites3;
Prerequistites3.Add(Task1);
Prerequistites3.Add(Task2);
Task3 = TGraphTask<FGraphTask3>::CreateTask(&Prerequistites3).ConstructAndDispatchWhenReady(TEXT("Task3"), DataObj);
FGraphEventArray Prerequistites4;
Prerequistites4.Add(Task3);
Task4 = TGraphTask<FGraphTask4>::CreateTask(&Prerequistites4).ConstructAndDispatchWhenReady(TEXT("Task4"), DataObj);
}
  • CreateTask函数有两个参数CreateTask(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread),第一个参数传入这个任务要依赖的其他任务的数组,第二个参数指定这个任务跑在什么类型的线程里。
  • ConstructAndDispatchWhenReady函数在创建任务后会立刻执行任务里的DoTask函数,并且使用C++11的构造函数传参的特性,可以将参数传递给对应的类的构造函数中。
  • FGraphEventArray类是UE封装的专门用于装载依赖任务的数组。

然后我们执行一下看看结果:

不使用自定义FGraphTask的TGraphTask任务

UE除了使用自定义的FGraphTask来执行任务外还提供一个使用Lambda表达式来执行任务的方式,我们直接先看一个例子,把上面的任务流程用新的方式在处理一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void AContainerctor::CreateTask()
{
FGraphEventRef Task1, Task2, Task3, Task4;
Task1 = FFunctionGraphTask::CreateAndDispatchWhenReady([]()->void {
UE_LOG(LogTemp, Log, TEXT("Doing Task1"));
FPlatformProcess::Sleep(3);
});
Task2 = FFunctionGraphTask::CreateAndDispatchWhenReady([]()->void {
UE_LOG(LogTemp, Log, TEXT("Doing Task2"));
FPlatformProcess::Sleep(3);
});
FGraphEventArray Prerequistites3;
Prerequistites3.Add(Task1);
Prerequistites3.Add(Task2);
Task3 = FFunctionGraphTask::CreateAndDispatchWhenReady([]()->void {
UE_LOG(LogTemp, Log, TEXT("Doing Task3"));
FPlatformProcess::Sleep(3);
}, TStatId{}, & Prerequistites3);
FGraphEventArray Prerequistites4;
Prerequistites4.Add(Task3);
Task3 = FFunctionGraphTask::CreateAndDispatchWhenReady([]()->void {
UE_LOG(LogTemp, Log, TEXT("Doing Task4"));
FPlatformProcess::Sleep(3);
}, TStatId{}, & Prerequistites4);
}
  • DoTask的逻辑就写在了Lambda表达式里了;
  • 按照源码的注释说明,CreateAndDispatchWhenReady接收的Lambda表达式必须是void() 或者 void(ENamedThreads::Type, const FGraphEventRef&)类型的,所以不可以传外部参数进入Lambda表达式,所以这种方式创建的任务流只能处理一些不依赖外部数据的简单任务。

6.游戏线程

前面有提到UE中的一些预制线程,包括线程池中的线程,TGraphTask中的线程,还有GameThread、RenderThread、RHIThread、AudioThread等,这里面用的最多就是GameThread了,所以我们主要讨论一下GameThread。

GameThread顾名思义就是用于承载游戏逻辑的线程,UE在创建了GameThread后会把线程ID存储到GGameThreadId这个全局变量中。

有一些事情是只能在GameThread中做的,比如创建UObject,AActor,Widget等,所有的BeginPlay函数,Tick函数都跑在GameThread中,所有的蓝图函数也都跑在GameThread中。

十三、模块

模块可以说是贯穿了整个引擎,整个UE引擎就是又一个个模块组合而成的,我们打开引擎源码的Source文件夹就可以看到如下文件夹:

  • Developer:存放跨平台工具以及一些引擎底层工具;
  • Editor:存放编辑器代码;
  • Programs:存放依赖于引擎的工具,如:UBT等;
  • Runtime:存放GamePlay相关的代码;
  • ThirdParty:存放第三方库和插件。

1.模块的描述

一个模块可以只由一个模块组成,也可也由多个模块组成,我们使用UE创建一个新项目的时候,这整个项目就是一个独立的模块,UE会默认为我们添加一些基础模块的引用,操作代码在.biuld.cs中,模块与模块之间可以互相引用,但是不可以出现循环引用。

模块的描述在.uproject文件中,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"FileVersion": 3,
"EngineAssociation": "4.27",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "UE4Cpp",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine",
"CoreUObject"
]
}
]
}

这是一个Json格式的文件,我们主要关注Modules数组里面的元素,一个元素就是一个模块,由于我的项目没有添加其他模块,所以只有一个元素。

  • Name:模块名;

  • Type:加载模块的场景,是一个EHostType::Type类型,在ModuleDescriptor.h中,主要有以下类型:

    Runtime:除了独立程序以外都加载;

    RuntimeNoCommandlet:除了独立程序和运行命令行模式的编辑器模式以外都加载;

    RuntimeAndProgram:任何情况下都加载;

    CookedOnly:只有被烘焙过的程序才加载;

    Developer:只有在Development运行模式下才加载;这个Development是游戏和编辑器的一种运行模式,除了Development还有GameDebug,打包的时候还有Shipping,编辑器的运行模式我们可以在VS编译的时候选择:

Editor:只在编辑器启动的时候加载;

EditorNoCommandlet:只在编辑器启动的时候加载,但不包括使用命令行启动的编辑器;

Program:只在独立程序中加载;

ServerOnly:除了专用客户端,其他情况都加载;

ClientOnly:除了专用服务器,其他情况都加载;

  • LoadingPhase:标注模块应该被加载的时机,是一个ELoadingPhase::Type的类型,也在在ModuleDescriptor.h中,主要有如下类型:

    EarliestPossible:尽早的被加载,一般用于Pak文件中有模块的情况,可以是模块从Pak文件中加载;

    PostConfigInit:在引擎被完全初始化之前,在配置文件系统被初始化之后加载,一般用于拦截底层消息是使用;

    PreEarlyLoadingScreen:在加载CoreUObject之前加载,可以设置手动加载屏幕,一般用于程序补丁;

    PreLoadingScreen:在引擎完全初始化之前加载,一般用于需要触发之前挂接到加载屏幕的模块;

    PreDefault:在默认阶段之前加载;

    Default:在引擎初始化之前,在游戏 模块被加载之后加载,创建项目是引擎给的默认值,也是我们用的最多的加载时机。

    PostDefault:在默认阶段之后加载;

    PostEngineInit:在引擎被初始化之后加载;

    None:不自动加载模块,引擎不会自动加载模块,需要我们自己手动的在程序里加载模块。

  • AdditionalDependencies:描述用于构建此模块的其他依赖项的列表。

2.创建自定义模块

创建自定义模块需要三个基础文件,并且需要单独的放在项目Source目录下的一个文件夹下。

这三个基础文件引擎不会自动为我们生成,我们需要手动添加进去,并且需要手动实现里面的内容。

.h

1
2
#pragma
#include "CoreMinimal.h"

.cpp

1
2
3
4
#include "MyModule.h"
#include "Modules/ModuleManager.h"

IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, MyModule, "MyModule");

这是最简单的.h和.cpp文件。

.Build.cs

1
2
3
4
5
6
7
8
9
10
11
using UnrealBuildTool;
using System.Collections.Generic;
public class MyModule : ModuleRules
{
public MyModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
PrivateDependencyModuleNames.AddRange(new string[] { });
}
}

然后右键.uproject文件,重新Generate Visual Studio project file,这样VS就可以识别到我们新添加的文件夹和文件了。

到这一步我们的空的自定义模块就创建完成了。

3.加载自定义模块

引擎自动加载

修改项目的XX.Target.cs、XXEditor.Target.cs和.uproject文件,将我们新建的模块添加到项目中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//XX.Target.cs
public class UE4CppTarget : TargetRules
{
public UE4CppTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V2;

ExtraModuleNames.AddRange( new string[] { "UE4Cpp", "MyModule" } );
}
}
//XXEditor.Target.cs
public class UE4CppEditorTarget : TargetRules
{
public UE4CppEditorTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Editor;
DefaultBuildSettings = BuildSettingsVersion.V2;

ExtraModuleNames.AddRange( new string[] { "UE4Cpp","MyModule"} );
}
}
//.uproject
{
"FileVersion": 3,
"EngineAssociation": "4.27",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "UE4Cpp",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine",
"CoreUObject"
]
},
{
"Name": "MyModule",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine",
"CoreUObject"
]
}
]
}

然后我们重新编译下项目,模块就会按照.uproject中的配置进行加载。

如何查看引擎有没有识别到模块呢?

打开引擎新建C++类的界面,如果下拉列表中出现了我们的自定义模块名称,就说明自定义模块被引擎识别了。

注意我这里用的是识别而不是加载,因为引擎识别到模块和引擎加载模块是两个不同的概念,引擎识别到模块,我就可以在引擎中往模块里添加新的类,但此时模块不一定已经加载了;而引擎加载模块即引擎识别到了模块并将模块初始化完成了。

那么如何查看模块有没有被引擎自动加载了呢?

这里我们就需要在模块的.h文件中实现一个IModuleInterface接口了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//.h
#pragma
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FMyModule : public IModuleInterface
{
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
//.cpp
#include "MyModule.h"
IMPLEMENT_PRIMARY_GAME_MODULE(FMyModule, MyModule, "MyModule");
void FMyModule::StartupModule()
{
UE_LOG(LogTemp, Error, TEXT("Loaded MyModule"));
}
void FMyModule::ShutdownModule()
{
}

IModuleInterface在ModuleManager.h中,IModuleInterface提供如下接口:

  • StartupModule:在模块被加载后调用;
  • ShutdownModule:在模块被卸载之前调用;
  • PreUnloadCallback:在模块被卸载之前调用,调用时机在ShutdownModule之前;
  • PostLoadCallback:在模块被重新加载后调用;
  • SupportsDynamicReloading:设置模块是否允许动态卸载,默认为允许;
  • SupportsAutomaticShutdown:设置模块是否允许在程序关闭时自动卸载清理,默认为允许;
  • IsGameModule:表示模块是否属于GamePlay,默认为false,如果模块要实现游戏逻辑,那么需要重写这个函数放回true;

同时IMPLEMENT_PRIMARY_GAME_MODULE宏就不能使用默认的FDefaultGameModuleImpl类了,需要使用自己实现的FMyModule类了,FDefaultGameModuleImpl类是一个UE预制的继承自IModuleInterface的空类,IModuleInterface接口就是模块暴露给外部使用的指针,外部要获取模块的引用可以通过FModuleManager.Get().GetModule或者在动态加载时直接保存引用。

实现了上面的操作后,在模块加载时即可在日志文件中找到对应的日志打印了。

代码手动加载

前有提到模块可以被引擎自动加载,也可以通过代码手动加载,UE提供一下四种方式来动态加载模块:

  • FModuleManager::Get().LoadModule:加载指定模块,返回操作模块的IModuleInterface指针;
  • FModuleManager::Get().LoadModuleChecked:加载指定模块,在加载前会检查模块是否可用,返回操作模块的IModuleInterface指针;
  • FModuleManager::Get().LoadModuleWithCallback:加载指定模块,可以使用FOutputDevice接收加载失败的错误信息,如果加载成功则会调用模块的PostLoadCallback函数;
  • FModuleManager::Get().LoadModuleWithFailureReason:加载指定模块,并且可以使用EModuleLoadResult对象接收加载信息。

4.引用模块的资源

前面有提到一个模块可以引用另一个模块里的资源,其实引用的方式也比较简单,对于模块资源的引用是无论模块有没有加载都是可以引用的,加载不过是为模块做一些初始化操作。

这里我在主模块中引用自定义模块的类资源。

往前面定义的MyModule模块中添加一个AMyModuleActor类,然后在主模块UE4Cpp中引用这个类的头文件并实力化Actor。

首先需要通过.Build.cs添加MyModule模块的引用:

1
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","MyModule"});

然后就可以在想要引用资源的地方添加头文件使用类资源了:

1
2
3
4
5
6
7
#include "../MyModule/MyModuleActor.h"
//...
void AContainerctor::BeginPlay()
{
Super::BeginPlay();
AMyModuleActor* Actor = GWorld->SpawnActor<AMyModuleActor>();
}

运行下看看结果:

可以看到AMyModuleActor已经被实例化出来了。

5.模块的构建

在模块的构建过程中有下面三个宏是需要注意的:

  • IMPLEMENT_MODULE(FModuleClass, ModuleName):一般没有什么特殊的模块都使用这个宏;
  • IMPLEMENT_GAME_MODULE(FModuleClass, ModuleName):如果模块有包含游戏逻辑,那么就使用这个宏,看了源码这个宏就是对IMPLEMENT_MODULE宏的调用,二者没有任何区别,应该是UE预留的宏;
  • IMPLEMENT_PRIMARY_GAME_MODULE(FModuleClass, ModuleName,”GameName”):如果模块是项目的主模块就是用这个宏,一个项目中必须要至少有一个主模块。

这三个宏做的事情就是向外部提供模块的IModuleInterface接口,并将模块构成静态库或者动态库,默认情况下都构建成动态库,存放在Binaries目录对应平台名目录下。

6.模块打包的问题

由于要验证第五小节模块的构建,所以需要将引用了自定义模块的项目打包,但是打包的出现了很严重的问题,即引用了自定义模块资源的项目打包始终无法通过,无论使用原有的项目还是新建项目,无论是使用原有的模块还是新建新的模块,只要主项目使用了模块的资源,打包的时候都会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
UATHelper: Packaging (Windows (64-bit)):     AModule.cpp.obj : error LNK2005: "wchar_t const * const GLiveCodingEngineDir" (?GLiveCodingEngineDir@@3PEB_WEB) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "wchar_t const * const GLiveCodingProject" (?GLiveCodingProject@@3PEB_WEB) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "class FChunkedFixedUObjectArray * & GObjectArrayForDebugVisualizers" (?GObjectArrayForDebugVisualizers@@3AEAPEAVFChunkedFixedUObjectArray@@EA) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "wchar_t * GInternalProjectName" (?GInternalProjectName@@3PA_WA) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "wchar_t const * const GForeignEngineDir" (?GForeignEngineDir@@3PEB_WEB) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void * __cdecl operator new(unsigned __int64)" (??2@YAPEAX_K@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void * __cdecl operator new(unsigned __int64,struct std::nothrow_t const &)" (??2@YAPEAX_KAEBUnothrow_t@std@@@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void __cdecl operator delete(void *)" (??3@YAXPEAX@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void __cdecl operator delete(void *,struct std::nothrow_t const &)" (??3@YAXPEAXAEBUnothrow_t@std@@@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void __cdecl operator delete(void *,unsigned __int64)" (??3@YAXPEAX_K@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void __cdecl operator delete(void *,unsigned __int64,struct std::nothrow_t const &)" (??3@YAXPEAX_KAEBUnothrow_t@std@@@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void * __cdecl operator new[](unsigned __int64)" (??_U@YAPEAX_K@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void * __cdecl operator new[](unsigned __int64,struct std::nothrow_t const &)" (??_U@YAPEAX_KAEBUnothrow_t@std@@@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void __cdecl operator delete[](void *)" (??_V@YAXPEAX@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void __cdecl operator delete[](void *,struct std::nothrow_t const &)" (??_V@YAXPEAXAEBUnothrow_t@std@@@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void __cdecl operator delete[](void *,unsigned __int64)" (??_V@YAXPEAX_K@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "void __cdecl operator delete[](void *,unsigned __int64,struct std::nothrow_t const &)" (??_V@YAXPEAX_KAEBUnothrow_t@std@@@Z) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "struct FTargetNameRegistration GTargetNameRegistration" (?GTargetNameRegistration@@3UFTargetNameRegistration@@A) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): AModule.cpp.obj : error LNK2005: "unsigned char * * GNameBlocksDebug" (?GNameBlocksDebug@@3PEAPEAEEA) �Ѿ��� ModulePackage.cpp.obj �ж���
UATHelper: Packaging (Windows (64-bit)): ���ڴ����� D:\Codes\UE4\ModulePackage\Binaries\Win64\ModulePackage.lib �Ͷ��� D:\Codes\UE4\ModulePackage\Binaries\Win64\ModulePackage.exp
UATHelper: Packaging (Windows (64-bit)): D:\Codes\UE4\ModulePackage\Binaries\Win64\ModulePackage.exe : fatal error LNK1169: �ҵ�һ���������ض���ķ���
LogSlate: Took 0.021891 seconds to synchronously load lazily loaded font '../../../Engine/Content/Slate/Fonts/NotoNaskhArabicUI-Regular.ttf' (144K)
LogSlate: Last resort fallback font was requested. Font: '../../../Engine/Content/Slate/Fonts/DroidSansFallback.ttf', Character: 'Ͷ (U+0376)'
LogSlate: Took 0.075434 seconds to synchronously load lazily loaded font '../../../Engine/Content/SlateDebug/Fonts/LastResort.ttf' (5269K)
UATHelper: Packaging (Windows (64-bit)): Took 153.3858471s to run UnrealBuildTool.exe, ExitCode=6
UATHelper: Packaging (Windows (64-bit)): UnrealBuildTool failed. See log for more details. (C:\Users\Goulandis\AppData\Roaming\Unreal Engine\AutomationTool\Logs\D+UE_4.27\UBT-ModulePackage-Win64-Development.txt)
UATHelper: Packaging (Windows (64-bit)): AutomationTool exiting with ExitCode=6 (6)
UATHelper: Packaging (Windows (64-bit)): BUILD FAILED
PackagingResults: Error: Unknown Error

error LNK2005的原因是说有重定义,但是新建的类就是一个空类始终无法想明白是什么原因导致的,无论是使用自定的Actor还是自定义的C++类都一样。

而网上关于自定义模块的打包的资料基本没有我在内外网搜索都翻个7-8页基本挨边的资料都找不到,唉,这就是UE最痛苦的地方。

在测试的过程中还碰到另一个问题,就是往自定义模块添加类的时候,引擎添加新类会出现中断,这个情况下去引用模块的Actor类资源会出现.generated.h文件打不开,导致VS编译始终报错,具体原因也不明,不知道为什么编辑器向自定义模块添加新类会出现中断,也不知道为什么.generated.h文件命名存在VS就是打不开,即使把目录包含到项目属性的VC++目录也一样。

十四、引用第三方库

第三方库份两种,一种是动态库(dll)一种是静态库(lib),想找一个简单的库来做测试最好的方式还是自己创建一个,我就创建一个计算圆面积的库。

1.创建Lib库

使用VS新建一个空项目,往项目中添加四个文件,Mian.cpp,Area.h,Area.cpp,Source.def

  • Main.cpp:是用来放main函数,我自己操作的时候没有main函数会报无法解析外部符号的错误,不过在网上看别人制作库的时候却并没有添加main函数,可能是使用C++版本或是VS版本不同的原因吧;

    1
    2
    3
    4
    int main()
    {
    return 0;
    }
  • Area.h:声明方法的头文件,也是提供给外部调用时include的头文件;

    1
    2
    3
    4
    5
    6
    7
    8
    #pragma once
    #include <math.h>
    class Area
    {
    public:
    static float GetArea(float R);
    };

    这里将方法封装在了一个类,当然我们也可以不用类来封装,而是直接写文件里作为全局函数来使用。

  • Area.cpp:实现方法的Cpp;

    1
    2
    3
    4
    5
    #include "Area.h"
    float Area::GetArea(float R)
    {
    return 3.14*pow(R,2);
    }
  • Source.def:是在右键项目->添加->添加新项->代码->模块定义文件中添加的,用来描述库需要导出的内容,这是因为一个项目要导出库不是所有内容都要导出的,.def文件就给予我们导出的灵活性。

    1
    2
    3
    LIBRARY CircleArea
    EXPORTS
    GetArea

    至于.def的具体语法在这里不是重点,就没有深入去研究了。

然后将项目改成x64平台,因为UE的项目默认都是x64的,如果平台不一致,库将没有办法在UE中使用。

然后将项目属性/配置属性/常规/配置类型改为静态库。

然后生成项目,我们就可以在项目目录/x64/Debug/下看到我们的lib库了。

2.添加Lib库

第三方库UE有专门的文件夹放置—项目目录/Source/ThirdParty,ThirdParty文件夹还有include,lib两个文件夹,include放置第三方库的头文件,lib放置地方库的lib文件。

我们将上面制作出来的CircleArea库用到的头文件放入include中,将lib文件放库lib中。

然后编辑项目.Build.cs文件,哪个模块需要使用第三方库就编辑哪个模块的.Build.cs。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using UnrealBuildTool;
using System.Collections.Generic;
using System.IO;
public class UE4Cpp : ModuleRules
{
private string ModulePath
{
get { return ModuleDirectory; }
}
private string ThirdPartyPath
{
get { return Path.GetFullPath(Path.Combine(ModulePath, "../ThirdParty")); }
}
public UE4Cpp(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","AModule"});
PrivateDependencyModuleNames.AddRange(new string[] { });
PublicIncludePaths.Add(Path.Combine(ThirdPartyPath, "include"));
PublicAdditionalLibraries.Add(Path.Combine(ThirdPartyPath, "lib", "CircleArea.lib"));
}
}

在类里我对ModuleDirectory进行了封装,因为VS对UBT的支持还不是很好,很多UE提供的函数或变量VS是没有提示的,为了方便使用所以进行一次封装,同时也把ThirdParty文件路径也封装了。

在PublicIncludePaths中添加头文件的路径,在PublicAdditionalLibraries中添加lib库的路径,如此我们的库就添加进UE4中了

3.使用Lib库

使用Lib库的方法就比较简单了,直接在要使用的地方include头文件,然后就可以使用库里的类和方法了。

1
2
3
4
5
6
7
8
#include "../ThirdParty/include/Area.h"
//...
void AContainerctor::BeginPlay()
{
Super::BeginPlay();
float Area = Area::GetArea(10);
UE_LOG(LogTemp, Log, TEXT("Area=%f"), Area);
}

运行结果:

4.创建Dll库

创建动态库的方法和静态库一样,只是把配置类型改为动态库,然后生成,就可以在x64/Debug/目录下看到生成的.dll文件和.lib文件。

5.添加Dll库

添加动态库的方式有两种,其一:

在.Build.cs文件中添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnrealBuildTool;
using System.Collections.Generic;
using System.IO;
public class UE4Cpp : ModuleRules
{
private string ModulePath
{
get { return ModuleDirectory; }
}
private string ThirdPartyPath
{
get { return Path.GetFullPath(Path.Combine(ModulePath, "../ThirdParty")); }
}
public UE4Cpp(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","AModule"}); PublicDelayLoadDLLs.Add(Path.Combine(ThirdPartyPath,"dll","CircleArea.dll"));
}
}

这个方式又引擎自动管理动态库的添加时机,在使用时要提前将动态库目录加入搜索列表,否则UE将找不到动态库。

1
FPlatformProcess::PushDllDirectory(*(FPaths::GameSourceDir() / TEXT("ThirdParty/dll")));

其二就是在使用的时候直接使用绝对路径加载,详细可以直接看下一节的代码。

6.使用Dll库

如果使用第5节提到的第一种方法添加的动态库就这么使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef float(*_GetArea)(float R);
//..
void AContainerctor::BeginPlay()
{
Super::BeginPlay();
FPlatformProcess::PushDllDirectory(*(FPaths::GameSourceDir() / TEXT("ThirdParty/dll")));
void* DllHandle = FPlatformProcess::GetDllHandle(TEXT("CircleArea.dll"));
if (DllHandle)
{
_GetArea DllGetArea = (_GetArea)FPlatformProcess::GetDllExport(DllHandle, TEXT("GetArea"));
if (DllGetArea)
{
float Area = DllGetArea(10);
UE_LOG(LogTemp, Log, TEXT("Area=%f"), Area);
}
}
}

如果使用的是第二种方法就这么使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef float(*_GetArea)(float R);
//...
void AContainerctor::BeginPlay()
{
Super::BeginPlay();
FString DllPath = FPaths::Combine(*FPaths::GameSourceDir(), TEXT("ThirdParty/dll"), TEXT("CircleArea.dll"));
if (FPaths::FileExists(DllPath))
{
void* DllHandle = FPlatformProcess::GetDllHandle(*DllPath);
if (DllHandle)
{
_GetArea DllGetArea = (_GetArea)FPlatformProcess::GetDllExport(DllHandle, TEXT("GetArea"));
if (DllGetArea)
{
float Area = DllGetArea(10);
UE_LOG(LogTemp, Log, TEXT("Area=%f"), Area);
}
}
}
}
  • typedef float(*_GetArea)(float R):这是定义了一个函数指针_GetArea,函数接收一个float类型参数,返回一个float类型的值,和dll中的GetArea函数对应。
  • FPlatformProcess::GetDllHandle:获取Dll的引用;
  • (_GetArea)FPlatformProcess::GetDllExport:通过名字获取Dll中的对应函数入口,并强转成我们定义的函数指针类型,因为函数指针的类型是我们根据Dll中对应函数来定义的,所以一般强转都是可以成功的;
  • DllGetArea(10):通过函数指针来调用Dll中的函数。

7.碰到的问题

在试验的过程中碰到两个问题。

其一就是如果一个动态库里有两个不同的类但是两个类都拥有一个同名的函数,我们在调用的的时候应该如何区分呢?这是看到UE是通过函数名来获取Dll中的函数想到的问题,这个其实对于使用者来说不需要考虑,因为Dll在导出的时候就要求了不能有同名函数,否则编译会报错。

其二就是我在网上查到的所有的使用Dll的文件基本都是告诉我们怎么去使用Dll中的函数,没有一篇是将怎么去使用Dll中的类的,有在C++中使用的方法,不过直接搬到UE4中似乎并不适用。

十五、断言

断言是UE4程序中的一种代码检查机制,有时我们在执行一段代码时,有些值是要确保一定存在的,不存在程序将不能在往下执行直接中断,断言就被用来处理这个问题的,断言是一种来调试代码的工具,断言不会被编译进要发布的代码中,所以断言不会影响Shipping版本。

断言判断的内容必须为true,否则将执行中断。

断言的定义在AssertionMacros.h文件中,断言分三类,check、verify和ensure。

1.Check类型断言

check(expr)

expr为false直接中断程序,可以在Debug、Development、Shipping的Editor版本中运行。

1
2
3
4
5
void AContainerctor::CheckTest()
{
check(Count == 1);
UE_LOG(LogTemp, Log, TEXT("Count=%d"),Count);
}

checkSlow(expr)

expr为false直接中断程序,只能在Debug版本中运行

1
2
3
4
5
void AContainerctor::CheckTest()
{
checkSlow(Count == 1);
UE_LOG(LogTemp, Log, TEXT("Count=%d"),Count);
}

按照官方文档的说明是只能在Debug版本中运行,但是我在实际调试中发现,checkSlow似乎在任何情况下都没有运行,于是我打开Build.h看了一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#if UE_BUILD_DEBUG
#ifndef DO_GUARD_SLOW
#define DO_GUARD_SLOW 1
#endif
#ifndef DO_CHECK
#define DO_CHECK 1
#endif
#ifndef DO_ENSURE
#define DO_ENSURE 1
#endif
#ifndef STATS
#define STATS ((WITH_UNREAL_DEVELOPER_TOOLS || !WITH_EDITORONLY_DATA || USE_STATS_WITHOUT_ENGINE || USE_MALLOC_PROFILER || FORCE_USE_STATS) && !ENABLE_STATNAMEDEVENTS)
#endif
#ifndef ALLOW_DEBUG_FILES
#define ALLOW_DEBUG_FILES 1
#endif
#ifndef ALLOW_CONSOLE
#define ALLOW_CONSOLE 1
#endif
#ifndef NO_LOGGING
#define NO_LOGGING 0
#endif
#elif UE_BUILD_DEVELOPMENT
#ifndef DO_GUARD_SLOW
#define DO_GUARD_SLOW 0
#endif
#ifndef DO_CHECK
#define DO_CHECK 1
#endif
#ifndef DO_ENSURE
#define DO_ENSURE 1
#endif
#ifndef STATS
#define STATS ((WITH_UNREAL_DEVELOPER_TOOLS || !WITH_EDITORONLY_DATA || USE_STATS_WITHOUT_ENGINE || USE_MALLOC_PROFILER || FORCE_USE_STATS) && !ENABLE_STATNAMEDEVENTS)
#endif
#ifndef ALLOW_DEBUG_FILES
#define ALLOW_DEBUG_FILES 1
#endif
#ifndef ALLOW_CONSOLE
#define ALLOW_CONSOLE 1
#endif
#ifndef NO_LOGGING
#define NO_LOGGING 0
#endif
#elif UE_BUILD_TEST
#ifndef DO_GUARD_SLOW
#define DO_GUARD_SLOW 0
#endif
#ifndef DO_CHECK
#define DO_CHECK USE_CHECKS_IN_SHIPPING
#endif
#ifndef DO_ENSURE
#define DO_ENSURE USE_ENSURES_IN_SHIPPING
#endif
#ifndef STATS
#define STATS ((USE_MALLOC_PROFILER || FORCE_USE_STATS) && !ENABLE_STATNAMEDEVENTS)
#endif
#ifndef ALLOW_DEBUG_FILES
#define ALLOW_DEBUG_FILES 1
#endif
#ifndef ALLOW_CONSOLE
#define ALLOW_CONSOLE 1
#endif
#ifndef NO_LOGGING
#define NO_LOGGING !USE_LOGGING_IN_SHIPPING
#endif
#elif UE_BUILD_SHIPPING
#if WITH_EDITOR
#ifndef DO_GUARD_SLOW
#define DO_GUARD_SLOW 0
#endif
#ifndef DO_CHECK
#define DO_CHECK 1
#endif
#ifndef DO_ENSURE
#define DO_ENSURE 1
#endif
#ifndef STATS
#define STATS 1
#endif
#ifndef ALLOW_DEBUG_FILES
#define ALLOW_DEBUG_FILES 1
#endif
#ifndef ALLOW_CONSOLE
#define ALLOW_CONSOLE 0
#endif
#ifndef NO_LOGGING
#define NO_LOGGING 0
#endif
#else
#ifndef DO_GUARD_SLOW
#define DO_GUARD_SLOW 0
#endif
#ifndef DO_CHECK
#define DO_CHECK USE_CHECKS_IN_SHIPPING
#endif
#ifndef DO_ENSURE
#define DO_ENSURE USE_ENSURES_IN_SHIPPING
#endif
#ifndef STATS
#define STATS (FORCE_USE_STATS && !ENABLE_STATNAMEDEVENTS)
#endif
#ifndef ALLOW_DEBUG_FILES
#define ALLOW_DEBUG_FILES 0
#endif
#ifndef ALLOW_CONSOLE
#define ALLOW_CONSOLE ALLOW_CONSOLE_IN_SHIPPING
#endif
#ifndef NO_LOGGING
#define NO_LOGGING !USE_LOGGING_IN_SHIPPING
#endif
#endif
#else
#error Exactly one of [UE_BUILD_DEBUG UE_BUILD_DEVELOPMENT UE_BUILD_TEST UE_BUILD_SHIPPING] should be defined to be 1
#endif

确实DO_GUARD_SLOW宏只有在Debug模式下才被置为1其他模式都是0,我把Developer模式下的DO_GUARD_SLOW宏也设置成1,发现checkFlow在Debug模式和Developer模式下都运行了,不知道是不是bug。

checkf(expr,text)

expr为false直接中断程序,并把Text打印到日志,可以在Debug、Development、Shipping Editor版本中运行。

1
2
3
4
5
void AContainerctor::CheckTest()
{
checkf(Count == 1,TEXT("Count!=1"));
UE_LOG(LogTemp, Log, TEXT("Count=%d"),Count);
}

关于checkf我们不能使用VS拉起来的引擎测试效果,VS拉起来的引擎会直接在VS中中断,导致错误信息无法打印到日志中。

checkfSlow(expr,text)

checkSlow的checkf版,和checkSlow一样也有bug。

1
2
3
4
5
void AContainerctor::CheckTest()
{
checkfSlow(Count == 1,TEXT("Count!=1"));
UE_LOG(LogTemp, Log, TEXT("Count=%d"),Count);
}

checkCode(code)

直接执行()中的代码,实际测试….这他呀的根本不会中断,感觉没什么卵用。

1
2
3
4
5
6
7
8
9
10
void AContainerctor::CheckTest()
{
checkCode(
if (Count != 1)
{
Count = 2;
UE_LOG(LogTemp, Error, TEXT("Count=%d"), Count);
}
);
}

checkNoEntry()

如果程序执行了此语句就直接中断,用来截断不能被执行的代码。

1
2
3
4
void AContainerctor::CheckTest()
{
checkNoEntry();
}

checkNoReentry()

如果此语句被执行超过1次,就中断程序,用来标注只能被执行一次的代码。

1
2
3
4
void AContainerctor::CheckTest()
{
checkNoReentry();
}

checkNoRecusion()

判断函数是否递归,如果递归则中断。

1
2
3
4
5
6
7
8
void AContainerctor::CheckTest()
{
checkNoRecursion();
if (Count == 1)
{
this->CreateThread();
}
}

unimplemented()

如果被执行则中断,效果和checkNoEntry一样,不过主要用于因该被覆盖而不会被调用的虚函数,实则没什么乱用,我不理解不会被调用的函数留着干嘛。

1
2
3
4
void AContainerctor::CheckTest()
{
unimplemented();
}

2.Verify类型断言

Verify类型的断言中表达式会独立于断言运行,就是说无论断言是否生效(DO_CHECK=1断言生效,DO_CHECK=0断言不生效),代码执行到此语句的时候表达式都会运行,而Check类型的断言则是断言不生效时,表达式就不会执行。

verify有verify、verifySlow、verifyf、verifyfSlow四种,用法上check一样,当然bug也是一样的。

3.Ensure类型断言

ensure类型的断言和verify有点类似,在不出现会导致程序崩溃的错误时(如读取空指针),ensure都不会中断,程序依然会执行但是ensure会在首次执行时将消息通知给引擎的崩溃报告器。

ensure(exps)

首次执行时将false消息通知到崩溃报告器,不执行中断。

1
2
3
4
5
void AContainerctor::EnsureTest()
{
ensure(Count == 1);
UE_LOG(LogTemp, Log, TEXT("Ensure"));
}

通过VS拉起的引擎在执行到ensure时会直接在VS中断,所以要验证效果的话,引擎不能通过VS拉起。

执行了ensure程序不会中断后面的代码依然执行,并且会在首次执行时将错误报告打印在日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
LogOutputDevice: Warning: Script Stack (2 frames):
Untitled_C.ExecuteUbergraph_Untitled
Untitled_C.InpActEvt_Three_K2Node_InputKeyEvent_0
LogStats: FPlatformStackWalk::StackWalkAndDump - 0.076 s
LogOutputDevice: Error: === Handled ensure: ===
LogOutputDevice: Error: Ensure condition failed: Count == 1 [File:D:\Codes\UE4\UE4Cpp\Source\UE4Cpp\Containerctor.cpp] [Line: 33]
LogOutputDevice: Error: Stack:
LogOutputDevice: Error: [Callstack] 0x00007ff92cfea1a8 UE4Editor-UE4Cpp.dll!<lambda_39fe32f94e8347501feb44ad8f6d752a>::operator()() [D:\Codes\UE4\UE4Cpp\Source\UE4Cpp\Containerctor.cpp:33]
LogOutputDevice: Error: [Callstack] 0x00007ff92cfe4b8e UE4Editor-UE4Cpp.dll!AContainerctor::CreateThread() [D:\Codes\UE4\UE4Cpp\Source\UE4Cpp\Containerctor.cpp:33]
LogOutputDevice: Error: [Callstack] 0x00007ff9667019c4 UE4Editor-CoreUObject.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9669bf37b UE4Editor-CoreUObject.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9669e2573 UE4Editor-CoreUObject.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9669e4d8d UE4Editor-CoreUObject.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9669b341a UE4Editor-CoreUObject.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9669e497c UE4Editor-CoreUObject.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9669e4d8d UE4Editor-CoreUObject.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9669e4184 UE4Editor-CoreUObject.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9667019c4 UE4Editor-CoreUObject.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9669e3ca3 UE4Editor-CoreUObject.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff947ce942a UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff948ffec64 UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff94902eeee UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff948b16460 UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff948b2763c UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff948b14c2b UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff948b26739 UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff947ccc06e UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff948ee410e UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff948eed2e4 UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff966de2398 UE4Editor-Core.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff966de27ce UE4Editor-Core.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff966df40bd UE4Editor-Core.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff948f093be UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff948f0faba UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9485f349f UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9485fe2fc UE4Editor-Engine.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9465714c3 UE4Editor-UnrealEd.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff946e72ba6 UE4Editor-UnrealEd.dll!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff664aa87a0 UE4Editor.exe!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff664ac0fcc UE4Editor.exe!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff664ac10ba UE4Editor.exe!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff664ac40dd UE4Editor.exe!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff664ad5984 UE4Editor.exe!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff664ad853a UE4Editor.exe!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9d88f7034 KERNEL32.DLL!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ff9da0fd0d1 ntdll.dll!UnknownFunction []
LogStats: SubmitErrorReport - 0.000 s
LogStats: SendNewReport - 1.662 s
LogStats: FDebug::EnsureFailed - 1.787 s
LogTemp: Ensure

ensureMsgf(exps,text)

首次执行时将错误信息通知到崩溃报告器,并将text打印到日志。

1
2
3
4
void AContainerctor::EnsureTest()
{
ensureMsgf(Count == 1,TEXT("Count!=1"));
}

ensureAlways(exps)

区别于ensure只在首次执行时通知崩溃报告器,ensureAlways只要执行就将错误信息通知到崩溃报告器,无论执行多少次。

1
2
3
4
void AContainerctor::EnsureTest()
{
ensureAlways(Count == 1);
}

ensureAlwaysMsgf(exps,text)

ensureMsgf的Always版本

1
2
3
4
void AContainerctor::EnsureTest()
{
ensureAlways(Count == 1,TEXT("Count!=1"));
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!