UE4中的C++编程简介

发布时间 2023-11-04 10:52:43作者: XTG111

对官方文档的学习链接

利用UE创建一个C++基类

在编辑器中可以选择父类,根据这个父类我们可以创建一个基类用于后续的蓝图类制作。
以Actor父类为例创建基类,其头文件会包含一个构造函数,一个Tick函数的重载和一个BeginPlay函数的重载。
BeginPlay函数告诉Actor以可运行状态进入了游戏。这是启动类Gameplay逻辑的好位置。Tick 每帧调用一次,使用自上次调用传递以来经过的时间,可以在这里执行任何重复逻辑。
对于Tick函数不需要的话最好删除,这样可以节省性能,并将构造函数中的变量设为false

PrimaryActorTick.bCanEverTick = false;

将属性暴露给蓝图

我们在头文件中定义的变量就是这个类的属性,比如一个Actor的形状,速度,粒子特效。如果我们要在蓝图中操作改变这些属性就需要在定义前加上UPROPERTY(EditAnywhere)
括号内可以添加一些属性说明符,用于说明改变量在蓝图中的权限。属性说明符
对于蓝图来说主要有BlueprintReadWrite,BlueprintReadOnly等,还可以利用Category="Name",来对属性变量进行分类,相同Name的变量在蓝图中将出现在同一个标题下面。

设置变量值

对于属性说明符为EditAnywhere的变量,我们可以在C++代码或者蓝图中设置这些变量的值。
在C++代码中,主要有3种方法来初始化变量值。

  1. 头文件定义时赋值
UPROPERTY(EditAnywhere)
float Total = 100.0f;
  1. 构造函数赋值
AMyActor::MyActor
{
  Total = 300.0f;
}
  1. 构造函数初始化列表赋值
AMyActor::MyActor : Total(200.0f)
{
  Total = 300.0f;
}

三个方法可以同时使用,编译时会先构建头文件,再初始化列表,最后是构造函数。
所以前者会被后者的值覆盖。
初始化列表的顺序不是书写顺序而是在头文件中的声明顺序。
4. 利用PostInitProperties()初始化VisibleAnywhere属性的变量。
VisibleAnywhere属性的变量也可以通过上述3种方法初始化,只不过不能通过蓝图进行更改。需要使用PostInitProperties()虚方法。
该方法可以用于蓝图中改变变量值后,VisibleAnywhere属性的变量变换。该方法在构造函数之后,所有属性初始化之后才被调用。
在头文件中声明该虚函数

virtual void PostInitProperties() override;

在源文件中定义该虚函数

void AMyActor::PostInitProperties()
{
  Super::PostInitProperties();
  Totalper = Total/time;
}
  1. 实时更新VisibleAnywhere属性的变量
    如果我们在UE蓝图中更改了Total和time,想要Totalper也随之更改,就需要使用PostEditChangeProperty()方法,并且该方法需要编写在ifdef的内部,这样才能在构建游戏时只编译真正需要的代码,删除任何多余的、导致可执行文件大小增大的代码
#ifdef WITH_EDITOR
void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
    Totalper = Total/time;

    Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif

将函数暴露给蓝图

c++代码函数需要暴露给蓝图需要使用UFUNCTION(BlueprintCallable)
还有两种属性符号:BlueprintImplementableEvent和BlueprintNativeEvent。

  1. BlueprintImplementableEvent
    C++代码中只能声明,不能定义。定义需要在蓝图中重写。
  2. BlueprintNativeEvent
    C++代码中可以定义+声明,蓝图中可以选是否重写覆盖或者是否调用C++父类。在C++源文件中定义时需要将函数名改为
    <Func_Name>_Implementation()才能够被识别。
//.h文件
UFUNCTION(BlueprintNativeEvent, Category="Damage")
void CalledFromCpp();

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

Gameplay类

Gameplay类派生的4个主要类->UObject类,AActor类,UActorComponent类,UStruct类。

UObject类

Gameplay的基本,结合UClass,提供引擎服务

  • 反射属性和方法
  • 序列化属性
  • 垃圾回收
  • 按名称查找UObject
  • 属性的可配置值
  • 属性和方法的联网支持
    UObject派生的每一个类都会有自己的UClass,UObject和UClass位于Gameplay对象在其生命周期所有作用的根部位置。UClass主要描述的是UObject实例的样子、可序列化和联网的属性。

AActor类

UObject派生而来,一个关卡中的所有对象都是从该类扩展而来的。可以显式销毁。AActor可以在联网时复制的基本类型。在网络复制期间,Actor还可以分发其拥有的,需要网络支持或同步的任何UActorComponent的信息。
Actor还作为ActorComponent层级容器。每个Actor实例对象都有一个RootComponent,其包含一个USceneComponent,而后者继而包含许多其他的Component。在可以将Actor放入关卡之前,它必须包含至少一个Scene Component,Actor可以从后者绘制其平移、旋转和缩放。
Actor包含在AActor生命周期中调用的一系列事件。以下列表是一组简化的事件,描绘了整个生命周期:

  • BeginPlay:Actor首次在Gameplay中存在时调用。
  • Tick:每帧调用一次,随着时间的进行持续完成工作。在编写自己的Tick函数时,必须确保调用Super::Tick
  • EndPlay:对象离开Gameplay空间时调用。

一个Actor的生命周期

Actor加载并存在,最后关卡被卸载后,Actor被销毁
Actor的产生:需要注册到多个运行时系统才能满足其所有需要。比如需要设置Actor的初始位置和旋转,那么物理模块需要知道这些信息,负责告诉Actor执行tick事件的管理器也需要知道。
所以UE专门定义了一个方法来生产Actor--SpawnActor。当成功产生Actor后,引擎会调用它的 BeginPlay方法,下一帧调用 Tick。
Actor生命周期结束时,您可以调用 Destroy 来将它销毁。在该过程中,将调用 EndPlay,让您能在Actor进入回收站之前执行自定义逻辑。
另一个控制Actor生命周期时长的方法是使用 Lifespan 成员。您可以在对象的构造函数中设置Actor的时间跨度,也可以在运行时使用其他代码进行设置。当这段时间到期后,会自动对该Actor调用 Destroy。
在构造函数中初始化Actor生命周期,蓝图中为set life span

InitialLifeSpan = 3.0f;

UActorComponent

一般需要依附在一个Actor类下的RootComponent,可以用来提供网格体、粒子效果、摄像机视角和物理互动。组件也可以与其他组件相连接,或者可以成为Actor的根组件。一个组件只能连接到一个父组件或Actor,但可以连接多个子Actor。
image

UStruct

不需要从其它类派生,只需要使用UStruct标记结构体即可,其不会被垃圾回收。为纯数据类型。如果创建动态结构体实例,必须自己管理生命周期。

虚幻的反射系统

UE使用自己的反射系统实现垃圾回收、序列化、网络复制、蓝图通信等动态功能。需要将正确的标记添加到类型才能开启这些功能,否则UE不会生成反射数据,一般为U开头的标记

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

对于我们新建的一个类对象,头文件中会默认包含#include "Class_Name.generated.h",该语句必须使标头文件的最后一个语句。其作用是UE将生成所有的反射文件放入该文件中。

对象/Actor迭代器

  1. 对象迭代器
TObjectIterator<Class_Name>

一般只能用来迭代UObject或者其的子类的所有实例。
通过一个循环来获取

for (TObjectIterator<UObject> It; It; It++)

UE编辑器中:使用对象迭代器会返回游戏一个关卡中创建的所有UObject实例,并且还会返回编辑器使用的实例。
2. Actor迭代器
只能用于迭代AActor派生的对象。 并且只会返回游戏当前关卡实例所使用的对象。

TActorIterator<Class_Name>

对于Actor的迭代需要指定一个指向UWorld的指针,可以通过GetWorld()方法来获取

APlayerController* MyPC = GetMyPlayerControllerFromSomewhere();
UWorld* World = MyPC->GetWorld();
// 正如对象迭代器一样,您可以提供一个具体类来仅获得
// 属于该类或派生自该类的对象
for (TActorIterator<AEnemy> It(World); It; ++It)
{
    // ...
}

内存管理和垃圾回收

同样是利用反射系统来实现垃圾回收,只有派生自UObject的类才能进行垃圾回收操作。
垃圾回收程序中,有一个根集的成员列表,在这个列表中的成员不会被垃圾回收程序进行垃圾回收,如果一个对象为该列表成员的引用,该对象也不会被回收。
反之,不存在此类路径,该对象无法访问,在下一次运行垃圾回收程序是将进行回收操作。UE一般按一定的时间间隔自动进行垃圾回收操作。
根集成员的引用:一般来说UPROPERTY或者UE容器类(TArray)修饰的UObject指针对象都可以当作引用,不会被执行垃圾回收。

Actor对象

Actor类的对象,在关卡关闭之前,都不会被垃圾回收,只有使用Destroy方法,立刻将对象从关卡中移除,他会立即从游戏中删除,但只有在下一次垃圾回收时才能被完全删除回收。

垃圾回收

一个UObject被垃圾回收时,通过UPROPERTY修饰的对象都会设置为空指针,所以可以通过判断是否为空,确保对象的正确调用

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

特别的是对于Actor类对象,如果使用Destroy销毁对象,因为只有在下一次垃圾回收的时候才会变为空指针,所以就需要使用IsPendingKill方法来判断该对象是否存在,如果为true则表明对象已经被销毁

if(IsPendingKill(MyActor) == false)
{
  //TODO
}

UStructs和非对象引用

UStructs为UObejct的轻量版本,不能使用垃圾回收装置,如果必须使用UStructs实例,则只能使用智能指针。
C++对象(非派生自 UObject)也能够添加对对象的引用并防止垃圾回收。为此,对象必须派生自 FGCObject 并覆盖其 AddReferencedObjects 方法。

class FMyNormalClass : public FGCObject

void AddReferencedObjects(FReferenceCollector& Collector) override
{
  Collector.AddReferencedObject(SafeObject);
}

使用 FReferenceCollector 来手动添加对需要且不希望垃圾回收的 UObject 的硬引用。当该对象被删除且其析构函数运行时,该对象将自动清除其所添加的所有引用。

FName

用于存储反复出现的字符串,来节省内存和CPU时间。=FName使用空间来存储索引,而不是对每个引用FName的对象存储一个值。调用通过检测索引的匹配来确定字符串是否相同。

容器

TArray:类似vector
TMap:类似map,键值对用于查找,添加,删除
Tset:类似set
容器迭代器:Name.CreateIterator()

void RemoveDeadEnemies(TSet<AEnemy*>& EnemySet)
{
    // 从集开头处开始,迭代至集末尾
    for (auto EnemyIterator = EnemySet.CreateIterator(); EnemyIterator; ++EnemyIterator)
    {
        // *运算符获取当前元素
        AEnemy* Enemy = *EnemyIterator;
        if (Enemy.Health == 0)
        {
            //"RemoveCurrent"受TSet和TMap支持
            EnemyIterator.RemoveCurrent();
        }
    }
}

还可以利用for-each语法:来循环元素,对于TArray和TSet将返回元素值,TMap返回键值对