UE4 C++脚本入门

528 阅读15分钟

Actor类

参考docs.unrealengine.com/4.27/zh-CN/…

#include "GameFramework/Actor.h"
#include "MyActor.generated.h"UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()
​
public:
    // 设置该Actor属性的默认值
    AMyActor();
​
    // 每一帧都调用
    virtual void Tick( float DeltaSeconds ) override;
​
protected:
    // 游戏开始或产生时调用
    virtual void BeginPlay() override;
};
AMyActor::AMyActor()
​
{
​
    // 将该Actor设置为每帧调用一次Tick()。如果您没有这个需要,可以将其关闭来改善性能。
​
    PrimaryActorTick.bCanEverTick = true;
​
}

让属性出现在编辑器中

将属性公开给编辑器非常简单,只需要使用说明符 UPROPERTY 即可实现。您只需在属性声明的上一行加入 UPROPERTY(EditAnywhere)

UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()
public:
​
    UPROPERTY(EditAnywhere)
    int32 TotalDamage;
​
    ...
};

如果您想要TotalDamage属性出现在包含相关属性的某个部分中

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

当用户想要编辑该属性时,它现在会出现在"伤害(Damage)"标题下面,与您已经标记为此类别名称的任何其他属性在一起。这是将常用设置放在一起以供设计师编辑的好方法。

现在,让我们将同一个属性公开给蓝图。

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

如您所见,有一个说明符可以让属性在蓝图图表中可供读写。有一个单独的说明符 BlueprintReadOnly,如果您希望属性在蓝图中被视为 常量,可以使用这个选项。还有一些选项可用来控制将属性公开给编辑器的方式。

UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient, Category="Damage")
    float DamagePerSecond;

Transient 说明符意味着,它不会保存或从磁盘加载;它就是一个派生的非持久值,所以没有必要存储它。下图显示作为类默认值一部分的属性。

image alt text

构造函数中设置默认值

在构造函数中为属性设置默认值的方式与典型的C++类一样。下面是两个在构造函数中设置默认值的示例,它们在功能上是等效的。

AMyActor::AMyActor()
{
    TotalDamage = 200;
    DamageTimeInSeconds = 1.0f;
}
​
AMyActor::AMyActor() :
    TotalDamage(200),
    DamageTimeInSeconds(1.0f)
{
}

这是在构造函数中添加默认值后的相同属性视图。

image alt text

计算初始化

为了按实例支持设计师设置属性,还会从给定对象的实例数据加载值。该数据在构造函数之后应用。您可以根据设计师设置值创建默认值,方法是钩入 PostInitProperties() 调用链中。下面示例展示了 TotalDamageDamageTimeInSeconds 为设计师指定值的流程。尽管它们是设计师指定的,但您仍可以为它们提供合理的默认值,就像上述示例一样。

如果您不提供属性的默认值,引擎会自动将该属性设置为0或空指针(如果是指针类型)。

void AMyActor::PostInitProperties()
{
    Super::PostInitProperties();
    DamagePerSecond = TotalDamage / DamageTimeInSeconds;
}

这里同样是添加了上述 PostInitProperties() 代码后的属性视图。

image alt text

我们仅计算了从加载过程初始化属性后的每秒伤害值。编辑器中的运行时更改没有考虑在内。这个问题有一种简单的解决方案,因为引擎会在编辑器中发生变化时通知目标对象。下面的代码显示了为了计算派生值在编辑器中发生变化时的值而添加的钩。

void AMyActor::PostInitProperties()
{
    Super::PostInitProperties();
​
    CalculateValues();
}
​
void AMyActor::CalculateValues()
{
    DamagePerSecond = TotalDamage / DamageTimeInSeconds;
}
​
#if WITH_EDITOR
void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
    CalculateValues();
​
    Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif

需要注意的一点是,PostEditChangeProperty 方法位于特定于编辑器的 #ifdef 内部。这样才能在构建游戏时只编译真正需要的代码,删除任何多余的、导致可执行文件大小增大的代码。现在,我们已经编译了代码,DamagePerSecond 值与我们预期的值匹配,如下图所示。

image alt text

在C++和蓝图边界中调用函数

首先我们来让CalculateValues()能够从蓝图调用。将函数公开给蓝图就像公开属性一样简单。只需在函数声明前放置一个宏即可!以下代码片段显示了所需内容。

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

UFUNCTION() 宏负责处理将C++函数公开给反射系统。BlueprintCallable 选项将其公开给蓝图虚拟机。每一个公开给蓝图的函数都需要一个与之关联的类别,这样右键点击快捷菜单的功能才能正确生效。下图显示了类别对快捷菜单的影响:

image alt text

如您所见,该函数可以从 伤害(Damage) 类别中选择。下面的蓝图代码显示了TotalDamage值的变化,后面是用来重新计算依赖数据的调用。

image alt text

这里使用了我们之前添加的用来计算相关属性的同一个函数。引擎的大部分都通过 UFUNCTION() 宏公开给蓝图,因此用户可以直接构建游戏,而不必编写C++代码。但是,最佳方法是使用C++构建基本Gameplay系统和性能关键代码,用蓝图自定义行为或从C++构建块创建组合式行为。

现在,设计师已经可以调用C++代码了,接下来探索一种更强大的C++/蓝图边界交叉调用方法。该方法让C++代码能够调用蓝图中定义的函数。我们通常使用这种方法,将设计师在认为合适时可以响应的事件通知给设计师。这通常包括产生效果或其他视觉影响,如隐藏或取消隐藏Actor。下面的代码片段显示了蓝图实现的函数。

UFUNCTION(BlueprintImplementableEvent, Category="Damage")
void CalledFromCpp();

该函数的调用方法与任何其他C++函数一样。在后台,虚幻引擎生成基本C++函数实现,用以理解如何在蓝图VM中调用。这通常称为形实替换(Thunk)。如果所提及蓝图不为这种方法提供函数体,则函数行为就像没有实体行为的C++函数一样:不执行任何操作。如果想要提供C++默认实现,同时仍允许蓝图覆盖此方法,该怎么办呢?或许可以使用UFUNCTION()宏的一个选项。以下代码片段显示了为达到此目的需要在标头中进行的更改。

UFUNCTION(BlueprintNativeEvent, Category="Damage")
void CalledFromCpp();

该版本仍会生成用于在蓝图VM中调用的形实替换方法。那么如何提供默认实现呢?工具还会生成一个新的函数声明,类似于<函数名>_Implementation()您必须提供该版本的函数,否则项目无法建立关联。下面是对上述声明的实现代码。

void AMyActor::CalledFromCpp_Implementation()
{
    // 这里可以添加些有趣的代码
}

现在,该版本函数会在所提及蓝图不覆盖此方法时被调用。注意,在先前版本的构建工具中,会自动生成_Implementation()声明。在4.8或更高版本中,您需要显式将该声明添加到标头中。

\

虚幻反射系统

docs.unrealengine.com/4.27/zh-CN/…

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

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

以下是UCLASS声明示例:

#include "MyObject.generated.h"
​
UCLASS(Blueprintable)
class UMyObject : public UObject
{
    GENERATED_BODY()
​
public:
    MyUObject();
​
    UPROPERTY(BlueprintReadOnly, EditAnywhere)
    float ExampleProperty;
​
    UFUNCTION(BlueprintCallable)
    void ExampleFunction();
};

首先您会注意到包含了 MyClass.generated.h。UE4将生成所有反射数据并放入该文件中。您必须将该文件作为声明类型的标头文件中的最后一个包含语句,将其包含进去。

该示例中的 UCLASSUPROPERTYUFUNCTION 标记包含一些其他说明符。这些虽不是必需的,但为了演示目的,已经添加了一些常见说明符。这样我们可以指定特定行为或属性。

  • Blueprintable - 该类可以由蓝图扩展。
  • BlueprintReadOnly - 该属性可以从蓝图读取,但不能写入蓝图。
  • EditAnywhere - 该属性可以在原型和实例上的属性窗口中编辑。
  • Category - 定义该属性将出现在编辑器"细节(Details)"视图下面的哪个部分。这对于整理结构而言十分有用。
  • BlueprintCallable - 该功能可以从蓝图调用。

说明符众多,不便在此一一列出,但可以参考下面的链接:

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中需要谨慎!

UStructs

如前所述,UStructsUObject 的轻量级版本。因此,不能将 UStructs 垃圾回收。如果必需使用 UStructs 的动态实例,可以使用智能指针,我们稍后将进行介绍。

非对象引用

通常,C++对象(非派生自 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位有符号/无符号整数

浮点数也支持标准 float(32位)和 double(64位)类型。

虚幻引擎有一个模板TNumericLimits,用于查找值类型可以拥有的最小和最大范围。有关更多信息,请单击该链接

字符串

UE4提供多个不同的类,便于您根据需要处理字符串。

完整主题:字符串处理

FString

FString 是一个可变字符串,类似于 std::stringFString 拥有很多方法,方便您处理字符串。要创建新的 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 使用较小的存储空间索引来映射到给定字符串,而不是在引用它的每个对象中多次存储完整字符串。这样会将字符串内容存储一次,节省在多个对象中使用该字符串时占用的内存。FName 比较更快是因为UE4能够检查其索引值来确认其是否匹配,而无须检查每一个字符是否相同。

UObject::FName完整主题:FName API

TCHAR

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中)。

内存管理和垃圾回收

docs.unrealengine.com/4.27/zh-CN/…

UObject和垃圾回收

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

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

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

UPROPERTY 或UE4容器类(例如TArray)中存储的任意 UObject 指针都被视为垃圾回收的"引用"。首先让我们从简单示例入手。

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

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

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,您应将对象视为已销毁,不要再使用它。