目录
使用 C++ 代码进行游戏元素编程时,每个模块都会包含许多 C++ 类;每个类定义了新Actor或对象的模板,类头文件里声明了类和类的函数和属性;类还可包括结构体这种有助于进行相关属性组织和操作的数据结构;结构体也可被自行定义; 通过Interfaces可以使不同的类应用额外的游戏行为;
在UE中编程时,可使用标准 C++ 类、函数和变量,及使用标准 C++ 语法对它们进行定义; 然而,UCLASS()、UFUNCTION() 和 UPROPERTY() 宏可使引擎识别新的类、函数和变量;如,以 UPROPERTY() 宏声明的变量可被引擎垃圾回收, 也可在编辑器中显示和编辑;此外还有 UINTERFACE() 和 USTRUCT() 宏, 以及其他宏关键词(用于指定 类、函数、属性、接口或结构体在引擎和编辑器中的行为);

一,Asserts
在C和C++编程中,assert 可在开发期间帮助检测和诊断不正常或无效的运行时条件;这些条件通常检查是否指针为非空、除数为非零、函数并非递归运行,或代码要求的其他重要假设;但每次检查会使得效率十分低下;某些情况下,assert 会在延迟崩溃发生之前发现导致该崩溃的bug,如删除未来tick所需的对象,协助开发人员发现引起崩溃的根本原因;assert 的关键特性之一是不存在于发布代码中,这意味着不但不会影响发布产品的性能,也没有任何副作用;对 assert 最简单的理解就是:"断言"必须一律为true,否则程序会停止运行;
引擎提供 assert 等同项的三个不同族系:check、verify 和 ensure;若要检查这些功能背后的代码,可在 Engine/Source/Runtime/Core/Public/Misc/AssertionMacros.h 中找到相关的宏;各个功能的行为略有不同,但它们都是开发期间使用的诊断工具,目标大致相同;
Check
Check族系最接近基础 assert,因为当第一个参数得出的值为false时,此族系的成员会停止执行,且默认不会在发布版本中运行;
| Macro | Parameters | Behavior |
check or checkSlow | Expression | 若 Expression 为false,停止执行; |
checkf 或 checkfSlow | Expression、FormattedText、... | 若 Expression 为false,则停止执行并将 FormattedText 输出到日志; |
| checkCode | Code | 在运行一次的do-while循环结构中执行 |
| checkNoEntry | / | 若此行被hit,则停止执行,类似于 check(false),但主要用于不可到达的代码路径; |
| checkNoReentry | / | 若此行被hit超过一次,则停止执行; |
| checkNoRecursion | / | 若此行被hit超过一次而未离开作用域,则停止执行; |
| unimplemented | / | 若此行被hit,则停止执行,类似于 check(false),但主要用于应被覆盖而不会被调用的虚拟函数; |
检查宏仅在调试(Debug)、开发(Development)版本中运行;"Slow"宏仅在调试(Debug)版本中运行;设置 USE_CHECKS_IN_SHIPPING=1 ,可使Check宏在测试(Test)和发布编辑器(Shipping Editor)版本中运行;此法在以下情况中十分实用:怀疑Check宏中的代码正在修改值,发现了仅存在于在发布版本中且难以追踪的bug,但认为现有Check宏能找到这些bug;项目发布时应将 USE_CHECKS_IN_SHIPPING 设为默认值 0;
Verify
在大部分版本中,Verify族系的行为与Check族系相同;但即便在禁用Check宏的版本中,Verify宏也会计算其表达式的值,意味着仅当该表达式需要独立于诊断检查之外运行时,才应使用Verify宏;如,若某个函数执行操作,然后返回 bool 来说明该操作是否成功,则应使用Verify而非Check来确保该操作成功;因为在发布版本中Verify将忽略返回值,但仍将执行操作;而Check在发布版本中根本不调用该函数,所以行为才会有所不同;
| Macro | Parameters | Behavior |
verify or verifySlow | Expression | 若 Expression 为false,停止执行; |
verifyf or verifyfSlow | Expression、FormattedText、... | 若 Expression 为false,则停止执行并将 FormattedText 输出到日志; |
验证宏在调试(Debug)、开发(Development)、测试(Test)和发布编辑器(Shipping Editor)版本中完整运行(以"Slow"结尾的宏除外,其仅在调试(Debug)版本中运行);定义 USE_CHECKS_IN_SHIPPING 来保留一个true值(通常为 1),从而覆盖此行为;在所有其他情况下,Verify宏将计算其表达式,但不会停止执行或将文本输出到日志;
Ensure
Ensure族系类似于Verify族系,但可在出现非致命错误时使用;这意味着,若Ensure宏的表达式计算得出的值为false,引擎将通知崩溃报告器,但仍会继续运行;为避免崩溃报告器收到太多通知,Ensure宏在每次引擎或编辑器会话中仅报告一次;若实际情况需要Ensure宏在每次表达式计算得值为false时都报告一次,则使用"Always"版本的宏;
| Macro | Parameters | Behavior |
| ensure | Expression | Expression 首次为false时通知崩溃报告器; |
| ensureMsgf | Expression、FormattedText、... | Expression 首次为false时通知崩溃报告器并将 FormattedText 输出到日志; |
| ensureAlways | Expression | Expression 为false时通知崩溃报告器; |
| ensureAlwaysMsgf | Expression、FormattedText、... | Expression 为false时通知崩溃报告器并将 FormattedText 输出到日志; |
Ensure宏在所有版本中计算其表达式的值,但仅在调试(Debug)、开发(Development)、测试(Test)和发布编辑器(Shipping Editor)版本中联系崩溃报告器;
Usage Examples
以下假设情况展示了一些用例,其中Check、Verify和Ensure可帮助理清代码或协助调试;
// 决不可使用空JumpTarget调用此函数;若发生此情况,须停止程序;
void AMyActor::CalculateJumpVelocity(AActor* JumpTarget, FVector& JumpVelocity)
{
check(JumpTarget != nullptr);
// 计算在JumpTarget上着陆所需的速度,现在可确定JumpTarget为非空;
}
// 这行代码捕获了在产品发布版本中可能出现的小错误;
// 此错误较小,无需停止执行便可解决;
// 虽然该bug已修复,但开发者仍然希望了解之前是否曾经出现过此bug;
void AMyActor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// 确保bWasInitialized为true,然后再继续;若为false,则在日志中记录该bug尚未修复;
if (ensureMsgf(bWasInitialized, TEXT("%s ran Tick() with bWasInitialized == false"), *GetActorLabel()))
{
// 执行一些需要已正确初始化AMyActor的操作
}
}
// 若添加新形状类型,但忘记在此切换块中处理,则此代码将停止;
switch (MyShape) {
case EShapes::S_Circle:
//(处理圆圈。)
break;
case EShapes::S_Square:
//(处理方块。)
break;
default:
// 每种形状类型都应有相应情况,因此这种情况不应该发生;
checkNoEntry();
break;
}
// 此UObject拥有测试函数IsEverythingOK,没有副作用,若出现问题则返回false;
// 若发生这种情况,将出现致命错误并终止;
// 因为代码无副作用,仅作诊断之用,因此无需在发布版本中运行;
checkCode(
if (!IsEverythingOK())
UE_LOG(LogUObjectGlobals, Fatal, TEXT("Something is wrong with %s!Terminating."), *GetFullName());
);
// 此列表中不应有圆圈,若有,程序将停转;但检查圆圈耗时较长,因此建议在调试版本中操作;
checkSlowf(!MyLinkedList.HasCycle(), TEXT("Found a cycle in the list!"));
// 遍历列表,在各个元素上运行一些代码;
二,Gameplay Classes
引擎中每个游戏类都由一个头文件(.h)和一个源文件(.cpp)构成;头文件包含类和类成员(如变量和函数)的声明,而源文件是类功能的实现;类通过首字母或前缀来标准化的命名;
Adding Classes
主菜单Tools > New C++ Class...是一种快速简便的方法,将本机C++代码类添加到项目中,以便根据需要扩展功能;这会将纯内容项目转换为代码项目;确保安装了兼容版本的Visual Studio;


Class Headers
引擎中的类通常拥有单独且唯一的头文件,通常这些文件的命名与其中定义的类相匹配(减去 A 或 U 前缀,并使用 .h 文件扩展名),如AActor 类的类头文件命名为 Actor.h;
头文件使用标准 C++ 语法,并结合专门的宏,以简化类、变量和函数的声明过程;
// 在每个头文件都包含自动生成的头文件;
#include "ClassName.generated.h"
Class Declaration
类声明定义类的名称、其继承的类,以及继承的函数和变量;通过类说明符和元数据,类声明还定义了引擎和编辑器的特定行为;
UCLASS([specifier, specifier, ...], [meta(key = value, key = value, ...)])
class ClassName : public ParentName
{
GENERATED_BODY() // 必须被放置在类体的最前方
}
Class Specifiers
在类声明中添加类说明符,以控制类在引擎和编辑器的各个方面如何表现;
| Class Specifier | Effect |
| Abstract | 声明为"抽象基类",不可向关卡中添加此类的Actor,如 |
| Blueprintable | 将此类公开为用于创建蓝图的可接受基类,默认为NotBlueprintable,子类会继承; |
| NotBlueprintable | 指定此类不是可用于创建蓝图的可接受基类,此为默认说明符,子类会继承; |
| BlueprintType | 将此类公开为可用于蓝图变量的类型; |
| Const | 此类中的所有属性和函数都是const并且导出为const,子类会继承; |
| ConversionRoot | 根转换,将子类限制为仅可沿层级向上转换为第一个根类的子类; |
| CustomConstructor | 阻止构造函数声明自动生成; |
| DefaultToInstanced | 此类的所有实例都被认为是"实例化的",实例化的类(组件)将在构造时被复制,子类会继承; |
| EditInlineNew | 可从编辑器"Property"窗口创建此类的对象,而非从现有资源引用;默认仅可通过"Property"窗口指定对现有对象的引用; 此说明符会传播到所有子类,子类可通过 NotEditInlineNew 说明符覆盖它; |
| Intrinsic | 此类直接在C++中声明,无 Unreal Header Tool 生成的样板,请勿在新类上使用此说明符; |
| MinimalAPI | 仅导出类的类型信息供其他模块使用; 该类可被类型转换,但无法调用该类的函数(内联方法除外); 对于不需要在其他模块中访问所有函数的类,通过不导出所有内容,可以缩短编译时间; |
| NoExport | 此类的声明不应包含在自动生成的C++头文件中,必须在单独的头文件中手动定义该C++类声明; 仅对本地类有效,请勿对新类使用此说明符; |
| Transient | 属于此类别的对象将永远不会保存到磁盘,与非持久性的本地类别(如玩家或窗口)结合使用时非常有用; 此说明符会传递给子类,但可以被非瞬态说明符覆盖; |
| NonTransient | 使继承自基类的Transient说明符无效; |
| Placeable | 可在编辑器中创建此类,且可将此类放置到关卡、UI场景或蓝图(取决于类类型)中; 此标志会传播到所有子类,子类可使用 NotPlaceable说明符覆盖此标志; |
| NotPlaceable | 使继承自基类的Placeable说明符无效,不可以在编辑器中将此类的对象放置到关卡、UI场景或蓝图中; |
| PerObjectConfig | 此类别的配置信息将按对象存储,其中每个对象在.ini文件中都有一个以对象名称命名的部分,格式为[ObjectName ClassName],此说明符会传递给子类; |
| Within=OuterClassName | 此类的对象无法在 要创建此类的对象,需要提供 |
| AdvancedClassDisplay | 强制类的所有属性仅显示在细节面板的"Advanced"部分; |
| ClassGroup=GroupName | 在编辑器的Actor浏览器中启用 Group View 时,Actor Browser 应在指定的 GroupName 中包含此类及其所有子类; |
| Config=ConfigName | 此类可在配置文件( 会传播到所有子类并且无法使其无效,但是子类可通过重新声明 |
| DependsOn=(ClassName1, ClassName2, ...) | 所列的类将先于此类被编译,提供的类名必须指明是同一(或之前)包中的类; 可使用单个 当某个类使用在另一个类中声明的结构体或枚举时,这非常重要,因为编译器只知道它已经编译过的类中的内容; |
| Deprecated | 此类已弃用,序列化时将不保存此类的对象,子类会继承; |
| AutoCollapseCategories=(Category1, Category2, ...) | 使父类上的 AutoExpandCategories 说明符列出类别的效果无效; |
| AutoExpandCategories=(Category1, Category2, ...) | 指定应自动在编辑器属性窗口中展开的一个或多个类别; 要自动展开未使用类别变量,使用类名称; |
| DontAutoCollapseCategories=(Category, Category, ...) | 使列出类别的继承自父类的AutoCollapseCategories说明符无效; |
| CollapseCategories | 此类的属性不应划分到编辑器属性窗口的类别中,会传播到子类,可由DontCollapseCategories说明符覆盖; |
| DontCollapseCategories | 使继承自基类的CollapseCatogories说明符无效; |
| ShowCategories=(Category1, Category2, ...) | 使继承自基类的HideCategories说明符无效; |
| HideCategories=(Category1, Category2, ...) | 列出对用户完全隐藏的类别,此说明符会传播到子类; 要隐藏未使用类别声明的属性,使用声明变量的类的名称; |
| ShowFunctions=(Category1, Category2, ...) | 在属性查看器中显示列出的类别中的所有函数; |
| HideFunctions=(Category1, Category2, ...) | 让指定分类中的所有函数都对用户完全隐藏; |
| ShowFunctions=FunctionName | 在属性查看器中显示指定的函数; |
| HideFunctions=FunctionName | 将提到的函数对用户完全隐藏; |
| HideDropdown | 阻止此类在属性窗口组合框中显示; |
Metadata Specifiers
在声明类、接口、结构体、枚举/枚举值、函数或属性时,可以添加元数据说明符来控制其与引擎和编辑器各个方面的交互方式;每种数据结构或成员类型都有其自己的元数据说明符列表,元数据仅存在于编辑器中,请勿编写访问元数据的游戏逻辑;
| Class Meta Tag | Effect |
| BlueprintSpawnableComponent | 如其存在,组件类可由蓝图生成; |
| BlueprintThreadSafe | 只在蓝图函数库上有效,将把此类中的函数在动画蓝图中的非游戏线程上标记为可调用; |
| ChildCannotTick | 用于Actor和组件类,如本地类无法tick,那么基于此Actor或组件的蓝图生成类则无法tick,即使 bCanBlueprintsTickByDefault 为true也同样如此; |
| ChildCanTick | 用于Actor和组件类,如本地类无法tick,那么可以覆盖基于此Actor或组件的蓝图生成类的 bCanEverTick 标签,即使 bCanBlueprintsTickByDefault 为false也同样如此; |
| DeprecatedNode | 用于行为树节点,说明类已废弃,编译时将显示一条警告; |
| DeprecationMessage="Message Text" | 如类已废弃,尝试编译使用此类的蓝图时,会添加到标准废弃警告; |
| DisplayName="Blueprint Node Name" | 此节点在蓝图中的命名将被此处提供的值所取代,而非代码生成的命名; |
| DontUseGenericSpawnObject | 不使用蓝图中的通用Create Object节点来生成类的一个对象,此说明符只有在用于既非Actor又非ActorComponent的BluprintType类时才有意义; |
| ExposedAsyncProxy | 在 Async Task 节点中公开此类的一个代理对象; |
| IgnoreCategoryKeywordsInSubclasses | 用于让一个类的首个子类忽略所有继承的 ShowCategories 和 HideCategories 说明符; |
| IsBlueprintBase="true/false" | 说明此类是否为创建蓝图的一个可接受基类,与 UCLASS 说明符Blueprintable 或 NotBlueprintable 相似; |
| KismetHideOverrides="Event1, Event2, .." | 不允许被覆盖的蓝图事件的列表; |
| ProhibitedInterfaces="Interface1, Interface2, .." | 列出与类不兼容的接口; |
| RestrictedToClasses="Class1, Class2, .." | 由蓝图函数库类使用,用于限制列表中命名类的用法; |
| ToolTip="Hand-written tooltip" | 覆盖从代码注释自动生成的提示文本; |
| ShortToolTip="Short tooltip" | 完整提示文本过长时使用的简短提示文本,如父类选取器对话; |
| ShowWorldContextPin | 说明放置在此类拥有的图表中的蓝图节点必须显式其World情景引脚(即使其通常状态下为隐藏也同样如此),因为此类的对象无法被用作World情景; |
| UsesHierarchy | 表示类使用层级数据,用于实例化"细节"面板中的层级编辑功能; |
| ScriptName="DisplayName" | 在将此类、属性或函数导出到脚本语言时使用的名称,可以包含已弃用的名称,作为额外的以分号分隔的条目; |
Class Implementation
所有类必须使用 GENERATED_BODY 宏才能正确实现,这是在定义类及其所有变量和函数的类头(.h)文件中完成的;最佳方法是使类源和头文件的命名与实现的类相匹配,减去 A 或 U 前缀,如AActor 类的源文件命名为 Actor.cpp,其头文件命名为 Actor.h(对编辑器中"Add C++ Class"而言是自动进行);
源文件(.cpp)必须包含 C++ 类声明的头文件(.h),通常为自动生成,但也可手动生成(如有必要),如 AActor 类的 C++ 声明在 EngineClasses.h 头文件中自动生成,Actor.cpp 文件必须包括 EngineClasses.h 文件或包含它的另一个文件;
Class Constructor
UObjects 使用 Constructors 设置属性和其他变量的默认值,并执行其他必要的初值设定;类构造函数通常放置在类实现文件中,如 AActor::AActor 构造函数位于 Actor.cpp 中;
注,部分构造函数可能以每个模块为基础放置在一个特殊的"constructors"文件中;
也可以将构造函数内联放置在类头文件中;然而,如果构造函数在类头文件中,UClass 必须结合 CustomConstructor 说明符进行声明,因为这阻止了自动代码生成器在标头中创建构造函数声明;
Constructor Format
// UObject 构建函数最基本的形式
UMyObject::UMyObject()
{
// 在此处初始化 Class Default Object 属性
}
// 另一个辅助构造函数,支持一个特殊的属性调整结构;
UMyObject::UMyObject(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Initialize CDO properties here.
}
尽管上述两个构造函数实际上都没有执行任何初始化操作,但引擎已经将所有字段初始化为零、NULL或其默认构造函数所实现的任何值;任何在构造函数中的初始化代码都将应用于CDO,也会被复制到正确创建的对象的任何新实例中,就像使用CreateNewObject或SpawnActor一样;
传递给构造函数的FObjectInitializer参数,尽管被标记为const,但可以通过内置的可变函数进行配置,以覆盖属性和子对象;正在创建的UObject将受到这些更改的影响,并且这可用于更改已注册属性或组件的值;
AUDKEmitterPool::AUDKEmitterPool(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.DoNotCreateDefaultSubobject(TEXT("SomeComponent")).DoNotCreateDefaultSubobject(TEXT("SomeOtherComponent")))
{
// Initialize CDO properties here.
}
在上例中,超类将在其构建函数中创建名为"SomeComponent"和"SomeOtherComponent"的子对象,但由于 FObjectInitializer 的原因,该操作将不会执行;在下例中,SomeProperty 在 CDO 中将默认为 26,因此在 AUTDemoHUD 的每个新实例中均为如此;
AUTDemoHUD::AUTDemoHUD()
{
// 在此处初始化 CDO 属性
SomeProperty = 26;
}
Constructor Statics and Helpers
为更复杂的数据类型(尤其是类引用、名称和资源引用)设置值时,需在构造函数中定义并实例化一个 ConstructorStatics 结构体来保存所需的各种属性值;该结构体仅在构造函数首次运行时创建,在后续运行中,它只需复制一个指针,因此速度极快;在创建 ConstructorStatics 结构体时,会将值分配给该结构体的成员,以便在构造函数后续将值分配给实际属性时进行访问;
ContructorHelpers 是在ObjectBase.h中定义的一个特殊命名空间,其中包含用于执行特定于构造函数的常见操作的辅助模板,如存在用于查找资源或类引用以及创建和查找组件的辅助模板;
Asset References
理想情况下,类中不应存在资源引用,硬编码的资源引用易出错,更推荐使用蓝图来配置资源属性;不希望每次构造对象时都搜索资源,通过一个静态结构体,确保只进行一次资源搜索;
ConstructorHelpers::FObjectFinder 通过 StaticLoadObject 函数查找指定 UObject 的引用,通常用于引用存储在内容包中的资源,如未找到对象,则会报告失败;
ATimelineTestActor::ATimelineTestActor()
{
// Structure to hold one-time initialization
struct FConstructorStatics
{
ConstructorHelpers::FObjectFinder<UStaticMesh> Object0;
FConstructorStatics()
: Object0(TEXT("StaticMesh'/Game/UT3/Pickups/Pickups/Health_Large/Mesh/S_Pickups_Base_Health_Large.S_Pickups_Base_Health_Large'"))
{
}
};
static FConstructorStatics ConstructorStatics;
// Property initialization
StaticMesh = ConstructorStatics.Object0.Object;
}
Class References
ConstructorHelpers::FClassFinder 为特定的 UClass 寻找引用,如类未找到,则报告失败;
APylon::APylon(const class FObjectInitializer& ObjectInitializer)
:Super(ObjectInitializer)
{
// 进行一次性初始化的结构
static FClassFinder<UNavigationMeshBase> ClassFinder(TEXT("class'Engine.NavigationMeshBase'"));
if (ClassFinder.Succeeded())
NavMeshClass = ClassFinder.Class;
else
NavMeshClass = nullptr;
}
在许多情况下,可只使用 USomeClass::StaticClass(),绕开复杂的全部 ClassFinder;对跨模块的引用而言,使用 ClassFinder 法较好;
NavMeshClass = UNavigationMeshBase::StaticClass();
Components and Sub-Objects
在构造函数中也可创建组件子对象并将其附加到角色的层次结构中;当生成一个角色时,其组件将从CDO中克隆;为了确保组件始终被创建、销毁并正确地进行垃圾回收,应在所属类的UPROPERTY中存储一个指向构造函数中创建的每个组件的指针;
UCLASS()
class AWindPointSource : public AActor {
GENERATED_BODY()
public:
UPROPERTY()
UWindPointSourceComponent* WindPointSource;
UPROPERTY()
UDrawSphereComponent* DisplaySphere;
};
AWindPointSource::AWindPointSource()
{
// Create a new component and give it a name.
WindPointSource = CreateDefaultSubobject<UWindPointSourceComponent>(TEXT("WindPointSourceComponent0"));
// Set our new component as the RootComponent of this actor, or attach it to the root if one already exists.
if (RootComponent == nullptr)
RootComponent = WindPointSource;
else
WindPointSource->AttachTo(RootComponent);
// Create a second component. This will be attached to the component we just created.
DisplaySphere = CreateDefaultSubobject<UDrawSphereComponent>(TEXT("DrawSphereComponent0"));
DisplaySphere->AttachTo(RootComponent);
// Set some properties on the new component.
DisplaySphere->ShapeColor.R = 173;
DisplaySphere->ShapeColor.G = 239;
DisplaySphere->ShapeColor.B = 231;
DisplaySphere->ShapeColor.A = 255;
DisplaySphere->AlwaysLoadOnClient = false;
DisplaySphere->AlwaysLoadOnServer = false;
DisplaySphere->bAbsoluteScale = true;
}
三,Gameplay Modules
将属于同一游戏项目的游戏类集合编译成动态链接库(DLL);
与引擎本身由一系列模块组成的方式相同,每个游戏也由一个或多个游戏模块组成;这些模块类似于引擎早期版本中的包,因为它们是相关类集合的容器;由于Gameplay都是用C++编写的,模块实际上是动态链接库(DLL),而不是专有的包文件;
模块创建
游戏模块至少必须包含一个头文件(.h)、一个C++文件(.cpp)和一个构建文件(*.Build.cs);
- 头文件,必须位于模块目录的Public文件夹中,即[GameName] \Source\ [ModuleName]\Public;此文件包含类所需的任何头文件,包括模块自动生成的头文件;
- C++文件,位于模块目录的Private文件夹中,即[GameName] \Source\ [ModuleName]\Private,用于注册和实现该模块;
- 构建文件,位于游戏模块的根目录,即 [GameName] \Source\ [ModuleName],定义构建工具(UnrealBuildTool)编译模块时使用的某些信息;
#include "Engine.h"
#include "EnginePrivate.h"
#include "<ModuleName>Classes.h"
// Include our game's header file
#include "<ModuleName>.h"
// 游戏中至少有一个模块必须使用 IMPLEMENT_PRIMARY_GAME_MODULE 进行注册;其他模块可以使用备选的 IMPLEMENT_GAME_MODULE 方法;
IMPLEMENT_PRIMARY_GAME_MODULE( <ModuleName>, "<GameName>" );
using UnrealBuildTool;
public class <ModuleName> : ModuleRules
{
public <ModuleName>(TargetInfo Target)
{
PublicDependencyModuleNames.AddRange(new string[]{ "Core", "Engine" });
PrivateDependencyModuleNames.AddRange(new string[]{ "RenderCore" });
}
}
INI文件设置
由于新的游戏模组将包含UObject代码,因此需要进行一些必要的配置;
[UnrealEd.EditorEngine]
+ EditPackages = <ModuleName>
[Launch]
Module = <ModuleName>
[/Script/Engine.UObjectPackages]
+NativePackages=<ModuleName>
多个游戏模块
每个团队根据自身需求和原则可将游戏拆分为多个DLL文件,但可能会带来更多麻烦;使用多个游戏模块将带来更短的链接时间和更快的代码迭代,但随着模块数量的增加,将需要更频繁地处理DLL导出和/或接口类;
可创建一个主游戏模块,然后创建任意数量的特定游戏模块;可为这些新模块创建 *.Build.cs 文件,然后将这些模块的引用添加到 Target.cs 文件中(OutExtraModuleNames数组);在C++代码中,一定要为游戏模块使用适当的宏;至少有一个模块必须使用 IMPLEMENT_PRIMARY_GAME_MODULE 宏,而所有其他模块则应使用 IMPLEMENT_GAME_MODULE 宏;然后,UBT 应自动发现这些模块并编译额外的游戏DLL文件;
确实支持创建相互依赖的模块(即彼此之间有导出和导入函数和数据,例如Engine和UnrealEd模块),但这对编译时间来说并不理想,有时可能会导致变量静态初始化的问题。不相互依赖的游戏模块更难设计和维护,但代码可能会因此更清晰;
四,Gameplay Tags
Gameplay Tags 是用户定义的分层标签系统,可应用于项目中的对象,并对其求值以驱动Gameplay实现;可使用它们传达许多不同的概念,包括:
- 对象的属性,如
Character.Enemy.Zombie; - 对象在执行或能够执行的事情,如
Movement.Mode.Swimming; - 游戏事件和触发器,如
GameplayEvent.RequestReset;
Gameplay Tags 有任意数量的分层级别,以 . 字符分隔表示,如标签 Event.Movement.Dash 有三个级别,其中 Event 是层级中最宽泛的标识符,而 Dash 是最具体的;
Defining Gameplay Tags
必须将Gameplay Tags添加到标签字典,以便虚幻引擎识别它们;可以使用以下某种方法添加(或删除)标签:
- 直接在 Project Settings 中添加或删除;
- 从 Data Table 资产导入;
- 使用 C++ 定义;
以上所有方法都在 Project Settings 的 GameplayTags 分段中下设置;

定义新Gameplay Tags的最简单方式是,直接在 Project Settings 中添加;
- 启用 Import Tags From Config;
- 会导入
.ini文件中的所有Gameplay Tags,包括Config/DefaultGameplayTags.ini以及Config/Tags中的所有标签;
- 会导入
- 点击 Add new Gameplay Tag source 按钮;
- 在
Config/Tags中创建新的源.ini文件来存储Gameplay Tags;为项目的各个方面创建单独的源文件,可能对于大型项目的组织和协作很有用;
- 在
- 点击 Gameplay Tag List 条目旁边的 Manage Gameplay Tags 按钮,会打开 GameplayGameplay Tag Manager 窗口;
- 在 Gameplay Tag Manager 窗口中,点击左上角的 添加(Add (+)) 按钮;
- 输入所需的 Name 、 Comment 和 Source;Comment显示在标签的提示文本上,Source 是存储标签的
.ini文件; - 点击 Add New Tag 按钮;
可重命名、删除、复制标签或向其添加新的子标签;若标签的来源不是 .ini 文件,则不能在 Gameplay Tag Manager 窗口中重命名或删除;

注,可使用文本编辑器编辑标签 .ini 源文件,但必须重启编辑器才能加载更改;
可使用行类型 GameplayTagTableRow 从 Data Table 资产导入Gameplay Tags;使用此方法可以:
- 在 Data Table Editor 中管理标签;
- 在编辑器运行期间更改数据表;
- 通过将
.csv或.json文件作为数据表导入来添加标签;
要从数据表导入标签,在 Project Settings 中执行以下操作:
- 点击 Gameplay Tag Table List 旁边的 Add Element (+) 按钮;
- 点击新索引的下拉菜单并选择数据表;
可使用 NativeGameplayTags.h 中定义的宏,在C++定义Gameplay Tags:
UE_DECLARE_GAMEPLAY_TAG_EXTERN:在.h文件中用于声明.cpp文件中定义的标签;UE_DEFINE_GAMEPLAY_TAG:在.cpp文件中用于定义.h文件中声明的标签,不带提示文本注释;UE_DEFINE_GAMEPLAY_TAG_COMMENT:在.cpp文件中用于定义.h文件中声明的标签,带有提示文本注释;UE_DEFINE_GAMEPLAY_TAG_STATIC:在.cpp文件中用于定义仅对定义文件可用的标签;不同于其他DEFINE宏,这不应该与DECLARE宏调用配对;
//Example Implementation
// In .h file
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Movement_Mode_Walking);
// In .cpp file
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Movement_Mode_Walking, "Movement.Mode.Walking", "Default Character movement tag");
注,必须将 GameplayTags 模块添加到项目的 Build.cs 文件,才能在C++中访问Gameplay Tags功能;
Using Defined Gameplay Tags
定义后,可将标签应用于对象并对标签求值,以在项目中驱动Gameplay;
- 将 Gameplay Tag Container (FGameplayTagContainer) 类型变量添加到对象,此变量存储多个Gameplay Tags;
- AddTag 函数添加标签到容器;
- RemoveTag 函数从容器移除标签;
- AppendTags 函数将多个标签拼接到一起;
注,FGameplayTag 表示单个标签,FGameplayTagContainer 表示一组标签容器(通常使用它);

可基于对象的标签来驱动Gameplay实现;要对存储在对象的Gameplay Tag Containers中的标签求值,可以使用各种条件函数,如:
- 有标签(HasTag)
- 有任何标签(HasAny)
- 有所有标签(HasAll);
除了 HasAll 之类的之外(因为容器中的所有标签在源集内都没有缺失;),使用空的Gameplay Tag Containers作为输入参数时会返回false;
Gameplay Tag Queries
Gameplay Tag Query(FGameplayTagQuery)类型变量组合了条件函数,以更直白精简的方式建立复杂逻辑;
Gameplay Tag 查询支持以下表达式:
- Any Tags Match :测试是否能在容器中发现查询中的至少一个标签;
- All Tags Match :测试查询中的所有标签是否都在容器中;如查询为空,这会返回true;
- No Tags Match :测试查询中的所有标签是否都不在容器中;如果查询为空,这会返回true;
此外,查询支持基于子表达式求值的以下根表达式:
- Any Expressions Match :测试是否有任何子表达式返回true;
- All Expressions Match :测试是否所有子表达式都返回true;如没有子表达式,这会返回true;
- No Expressions Match :测试是否没有子表达式返回true;如没有子表达式,这会返回true;
Advanced Topics
Setting Tag Editing Restrictions
可限制用户对Gameplay Tag进行编辑(在任意层级级别);在 Project Settings 的 Advanced Gameplay Tags > Advanced 下进行以下设置:
- Restricted Config Files:用于存储受限制标签的
.ini文件列表,这些标签与具有编辑权限的 所有者(Owners) 列表配对; - Restricted Tag List :显示 Gameplay Tag Manager 窗口,可在该窗口中修改受限制标签;
如有用户(非列表中的所有者)尝试编辑受限制的标签,将弹出警告消息,要求用户确认自己已获得所有者的编辑授权;如用户无法确认,则不会做出编辑;

注,受限制的标签在创建之后,不能在编辑器中删除;要删除受限制的标签,必须直接编辑 .ini 文件;
Streamlining Tag Access in C++
可使用 IGameplayTagAssetInterface 来改进Gameplay标签实现;该接口提供了以下优势:
- 不用显式将对象进行转换,就可以获取对象的标签;
- 可以为每种可能的类型编写自定义代码;
实现该接口并重载 GetOwnedGameplayTags 函数,就能创建一种能够被蓝图访问的方法,来为 Gameplay Tag Container 填充与该对象关联的标签;大部分情况下,意味着将基类中的标签复制到新容器中,但实现可以从多个容器收集标签,或调用蓝图函数以访问蓝图声明的标签或对象需要的任意内容;
五,Logging in Unreal
日志是一种非常实用的调试工具,可以详细说明代码当前的执行逻辑;可以检查函数之间传递的数据值,并报告潜在的问题;
-
日志提供多种方法来有序记录运行时特定时刻的事件、函数调用和变量值;
-
在 Window > Output Log 查看编辑器中的日志;
- 日志保存在项目
Saved/Logs目录的.txt文件中;

// 在.h头文件中定义一个跨文件/全局的自定义日志类别;
// 声明(.h 中)必须和定义(.cpp 中)配对使用,否则链接会报错;
// .cpp文件定义 DEFINE_LOG_CATEGORY(MyCategory);
DECLARE_LOG_CATEGORY_EXTERN(MyCategory, Log, All);
// 在.cpp源文件中定义一个仅当前文件可见(静态)的自定义日志类别;
DEFINE_LOG_CATEGORY_STATIC(LogBlankProgram, Log, All);
// MyCategory,自定义类别名;
// Log,运行时的默认详细级别,枚举ELogVerbosity;
// Fatal,致命错误(触发断点/崩溃);
// Error,严重问题(程序能跑,打印红色文字);
// Warning,警告/潜在问题(不影响主流程,打印黄色文字);
// Display,显示;
// Log,普通日志;
// Verbose,详解日志;
// VeryVerbose,极详细日志;
// All,编译时过滤级别(相当于总开关),与上一参数一样;
// NoLogging,不输出日志;
// All,保留所有日志;等同于 VeryVerbose;
UE_LOG
// 临时日志类别,通常不应使用它去检测,位于 Runtime/Core/CoreGlobals.h ;
CORE_API DECLARE_LOG_CATEGORY_EXTERN(LogTemp, Log, All);
// UE_LOG 是一个将格式化信息记录到日志文件中的宏;
UE_LOG(LogTemp, Warning, TEXT("Hello World"));
// LogTemp 是类别名称(提供给 DEFINE_LOG_CATEGORY 宏)
// Warning 是一个日志详细级别,用于将警告打印到控制台和日志文件中;可设置不同的日志详细程度,调整日志的换行模式,或者设置日志的文本颜色;
// Text 是C语言库函数printf样式中字符串文字的格式;
Log Verbosity
ELogVerbosity 是一个定义日志详细级别的枚举,还定义了额外的非日志详细级别(允许给定日志换行或设置日志文本的颜色);
| Enum | Description |
| Fatal | 始终将致命错误打印到控制台和日志文件,然后出现崩溃(即使禁用日志); |
| Error | 将错误打印到控制台和日志文件;Commandlet和编辑器会收集并报告错误;错误消息导致commandlet故障; |
| Warning | 将警告打印到控制台和日志文件;Commandlet和编辑器会收集并报告警告; |
| Display | 将消息打印到控制台和日志文件; |
| Log | 将消息打印到日志文件,但不打印到控制台; |
| Verbose | 如为给定类别启用了冗长日志记录,则将冗长消息打印到日志文件;这通常用于详细日志记录; |
| VeryVerbose | 将冗长消息打印到日志文件;如启用了极其冗长日志记录,则用于详细日志记录,否则将产生垃圾信息输出; |
// 从命令行更改不同日志类别的冗长度
// 此命令允许指定多个类别(包括一个all类别)
-LogCmds="LogDerivedDataCache Verbose"
// 使用控制台命令更改冗长度
log LogDerivedDataCache verbose
Logging Fundamental Data Types
| Data Type | Example |
| Fstring | |
| Bool | |
| Integer | |
| Float | |
| FVector | |
| Multiple Specifiers | |
Define Your Own Log Category
可自行定义日志类别,并将其用在C++代码示例中;如有多个文件或框架需要进行不同的分类,此功能可能会很有用;
// 在相关的头文件中,在include指令下方添加以下内容:
DECLARE_LOG_CATEGORY_EXTERN(<LOG_CATEGORY>, <VERBOSITY_LEVEL>, All);
// 在相关的C++文件中,在include指令下方添加以下内容:
DEFINE_LOG_CATEGORY(<LOG_CATEGORY>);
| Verbosity Level | Printed in Console | Printed in Editor Log | Text Color | Additional Information |
| Fatal | Y | NA | NA | 会话崩溃; |
| Error | Y | Y | Red | NA |
| Warning | Y | Y | Yellow | NA |
| Display | Y | Y | Grey | NA |
| Log | N | Y | Grey | NA |
| Verbose | N | N | NA | NA |
| VeryVerbose | N | N | NA | 可使用日志掩码和特殊枚举值设置文本颜色; |
注,可以继承 FOutputDevice,以实现自己的输出;
UE_LOGFMT
UE_LOGFMT 会记录结构化日志事件,支持 Positional 或 Named 参数,但不能混合使用两种类型;要使用此宏,需要包含 Logging/StructeredLog.h 库声明;
注,UE5.2中引入了 UE_LOGFMT 宏,检查当前的引擎是否为最新版本;
| Parameter Name | Description |
| Positional | 字段值必须与格式引用的字段完全匹配; |
| Named | 字段值必须包含格式引用的每个字段;顺序无关紧要,而且允许使用额外字段; 字段名称必须匹配"[A-Za-z0-9_]+"格式,并且在此日志事件中必须唯一;字段值使用SerializeForLog或运算符
|
On-screen debug messages
// 添加屏幕调试消息
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::White, TEXT("This is an Example on-screen debug message."));
}
// key 会获取一个唯一的整型值,用于防止同一消息被多次添加;
// TimeToDisplay 会获取一个浮点值,用于表示消息在显示多少秒后消失;
// DisplayColor 用于指定文本显示的颜色;
// DebugMessage 是要显示的消息;可以像使用日志那样,在屏幕调试消息中使用格式说明符和变量;
253



被折叠的 条评论
为什么被折叠?



