# Actors

一个Actor是可被放置在关卡中的任意对象,它是支持3D变换(translation,rotation,scale)的普遍使用的类,它能通过C++或蓝图生成或销毁,在C++中AActor是所有Actors的基类。

有很多类型的Actor,比如StaticMeshActor,CameraActor,PlayerStartActor。注意actors并不直接存储变换(Location,Rotation,Scale)的数据,这些变换数据存储在Actor的根组件中。

# 生命周期

这部分在一个比较高的层面展示Actor的生命周期,Actor是如何被Spawned到关卡中的以及如何被摧毁的。下面的流程图展示了三个Actor被实例化的主要方法,不管Actor如何被创建,它们都会遵循同样的路径被摧毁。

ActorLifeCycle1

# 从磁盘加载

这种情况主要适用于已经在关卡中的任何Actor,在使用LoadMap时,或者AddToWorld在流送或者加载子关卡被调用时。

  1. 关卡或包中的Actor从磁盘上被加载。
  2. PostLoad:在Actor被加载完毕后,被Actor序列器调用,任何版本控制或修复的行为会在这发生,PostLoad只和PostActorCreated有关系。
  3. InitializeActorsForPlay。
  4. RouteActorInitialize,针对任何没有被初始化的Actors。
    1. PreInitializeComponents:会在Actor组件的InitializeComponent调用前调用。
    2. InitializeComponent:帮助创建Actor上每个组件的工具函数。
    3. PostInitializeComponents:在Actor所有组件被初始化后调用。
  5. BeginPlay:在关卡开始的时候被调用。

# PlayInEditor

大部分都同从磁盘加载一样,但是Actors从来没有被从磁盘加载,他们都是从编辑器复制的。

  1. 编辑器中的Actor被复制到一个新World中。
  2. PostDuplicate被调用。
  3. InitializeActorsForPlay。
  4. RouteActorInitialize,针对任何没有被初始化的Actors。
    1. PreInitializeComponents:会在Actor组件的InitializeComponent调用前调用。
    2. InitializeComponent:帮助创建Actor上每个组件的工具函数。
    3. PostInitializeComponents:在Actor所有组件被初始化后调用。
  5. BeginPlay:在关卡开始的时候被调用。

# Spawning

这个场景适用于实例化一个Actor。

  1. SpawnActor调用。
  2. PostSpawnInitialize。
  3. PostActorCreated:在Actor生成后调用,类似于构造器,它只和PostLoad有关系。
  4. ExecuteConstruction:
    1. OnConstruction:Actor的构造器,在这个阶段蓝图Actors会创建它们的组件,蓝图变量被初始化。
  5. PostActorConstruction:
    1. PreInitializeComponents:会在Actor组件的InitializeComponent调用前调用。
    2. InitializeComponent:帮助创建Actor上每个组件的工具函数。
    3. PostInitializeComponents:在Actor所有组件被初始化后调用。
  6. OnActorSpawned在UWorld上广播。
  7. BeginPlay被调用。

# DeferredSpawn

一个Actor可以通过设置"Expose on Spawn"来实现推迟生成。

  1. SpawnActorDeferred:打算生成程序化的Actor,允许在蓝图构造脚本之前有额外的设置。
  2. SpawnActor中的所有东西都会出现,但是在PostActorCreated之后会出现如下事情:
    1. 使用一个有效但是并不完整的Actor实例来调用各种初始化函数。
    2. FinishSpawningActor:通过调用它来完成一个完整的Actor,接下来会调用ExecuteConstruction。

# 生命周期结束

会有很多方法来销毁Actors,但所有的实现最后都是一样的原理。

# 游戏运行时

因为很多Actors不会在运行时被销毁,可能只是被禁用或者不被显示,所以下面这些方法是可选的。

  • Destroy - 在Actor在游戏中被打算销毁的时候调用,但是它在gameplay中还存在,这个Actor被标记为待销毁,后面会从记录Level中的Actors的数组中移除。

  • EndPlay - 它会被用在几个地方中来保证Actor的生命周期会结束,在游戏运行中,Destroy会触发这个,切换关卡时,一个流送关卡被卸载时,下面是EndPlay被调用的地方:

    • 显式调用Destroy。
    • 在编辑器中运行结束时。
    • 关卡切换(无缝加载或者加载地图)。
    • 包含Actor的流送关卡被卸载时。
    • Actor的生命周期到期了。
    • 应用被关闭,所有的Actors被摧毁。

    不管是哪种情况,Actor会被标记为RF_PendingKill,并在下一次垃圾回收的时候回收,可以考虑使用FWeakObjectPtr而不是手动检测是否已被销毁。

  • OnDestroy - 这是使用Destroy的一种方法,现在被遗弃了,你应该把这部分逻辑移动到EndPlay,它后面会被关卡切换和其他游戏清理机制调用。

# GarbageCollection

在一个对象被标记为要销毁的一段时间后,垃圾回收将该对象移除内存,释放它占用的资源。

下面是在对象销毁时被调用的函数:

  1. BeginDestroy - 这时候开始释放内存,并处理其他多线程资源(比如图像线程代理对象),大部分同销毁有关的gameplay函数都应在较早的时候已被处理,比如EndPlay。
  2. IsReadyForFinishDestroy - 垃圾回收进程调用这个函数来判定这个对象是否准备好被永久回收了。通过返回False,这个函数可以延期到下一次垃圾回收时再被销毁。
  3. FinishDestroy - 最后,这个对象将要被销毁,这时会释放内部数据结构,这时内存释放前最后一次被调用。

# 高级垃圾回收

UE4中的垃圾回收会将要销毁的对象打包到一块,这样相对比销毁单个对象来说,能减少使用的总时间和内存,当一个对象加载时,它会创建子对象,通过把它和它的子对象打包到一块,引擎可以延迟释放对象使用的资源直到该对象准备好被销毁的时候,然后一下全部释放它们用的资源。

对于大部分项目来说,垃圾回收不需要被配置或改变,但是还有某些具体场景,可以通过改变打包行为来提高效率:

  1. Clustering(打包) - 需要把打包关闭,在Project Settings->Garbage Collection->Create Garbage Collector UObject Clusters设为False,对于大部分项目,这会降低垃圾回收系统的效率,建议只在性能优化有明显作用的场景中使用。
  2. Cluster Merging(打包合并) - 如果打包被设置为True,Project Settings->Garbage Collection->Merge GC Clusters可以被设置为True来激活打包合并,这个行为默认是被关闭的,它并不适用于每个项目,在打包对象的过程中,对象会被检查,在其中可能找到其他对象的引用。如果没有打包合并(默认是这样的),这些引用会被记录,但是被加载的对象以及它的子对象,仍然留在它最原始的包中。如果使用打包合并,对象的包会被加载,被引用的对象会被合并。比如,一个粒子系统资产可能引用一个材质资产,如果关闭打包合并,在垃圾回收时,材质和粒子系统会留在各自的包中,如果打开打包合并,粒子资产包会同材质包合并,因为粒子系统引用了材质。这个行为不能在游戏流送内容的时候使用,比如开放世界,如果很多包合并会形成一个很大的包,会包含各种对象。因为包中的对象不会被单独地销毁,如果等到包中的每个对象被销毁,这可能会导致内存中有很多对象而实际正在使用到的很少,但是,激活打包合并后然后在代码中手动添加包,比如很多使用的对象引用了很多资产,这些资产并没有被其他对象共享使用,在这种场景中就可以通过把这些对象统一合并起来来提高性能,因为不用对每个对象再单独维护。

# 创建Actors

创建AActor类的实例被称为spawning,可通过泛型函数SpawnActor()或者它的某个模板来实现。

# SpawnActor方法

UWorld::SpawnActor()方法只能用来实例化继承自Actor的类。

AActor* UWorld::SpawnActor
(
    UClass*         Class,
    FName           InName,
    FVector const*  Location,
    FRotator const* Rotation,
    AActor*         Template,
    bool            bNoCollisionFail,
    bool            bRemoteOwned,
    AActor*         Owner,
    APawn*          Instigator,
    bool            bNoFail,
    ULevel*         OverrideLevel,
    bool            bDeferConstruction
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

下面是对这些参数给出的解释:

参数名 描述
Class 指定要实例化的哪个Actor类。
InName 给实例化的对象指定Name,如果没有指定值,会以[Class]_[Number]格式生成。
Location 提供Actor生成的初始Location的FVector。
Rotation 提供Actor生成的初始Rotation的FRotator。
Template 生成新Actor的模板类,会使用模板类的默认值,如果没有指定,会使用class default object(CDO)。
bNoCollisionFail 在生成Actor的时候决定是否进行碰撞检测,如果设置为True,在生成Actor的时候就不管模板Actor根组件的碰撞设置怎样,都不会进行碰撞检测。
bRemoteOwned actor是否被远程拥有,如果一个actor在客户端被创建需要在服务器上复制的时候就应该设置为True。
Owner 拥有生成Actor的Actor。
Instigator 负责处理生成Actor的伤害处理的APawn。
bNoFail 当某个条件不满足的时候是否还要生成Actor,如果设置为true,生成不会失败因为被生成类bStatic设置为true或者模板类同生成的类不一致。
OverrideLevel 生成Actor的ULevel,比如Actor的外部,如果没有指定,会使用Owner的Outer。如果没有指定Owner,就使用persistent level。
bDeferConstruction 决定构造脚本是否运行,如果设置为true,生成的Actor不会运行构造脚本,只有模板类是蓝图的时候这个选项才可拥。

# 用法

AKAsset* SpawnedActor1 = (AKAsset*) GetWorld()->SpawnActor(AKAsset::StaticClass(), NAME_None, &Location);
1

# Spawn模板

为了让生成Actor更友好,这里提供几个函数模板来应对常见的场景。这让创建Actors更简单因为它们需要更少的参数,也能指定返回的Actor类型。

//第一种 Spawn T Instance, Return T Pointer
/** Spawns and returns class T, respects default rotation and translation of root component. */
template< class T >
T* SpawnActor
(
    AActor* Owner=NULL,
    APawn* Instigator=NULL,
    bool bNoCollisionFail=false
)
{
    return (T*)(GetWorld()->SpawnActor(T::StaticClass(), NAME_None, NULL, NULL, NULL, bNoCollisionFail, false, Owner, Instigator));
}

//使用
MyHUD = SpawnActor<AHUD>(this, Instigator);

//第二种 Spawn T Instance with Transform, Return T Pointer
/** Spawns and returns class T, forcibly sets world position. */
template< class T >
T* SpawnActor
(
    FVector const& Location,
    FRotator const& Rotation,
    AActor* Owner=NULL,
    APawn* Instigator=NULL,
    bool bNoCollisionFail=false
)
{
    return (T*)(GetWorld()->SpawnActor(T::StaticClass(), NAME_None, &Location, &Rotation, NULL, bNoCollisionFail, false, Owner, Instigator));
}

//使用
Controller = SpawnActor<AController>(GetLocation(), GetRotation(), NULL, Instigator, true);

//第三种 Spawn Class Instance, Return T Pointer
/** Spawns given class and returns class T pointer, respects default rotation and translation of root component. */
template< class T >
T* SpawnActor
(
    UClass* Class,
    AActor* Owner=NULL,
    APawn* Instigator=NULL,
    bool bNoCollisionFail=false
)
{
    return (Class != NULL) ? Cast<T>(GetWorld()->SpawnActor(Class, NAME_None, NULL, NULL, NULL, bNoCollisionFail, false, Owner, Instigator)) : NULL;
}

//使用
MyHUD = SpawnActor<AHUD>(NewHUDClass, this, Instigator);

//第四种 Spawn Class Instance with Transform, Return T Pointer
/** Spawns given class and returns class T pointer, forcibly sets world position. */
template< class T >
T* SpawnActor
(
    UClass* Class,
    FVector const& Location,
    FRotator const& Rotation,
    AActor* Owner=NULL,
    APawn* Instigator=NULL,
    bool bNoCollisionFail=false
)
{
    return (Class != NULL) ? Cast<T>(GetWorld()->SpawnActor(Class, NAME_None, &Location, &Rotation, NULL, bNoCollisionFail, false, Owner, Instigator)) : NULL;
}

APawn* ResultPawn = SpawnActor<APawn>(DefaultPawnClass, StartLocation, StartRotation, NULL, Instigator);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# Components

组件是一种特殊的Object类型,Actor能把它依附到自己身上成为子对象,组件可用来复用共同行为逻辑,这是和项目相关的概念,Actors在某种意义上可以被看成是拥有组件对象的容器,不同类型的组件可实现不同的功能,比如可用来控制Actor如何移动,如何被渲染等,Actors的其他函数主要用来通过网络对属性和函数的复制。组件主要是和包含它们的Actor相关联的。

一些关键的组件如下:

  • UActorComponent 它是所有组件的基类,因为组件是唯一渲染网格,图片,实现碰撞,播放音频等,所有玩家能看到和交互的东西基本都是用组件。它基本负责抽象行为,比如移动,库存,属性管理以及其他非物理相关的概念,它并没有transform,这意味着这个组件在游戏世界中没有任何物理的location或rotation。
  • USceneComponent(UActorComponent子类) 该组件处理变换相关的,也就是在世界中的位置,由location,rotation和scale定义,它可被依附到某个层次结构中,一个Actor的location,rotation,scale都是取决于根节点上的SceneComponent组件里的相关值。主要负责游戏中定位相关且不需要图形展示的相关行为,比如spring arms,cameras等。
  • UPrimitiveComponent(USceneComponent子类) 它是SceneComponent的某种图形展示(可能是mesh,也可能是粒子系统),一般用来显示图形,渲染可视化元素,比如Static Meshes,Skeletal Meshes,sprites或多边形体积等,很多physics和collision设置都在这。

下面来看一个Actor以及它的组件的层次结构:

GoldActorHierarchy

# 注册组件

为了让Actor组件更新并影响场景,引擎必须注册它们,这在Actor的生成过程中,这对于Actor上的组件来说是自动完成的,但是在游戏过程中的组件的创建需要手动注册。RegisterComponent函数提供了相关功能,这需要组件和一个Actor联系起来。注意在运行中注册一个组件可能会影响性能,所以你应该只一些必须的场景中做这些。

# 注册事件

函数名 描述
OnRegister 添加代码以注册组件
CreateRenderState 初始化组件的渲染状态
OnCreatePhysicsState 初始化组件的物理状态

# 取消注册组件

要从更新,模拟,渲染的进程中移除Actor组件,可以使用UnregisterComponent函数。

# 取消注册事件函数

函数名 描述
OnUnregister 取消注册组件
DestroyRenderState 取消组件的初始化渲染状态
OnDestroyPhysicsState 取消组件的初始化物理状态

# Updating

Actor组件同Actor一样有能力在每一帧进行更新,TickComponent函数让组件在每一帧运行代码,比如USkeletalMeshComponent使用TickComponent来更新动画和骨骼控制器,UParticleSystemComponent则更新它的发射器以及处理粒子事件。

默认,Actor组件并不会更新,为了让Actor组件在每帧更新,你需要在构造函数把PrimaryComponentTick.bCanEverTick设置为True,或者在构造函数或者其他地方可以调用PrimaryComponentTick.SetTickFunctionEnable()来打开关闭更新,如果你知道你的组件从不需要更新,如果你打算手动调用自己定义的函数,可以把PrimaryComponentTick.bCanEverTick设置为false。

# RenderState

为了渲染,Actor组件需要创建一个渲染状态,这个渲染状态告诉引擎组件哪部分更新了,然后把这部分渲染的数据更新好,当发生变化时,渲染状态被标记为"dirty"。如果你构建自己的组件,你可以使用MarkRenderStateDirty函数来吧渲染数据标记为"dirty"。在帧结束的时候,所有被标记为dirty的组件会让它们的渲染数据在引擎中更新,SceneComponent(包括PrimitiveComponents)会默认创建渲染状态,但是ActorComponent却并不是。

# PhysicsState

为了和引擎的物理模拟系统交互,Actor组件需要一个物理状态,它会在发生变化的时候立马更新状态,防止出现"帧后"更新的效果,然后移除"dirty"标记。默认Actor组件和Scene组件都没有物理状态,但是Primitive组件有,可以重写ShouldCreatePhysicsState函数来决定你的组件类是否需要物理状态。

如果你的类需要物理模拟,不建议只是返回true,参照UPrimitiveComponent版本的函数你可以知道在什么情况下不该创建物理状态,比如在组件的销毁时,在一些只返回true的情况下你可以返回Super::ShouldCreatePhysicsState。

# VisualizationComponents

一些Actor和组件没有视觉展示,这让它们很难被选择,或者有一些重要属性不能被可视化。在编辑器中工作时,开发者可以添加额外的组件来显示信息,但是在Play In Editor中或者打包的时候是不需要这些组件的,为了满足这点,编辑器支持虚拟可视化组件的概念,它只在编辑器中工作。

为了创建一个虚拟可视化组件,先创建一个常规的组件,然后调用它的SetIsVisualizationComponent,因为组件并不存在于编辑器的外面,所有对这些组件的引用都是在预处理器中检查是否是WITH_EDITORONLY_DATA 或 WITH_EDITOR,这会确保在打包的时候不会被这些组件影响,并且保证不会在代码中引用这些组件。比如摄像机组件使用了几个其他组件来在编辑器中展示有用信息,包括使用Draw Frustum Component来展示它的视锥,在头文件中,Draw Frustum Component都会在类中像下面这样定义:

#if WITH_EDITORONLY_DATA
    // The frustum component used to show visually where the camera field of view is
    class UDrawFrustumComponent* DrawFrustum;
    // ...
#endif
1
2
3
4
5

同上面类似的,在源文件中所有对这个组件的引用都需要在预处理器中检查WITH_EDITORONLY_DATA,下面这段代码,在OnRegister中检查WITH_EDITORONLY_DATA,用来看这个摄像机组件是否依附在一个有效的Actor上,然后再添加Draw Frustum Component代码。

void UCameraComponent::OnRegister()
{
#if WITH_EDITORONLY_DATA
    if (AActor* MyOwner = GetOwner())
    {
        // ...
        if (DrawFrustum == nullptr)
        {
            DrawFrustum = NewObject<UDrawFrustumComponent>(MyOwner, NAME_None, RF_Transactional | RF_TextExportTransient);
            DrawFrustum->SetupAttachment(this);
            DrawFrustum->SetIsVisualizationComponent(true);
            // ...
        }
    }
    // ...
#endif
    Super::OnRegister();
    // ... Additional code (to run in all builds) goes here ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

现在DrawFrustum只存在于编辑器中,被认为是一个虚拟可视化组件,这意味着它不会在编辑器运行的时候出现。

# SceneComponents

该组件负责Actor组件存在于世界中的具体物理位置,这个位置是通过transform(FTransform)定义的,它包括location,rotation以及scale。SceneComponent支持树状层次结构,它能依附到另一个SceneComponent上,Actor可以指定一个SceneComponent为根,意味着Actor世界的location,rotation和scale都从该组件上获取。

Actors支持有层次结构的SceneComponent,每个Actor都有一个RootComponent属性,用来指示哪个组件是Actor的根,Actors本身并没有位置变换信息,因此没有locations,rotations或者scales的参数。它们依赖于组件的位置变换,更具体的说是它们的根组件,如果这个组件是SceneComponent,它会提供Actor的位置变换信息,否则Actor将不会有变换信息,其他拥有位置变换的组件是相对于它的父组件来说的。

# Attachment

只有SceneComponent(USceneComponent以及它的子类)能附着到另一个SceneComponent上,由于需要transform来描述子组件和父组件的空间位置关系,尽管一个SceneComponent可以有任意多的子节点,但它只能有一个父节点,或者直接被放到游戏世界中,Scene Component系统并不支持循环依附,这块主要用到的方法有两个,分别是SetupAttachment和AttachToComponent,第一个用来在构造函数处理还没有注册的组件,第二个是在运行时将一个SceneComponent立即依附到另一个上。这依附系统也可以通过Actor的root组件依附到其他Actor的根组件上从而让该Actor依附到其他Actor上。

# PrimitiveComponents

Primitive组件(UPrimitiveComponent类)是能包含或生成一些几何体的SceneComponent,一般用来渲染或处理碰撞。这有几种类型的几何体,一般是Box Component, Capsule Component, Static Mesh Component, 和Skeletal Mesh Component。Box组件和Capsule组件一般生成用于碰撞检测不可见的几何体,Static Mesh Component和Skeletal Mesh Component包含要渲染的预构建几何体,在需要的时候可用于碰撞检测。

# SceneProxy

Primitive组件的SceneProxy(FPrimitiveSceneProxy类)压缩了引擎要用来渲染游戏线程中多个平行组件的场景数据,每种类型的primitive有它自己的Scene Proxy子类来存储它所需要的具体渲染数据。

# Ticking

Ticking意味着在一个固定时间间隔里运行Actor或组件上的一段代码或蓝图脚本,一般每一帧运行一次。理解游戏中Actor或组件上的Tick相对运行顺序以及引擎每帧执行的任务有助于避免掉帧的问题,也能保证游戏运行的一致性。Actor和组件可被设置为在每帧Tick,也可以设置为在一个最小固定时间间隔Tick,或者完全不Tick。另外,它们能被分组到引擎每帧更新的不同的阶段,能让其在开始的时候单独地等待某个Tick完成。

它关系到Actor在UE4中如何更新,所有的Actors都能在每一帧中Tick,或者是用户自定义的最小时间间隔,它允许你在必要的时候执行实时更新的计算或行为,所有的Actors默认都会执行Tick()函数。ActorComponent默认都能在每一帧更新,尽管它们也可以使用TickComponent做到。

# TickGroup

如果不指定最小时间间隔,那么Actor和Component会在每一帧中Tick。Ticking会根据Tick分组来进行,这可以在代码或蓝图中指定。一个Actor或组件的tick分组常被用来决定它应该在一帧的什么时候Tick,相对于引擎中每一帧处理的其他事(主要物理模拟)。每个Tick分组都会在下次Tick组开始之前完成它里面所有Actor和Component的Tick。除了Tick分组外,Actor和组件可以设置Tick依赖顺序,这意味着某些Tick可以被设置为等到某些Actor或组件的Tick函数完成后才进行。使用Tick分组和Tick依赖可能不利于这几种情况,注重实现基于物理的方面的行为,包含连续多个Actor或组件的游戏行为。

# TickGroupOrder

下面说明了gameplay可用的Tick分组,它们在一帧中运行的顺序,以及具体的内容:

Tick分组 引擎行为
TG_PrePhysics 在一帧开始的时候
TG_DuringPhysics 在这一步物理模拟开始,在这步进行时或所有成员完成Tick后,模拟会完成或更新引擎的物理数据。
TG_PostPhysics 物理模拟完成,在这帧开始时候,引擎会使用当前帧的数据。
n/a 处理隐藏行为,tick世界时间管理器,摄像机更新,更新关卡流送体积与流送操作。
TG_PostUpdateWork n/a
n/a 处理在帧中早期创建延迟生成的Actors,这一帧结束完成渲染。

# TG_PrePhysics

  • 这个阶段主要完成同物理对象交互,包括基于物理的依附,这时actor的移动完成,主要考虑物理模拟相关的东西。
  • 在这次Tick的物理模拟数据会持续一帧,比如被渲到屏幕的最后一帧数据。

# TG_DuringPhysics

  • 因为和物理模拟同时运行,并不知道这次Tick的物理数据是来自上一帧还是当前帧。物理模拟可在任何时候完成,而且不会有任何提示完成的信息。
  • 因为物理模拟数据可能是当前帧也可能是上一帧,在这个Tick分组中建议写那些不关心物理数据且能一帧完成的逻辑,常见的应用场景可能有更新仓库画面与小地图显示,它们的物理数据是完全不相关的,即便是有一点延迟也没关系。

# TG_PostPhysics

  • 到这个分组帧的物理模拟就完成了。
  • 这个分组一个好的应用场景可能是武器和移动的追踪,所有的物理对象都知道它们的最终位置,因为它们即将被渲染。比较常见的应用场景是设计游戏中的激光瞄准器,激光发射应该出现在玩家的枪的最后位置,一帧的延迟都很容易看出来。

# TG_PostUpdateWork

  • 这个分组是在TG_PostPhysics之后运行,这个函数可以将最后时刻的信息送入粒子系统。
  • 这个分组在摄像机更新之后运行,如果你有一些效果需要知道摄像机指向哪个地方,在这个地方能很好地让actors来控制这些效果。
  • 这个用于帧中最靠后运行的逻辑,比如在一个格斗游戏的一帧中两个角色尝试抓住对方。

# Tick依赖

AddTickPrerequisiteActor和AddTickPrerequisiteComponent这两个函数,都可存在于actors和组件中,用来设置让目标的Tick函数直到其他Actor和组件的Tick函数运行完之后再开始运行。这在一帧中运行几个有前后关系的Tick时比较有用,之所以需要Tick依赖的原因是如果很多Actor在同样的Tick组中,它们会同时更新,但是如果一些目标Actors只依赖于一个或两个Actors,那么完全没有必要把一组Actors移动到一个全新组中,让目标Actors等待这个组中的所有Actors更新完之后再更新。

# 例子

这块主要讲的是Tick组,Tick依赖的相关例子。说一个关于如何使用Tick分组的相关例子,在一个游戏中玩家控制一个Actor,它可使用一个特殊瞄准线来进行激光瞄准,只要激光瞄准目标,就会显示距离多少米,使用HUD来在屏幕上显示这个米数。

玩家控制的Actor会在TG_PrePhysics时进行移动和动画,它需要在物理模拟之前完成动画以让它能和有物理特性的对象正确地交互。

HUD可在任何Tick组中更新,但TG_DuringPhysics是一个很好的选择,有两个原因,首先,TG_DuringPhysics是可接受的因为它并不直接同游戏的物理模拟交互,或使用物理交互的数据。其次,没有理由去让物理模拟等待HUD完成更新,或者让HUD来等待物理模拟完成。注意HUD一般都是在后一帧中,比如这帧指向的目标在下一帧中才会被显示出来。

瞄准线Actor应该在TG_PostPhysics更新,因为它在帧的最后渲染出来,所以瞄准线知道它追踪的对象,然后在目标对象上显示距离多少米,并基于目标对象的位置来更新这个米数。

最后在TG_PostUpdateWork中,激光粒子特效会在瞄准对象和瞄准线的位置来更新。

Tick依赖需要根据TG_PostUpdateWork的需求来评估使用,激光粒子可和瞄准线一块放在TG_PostPhysics中,使用Tick依赖来确保激光是随着瞄准线的位置已更新完后再进行更新的,这需要设置激光的Tick依赖于瞄准线的Tick,我们要确保激光不会更新的太早,但是也不用等和它不相关的post-physics Tick。这比把激光移到一个不同的Tick组更有效。

举个不能从Tick依赖受益的例子,瞄准线它自己是不需要依赖于瞄准目标的,即便它需要瞄准目标在开始自己Tick之前完成Tick。在这里不要Tick依赖的原因是在瞄准对象处在pre-physics阶段时瞄准线是处在post-physics中的,因为它们在不一样的Tick分组中,我们就认为它们已按着自己所在分组所在的顺序执行逻辑,因为每个Tick分组会在下一个Tick分组开始之前完成它里面所有Actor和组件的Tick。

# 生成Actor

在BeginPlay时,Actor会在引擎中注册它所有的Tick函数,以及组件的Tick函数。一个Actor的Tick函数可通过成员PrimaryActorTick被设置为某个Tick分组,或者完全禁用。这一般是在构造函数中完成的,在BeginPlay被调用之前确保所有数据正确设置。看下面的例子:

PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bTickEvenWhenPaused = true;
PrimaryActorTick.TickGroup = TG_PrePhysics;
1
2
3

# 组件Tick

正如Actors会被分隔成几个Tick组,所以Component也是这样,在之前,Actor会在Actor的Tick中运行完它所有组件的Tick,现在还是这样,但是需要不同Tick分组中的组件被添加到一个统一管理的列表,组件需要同Actor一样被分配Tick分组,虽然变量命名有点差异,但是以相同的方式工作:

PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bTickEvenWhenPaused = true;
PrimaryComponentTick.TickGroup = TG_PrePhysics;
1
2
3

注意PrimaryActorTick使用Actor的Tick()函数,而PrimaryComponentTick使用ActorComponent的TickComponent()函数。

# 高级Tick函数

Actor或组件的默认Tick函数可通过AActor::SetActorTickEnabled和UActorComponent::SetComponentTickEnabled来激活或禁用。注意,一个Actor或组件可以有多个Tick函数,这可通过创建一个继承自FTickFunction的结构体,然后重写ExecuteTick和DiagnosticMessage函数来实现,默认Actor和组件的Tick函数结构体可作为一个很好的例子来仿写,你可以在EngineBaseTypes.h中找到名为FActorTickFunction和FComponentTickFunction的函数。

一旦你添加自己的Tick函数结构体到Actor或组件中,它可以被初始化,这通常是在所属类的构造体中完成,为了激活和注册Tick函数,最常见的路线是重写AActor::RegisterActorTickFunctions然后添加Tick函数结构体的SetTickFunctionEnable,然后添加RegisterTickFunction(需要传入所属Actor所在的关卡)。最后你创建的任何Actor或组件可以Tick多次,包括在不同的分组以及单独的依赖中Tick。为了手动设置一个Tick依赖,调用Tick函数结构体的AddPrerequisite然后传入你想依赖的Tick函数的结构体。

# Replication

这是用来多人网络游戏中保证Actor同步的,属性和函数调用都可以被复制,允许在所有客户端完全控制游戏状态。

# 销毁Actor

Actors一般不会被垃圾回收,World对象会保存Actor引用的列表,Actor可通过调用Destroy()被显式摧毁,这会把它们从场景中移除,并标记为"待销毁",这意味着它们会在下一次垃圾回收的时候被清除。