【UE4】C++编程笔记(一)


笔记参考文档

UPROPERTY()宏

公开属性变量给蓝图

如何使用

在属性声明前使用

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
int32 TotalDamage;

属性说明符

全部说明符解释传送门:UPROPERTY()宏的使用说明符

属性解释
EditAnyWhere实例化可编辑
BlueprintReadWrite可在蓝图中Get和Set
BlueprintReadOnly只能在蓝图中Get
Category在蓝图目录中给变量分类
Transient属性为临时,意味着其无法被保存或加载。以此方法标记的属性将在加载时被零填充。

UFUNCTION()宏

公开函数方法给蓝图,也可以实现蓝图与C++的交互调用

如何使用

在函数声明前调用

UFUNCTION(BlueprintCallable, Category="Damage")
void CalculateValues();

属性说明符

全部说明符解释传送门:UFUNCTION()宏的使用说明符

属性解释
BlueprintCallable允许在蓝图中调用此方法
Category在蓝图目录中给方法分类
BlueprintImplementableEvent允许在蓝图中实现此方法,C++中即可调用此方法
BlueprintNativeEvent允许C++提供默认实现方式,同时允许在蓝图中复写此方法并覆盖C++的实现

*若在C++中提供默认实现方法,实现方法的命名需要类似于<函数名>_Implementation()

void AMyActor::CalledFromCpp_Implementation()
{
    // 这里添加实现代码
}

Gameplay类

从大部分Gameplay类可以派生出4种主要类型的类。它们分别是 UObject、AActor、UActorComponent 和 UStruct。

UObject

虚幻引擎中的基本构建块叫做UObject。该类结合 UClass,可以提供引擎中的多个最重要的基本服务:

  • 反射属性和方法
  • 序列化属性
  • 垃圾回收
  • 按名称查找UObject
  • 属性的可配置值
  • 属性和方法的联网支持

从UObject派生的每个类都会创建有一个UClass,UClass包含有关该类实例的所有元数据。UObject和UClass一起位于Gameplay对象在其生命周期所有作用的最根部位置。如果要解释UClass和UObject的差异在哪里,最合适的方法是UClass描述的是UObject实例的样子、可序列化和联网的属性等。大多数Gameplay开发不会直接从UObject派生,而是从AActor和UActorComponent派生。您无需知道UClass/UObject工作方式细节,这并不影响您编写Gameplay代码,知道这些系统的存在即可。

AActor

AActor由设计师放在关卡中,或者通过Gameplay系统在运行时创建。可以放入关卡的所有对象都是从该类扩展而来的。示例包括 AStaticMeshActor、ACameraActor 和 APointLight Actor。AActor派生自UObject,因此可以使用上一节所列的所有标准功能。AActor可以显式销毁(通过蓝图或C++代码),或者在所属关卡从内存中卸载时通过标准的垃圾回收机制销毁。AActor还是可以在联网时复制的基本类型。
AActor包含在AActor生命周期中调用的一系列事件。以下列表是一组简化的事件,描绘了整个生命周期:

  • BeginPlay——对象首次在Gameplay中存在时调用。
  • Tick——每帧调用一次,随着时间的进行持续完成工作。
  • EndPlay——对象离开Gameplay空间时调用。
    *详见逻辑编程-Actors了解更多AActors内容!

声明周期
我们有一种方法专门用来产生Actor,叫做 UWorld::SpawnActor()。成功产生Actor后,会调用它的 BeginPlay() 方法,下一帧调用 Tick()。
Actor生命周期结束时,您可以调用 Destroy() 来将它销毁。在该过程中,将调用 EndPlay(),供您编写任何自定义销毁逻辑。另一个控制Actor生命周期时长的方法是使用Lifespan成员。您可以在对象的构造函数中设置时间跨度,也可以在运行时使用其他代码进行设置。当这段时间到期后,会自动对该Actor调用 Destroy()。

UActorComponent

AActor通常提供与其游戏总体角色有关的高级目标,而UActorComponent通常执行用于支持这些更高级目标的单独任务。组件也可以与其他组件相连接,或者可以成为Actor的根组件。

  • RootComponent——这是AActor的一个成员,用于保存AActor组件树中的顶级组件。
  • Ticking——在所属AActor的Tick()过程中执行tick事件的组件。

UStruct

要使用UStruct,您不必从任何特定类扩展,只需用USTRUCT()标记该结构体,构建工具就会为您完成基本工作。与UObject不同的是,UStruct不会被垃圾回收。如果您要创建它们的动态实例,必须自行管理其生命周期。UStruct应该是纯传统数据类型,包含UObject反射支持,可以在虚幻编辑器、蓝图操控、序列化、联网等中编辑。

引擎工作方式

虚幻反射系统

UE4使用其自己的反射实现来支持动态功能,如垃圾回收、序列化、网络复制和蓝图/C++通信。这些功能是可选的,意味着您必须将正确的标记添加到类型,否则虚幻将忽略它们,而不会为它们生成反射数据。下面是对基本标记的简要概述:

  • UCLASS()——用于告诉虚幻为结构体生成反射数据。类必须派生自UObject。
  • USTRUCT()——用于告诉虚幻为结构体生成反射数据。
  • GENERATED_BODY()——UE4将这个标记替换为将为该类型生成的所有必要的样板代码。
  • UPROPERTY()——支持将UCLASS的成员变量或USTRUCT用作UPROPERTY。UPROPERTY有很多用法。它可以允许复制变量、序列化变量和从蓝图访问变量。它们可以供垃圾回收程序使用,用来跟踪对UObject的引用次数。
  • UFUNCTION()——支持将UCLASS的类方法或USTRUCT用作UFUNCTION。UFUNCTION可以允许从蓝图调用类方法,用作RPC等多种用途。

说明符传送门

UCLASS说明符列表
UPROPERTY说明符列表
UFUNCTION说明符列表
USTRUCT说明符列表

对象/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)
{
    // ...
}

在PIE(编辑器中运行)中使用对象迭代器会导致意外结果。由于编辑器已经加载,对象迭代器将返回为游戏场景实例创建的所有UObject,此外还有编辑器使用的实例。
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)
{
    // ...
}

由于AActor派生自UObject,因此您也可以使用 TObjectIterator 来查找AActor的实例。只是在PIE中需要谨慎!

内存管理和垃圾回收

UObject和垃圾回收

UE4使用反射系统来实现垃圾回收系统。通过垃圾回收,您将不必手动删除UObject,只需维护对它们的有效引用即可。类需要派生自UObject才能对其进行垃圾回收。简单示例类:

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

在垃圾回收程序中,有一个概念叫做根集。该根集基本上是一个对象列表,这些对象是回收程序知道将不会被垃圾回收的对象。只要根集中的某个对象到一个对象存在引用路径,就不会对所涉及对象进行垃圾回收。如果某个对象不存在到根集的此类路径,则称为无法访问,将会在下次运行垃圾回收程序时将其回收(删除)。引擎按特定的时间间隔运行垃圾回收程序。

下属代码声明的引用将会被垃圾回收机制回收:

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

当我们调用上述函数时,我们会创建一个新UObject,但不会在任何UPROPERTY中存储指向它的指针,因此它不是根集的一部分。最终,垃圾回收程序会检测到该对象无法访问,从而将其销毁。

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是UObject的轻量级版本。因此,不能将UStructs垃圾回收。如果必需使用UStructs的动态实例,可以使用智能指针,我们稍后将进行介绍。

非对象引用

通常,非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的硬引用。当该对象被删除且其析构函数运行时,该对象将自动清除其所添加的所有引用。

类命名前缀

  • 派生自 Actor 的类带有 A 前缀,如AController。
  • 派生自 Object 的类带有 U 前缀,如UComponent。
  • Enums 的前缀是 E,如EFortificationType。
  • Interface 的前缀通常是 I,如IAbilitySystemInterface。
  • Template 的前缀是 T,如TArray。
  • 派生自 SWidget 的类(Slate UI)带有前缀 S,如SButton。
  • 其他类的前缀为字母F ,如FVector。

数字类型

由于不同平台有不同的基本类型大小,如 短整型、整型 和 长整型,因此UE4提供以下类型供您备选:

  • int8/uint8 :8位有符号/无符号整数
  • int16/uint16 :16位有符号/无符号整数
  • int32/uint32 :32位有符号/无符号整数
  • int64/uint64 :64位有符号/无符号整数
  • 浮点数也支持标准 浮点(32位) 和 双精度**(64位)类型。

虚幻引擎有一个模板TNumericLimits<t>,用于查找值类型可以拥有的最小和最大范围。

字符串

完整主题:字符串处理

FString

FString 是一个可变字符串,类似于std::string。FString拥有很多方法,方便您处理字符串。要创建新的FString,请使用 TEXT() 宏:FString MyStr = TEXT(“Hello, Unreal 4!”).
FString API 传送门

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 传送门

FName

FName 存储通常反复出现的字符串作为辨识符,以在比较时节省内存和CPU时间。如果有多个对象引用一个字符串,FName使用较小的存储空间 索引 来映射到给定字符串,而不是在引用它的每个对象中多次存储完整字符串。这样会将字符串内容存储一次,节省在多个对象中使用该字符串时占用的内存。通过检查确认 NameA.Index 是否等于 NameB.Index 可以快速比较两个字符串,避免检查字符串中的每一个字符是否相同。
FName API 传送门

FCHAR

TCHAR 是独立于所用字符集存储字符的方法,字符集或许会因平台而异。实际上,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'

FChar类型定义为TChar(因为它列示在该API中)。

容器

最常见的这些类包括 TArray、TMap 和 TSet。每个类都会自动调节大小,因此增长到您所需的大小。
容器API传送门

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介绍传送门
TArray API传送门

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完整介绍
TMap API 传送门

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 传送门
目前,唯一能标记为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)。

  • 4
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值