使用Rider编程
通过 Rider 运行项目并启动 UE4 编辑器,在修改代码后,点击 Build -> Build Startup Project:XXX
编译。这样会热重载,让 C++ 代码的修改对编辑器生效。
类命名规范
U
- UObject 派生类(Actor系列除外),例如 UTexture2D。
A
- AActor 派生类,例如 APawn。
T
- Template 模板,例如 TArray、TMap、TQueue。
I
- 接口类,例如 ITransaction。
E
- 枚举类型,例如 ESelectionMode。
S
- 派生自 SWidget 的类(Slate UI),例如 SButton。
F
- 其他的类或结构体,例如 FText、FVector。
反射机制&相关的宏定义
类
Class Specifier | 含义 |
---|---|
Abstract | 指定这个类为虚基类,不可被实例化。例如 UActorComponent 就是一个虚基类。 |
Blueprintable/NotBlueprintable | 指定这个 class 是否可以作为 Blueprint 的基类。默认值是 NotBlueprintable,这个值会被继承。 |
BlueprintType | 指定这个类的对象可以在 Blueprint 作为变量使用。 |
ClassGroup=GroupName | 在编辑器的 Actor Browser 视图中使用指定的名称进行分组显示。 |
Config=FileName | 例如Config=Game,如果这个类中有可配置变量 UPROPERTY(config或globalconfig),指定这些变量可以存储到哪个 ini 文件中。 |
HideCategies=(Category1, Category2, ...) | 在 Unreal Editor 的属性查看界面中隐藏某些分类的属性。 |
Placeable/NotPlaceable | 指定该类的实例是否可以在关卡中放置。 |
Transient | 指定这个类的对象不必保存到磁盘文件中。 |
UCLASS([specifier, specifier, ...], [meta(key=value, key=value, ...)])
class ClassName : public ParentClassName
{
GENERATED_BODY()
}
UPROPERTY()宏
公开属性变量给蓝图
Property Specifier | 含义 |
---|---|
Category=CategoryName | 在蓝图目录中给变量分类,如果有层级则用 | 分隔。 |
BlueprintReadOnly | 只能在蓝图中Get |
BlueprintReadWrite | 可在蓝图中Get和Set |
EditAnywhere | 实例化可编辑,在蓝图中可以在细节面板设置默认值 |
VisibleAnywhere | 实例化可见,但不可编辑。 |
Transient | 属性为临时,意味着其无法被保存或加载。以此方法标记的属性将在加载时被零填充。 |
UPROPERTY([specifier, specifier, ...], [meta(key=value, key=value, ...)])
Type VariableName;
UFUNCTION()宏
公开函数方法给蓝图,也可以实现蓝图与C++的交互调用
Function Specifier | 含义 |
---|---|
Category=CategoryName | 在蓝图目录中给方法分类 |
BlueprintCallable | 允许在蓝图中调用此方法 |
BlueprintImplementableEvent | 允许在蓝图中实现此方法,C++中即可调用此方法。C++ 里可只写声明不写实现 |
BlueprintNativeEvent | 允许C++提供默认实现方式,同时允许在蓝图中复写此方法并覆盖C++的实现。但是实现里函数名要跟上 _Implementation |
BlueprintPure | 类似 Getter 函数,只是获取一个数据。 |
UFUNCTION([specifier, specifier, ...], [meta(key=value, key=value, ...)])
ReturnType FunctionName([Parameter, Parameter, ...])
基本数据类型
字符串
FString
是一个可变字符串,类似于 std::string
。FString
拥有很多方法,方便您处理字符串。需要使用宏 TEXT
来赋值。例如:FString MyStr = TEXT("Hello, Unreal 4!");
您还可以使用 LOCTEXT 宏,这样只需要每个文件定义一个名称空间即可。确保在文件结束时取消定义。
// 在GameUI.cpp中
#define LOCTEXT_NAMESPACE "Game UI"
//...
FText MyText = LOCTEXT("Health Warning Message", "Low Health!")
//...
#undef LOCTEXT_NAMESPACE
// 文件结束
FText
需要使用宏 NSLOCTEXT
来赋值,它支持大量文本和本地化。
FName
存储通常反复出现的字符串作为辨识符,以在比较时节省内存和CPU时间。需要使用宏 TEXT
来赋值,能够更快速的查找和比较。
FCHAR
是独立于所用字符集存储字符的方法,字符集或许会因平台而异。实际上,UE4字符串使用 TCHAR
数组来存储 UTF-16
编码的数据。您可以使用重载的解除引用运算符(它返回 TCHAR
)来访问原始数据。
某些函数需要使用它,例如 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'
容器
最常见的有 TQueue
、TSet
、TMap
、TList
、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;
};
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();
}
};
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();
目前,唯一能标记为 UPROPERTY
的容器类是 TArray
。这意味着,其他容器类不能复制、保存或对其元素进行垃圾回收。
容器迭代器
通过使用迭代器,您可以循环遍历容器的所有元素。以下是该迭代器语法的示例,使用的是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-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 关键字不会自动指定指针/引用,您需要自行添加。
自己的类型与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
和 TMap<FMyClass, …>
在对键进行散列处理时将使用正确的散列函数。如果使用指针作为键(即,TSet<FMyClass*>
),则还要实现 uint32 GetTypeHash(const FMyClass* MyClass)
。
智能指针
TSharedPtr
事件委托
使用宏 DECLARE_DELEGATE
声明一个委托类型,然后用这个类型声明一个变量。
// 声明委托类型
DECLARE_DELEGATE(FOnConfirmMenuItem);
// 声明委托变量
FOnConfirmMenuItem OnConfirmMenuItem;
// 绑定委托方法
OnConfirmMenuItem.BindSP(某对象, 某方法);
// 执行委托方法
OnConfirmMenuItem.Execute();
数字类型
由于不同平台有不同的基本类型大小,如 短整型、整型 和 长整型,因此 UE4 提供以下类型供您备选:
int8/uint8
:8位有符号/无符号整数
int16/uint16
:16位有符号/无符号整数
int32/uint32
:32位有符号/无符号整数
int64/uint64
:64位有符号/无符号整数
浮点数也支持标准浮点(32位)和双精度(64位)类型。
理解UObject
反射机制(Reflection)
UClass *MyClass = this->GetClass();
for (TFieldIterator<UProperty> PropIter(MyClass); PropIter; ++PropIter) {
UProperty *Property = *PropIter;
UE_LOG(LogTemp, Log, TEXT("Property Name = %s"), *(Property->GetName));
}
for (TFieldIterator<UFunction> FuncIter(MyClass); FuncIter; ++FuncIter) {
UFunction *Function = *FuncIter;
UE_LOG(LogTemp, Log, TEXT("Function Name = %s"), *(Function->GetName));
}
自动垃圾回收
如果是 UObject
的派生类,加上宏 UPROPERTY
就会被自动垃圾回收,不加则不会自动垃圾回收。
如果不是 UObject
的派生类,则需要手动调用 Collector.AddReferencedObject()
函数。
通常,非 UObject
也能够添加对对象的引用并防止垃圾回收。为此,对象必须派生自 FGCObject
并覆盖其 AddReferencedObjects
类。
class FMyNormalClass : public FGCObject
{
public:
UObject* SafeObject;
FMyNormalClass(UObject* Object)
: SafeObject(Object)
{
}
void AddReferencedObjects(FReferenceCollector& Collector) override
{
Collector.AddReferencedObject(SafeObject);
}
};
创建UObject对象
不要直接使用 C++ 的方式创建 UObject
对象,否则不能使用自动垃圾回收。可使用NewObject
模板方法。
如果是创建 AActor
类的继承类实例,可使用 UWorld::SpawnActor
方法。Actor
通常不会被垃圾回收。一旦产生后,必须手动对它们调用 Destroy()
。它们将不会被立即删除,而是在下次垃圾回收时进行清理。
可用 IsPendingKill()
方法,来确认 UObject
是否正在等待删除。如果该方法返回 true
,您应将对象视为已销毁,不要再使用它。
UObject对象遍历
在 PIE
(编辑器中运行)中使用对象迭代器会导致意外结果。由于编辑器已经加载,对象迭代器将返回为游戏场景实例创建的所有 UObject
,此外还有编辑器使用的实例。
// 遍历某个类型
for (TObjectIterator<XXXX> It; It; ++It) {
*It;
}
Actor
迭代器与对象迭代器十分类似,但仅适用于从 AActor
派生的对象。Actor
迭代器不存在上面所注明的问题,仅返回当前游戏场景实例使用的对象。
在创建 Actor
迭代器时,您需要为其指定一个指向 UWorld
的指针。类似 APlayerController
等许多 UObject
类都会提供一个 GetWorld
方法来帮助您。如果您不需确定,可以检查 UObject
上的 ImplementsGetWorld
方法来确认它是否实现 GetWorld
方法。
// 遍历指定世界下的所有Actor
UWorld *World = GetWorld();
for (TActorIterator<AActor> It(World); It; ++It) {
*It;
}
资源对象的加载
使用 StaticLoadObject
方法来加载资源对象。
VS中删除C++类
1.先删除文件。
2.再删除项目根目录下的 Binaries
目录。
3.最后右键 .uproject
文件,选择 Generate Visual Studio project files
。
4.打开 .uproject
,提示点击确定即可。
属性的class修饰符
如果不加 class
,可能 VS 不能识别这个类。
UPROPERTY(EditAnywhere)
class UCameraComponent* Camera;
如果 VS 不能识别的时候,可以在头文件类的定义的前面声明这些类,头文件导入一般都是放到 .cpp
中的。
class UCameraComponent;
class UPlayer;
class UPrimitiveComponent;
// ...
PlayerStart
在场景中放置了 PlayerStart
后,还需要在世界场景设置中选择我们自己的 GameMode
,并设置默认 Pawn
类。
创建组件
UBoxComponent TriggerBox = CreateDefaultSubobject<UBoxComponent>(TEXT("TriggerBox"));
创建UI控件
添加 UI 的代码可以放到 PlayerController
中,然后在世界场景设置中的 GameMode
里设置。
1.在 项目名.Build.cs
文件中添加 UMG
模块。
2.在编辑器中创建控件蓝图。
3.在 C++ 中实例化并显示。
先在蓝图中选择要实例化的 HUD
类型:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSubclassOf<UUserWidget> HUDAsset;
UUserWidget *HUD;
在 BeginPlay
中创建并添加到视口。
if (HUDAsset) {
HUD = CreateWidget<UUserWidget>(this, HUDAsset);
}
if (HUD) {
HUD->AddToViewpoint();
}
调试输出时打印当前函数名
// 类::函数
FString(__FUNCTION__)
// 函数
FString(__func__)
可蓝图化的枚举类型
UENUM(BlueprintType)
enum class EStatus: uint8 {
ES_Normal UMETA(DisplayName = "Normal"),
ES_Sprint UMETA(DisplayName = "Sprint"),
};