公开属性变量给蓝图
在属性声明前使用
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
int32 TotalDamage;
全部说明符解释传送门:UPROPERTY()宏的使用说明符
属性 | 解释 |
---|---|
EditAnyWhere | 实例化可编辑 |
BlueprintReadWrite | 可在蓝图中Get和Set |
BlueprintReadOnly | 只能在蓝图中Get |
Category | 在蓝图目录中给变量分类 |
Transient | 属性为临时,意味着其无法被保存或加载。以此方法标记的属性将在加载时被零填充。 |
公开函数方法给蓝图,也可以实现蓝图与C++的交互调用
在函数声明前调用
UFUNCTION(BlueprintCallable, Category="Damage")
void CalculateValues();
全部说明符解释传送门:UFUNCTION()宏的使用说明符
属性 | 解释 |
---|---|
BlueprintCallable | 允许在蓝图中调用此方法 |
Category | 在蓝图目录中给方法分类 |
BlueprintNativeEvent | 允许在蓝图中实现此方法,C++中即可调用此方法 |
BlueprintNativeEvent* | 允许C++提供默认实现方式,同时允许在蓝图中复写此方法并覆盖C++的实现 |
*若在C++中提供默认实现方法,实现方法的命名需要类似于<函数名>_Implementation()
void AMyActor::CalledFromCpp_Implementation()
{
// 这里添加实现代码
}
从大部分Gameplay类可以派生出4种主要类型的类。它们分别是 UObject、AActor、UActorComponent 和 UStruct。
虚幻引擎中的基本构建块叫做UObject。该类结合 UClass,可以提供引擎中的多个最重要的基本服务:
从UObject派生的每个类都会创建有一个UClass,UClass包含有关该类实例的所有元数据。UObject和UClass一起位于Gameplay对象在其生命周期所有作用的最根部位置。如果要解释UClass和UObject的差异在哪里,最合适的方法是UClass描述的是UObject实例的样子、可序列化和联网的属性等。大多数Gameplay开发不会直接从UObject派生,而是从AActor和UActorComponent派生。您无需知道UClass/UObject工作方式细节,这并不影响您编写Gameplay代码,知道这些系统的存在即可。
AActor由设计师放在关卡中,或者通过Gameplay系统在运行时创建。可以放入关卡的所有对象都是从该类扩展而来的。示例包括 AStaticMeshActor、ACameraActor 和 APointLight Actor。AActor派生自UObject,因此可以使用上一节所列的所有标准功能。AActor可以显式销毁(通过蓝图或C++代码),或者在所属关卡从内存中卸载时通过标准的垃圾回收机制销毁。AActor还是可以在联网时复制的基本类型。
AActor包含在AActor生命周期中调用的一系列事件。以下列表是一组简化的事件,描绘了整个生命周期:
声明周期
我们有一种方法专门用来产生Actor,叫做 UWorld::SpawnActor()。成功产生Actor后,会调用它的 BeginPlay() 方法,下一帧调用 Tick()。
Actor生命周期结束时,您可以调用 Destroy() 来将它销毁。在该过程中,将调用 EndPlay(),供您编写任何自定义销毁逻辑。另一个控制Actor生命周期时长的方法是使用Lifespan成员。您可以在对象的构造函数中设置时间跨度,也可以在运行时使用其他代码进行设置。当这段时间到期后,会自动对该Actor调用 Destroy()。
AActor通常提供与其游戏总体角色有关的高级目标,而UActorComponent通常执行用于支持这些更高级目标的单独任务。组件也可以与其他组件相连接,或者可以成为Actor的根组件。
要使用UStruct,您不必从任何特定类扩展,只需用USTRUCT()标记该结构体,构建工具就会为您完成基本工作。与UObject不同的是,UStruct不会被垃圾回收。如果您要创建它们的动态实例,必须自行管理其生命周期。UStruct应该是纯传统数据类型,包含UObject反射支持,可以在虚幻编辑器、蓝图操控、序列化、联网等中编辑。
UE4使用其自己的反射实现来支持动态功能,如垃圾回收、序列化、网络复制和蓝图/C++通信。这些功能是可选的,意味着您必须将正确的标记添加到类型,否则虚幻将忽略它们,而不会为它们生成反射数据。下面是对基本标记的简要概述:
UCLASS说明符列表
UPROPERTY说明符列表
UFUNCTION说明符列表
USTRUCT说明符列表
可用来迭代特定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中需要谨慎!
UE4使用反射系统来实现垃圾回收系统。通过垃圾回收,您将不必手动删除UObject,只需维护对它们的有效引用即可。类需要派生自UObject才能对其进行垃圾回收。简单示例类:
UCLASS()
class MyGCType : public UObject
{
GENERATED_BODY()
};
在垃圾回收程序中,有一个概念叫做根集。该根集基本上是一个对象列表,这些对象是回收程序知道将不会被垃圾回收的对象。只要根集中的某个对象到一个对象存在引用路径,就不会对所涉及对象进行垃圾回收。如果某个对象不存在到根集的此类路径,则称为无法访问,将会在下次运行垃圾回收程序时将其回收(删除)。引擎按特定的时间间隔运行垃圾回收程序。
下属代码声明的引用将会被垃圾回收机制回收:
void CreateDoomedObject()
{
MyGCType* DoomedObject = NewObject<MyGCType>();
}
当我们调用上述函数时,我们会创建一个新UObject,但不会在任何UPROPERTY中存储指向它的指针,因此它不是根集的一部分。最终,垃圾回收程序会检测到该对象无法访问,从而将其销毁。
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是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的硬引用。当该对象被删除且其析构函数运行时,该对象将自动清除其所添加的所有引用。
由于不同平台有不同的基本类型大小,如 短整型、整型 和 长整型,因此UE4提供以下类型供您备选:
虚幻引擎有一个模板TNumericLimits
完整主题:字符串处理
FString 是一个可变字符串,类似于std::string。FString拥有很多方法,方便您处理字符串。要创建新的FString,请使用 TEXT() 宏:FString MyStr = TEXT(“Hello, Unreal 4!”).
FString API 传送门
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 存储通常反复出现的字符串作为辨识符,以在比较时节省内存和CPU时间。如果有多个对象引用一个字符串,FName使用较小的存储空间 索引 来映射到给定字符串,而不是在引用它的每个对象中多次存储完整字符串。这样会将字符串内容存储一次,节省在多个对象中使用该字符串时占用的内存。通过检查确认 NameA.Index 是否等于 NameB.Index 可以快速比较两个字符串,避免检查字符串中的每一个字符是否相同。
FName API 传送门
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传送门
在所有三个容器中,您在虚幻引擎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 是键-值对的集合,类似于 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 存储唯一值集合,类似于 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风格的语法来循环元素。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的键,则需要先创建自己的散列函数。大部分通常想要这样使用的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