使用虚幻引擎中的C++导论

使用虚幻引擎中的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

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
    游戏引擎之所以要做内存管理,一个是加快内存分配速度,另一个就是处理内存泄漏问题。     1.先简单说处理内存泄漏这个问题,一般的引擎在debug 模式下 都有一个记录内存分配的结构体,每分配一段内存就记录这段内存的信息,包括大小,分配时间,是否是数组,前后越界的标记等等吧,其实这些都不是那么重要,因为你只知道这些,一旦泄漏出现,你虽然知道泄漏,但无法定位。相反如果你知道堆栈的调用信息,就能准确定位。我以前的实现,在debug下,只记录当前调用new的时候的行号和文件,也就是内部的__FILE__ __LINE___.。我看了同事那个能记录堆栈调用过程,简直觉得很牛逼(其实不是调用堆栈,只是打印出调用过程,继续往下看你就知道了),以前自己也想过,但不知道怎么去实现。如果U3里面也加入这个功能,那就更牛叉了。思想很简单,就是核心东西在一个函数,这个是系统函数,提供当前这行指令所在的地址,它会打印出来这行指令的文件名和行号。先详细说下数据在内存的分配     最早的计算机数据段和代码段区分的很严格,现在似乎没有这么严格了!对于全局变量和静态变量它的分配完全在数据段分配,知道运行结束才会收回内存!而对于自动变量(包括函数参数和函数定义的变量)则在堆栈分配!一般的分配情形是这样的:从栈下到栈顶依次是函数参数,函数结束后下一条指令的地址,寄存器保护,然后是函数定义的变量!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值