1. 前情提要
因工作需要,有在编辑器模式下执行Actor的Tick函数的需求。经过查阅资料,了解到重载Actor::ShouldTickIfViewportOnly函数可以实现在编辑器视口下也可以执行Tick函数。
已知Actor和ActorComponent都有自己的Tick函数,并且进入游戏并执行BeginPlay后才会开始Tick。
出于好奇心,产生了一系列的疑问:
- Actor和组件的Tick函数是由谁管理和统一调用的?
- 在执行BeginPlay后才会开始Tick,但是编辑器模式下默认并不会调用BeginPlay,那么为什么重载了ShouldTickIfViewportOnly()后照样开始了Tick?
以此衍生出新的问题- UE是如何控制Actor的Tick的开始的?
- 有哪些变量或者函数与之相关?
本文将从这些问题出发,简要地探究一下Actor的Tick机制。
2. Actor和ActorComponent Tick的实质
FTickFuntion结构体
众所周知,我们可以通过修改PrimaryActorTick.bCanEverTick
的方式来控制一个Actor是否会被Tick,以此为线索,我们很快就能找到相关的代码,也就是PrimaryActorTick
成员所属的结构体:FActorTickFunction
。
同样的,我们可以在Component中找到类似的结构体FActorComponentTickFunction
,这两个结构体都是继承了FTickFunction
结构体,区别在于FActorTickFunction
保存了指向Actor的指针,FActorComponentTickFunction
保存了指向Component的指针,除此以外也没有显著的区别了。
那么我们当前研究的重点就是FTickFunction
。
以上是这个结构体的主要结构。
关于这个类,网上已经有不少研究了,这里就直接使用知乎网友制作的图片,可以从文末的链接中找到原文。
其中有两个很重要的函数:
ExecuteTick()
该函数是一个虚函数,功能和名字一样,提供了一个统一执行Tick的功能。子类可以对其进行重写,在实际运行时,在注册的地方统一调用所有该类的ExecuteTick
函数,实现Tick的执行与Actor实现的解耦。
例如FActorTickFunction
的实现中,它会调用AActor::TickActor
,再调用Actor的Tick
函数。正如注释所言,它是Actrually execute the tick的,那么说明真正调用Tick函数的其实是Actor中的成员变量PrimaryActorTick
。
要想知道Tick是怎样注册并统一执行的,就得看看另一个主要函数。
RegisterTickFunction(class ULevel* Level)
这个函数很短,主要就是调用了FTickTaskManager::Get().AddTickFunction(Level, this);
,而这个函数则根据传入的Level,拿到Level的一个FTickTaskLevel
类型的成员变量,并调用该变量的AddTickFunction
函数,把FTickFunction
保存到FTickTaskLevel
的一个保存TickFunction的集合AllEnabledTickFunctions
中。
到这里为止,我们就能大致了解Actor的Tick到底是怎样被执行了。
在Actor刚被创建的时候,此时还没有任何一个地方会调用Tick函数,Actor处于静止的状态。
而后在游戏进程的某个阶段中,会将PrimaryActorTick
成员变量的FTickFunction::ExecuteTick
注册到Level中的一个变量里的集合中。例如,游戏运行的过程中,创建Actor会自动调用其BeginPlay
函数,在这个函数中就有注册Tick的操作。
通过这些操作,Level可以获取所有Actor的Tick函数,在World的Tick中,就可以通过遍历Level的方式获取所有已注册的Actor的Tick,并将其一起执行。
点到即止,关于FTickTaskLevel
的运行机制这里就不深究了,接下来我们看看FTickFuntion
有哪些重要参数需要我们注意:
- bCanEverTick:是否允许注册Tick。当这个值为False时,就不会将ExcuteTick函数注册,因此Tick函数将不再被执行。官方的注释还提到,这个值只应该在初始化的时候修改。
- bStartWithTickEnabled:是一个EditDefaultOnly的变量,在蓝图的名字叫做“启用Tick并开始"(UE5.1)。如果其值为false,不管有没有注册,那么Tick函数都不会被执行。这个值可以在运行的时候动态调整。
- TickInterval:设置Tick的间隔时间
- TickGroup:一个枚举变量,它指定该Tick在一次引擎Tick的什么时机执行
顺带一提,Actor
类有一个变量也值得注意:
- bAllowTickBeforeBeginPlay: ”允许开始播放前Tick“,哦我的上帝,看看这蹩脚的翻译。之前在编辑器中看到这个选项总是一头雾水,现在了解过源码后也知道了其含义:是否允许在调用BeginPlay函数前进行Tick。
网上总说Actor会在BeginPlay
函数调用后才会开始Tick,从之前的分析中我们也知道,在Beginplay中会对tick函数进行注册。那么在beginplay之前呢?也有地方调用注册函数吗?
文章的后半段,我会简要地从源码角度探究这个问题,并简要地了解Actor的初始化。
3. Actor的初始化(Tick注册相关)
这是生成(实例)Actor 时的路径。
- SpawnActor 被调用。
- PostSpawnInitialize
- PostActorCreated - 创建后即被生成的 Actor 调用,构建函数类行为在此发生。PostActorCreated 与 PostLoad 互斥。
- ExecuteConstruction:
- OnConstruction - Actor 的构建。蓝图 Actor 的组件在此处创建,蓝图变量在此处初始化
- PostActorConstruction:
- PreInitializeComponents - 在 Actor 的组件上调用 InitializeComponent 之前进行调用。
- InitializeComponent - Actor 上定义的每个组件的创建辅助函数。
- PostInitializeComponents - Actor 的组件初始化后调用。
- OnActorSpawned 在 UWorld 上播放。
- BeginPlay 被调用。
以上内容来自官方文档,这是三条路径的其中一条。
官方文档给出了三种不同的Actor生成方式,通过断点调试测试,发现大多数时候的Actor生成都会走上面的这条路线,包括SpawnActor生成、从文件浏览器拖入场景、PIE等。
可以看出,BeginPlay才是Actor初始化最后的一环,而前面还有很多初始化的环节。
AAcotr::PostSpawnInitialize
Actor初始化的大部分操作都在这个函数里进行,包括初始化Actor的位置、Actor的所有权、组件的初始化等等。上述路径的2-5步骤都在这个函数里进行。
然后在这个函数中的某一行,我们发现了RegisterAllComponents()
函数。很简洁明确的函数名,我们关注的Tick注册就在这个函数里。
RegisterAllComponents()
函数很简单,里面只有个AActor::IncrementalRegisterComponents
函数。
而终于在这个函数里,我们找到了注册Tick函数的入口
标签:调用,函数,BeginPlay,Actor,注册,UE5,Tick From: https://www.cnblogs.com/Qiu-Bai/p/17818008.html