Files
UnrealEngineUWP/Engine/Documentation/Source/Programming/Introduction/IntroToProgramming.CHN.udn
Mitchell Wilson 0b47855b71 Copying //UE4/Dev-Documentation to Samples-Main (//UE4/Samples-Main) CL - 4860397
#rb none

[CL 4860421 by Mitchell Wilson in Main branch]
2019-01-31 15:30:04 -05:00

903 lines
44 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
INTSourceChangelist:4693425
Title:UE4中的C++编程简介
Crumbs:
Description:面向初次使用虚幻引擎的C++程序员的入门指南
Availability:Public
Version:4.9
tags:Getting Started
tags:Programming
![image alt text](image_0.png)
(#unrealc++isawesome!)
## 虚幻C++太棒了!
本指南主要介绍如何在虚幻引擎中编写C++代码。不必担心虚幻引擎中的C++编程十分有趣入门也很简单我们通常会认为虚幻C++是“辅助C++”因为我们有很多功能可以让所有人都能更简单的使用C++。
在开始之前您必须已经对C++或另一种编程语言有所认知。在编写本页内容时我们假设您已经有了一些C++经验但如果您了解C#、Java或JavaScrip也会发现有许多相似之处。
如果您完全没有编程经验,我们也考虑到了这一种情况!请查看我们的[蓝图视觉脚本指南](Engine/Blueprints),您就有了基本认知并可以继续学习了。您可以使用蓝图脚本创建整个游戏!
在虚幻引擎中您可以编写“纯传统C++代码”,但阅读本指南并学习有关虚幻编程模型的基本知识后,您会取得更大的成功。接下来我们将详细介绍。
(#c++andblueprints)
## C++与蓝图
虚幻引擎提供了两种创建新Gameplay元素的方法C++和蓝图视觉脚本。程序员利用C++即可添加基础Gameplay系统然后设计师可基于这些系统进行构建或利用这些系统为某个特定关卡或游戏本身创建自定义Gameplay。在这些情况下C++程序员在他们喜欢的IDE通常是Microsoft Visual Studio或Apple Xcode中工作设计师则在虚幻编辑器的蓝图编辑器中工作。
Gameplay API和框架类在这两个系统中都可以使用可以单独使用但组合使用互补长短才能发挥出它们真正的作用。那么到底有何意义呢这意味着当程序员使用C++来创建Gameplay构建块设计师利用这些块创建有趣的Gameplay时引擎就能发挥最大作用。
言至于此我们来看看C++程序员为设计师创建构建块的典型工作流程。在此情况下我们将创建一个类稍后设计师或程序员可以通过蓝图扩展此类。在该类中我们将创建一些设计师可以设置的属性并且我们将根据这些属性派生新值。整个过程使用我们提供的工具和C++宏就可以完成,非常简单。
(#classwizard)
### 类向导
首先使用虚幻编辑器中的类向导来生成稍后将通过蓝图扩展的基本C++类。下图显示了向导的第一步即创建新Actor。
![image alt text](image_1.png)
该流程中的第二步是告诉向导,您已经生成的类的名称。这是使用默认名称的第二步。
![image alt text](image_2.png)
选择创建类后,向导会生成文件,并打开您的开发环境,便于您开始编辑。下面是为您生成的类定义。有关类向导的更多信息,请单击该[链接](Programming/Development/ManagingGameCode/CppClassWizard)。
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
// 设置该Actor属性的默认值
AMyActor();
// 每一帧都调用
virtual void Tick( float DeltaSeconds ) override;
protected:
// 游戏开始或产生时调用
virtual void BeginPlay() override;
};
类向导指定 **BeginPlay()** 和 **Tick()** 作为重载来生成类。**BeginPlay()** 事件告诉您Actor以可运行状态进入了游戏。这是启动类Gameplay逻辑的好位置。**Tick()** 每帧调用一次使用自上次调用传递以来经过的时间。您可以在这里执行任何重复逻辑。但是如果您不需要该功能最好将其移除这样对性能有益。如果将其移除确保移除构造函数中指示应开始发生tick事件的相应行。下面的构造函数就包含所提及的行。
AMyActor::AMyActor()
{
// 将该Actor设置为每帧调用一次Tick()。如果您没有这个需要,可以将其关闭来改善性能。
PrimaryActorTick.bCanEverTick = true;
}
(#makingapropertyshowupintheeditor)
### 让属性出现在编辑器中
创建类后,现在让我们创建一些设计师可以在虚幻编辑器中设置的属性。将属性公开给编辑器非常简单,只需要使用专用宏 **UPROPERTY()** 即可实现。您只需在属性声明前使用 **UPROPERTY(EditAnywhere)** 即可,如以下类中所示。
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
int32 TotalDamage;
...
};
只要完成上述操作,即可在编辑器中编辑该值。还有更多方法可以控制编辑该值的方式和位置。方法是将更多信息传递到 **UPROPERTY()** 宏。例如如果您想要TotalDamage属性出现在包含相关属性的某个部分中可以使用分类功能。具体请参见下面的属性声明。
UPROPERTY(EditAnywhere, Category="Damage")
int32 TotalDamage;
当用户想要编辑该属性时它现在会出现在“伤害Damage”标题下面与您已经标记为此类别名称的任何其他属性在一起。这是将常用设置放在一起以供设计师编辑的好方法。
现在,让我们将同一个属性公开给蓝图。
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
int32 TotalDamage;
如您所见,有一个特定于蓝图的参数,让属性可供读取。有一个单独的选项 BlueprintReadOnly如果您希望属性在蓝图中被视为常量可以使用这个选项。还有一些选项可用来控制将属性公开给引擎的方式。要查看更多选项请单击该[链接](Programming/UnrealArchitecture/Reference/Properties/Specifiers)。
再继续以下部分前我们来向该样本类添加几个属性。已经有一个属性可以控制该Actor将释放出的总伤害量但让我们更进一步让这个伤害随着时间而逐渐释放出来。下面的代码添加了一个可以由设计师设置的属性以及一个对设计师可见但不能更改的属性。
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
int32 TotalDamage;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
float DamageTimeInSeconds;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient, Category="Damage")
float DamagePerSecond;
...
};
DamageTimeInSeconds是设计师可以修改的属性。DamagePerSecond属性的值使用设计师的设置计算得出请参见下一节。VisibleAnywhere标志将该属性标记为在虚幻编辑器中可见但不可编辑。Transient标志意味着它不会保存或从磁盘加载它就是一个派生的非持久值。下图显示作为类默认值一部分的属性。
![image alt text](image_3.png)
(#settingdefaultsinmyconstructor)
### 在我的构造函数中设置默认值
在构造函数中为属性设置默认值的方式与典型的C++类一样。下面是两个在构造函数中设置默认值的示例,它们在功能上是等效的。
AMyActor::AMyActor()
{
TotalDamage = 200;
DamageTimeInSeconds = 1.f;
}
AMyActor::AMyActor() :
TotalDamage(200),
DamageTimeInSeconds(1.f)
{
}
这是在构造函数中添加默认值后的相同属性视图。
![image alt text](image_4.png)
为了按实例支持设计师设置属性,还会从给定对象的实例数据加载值。该数据在构造函数之后应用。您可以根据设计师设置值创建默认值,方法是钩入 **PostInitProperties()** 调用链中。下面示例展示了 **TotalDamage** 和 **DamageTimeInSeconds** 为设计师指定值的流程。尽管它们是设计师指定的,但您仍可以为它们提供合理的默认值,就像上述示例一样。
[REGION:note]
如果您不提供属性的默认值引擎会自动将该属性设置为0或空指针如果是指针类型
[/REGION]
void AMyActor::PostInitProperties()
{
Super::PostInitProperties();
DamagePerSecond = TotalDamage / DamageTimeInSeconds;
}
这里同样是添加了上述 **PostInitProperties()** 代码后的属性视图。
![image alt text](image_5.png)
(#hotreloading)
###热重载
虚幻有一个非常帮的功能如果您已习惯于在其他项目中进行C++编程可能会对这个功能感到惊奇。您不必关闭编辑器就可以编译C++更改!有两种方法可以达到这个目的:
1. 在编辑器继续运行的情况下启用该功能并像正常操作那样通过Visual Studio或Xcode构建。编辑器会检测出新编译的DLL并立即加载修改
![image alt text](image_6.png)
[REGION:note]
如果您已经连接了调试器需要先断开连接这样Visual Studio才会允许您构建。
[/REGION]
2. 或者直接点击编辑器主工具栏中的 **编译Compile** 按钮。
![image alt text](image_7.png)
在本教程的后续章节中,您将用到这个功能。
(#extendingac++classviablueprints)
### 通过蓝图扩展C++类
目前我们已经用C++类向导创建了简单的Gameplay类并添加了一些可供设计师设置的属性。现在来看一看设计师如何在我们已经完成的简要基础工作上开始创建独特的类。
首先要根据AMyActor类创建新的蓝图类。请注意下图中所选基类的名称显示为MyActor而不是AMyActor。这是故意为之目的是向设计师隐藏工具所用的命名约定让名称对设计师而言更加友好。
![image alt text](image_8.png)
选取了 **选择Select** 后会为您创建一个新的默认命名的蓝图类。在本例中我将名称设置为CustomActor1如下面 **内容浏览器** 截图中所示。
![image alt text](image_9.png)
这是我们将以设计师角色自定义的第一个类。首先,我们将更改伤害属性的默认值。在本例中,设计师将 **TotalDamage** 更改为300将释放该伤害所需的时间更改为2秒。这是这些属性现在的样子。
![image alt text](image_10.png)
我们计算的值与预期不符。它应该是150但默认值仍然是200。其原因是我们仅计算了从加载过程初始化属性后的每秒伤害值。虚幻编辑器中的运行时更改没有考虑在内。这个问题有一种简单的解决方案因为引擎会在编辑器中发生变化时通知目标对象。下面的代码显示了为了计算派生值在编辑器中发生变化时的值而添加的钩。
void AMyActor::PostInitProperties()
{
Super::PostInitProperties();
CalculateValues();
}
void AMyActor::CalculateValues()
{
DamagePerSecond = TotalDamage / DamageTimeInSeconds;
}
#if WITH_EDITOR
void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
CalculateValues();
Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif
需要注意的一点是,**PostEditChangeProperty()** 方法位于特定于编辑器的#ifdef内部。这样才能仅构建游戏所需的代码删除任何多余的、导致可执行文件大小增大的代码。现在我们已经编译了代码**DamagePerSecond** 值与我们预期的值匹配,如下图所示。
![image alt text](image_11.png)
(#callingfunctionsacrossthec++andblueprintboundary)
### 在C++和蓝图边界中调用函数
目前我们已经展示了如何将属性公开给蓝图但还有最后一个入门主题需要介绍然后才能更深入地探索引擎。在创建Gameplay系统期间设计师将需要能够调用C++程序员创建的函数而且Gameplay程序员能够从C++代码调用蓝图中实现的函数。首先我们来让CalculateValues()能够从蓝图调用。将函数公开给蓝图就像公开属性一样简单。只需在函数声明前放置一个宏即可!以下代码片段显示了所需内容。
UFUNCTION(BlueprintCallable, Category="Damage")
void CalculateValues();
**UFUNCTION()** 宏负责处理将C++函数公开给反射系统。BlueprintCallable选项将其公开给蓝图虚拟机因此用户可以从蓝图图表内调用函数。下图显示了类别对快捷菜单的影响
![image alt text](image_12.png)
如您所见,该函数可以从 **伤害Damage** 类别中选择。下面的蓝图代码显示了TotalDamage值的变化后面是用来重新计算依赖数据的调用。
![image alt text](image_13.png)
这里使用了我们之前添加的用来计算相关属性的同一个函数。引擎的大部分都通过 **UFUNCTION()** 宏公开给蓝图因此用户可以直接构建游戏而不必编写C++代码。但是最佳方法是使用C++构建基本Gameplay系统和性能关键代码用蓝图自定义行为或从C++构建块创建组合式行为。
现在设计师已经可以调用C++代码了接下来探索一种更强大的C++/蓝图边界交叉调用方法。该方法让C++代码能够调用蓝图中定义的函数。我们通常使用这种方法将设计师在认为合适时可以响应的事件通知给设计师。这通常包括产生效果或其他视觉影响如隐藏或取消隐藏Actor。下面的代码片段显示了蓝图实现的函数。
UFUNCTION(BlueprintImplementableEvent, Category="Damage")
void CalledFromCpp();
该函数的调用方法与任何其他C++函数一样。在后台虚幻引擎生成基本C++函数实现用以理解如何在蓝图VM中调用。这通常称为形实替换Thunk。如果所提及蓝图不为这种方法提供函数体则函数行为就像没有实体行为的C++函数一样不执行任何操作。如果想要提供C++默认实现同时仍允许蓝图覆盖此方法该怎么办呢或许可以使用UFUNCTION()宏的一个选项。以下代码片段显示了为达到此目的需要在标头中进行的更改。
UFUNCTION(BlueprintNativeEvent, Category="Damage")
void CalledFromCpp();
该版本仍会生成用于在蓝图VM中调用的形实替换方法。那么如何提供默认实现呢工具还会生成一个新的函数声明类似于`<函数名>_Implementation()`。您必须提供该版本的函数,否则项目无法建立关联。下面是对上述声明的实现代码。
void AMyActor::CalledFromCpp_Implementation()
{
// 这里可以添加些有趣的代码
}
现在该版本函数会在所提及蓝图不覆盖此方法时被调用。需要注意的一点是在先前版本的构建工具中会自动生成_Implementation()声明。在4.8的任意版本或更高版本中,您需要显式将该声明添加到标头中。
现在我们已经介绍了与设计师合作构建Gameplay功能的常见Gameplay程序员工作流程和方法接下来该由您自己选择前进方向了。您可以继续阅读本文进一步了解如何在引擎中使用C++,也可以直接参见我们在启动程序中提供的样本来获得更多实践经验。
(#divingdeeper)
## 深入探索
看来您决定继续阅读本文。很好。下面的讨论主题将围绕着Gameplay类层级展开。在本节中我们首先介绍基本构建块然后介绍它们彼此之间的关系。这里我们将说明虚幻引擎如何使用继承与复合来构建自定义Gameplay功能。
(#gameplayclasses:)
### Gameplay类对象、Actor和组件
从大部分Gameplay类可以派生出4种主要类型的类。它们分别是 **UObject**、**AActor**、**UActorComponent** 和 **UStruct**。下面几节将说明其中每一种构建块。当然,您可以创建不从任何类派生的类型,但它们不会参与到引擎中构建的功能。在 **UObject** 层级外部创建的典型类用法是:集成第三方库、包裹操作系统特定功能等。
(#unrealobjects(uobject))
#### 虚幻对象UObject
虚幻引擎中的基本构建块叫做UObject。该类结合 **UClass**,可以提供引擎中的多个最重要的基本服务:
* 反射属性和方法
* 序列化属性
* 垃圾回收
* 按名称查找UObject
* 属性的可配置值
* 属性和方法的联网支持
从UObject派生的每个类都会创建有一个UClassUClass包含有关该类实例的所有元数据。UObject和UClass一起位于Gameplay对象在其生命周期所有作用的最根部位置。如果要解释UClass和UObject的差异在哪里最合适的方法是UClass描述的是UObject实例的样子、可序列化和联网的属性等。大多数Gameplay开发不会直接从UObject派生而是从AActor和UActorComponent派生。您无需知道UClass/UObject工作方式细节这并不影响您编写Gameplay代码知道这些系统的存在即可。
(#aactor)
#### AActor
AActor是将会成为Gameplay体验的一部分的对象。AActor由设计师放在关卡中或者通过Gameplay系统在运行时创建。可以放入关卡的所有对象都是从该类扩展而来的。示例包括 **AStaticMeshActor**、**ACameraActor** 和 **APointLight** Actor。AActor派生自UObject因此可以使用上一节所列的所有标准功能。AActor可以显式销毁方法是使用Gameplay代码C++或蓝图或者在所属关卡从内存中卸载时通过标准的垃圾回收机制销毁。AActor负责游戏对象的高级行为。AActor还是可以在联网时复制的基本类型。在网络复制期间AActor还可以分发由该AActor拥有的、需要网络支持的任何UActorComponent的信息。
AActor还有它们自己的行为通过继承实现特殊化但它们也充当UActorComponent层级容器通过复合实现特殊化。这个过程通过AActor的RootComponent成员实现它包含一个UActorComponent而后者继而包含许多其他成员。在可以将AActor放入关卡之前AActor必须包含至少一个 **USceneComponent**后者包含该AActor的平移、旋转和缩放。
AActor包含在AActor生命周期中调用的一系列事件。以下列表是一组简化的事件描绘了整个生命周期。
* `BeginPlay`——对象首次在Gameplay中存在时调用。
* `Tick`——每帧调用一次,随着时间的进行持续完成工作。
* `EndPlay`——对象离开Gameplay空间时调用。
请参见[](Programming/UnrealArchitecture/Actors)以了解有关AActor的更详细讨论。
(#runtimelifecycle)
##### 运行时生命周期
我们在上文讨论了AActor生命周期的一小部分。对于关卡中放置的Actor了解生命周期是很容易想象的到的Actor加载并存在最终关卡被卸载后Actor被销毁。那么运行时创建和破坏的流程是怎样的呢虚幻引擎在运行时产生时调用AActor创建。产生Actor比在游戏中创建普通对象稍微复杂一点。原因是AActor需要注册到多个运行时系统才能满足其所有需要。需要设置Actor的初始位置和旋转。物理可能需要知道这些信息。负责告诉Actor执行tick事件的管理器也需要知道。诸如此类。因此我们有一种方法专门用来产生Actor叫做 **UWorld::SpawnActor()**。成功产生Actor后会调用它的 **BeginPlay()** 方法,下一帧调用 **Tick()**。
Actor生命周期结束时您可以调用 **Destroy()** 来将它销毁。在该过程中,将调用 **EndPlay()**供您编写任何自定义销毁逻辑。另一个控制Actor生命周期时长的方法是使用Lifespan成员。您可以在对象的构造函数中设置时间跨度也可以在运行时使用其他代码进行设置。当这段时间到期后会自动对该Actor调用 **Destroy()**。
要进一步了解产生Actor的信息请参阅[](Programming/UnrealArchitecture/Actors/Spawning)页面。
(#uactorcomponent)
#### UActorComponent
UActorComponent有自己的行为通常负责在许多类型AActor之间共享的功能例如提供视觉网格体、粒子效果、摄像机视角和物理互动。AActor通常提供与其游戏总体角色有关的高级目标而UActorComponent通常执行用于支持这些更高级目标的单独任务。组件也可以与其他组件相连接或者可以成为Actor的根组件。一个组件只能连接到一个父组件或Actor但可以连接多个子Actor。您可以想象一个组件树。子组件的位置、旋转和缩放相对于其父组件或Actor。
Actor和组件有很多用法一种方法是将Actor-组件关系视为Actor可能会回答问题“这是什么而组件可能会回答“这个东西是用什么做成的
* RootComponent——这是AActor的一个成员用于保存AActor组件树中的顶级组件。
* Ticking——在所属AActor的Tick()过程中执行tick事件的组件。
(#dissectingthefirstpersoncharacter)
##### 分解第一人称角色
在过去几节中我们讲了很多也演示了很多。为了描绘AActor及其UActorComponent之间的关系接下来将深入根据第一人称模板生成新项目时创建的蓝图。下图是 **FirstPersonCharacter** Actor的 **组件** 树。**RootComponent** 是 **CapsuleComponent**。与 **CapsuleComponent** 相连的是 **ArrowComponent**、**Mesh** 组件和 **FirstPersonCameraComponent**。最末端组件是Mesh1P组件它的父代是 **FirstPersonCameraComponent**,意味着第一人称网格体相对于第一人称摄像机。
![image alt text](image_14.png)
从视觉角度来看,这个 **组件** 树有点类似于下图您会在3D空间中看到除 **Mesh** 组件之外的所有其他组件。
![image alt text](image_15.png)
这个组件树与一个Actor类相连。如示例所示您可以使用继承和复合构建复杂Gameplay对象。如果想要自定义现有AActor或UActorComponent可以使用继承。如果希望许多不同的AActor类型共享功能可以使用复合。
(#ustruct)
#### UStruct
要使用UStruct您不必从任何特定类扩展只需用USTRUCT()标记该结构体构建工具就会为您完成基本工作。与UObject不同的是UStruct不会被垃圾回收。如果您要创建它们的动态实例必须自行管理其生命周期。UStruct应该是纯传统数据类型包含UObject反射支持可以在虚幻编辑器、蓝图操控、序列化、联网等中编辑。
现在我们已经介绍了Gameplay类构造中使用的基本层级接下来又到了您选择的时候。您可以在[此处](Programming/UnrealArchitecture/Reference/Classes)继续阅读Gameplay类内容访问启动程序中具有更多信息的样本也可以继续探索更多用于构建游戏的C++功能。
(#divingdeeperstill)
## 继续深入探索
显然您还想继续学习。让我们继续深入探索引擎的工作方式。
(#unrealreflectionsystem)
### 虚幻反射系统
[博文:虚幻属性系统(反射)](https://www.unrealengine.com/blog/unreal-property-system-reflection)
Gameplay类利用特殊标记因此在继续之前先来介绍一下虚幻属性系统的基础知识。UE4使用其自己的反射实现来支持动态功能如垃圾回收、序列化、网络复制和蓝图/C++通信。这些功能是可选的,意味着您必须将正确的标记添加到类型,否则虚幻将忽略它们,而不会为它们生成反射数据。下面是对基本标记的简要概述:
* **UCLASS()**——用于告诉虚幻为结构体生成反射数据。类必须派生自UObject。
* **USTRUCT()**——用于告诉虚幻为结构体生成反射数据。
* **GENERATED_BODY()**——UE4将这个标记替换为将为该类型生成的所有必要的样板代码。
* **UPROPERTY()**——支持将UCLASS的成员变量或USTRUCT用作UPROPERTY。UPROPERTY有很多用法。它可以允许复制变量、序列化变量和从蓝图访问变量。它们可以供垃圾回收程序使用用来跟踪对UObject的引用次数。
* **UFUNCTION()**——支持将UCLASS的类方法或USTRUCT用作UFUNCTION。UFUNCTION可以允许从蓝图调用类方法用作RPC等多种用途。
以下是UCLASS声明示例
#include "MyObject.generated.h"
UCLASS(Blueprintable)
class UMyObject : public UObject
{
GENERATED_BODY()
public:
MyUObject();
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="Example")
float ExampleProperty;
UFUNCTION(BlueprintCallable, Category="Example")
void ExampleFunction();
};
首先您会注意到包含了“MyClass.generated.h”。虚幻将生成所有反射数据并放入该文件中。您必须将该文件作为声明类型的标头文件中的最后一个包含语句将其包含进去。
该示例中的UCLASS、UPROPERTY和UFUNCTION标记包含一些其他说明符。这些虽不是必需的但为了演示目的已经添加了一些常见说明符。说明符可以启用或修改特定行为或属性。
* **Blueprintable** - 该类可以由蓝图扩展。
* **BlueprintReadOnly** - 该属性可以从蓝图读取,但不能写入蓝图。
* **EditAnywhere** - 该属性可以在原型和实例上的属性窗口中编辑。
* **Category** - 定义该属性将出现在编辑器“细节Details”视图下面的哪个部分。这对于整理结构而言十分有用。
* **BlueprintCallable** - 该功能可以从蓝图调用。
说明符众多,不便在此一一列出,但可以参考下面的链接:
[UCLASS说明符列表](Programming/UnrealArchitecture/Reference/Classes/Specifiers)
[UPROPERTY说明符列表](Programming/UnrealArchitecture/Reference/Properties/Specifiers)
[UFUNCTION说明符列表](Programming/UnrealArchitecture/Reference/Functions/Specifiers)
[USTRUCT说明符列表](Programming/UnrealArchitecture/Reference/Structs/Specifiers)
(#object/actoriterators)
### 对象/Actor迭代器
对象迭代器是非常有用的工具可用来迭代特定UObject类型及其子类的所有实例。
// 查找所有当前UObject实例
for (TObjectIterator<UObject> It; It; ++It)
{
UObject* CurrentObject = *It;
UE_LOG(LogTemp, Log, TEXT("Found UObject named:%s"), *CurrentObject->GetName());
}
您可以通过为迭代器提供更具体的类型来限制搜索范围。假设您有一个类名为UMyClass它是从UObject派生而来的。您可以像下面这样找到该类的所有实例以及从它派生而来的实例
for (TObjectIterator<UMyClass> It; It; ++It)
{
// ...
}
[REGION:warning]
在PIE编辑器中运行中使用对象迭代器会导致意外结果。由于编辑器已经加载对象迭代器将返回为游戏场景实例创建的所有UObject此外还有编辑器使用的实例。
[/REGION]
Actor迭代器与对象迭代器十分类似但仅适用于从AActor派生的对象。Actor迭代器不存在上面所注明的问题仅返回当前游戏场景实例使用的对象。
在创建Actor迭代器时您需要为其指定一个指向 **UWorld** 的指针。类似 **APlayerController** 等许多UObject类都会提供一个 **GetWorld** 方法来帮助您。如果您不需确定可以检查UObject上的 **ImplementsGetWorld** 方法来确认它是否实现GetWorld方法。
APlayerController* MyPC = GetMyPlayerControllerFromSomewhere();
UWorld* World = MyPC->GetWorld();
// 正如对象迭代器一样,您可以提供一个具体类来仅获得
// 属于该类或派生自该类的对象
for (TActorIterator<AEnemy> It(World); It; ++It)
{
// ...
}
[REGION:note]
由于AActor派生自UObject因此您也可以使用 **TObjectIterator** 来查找AActor的实例。只是在PIE中需要谨慎
[/REGION]
(#memorymanagementandgarbagecollection)
## 内存管理和垃圾回收
在本节中我们将介绍基本内存管理和UE4中的垃圾回收系统。
[Wiki垃圾回收和动态内存分配](https://wiki.unrealengine.com/Garbage_Collection_%26_Dynamic_Memory_Allocation)
(#uobjectsandgarbagecollection)
### UObject和垃圾回收
UE4使用反射系统来实现垃圾回收系统。通过垃圾回收您将不必手动删除UObject只需维护对它们的有效引用即可。您的类需要派生自UObject才能对其进行垃圾回收。下面是我们将使用的简单示例类
UCLASS()
class MyGCType : public UObject
{
GENERATED_BODY()
};
在垃圾回收程序中,有一个概念叫做根集。该根集基本上是一个对象列表,这些对象是回收程序知道将不会被垃圾回收的对象。只要根集中的某个对象到一个对象存在引用路径,就不会对所涉及对象进行垃圾回收。如果某个对象不存在到根集的此类路径,则称为无法访问,将会在下次运行垃圾回收程序时将其回收(删除)。引擎按特定的时间间隔运行垃圾回收程序。
怎样算作“引用”UPROPERTY中存储的任意UObject指针。首先让我们从简单示例入手。
void CreateDoomedObject()
{
MyGCType* DoomedObject = NewObject<MyGCType>();
}
当我们调用上述函数时我们会创建一个新UObject但不会在任何UPROPERTY中存储指向它的指针因此它不是根集的一部分。最终垃圾回收程序会检测到该对象无法访问从而将其销毁。
(#actorsandgarbagecollection)
### Actor和垃圾回收
Actor通常不会被垃圾回收。一旦产生后必须手动对它们调用 **Destroy()**。它们将不会被立即删除,而是在下次垃圾回收时进行清理。
有一种更为常见的情况即您的Actor具有UObject属性。
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY()
MyGCType* SafeObject;
MyGCType* DoomedObject;
AMyActor(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
SafeObject = NewObject<MyGCType>();
DoomedObject = NewObject<MyGCType>();
}
};
void SpawnMyActor(UWorld* World, FVector Location, FRotator Rotation)
{
World->SpawnActor<AMyActor>(Location, Rotation);
}
当我们调用上述函数时就会在场景中产生一个Actor。这个Actor的构造函数会创建两个对象。一个被分配UPROPERTY另一个分配有裸指针。由于Actor会自动成为根集的一部分因此SafeObject不会被垃圾回收因为可以从根集对象访问它。但DoomedObject则不是这种情况。我们没有用UPROPERTY来标记它因此回收程序不知道它被引用因此最终将其销毁。
当UObject被垃圾回收时所有对它的UPROPERTY引用都会设置为空指针。这样您就可以安全地检查某个对象是否已被垃圾回收。
if (MyActor->SafeObject != nullptr)
{
// 使用SafeObject
}
这一点很重要因为正如之前所说调用了Destroy()的Actor会在垃圾回收程序下次运行时才会删除。您可以检查 **IsPendingKill()** 方法来确认UObject是否正在等待删除。如果该方法返回true您应将对象视为已销毁不要再使用它。
(#ustructs)
### UStructs
如前所述UStructs是UObject的轻量级版本。因此不能将UStructs垃圾回收。如果必需使用UStructs的动态实例可以使用智能指针我们稍后将进行介绍。
(#non-uobjectreferences)
### 非对象引用
通常非UObject也能够添加对对象的引用并防止垃圾回收。为此对象必须派生自 **FGCObject** 并覆盖其 **AddReferencedObjects** 类。
class FMyNormalClass : public FGCObject
{
public:
UObject* SafeObject;
FMyNormalClass(UObject* Object)
: SafeObject(Object)
{
}
void AddReferencedObjects(FReferenceCollector& Collector) override
{
Collector.AddReferencedObject(SafeObject);
}
};
我们使用 **FReferenceCollector** 来手动添加对需要且不希望垃圾回收的UObject的硬引用。当该对象被删除且其析构函数运行时该对象将自动清除其所添加的所有引用。
(#classnamingprefixes)
### 类命名前缀
虚幻引擎提供了一些在构建过程中生成代码的工具。这些工具会期待看到一些类命名,并在名称与预期不符时触发警告或错误。以下类前缀列表描述了工具期望的名称。
* 派生自 **Actor** 的类带有 **A** 前缀如AController。
* 派生自 **Object** 的类带有 **U** 前缀如UComponent。
* **Enums** 的前缀是 **E**如EFortificationType。
* **Interface** 的前缀通常是 **I**如IAbilitySystemInterface。
* **Template** 的前缀是 **T**如TArray。
* 派生自 **SWidget** 的类Slate UI带有前缀 **S**如SButton。
* 其他类的前缀为[字母F](https://forums.unrealengine.com/showthread.php?60061-Unreal-trivia-What-does-the-F-prefix-on-classes-and-structs-stand-for)如FVector。
(#numerictypes)
### 数字类型
由于不同平台有不同的基本类型大小,如 **短整型**、**整型** 和 **长整型**因此UE4提供以下类型供您备选
* **int8**/**uint8 **8位有符号/无符号整数
* **int16**/**uint16 **16位有符号/无符号整数
* **int32**/**uint32 **32位有符号/无符号整数
* **int64**/**uint64 **64位有符号/无符号整数
浮点数也支持标准 **浮点**32位**** 和 **双精度**64位类型。
虚幻引擎有一个模板`TNumericLimits<t>`,用于查找值类型可以拥有的最小和最大范围。有关更多信息,请单击该[链接](https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Math/TNumericLimits/index.html)。
(#strings)
### 字符串
UE4提供多个不同的类便于您根据需要处理字符串。
[完整主题:字符串处理](Programming/UnrealArchitecture/StringHandling)
(#fstring)
#### FString
**FString** 是一个可变字符串类似于std::string。FString拥有很多方法方便您处理字符串。要创建新的FString请使用 **TEXT()** 宏:
FString MyStr = TEXT("Hello, Unreal 4!").
[完整主题FString API](https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Containers/FString/index.html)
(#ftext)
#### FText
**FText** 类似于FString但旨在用于本地化文本。要创建新的FText请使用 **NSLOCTEXT** 宏。该宏将使用默认语言的名称空间、键和值。
FText MyText = NSLOCTEXT("Game UI", "Health Warning Message", "Low Health!")
您还可以使用 **LOCTEXT** 宏,这样只需要每个文件定义一个名称空间即可。确保在文件结束时取消定义。
// 在GameUI.cpp中
#define LOCTEXT_NAMESPACE "Game UI"
//...
FText MyText = LOCTEXT("Health Warning Message", "Low Health!")
//...
#undef LOCTEXT_NAMESPACE
// 文件结束
[完整主题FText API](https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Internationalization/FText/index.html)
(#fname)
#### FName
**FName** 存储通常反复出现的字符串作为辨识符以在比较时节省内存和CPU时间。如果有多个对象引用一个字符串FName使用较小的存储空间 **索引** 来映射到给定字符串,而不是在引用它的每个对象中多次存储完整字符串。这样会将字符串内容存储一次,节省在多个对象中使用该字符串时占用的内存。通过检查确认 **NameA.Index** 是否等于 **NameB.Index** 可以快速比较两个字符串,避免检查字符串中的每一个字符是否相同。
[完整主题FName API](https://docs.unrealengine.com/latest/INT/API/Runtime/Core/UObject/FName/index.html)
(#tchar)
#### TCHAR
**TCHAR** 是独立于所用字符集存储字符的方法字符集或许会因平台而异。实际上UE4字符串使用TCHAR数组来存储 **UTF-16** 编码的数据。您可以使用重载的解除引用运算符它返回TCHAR来访问原始数据。
[完整主题:字符编码](Programming/UnrealArchitecture/StringHandling/CharacterEncoding)
某些函数需要使用它,例如 **FString::Printf****“%s”** 字符串格式说明符期待的是TCHAR而不是FString。
FString Str1 = TEXT("World");
int32 Val1 = 123;
FString Str2 = FString::Printf(TEXT("Hello, %s!You have %i points."), *Str1, Val1);
**FChar** 类型提供一组静态效用函数用来处理各个TCHAR。
TCHAR Upper('A');
TCHAR Lower = FChar::ToLower(Upper); // 'a'
[REGION:note]
FChar类型定义为`TChar<TCHAR>`因为它列示在该API中
[/REGION]
[完整主题TChar API](https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Misc/TChar/index.html)
(#containers)
### 容器
容器是一种类,它的主要功能是存储数据集合。最常见的这些类包括 **TArray**、**TMap** 和 **TSet**。每个类都会自动调节大小,因此增长到您所需的大小。
[完整主题容器API](https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Containers/index.html)
(#tarray)
#### TArray
在所有三个容器中您在虚幻引擎4中将会使用的主要容器是TArray它的功能与 **std::vector** 十分相似,但会提供更多功能。以下是一些常见操作:
TArray<AActor*> ActorArray = GetActorArrayFromSomewhere();
// 告知当前ActorArray中存储了多少个元素AActor
int32 ArraySize = ActorArray.Num();
// TArray基于0第一个元素将位于索引0处
int32 Index = 0;
// 尝试检索给定索引处的元素
TArray* FirstActor = ActorArray[Index];
// 在数组末尾添加新元素
AActor* NewActor = GetNewActor();
ActorArray.Add(NewActor);
// 在数组末尾添加元素,但前提必须是该元素尚不存在于数组中
ActorArray.AddUnique(NewActor); // 不会改变数组因为已经添加了NewActor。
// 从数组中移除“NewActor”的所有实例
ActorArray.Remove(NewActor);
// 移除指定索引处的元素
// 索引之上的元素将下移一位来填充空白空间
ActorArray.RemoveAt(Index);
// 更高效版本的“RemoveAt”但不能保持元素的顺序
ActorArray.RemoveAtSwap(Index);
// 移除数组中的所有元素
ActorArray.Empty();
TArray添加了对其元素进行垃圾回收的好处。这样会假设TArray已标记为UPROPERTY并且它存储UObject派生的指针。
UCLASS()
class UMyClass : UObject
{
GENERATED_BODY();
// ...
UPROPERTY()
TArray<AActor*> GarbageCollectedArray;
};
我们将在后续章节进一步介绍垃圾回收。
[完整主题TArray](Programming/UnrealArchitecture/TArrays)
[完整主题TArray API](https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Containers/TArray/index.html)
(#tmap)
#### TMap
**TMap** 是键-值对的集合,类似于 **std::map**。TMap具有一些根据元素键查找、添加和移除元素的快速方法。您可以使用任意类型来表示键因为它定义有 **GetTypeHash** 函数,我们稍后将进行介绍。
假设您创建了一个基于网格的游戏并需要存储和查询每一个正方形上的内容。TMap会为您提供一种简单的可用方法。如果板面较小并且尺寸不变那么显然有一些更有效的方法来达到此目的但为了举例说明我们来展开说明。
enum class EPieceType
{
King,
Queen,
Rook,
Bishop,
Knight,
Pawn
};
struct FPiece
{
int32 PlayerId;
EPieceType Type;
FIntPoint Position;
FPiece(int32 InPlayerId, EPieceType InType, FIntVector InPosition) :
PlayerId(InPlayerId),
Type(InType),
Position(InPosition)
{
}
};
class FBoard
{
private:
// 通过使用TMap我们可以按位置引用每一块
TMap<FIntPoint, FPiece> Data;
public:
bool HasPieceAtPosition(FIntPoint Position)
{
return Data.Contains(Position);
}
FPiece GetPieceAtPosition(FIntPoint Position)
{
return Data[Position];
}
void AddNewPiece(int32 PlayerId, EPieceType Type, FIntPoint Position)
{
FPiece NewPiece(PlayerId, Type, Position);
Data.Add(Position, NewPiece);
}
void MovePiece(FIntPoint OldPosition, FIntPoint NewPosition)
{
FPiece Piece = Data[OldPosition];
Piece.Position = NewPosition;
Data.Remove(OldPosition);
Data.Add(NewPosition, Piece);
}
void RemovePieceAtPosition(FIntPoint Position)
{
Data.Remove(Position);
}
void ClearBoard()
{
Data.Empty();
}
};
[完整主题TMap](Programming/UnrealArchitecture/TMap)
[完整主题TMap API](https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Containers/TMapBase/index.html)
(#tset)
#### TSet
**TSet** 存储唯一值集合,类似于 **std::set**。通过 **AddUnique** 和 **Contains** 方法TArray已经可以用作集。但是TSet可以更快地实现这些运算但不能像TArray一样将它们用作UPROPERTY。TSet也不会像TArray那样对元素编制索引。
TSet<AActor*> ActorSet = GetActorSetFromSomewhere();
int32 Size = ActorSet.Num();
// 向集添加元素,但前提是集尚未包含这个元素
AActor* NewActor = GetNewActor();
ActorSet.Add(NewActor);
// 检查元素是否已经包含在集中
if (ActorSet.Contains(NewActor))
{
// ...
}
// 从集移除元素
ActorSet.Remove(NewActor);
// 从集移除所有元素
ActorSet.Empty();
// 创建包含TSet元素的TArray
TArray<AActor*> ActorArrayFromSet = ActorSet.Array();
[完整主题TSet API](https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Containers/TSet/index.html)
请记住目前唯一能标记为UPROPERTY的容器类是TArray。这意味着其他容器类不能复制、保存或对其元素进行垃圾回收。
(#containeriterators)
#### 容器迭代器
通过使用迭代器您可以循环遍历容器的所有元素。以下是该迭代器语法的示例使用的是TSet。
void RemoveDeadEnemies(TSet<AEnemy*>& EnemySet)
{
// 从集开头处开始,迭代至集末尾
for (auto EnemyIterator = EnemySet.CreateIterator(); EnemyIterator; ++EnemyIterator)
{
// *运算符获取当前元素
AEnemy* Enemy = *EnemyIterator;
if (Enemy.Health == 0)
{
//“RemoveCurrent”受TSet和TMap支持
EnemyIterator.RemoveCurrent();
}
}
}
您可以用于迭代器的其他受支持的运算包括:
// 将迭代器向后移动一个元素
--EnemyIterator;
// 将迭代器向前/向后移动一定偏移量,这里的偏移量是个整数
EnemyIterator += Offset;
EnemyIterator -= Offset;
// 获取当前元素的索引
int32 Index = EnemyIterator.GetIndex();
// 将迭代器复位到第一个元素
EnemyIterator.Reset();
(#for-eachloop)
#### For-each循环
迭代器虽然好用但如果您只想每个元素循环一次未免有点麻烦。每个容器类还支持for each风格的语法来循环元素。TArray和TSet返回各个元素而TMap返回键-值对。
// TArray
TArray<AActor*> ActorArray = GetArrayFromSomewhere();
for (AActor* OneActor :ActorArray)
{
// ...
}
// TSet——与TArray相同
TSet<AActor*> ActorSet = GetSetFromSomewhere();
for (AActor* UniqueActor :ActorSet)
{
// ...
}
// TMap——迭代器返回键-值对
TMap<FName, AActor*> NameToActorMap = GetMapFromSomewhere();
for (auto& KVP :NameToActorMap)
{
FName Name = KVP.Key;
AActor* Actor = KVP.Value;
// ...
}
请记住,**auto** 关键字不会自动指定指针/引用,您需要自行添加。
(#usingyourowntypeswithtset/tmap(hashfunctions))
#### 将您自己的类型与TSet/TMap散列函数一起使用
TSet和TMap需要在内部使用*散列函数*。如果您创建自己的类想要在TSet中使用它或者用作指向TMap的键则需要先创建自己的散列函数。大部分通常想要这样使用的UE4类型已经定义了自己的散列函数。
散列函数使用指向您的类型的常量指针/引用并返回uint64。该返回值称为对象的*散列代码*,应该是特定于该对象的伪唯一数字。两个相同的对象应该始终返回相同的散列代码。
class FMyClass
{
uint32 ExampleProperty1;
uint32 ExampleProperty2;
// 散列函数
friend uint32 GetTypeHash(const FMyClass& MyClass)
{
// HashCombine是将两个散列值合并的效用函数
uint32 HashCode = HashCombine(MyClass.ExampleProperty1, MyClass.ExampleProperty2);
return HashCode;
}
// 为了演示目的,两个相同的对象
// 应该始终返回相同的散列代码。
bool operator==(const FMyClass& LHS, const FMyClass& RHS)
{
return LHS.ExampleProperty1 == RHS.ExampleProperty1
&& LHS.ExampleProperty2 == RHS.ExampleProperty2;
}
};
现在TSet&lt;FMyClass&gt;和TMap&lt;FMyClass, ...&gt;在对键进行散列处理时将使用正确的散列函数。如果使用指针作为键(即,`TSet<FMyClass*>`),则还要实现`uint32 GetTypeHash(const FMyClass* MyClass)`。
[博文值得了解的UE4库](https://www.unrealengine.com/blog/ue4-libraries-you-should-know-about)