在UE4中使用C++编程

/ 0评 / 4

使用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::stringFString 拥有很多方法,方便您处理字符串。需要使用宏 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'

容器

最常见的有 TQueueTSetTMapTListTArray 等,每个类都会自动调节大小,因此增长到您所需的大小。

在所有三个容器中,您在虚幻引擎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::mapTMap 具有一些根据元素键查找、添加和移除元素的快速方法。您可以使用任意类型来表示键,因为它定义有 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。通过 AddUniqueContains 方法,TArray 已经可以用作集。但是,TSet 可以更快地实现这些运算,但不能像 TArray 一样将它们用作 UPROPERTYTSet 也不会像 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;
    }
};

现在,TSetTMap<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"),
};

C++中使用自定义通道的射线检测

https://zhuanlan.zhihu.com/p/59507553

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注