使用虚幻引擎中的C++导论(四-内存管理与垃圾回收)(终)

时间:2023-12-23 22:47:25

使用虚幻引擎中的C++导论(四)(终)

第一,这篇是我翻译的虚幻4官网的新手编程教程,原文传送门,有的翻译不太好,但大体意思差不多,请支持我O(∩_∩)O谢谢。
第二,某些细节操作,这篇文章省略了,如果有不懂的,去看其他教程。
第三,没有C++编程经验,但有其他OOP语言经验的朋友。要先了解C++的基础。

内存管理与垃圾回收

在这一部分我们将学习虚幻4引擎基本的内存管理与垃圾回收。

UObjects 与 垃圾回收

虚幻4引擎使用反射系统(机制)去实现垃圾回收。关于垃圾回收,你不用进行手动的去销毁你的UObjects类对象,你只需要保持对他们的引用。你的类需要继承UObject 类以支持垃圾回收。下面有个简单的例子。

UCLASS()
class MyGCType : public UObject
{
    GENERATED_BODY()
};

在垃圾回收器里,这里有个概念被称为 root set (根集)。根集是一个基本的对象列表,回收器不会回收根集的对象。一个对象的引用路径如果在根集里,那么它将不会被回收。如果一个对象不存在这种引用路径,称之为“unreachable”(无法访问),并且在下次垃圾回收器运行时被回收(销毁)。引擎将在一定时间间隔运行垃圾回收。

怎样才算是一个引用?存储在一个UPROPERTY属性中的UObject 对象指针。让我们看看简单的例子。

void CreateDoomedObject()
{
    MyGCType* DoomedObject = NewObject<MyGCType>();
}

当我们调用上面的方法时,我们实例化一个新的UObject对象,但我们没有存储对象指针到任何UPROPERTY的属性中,所以它不是根集的一部分。事实上,垃圾回收器会检测这个对象是否无法访问,若是,则回收它。

Actors 与 垃圾回收

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的构造函数创建2个对象。一个被标记了UPROPERTY,另一个使用普通的指针。由于Actor本来就是根集的一部分,SafeObject 将不会被垃圾回收,因为它在根集中可以被访问。然而,DoomedObject将不会那么幸运,我们没有对它标记UPROPERTY,所以回收器将不知道它的引用,所以事实上它会被销毁。

当一个UObject 被垃圾回收,所有的UPROPERTY 类型的引用都将变成空指针。你最好在使用时检查一下是否存在空指针。

if (MyActor->SafeObject != nullptr)
{
    // Use SafeObject
}

正如我前面提到的,这非常的重要,Actor如果已经执行了Destroy() 方法,它将不会被移除,直到下次垃圾回收。你可以使用IsPendingKill() 方法去检查,这个UObject是否在被等待销毁。如果方法返回Ture,意味着这个UObject 已经无用了。

UStructs

UStructs,正如前面提到,可以理解为轻量级的UObject。比如说,UStructs 不会被垃圾回收。如果你必须要用UStructs 类型的动态实例,你可能需要使用智能指针去代替,我们后面会提到。

非UObject对象的引用

通常,非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 对象,手动添加一个硬引用,使其不能被垃圾回收。当这个对象(FMyNormalClass )被销毁并且析构函数执行,该对象将会自动清除它所添加的引用。

类命名前缀

在虚幻引擎运行时将为你生成代码,编辑器有一些命名规则,当类名不符合命名规则时,将触发警告或错误。下面的列表罗列出了这些预制的命名规则。

  • 继承Actor 的类,使用A作为前缀,如,AController。
  • 继承Object的类,使用U作为前缀,如,UComponent。
  • 枚举类型Enums ,使用E作为前缀,如,EFortificationType。
  • 接口类Interface ,使用I作为前缀,如,IAbilitySystemInterface。
  • 模板类Template ,使用T作为前缀,如,TArray。
  • 继承SWidget 的类(Slate UI),使用前缀S,如,SButton。
  • 除此之外的命名都用F前缀,如,FVector。(小故事:很久以前F代表的意思是Float,当时引擎的计算都是浮点数,但后来数学计算扩展到整数,而且引擎的传播很迅速,所以来不及改成更好的前缀字母了,恩就酱。)

数字类型

对于基本类型shortintlong来说,不同类型的平台有不同的长度,所以你应该使用虚幻4以下提供的变量类型:

  • int8/uint8:8位有符号/无符号整数。
  • int16/uint16 : 16位有符号/无符号整数。
  • int32/uint32 : 32位有符号/无符号整数。
  • int64/uint64 : 64位有符号/无符号整数。

虚幻引擎关于浮点数同样支持,float类型(32位)与double类型(64位)。

虚幻引擎有一个模板,TNumericLimits,可以发现变量类型所支持的最小和最大长度。更多的信息在这里,传送门

字符串类型

虚幻引擎在工作中提供了几个有差异的类型,请根据您的需要自己选择。
Full Topic: String Handling

FString

FString 是一个可变的String类型,类似于 std::string,FString拥有一套庞大的方法库用于工作。创建一个新的FString,使用TEXT() 宏命令。

FString MyStr = TEXT("Hello, Unreal 4!").

Full Topic: FString API

FText

FText 与FString 相似,但它意味着本地化文本。创建一个FText,使用NSLOCTEXT() 宏命令。这个宏命令包含,命名空间,键,默认值。

FText MyText = NSLOCTEXT("Game UI", "Health Warning Message", "Low Health!");

你也可以用LOCTEXT() 宏命令,得到同样的效果,你只需要在每个文件中定义命名空间,别忘了还要在文件末尾处用undefine。

// In GameUI.cpp
#define LOCTEXT_NAMESPACE "Game UI"

//...
FText MyText = LOCTEXT("Health Warning Message", "Low Health!")
//...

#undef LOCTEXT_NAMESPACE
// End of file

Full Topic: FText API

FName

FName通常存储使用频繁的用于比较的字符串,它们使用识别码来区分,所以可以接神记忆和CPU的时间。而不是通过对象的引用去读取完整的字符串。FName 使用类似于存储脚本序列Index 想哈希表那样获得字符串。每当存储一个字符串,这个字符串就可以被很多字符串使用了。通过检查NamaA.Index 和 NameB.Index两个字符串之间的对比会很快,避免检查他们的字符串信息是否相等。

Full Topic: FName API

TCHAR

TCHAR 是被用于存储字符集中独立字符,它可能在每个平台都不一样。虚幻4引擎的String是使用UTF-16编码的TCHAR 数组去存储数据的。您可以通过重载的引用操作返回TCHAR。

Full Topic: Character Encoding

这需要一些方法,比如,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'

注意:FChar 类型需要定义为TChar,更多信息在API中列出。
Full Topic: TChar API

容器

容器是一类用于存储数据的主要方法。最常见的容器有,TArray(数组),TMap(哈希表),TSet(集),他们都有动态的长度,而且根据你的需要是可增长的。

Full Topic: Containers API

TArray

你需要知道,对于这3种容器来说,在虚幻4引擎中TArray是我们主要使用的。它的作用类似于std::vector,但TArray提供了更多的功能,下面是一些常用操作:

TArray<AActor*> ActorArray = GetActorArrayFromSomewhere();

/* Tells how many elements (AActors) are currently stored in ActorArray.*/
int32 ArraySize = ActorArray.Num();

// TArrays are 0-based (the first element will be at index 0)
int32 Index = 0;
// Attempts to retrieve an element at the given index
TArray* FirstActor = ActorArray[Index];

// Adds a new element to the end of the array
AActor* NewActor = GetNewActor();
ActorArray.Add(NewActor);

/* Adds an element to the end of the array only if it is not already in the array */
ActorArray.AddUnique(NewActor); // Won't change the array because NewActor was already added

// Removes all instances of 'NewActor' from the array
ActorArray.Remove(NewActor);

// Removes the element at the specified index
// Elements above the index will be shifted down by one to fill the empty space
ActorArray.RemoveAt(Index);

// More efficient version of 'RemoveAt', but does not maintain order of the elements
ActorArray.RemoveAtSwap(Index);

// Removes all elements in the array
ActorArray.Empty();

TArray 有个附加的优势,它的元素是可被垃圾回收的。假设TArray被UPROPERTY标记了,并且它存储的元素是继承UObject 类的类指针。

UCLASS()
class UMyClass : UObject
{
    GENERATED_BODY();

    // ...

    UPROPERTY()
    TArray<AActor*> GarbageCollectedArray;
};

我们将在下一节,挖掘垃圾回收机制更深层次的内容。

Full Topic: TArrays

Full Topic: TArray API

TMap

TMap是一个键值对容器,和std::map类似,TMap可以元素key快速的查找、添加、删除元素。你可以使用任何类型的Key,只要它具有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:

    // Using a TMap, we can refer to each piece by its position
    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();
    }
};

Full Topic: TMaps

Full Topic: TMap API

TSet

TSet用于存储唯一的值,类似于std::set,使用AddUnique 方法和Contains 方法。TArrays 同样可以达到与TSet相同的效果。但是,TSet更高效的实现了这些操作,代价是不能像TArray一样添加UPROPERTY标记,TSet同样不能为元素添加index。

TSet<AActor*> ActorSet = GetActorSetFromSomewhere();

int32 Size = ActorSet.Num();

// Adds an element to the set, if the set does not already contain it
AActor* NewActor = GetNewActor();
ActorSet.Add(NewActor);

// Check if an element is already contained by the set
if (ActorSet.Contains(NewActor))
{
    // ...
}

// Remove an element from the set
ActorSet.Remove(NewActor);

// Removes all elements from the set
ActorSet.Empty();

// Creates a TArray that contains the elements of your TSet
TArray<AActor*> ActorArrayFromSet = ActorSet.Array();

Full Topic: TSet API

请记住,目前只有TArray可以被UPROPERTY 标记。这意味着其他容器对象不能被复制、保存,或者其他元素的容器元素不能被垃圾回收。

容器迭代器

使用迭代器,你可以遍历容器中的元素。下面的例子是遍历TSet容器中元素的语法。

void RemoveDeadEnemies(TSet<AEnemy*>& EnemySet)
{
    // Start at the beginning of the set, and iterate to the end of the set
    for (auto EnemyIterator = EnemySet.CreateIterator(); EnemyIterator; ++EnemyIterator)
    {
        // The * operator gets the current element
        AEnemy* Enemy = *EnemyIterator;
        if (Enemy.Health == 0)
        {
            // 'RemoveCurrent' is supported by TSets and TMaps
            EnemyIterator.RemoveCurrent();
        }
    }
}

其他迭代器可支持的方法:

// Moves the iterator back one element
--EnemyIterator;

// Moves the iterator forward/backward by some offset, where Offset is an integer
EnemyIterator += Offset;
EnemyIterator -= Offset;

// Gets the index of the current element
int32 Index = EnemyIterator.GetIndex();

// Resets the iterator to the first element
EnemyIterator.Reset();

Foreach循环

迭代器非常好,但是有点笨重,如果你只是想仅仅循环一次容器,每个容器类型都提供了这个遍历的方法,TArray和TSet返回他们的元素,TMap返回键值对。

// TArray
TArray<AActor*> ActorArray = GetArrayFromSomewhere();
for (AActor* OneActor : ActorArray)
{
    // ...
}

// TSet - Same as TArray
TSet<AActor*> ActorSet = GetSetFromSomewhere();
for (AActor* UniqueActor : ActorSet)
{
    // ...
}

// TMap - Iterator returns a key-value pair
TMap<FName, AActor*> NameToActorMap = GetMapFromSomewhere();
for (auto& KVP : NameToActorMap)
{
    FName Name = KVP.Key;
    AActor* Actor = KVP.Value;

    // ...
}

记住!auto关键字不会自动的为你指定 指针/引用,你需要自己添加。

在TSet/TMap中使用你自己的类型(哈希方法)

TSet和TMap内部需要使用哈希方法。如果你想把自己的类放进这2种容器中,你需要先新建你的哈希方法。通常你能够输入的虚幻4引擎的类型都已经默认拥有哈希方法了。

哈希方法需要你提供一个指针/引用类型,并返回一个uinit64类型的值。这个返回值就是这个对象所对应的哈希码。并且应该是一个伪唯一的代码指向该对象。2个相同的对象总是返回相同的哈希码。

class FMyClass
{
    uint32 ExampleProperty1;
    uint32 ExampleProperty2;

    // Hash Function
    friend uint32 GetTypeHash(const FMyClass& MyClass)
    {
        // HashCombine is a utility function for combining two hash values
        uint32 HashCode = HashCombine(MyClass.ExampleProperty1, MyClass.ExampleProperty2);
        return HashCode;
    }

    // For demonstration purposes, two objects that are equal
    // should always return the same hash code.
    bool operator==(const FMyClass& LHS, const FMyClass& RHS)
    {
        return LHS.ExampleProperty1 == RHS.ExampleProperty1
            && LHS.ExampleProperty2 == RHS.ExampleProperty2;
    }
};

现在,TSet<FMyClass>和TMap将再使用哈希键时使用合适的哈希方法。如果你使用指针作为键(TSet),则应该用这句实现,

uint32 GetTypeHash(const FMyClass* MyClass)

Blog Post: UE4 Libraries You Should Know About

翻译完了,好累,喝口水。