UE5: 探究Actor Tick的注册与执行
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函数的入口
bool AActor::IncrementalRegisterComponents(int32 NumComponentsToRegister, FRegisterComponentContext* Context)
{
...不关心
// If we are not a game world, then register tick functions now. If we are a game world we wait until right before BeginPlay(),
// so as to not actually tick until BeginPlay() executes (which could otherwise happen in network games).
if (bAllowTickBeforeBeginPlay || !World->IsGameWorld())
{
RegisterAllActorTickFunctions(true, false); // components will be handled when they are registered
}
...不关心
}
从if语句可以看出,当bAllowTickBeforeBeginPlay为true时或者当前World不是GameWorld时,会执行RegisterAllActorTickFunctions函数。这就与前面bAllowTickBeforeBeginPlay的介绍对应上了,解释了为什么该bool选项可以允许Actor在beginplay前进行Tick。
除此以外,如果当前World不是GameWorld,也会对Tick函数进行注册。众所周知,UE包含有多个world,除了GameWorld(worldType == Game)以外,还有EditorWorld、EditorPreviewWorld等等。也就是说,在gameworld之外,UE会帮我们注册好Actor的所有Tick函数。
RegisterAllActorTickFunctions(bool bRegister, bool bDoComponents)
官方注释是这么写的:当被调用时,将调用虚函数调用链来注册Actor和可选的所有组件的所有tick函数。
查看源码,发现调用了两个主要函数RegisterActorTickFunctions和RegisterAllComponentTickFunctions
void AActor::RegisterAllActorTickFunctions(bool bRegister, bool bDoComponents)
{
if(!IsTemplate())
{
// Prevent repeated redundant attempts
if (bTickFunctionsRegistered != bRegister)
{
...不关心
RegisterActorTickFunctions(bRegister);
bTickFunctionsRegistered = bRegister;
...不关心
}
if (bDoComponents)
{
for (UActorComponent* Component : GetComponents())
{
if (Component)
{
Component->RegisterAllComponentTickFunctions(bRegister);
}
}
}
...不关心
}
..不关心
}
点进去发现,这两个函数实际上就是调用了FTickFuntion的RegisterTickFunction函数。同样的,在if语句中我们也能看到bCanEverTick的作用,如果这个值为false,无论如何都不会有机会调用Tick函数了,因为注册在这一步就卡住了。
void AActor::RegisterActorTickFunctions(bool bRegister)
{
check(!IsTemplate());
if(bRegister)
{
if(PrimaryActorTick.bCanEverTick)
{
PrimaryActorTick.Target = this;
PrimaryActorTick.SetTickFunctionEnable(PrimaryActorTick.bStartWithTickEnabled || PrimaryActorTick.IsTickFunctionEnabled());
PrimaryActorTick.RegisterTickFunction(GetLevel());
}
}
..不关心
}
BeginPlay
前面探讨了BeginPlay之前是如何注册Tick函数的,那么BeginPlay是否也有类似的逻辑呢?
还真有
void AActor::BeginPlay()
{
... 无所谓
SetLifeSpan( InitialLifeSpan );
RegisterAllActorTickFunctions(true, false); // Components are done below.
TInlineComponentArray<UActorComponent*> Components;
GetComponents(Components);
for (UActorComponent* Component : Components)
{
// bHasBegunPlay will be true for the component if the component was renamed and moved to a new outer during initialization
if (Component->IsRegistered() && !Component->HasBegunPlay())
{
Component->RegisterAllComponentTickFunctions(true);
Component->BeginPlay();
ensureMsgf(Component->HasBegunPlay(), TEXT("Failed to route BeginPlay (%s)"), *Component->GetFullName());
}
...不想看
}
...不关心
}
可以看到,beginplay几乎没有进行什么条件判断,果断地对Actor和所有component进行了Tick函数地注册。也就是说,在调用BeginPlay后,Actor及其拥有的所有组件都会注册Tick函数。至于注册tick函数后是否会被执行,那么就是其他bool变量控制的了(如bStartWithTickEnabled变量)。
4. ShouldTickIfViewportOnly的原理
最后解决文章开头的问题,ShouldTickIfViewportOnly是如何在编辑器视口中发挥作用的?
最终通过断点调试找到了它发挥作用的地方。
总之,在World进行Tick的时候,会尝试执行所有已注册的Tick函数,如果ShouldTickIfViewportsOnly返回为true的话,就相当于给tick函数开了一个万能通行证,无论如何该Actor的组件都会被允许tick。
ExecuteTickHelper(UActorComponent* Target, bool bTickInEditor, float DeltaTime, ELevelTick TickType, const ExecuteTickLambda& ExecuteTickFunc)
{
if (Target && IsValidChecked(Target) && !Target->IsUnreachable())
{
... 不关心
if (Target->bRegistered)
{
AActor* MyOwner = Target->GetOwner();
//@optimization, I imagine this is all unnecessary in a shipping game with no editor
if (TickType != LEVELTICK_ViewportsOnly ||
(bTickInEditor && TickType == LEVELTICK_ViewportsOnly) ||
(MyOwner && MyOwner->ShouldTickIfViewportsOnly())
)
{
const float TimeDilation = (MyOwner ? MyOwner->CustomTimeDilation : 1.f);
ExecuteTickFunc(DeltaTime * TimeDilation);
}
}
}
}
参考
UE4中的Tick机制浅析 - 知乎 (zhihu.com)
UE4中的三种Tick方式 - 知乎 (zhihu.com)
[Actor 生命周期 | 虚幻引擎文档 (unrealengine.com)](https://docs.unrealengine.com/4.26/zh-CN/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Actors/ActorLifecycle/
UE5: 探究Actor Tick的注册与执行的更多相关文章
- 源码简析XXL-JOB的注册和执行过程
一,前言 XXL-JOB是一个优秀的国产开源分布式任务调度平台,他有着自己的一套调度注册中心,提供了丰富的调度和阻塞策略等,这些都是可视化的操作,使用起来十分方便. 由于是国产的,所以上手还是比较快的 ...
- xmanagr 注册机执行ubuntu 桌面程序,ubuntu无需安装 桌面环境
Xshell 5 注册码: 690313-111999-999313Xftp 5 注册码:101210-450789-147200 Xmanager 5 注册码:101210-450789-14720 ...
- WebShell代码分析溯源(十一)
WebShell代码分析溯源(十一) 一.一句话变形马样本 <?php $e = $_REQUEST['e'];declare(ticks=1);register_tick_function ( ...
- 剖析虚幻渲染体系(06)- UE5特辑Part 1(特性和Nanite)
目录 6.1 本篇概述 6.1.1 本篇内容 6.1.2 基础概念 6.2 UE5新特性 6.2.1 UE5编辑器 6.2.1.1 下载编辑器及资源 6.2.1.2 启动示例工程 6.2.1.3 编辑 ...
- Scala学习笔记--Actor和并发
感谢博主lyrebing 博文地址:http://blog.csdn.net/lyrebing/article/details/20446061 1. Actor用法 1.1 Actor的基本使用 ...
- 追源索骥:透过源码看懂Flink核心框架的执行流程
li,ol.inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-bottom:20px}dt, ...
- 一个用于实现并行执行的 Java actor 库
即使 Java 6 和 Java 7 中引入并发性更新,Java 语言仍然无法让并行编程变得特别容易.Java 线程.synchronized 代码块.wait/notify 和java.util.c ...
- 透过源码看懂Flink核心框架的执行流程
前言 Flink是大数据处理领域最近很火的一个开源的分布式.高性能的流式处理框架,其对数据的处理可以达到毫秒级别.本文以一个来自官网的WordCount例子为引,全面阐述flink的核心架构及执行流程 ...
- Spring笔记(8) - @EventListener注解探究
在上文中讲了Spring的事件监听机制,流程是:定义事件.监听器,发布事件,控制台输出监听到的事件内容. 在上文的扩展中 使用 @EventListener 注解来自定义监听器,监听指定的事件,比如下 ...
- Dapr + .NET Core实战(五)Actor
什么是Actor模式 Actors 为最低级别的"计算单元" 以上解释来自官方文档,看起来"晦涩难懂".大白话就是说Actors模式是一段需要单线程执行的代码块 ...
随机推荐
- matlab关于阶梯图和图窗操作
1阶梯信号绘制 Matlab 中绘制阶梯图函数:stairs x = [30 33 37 40 37 33 30 27 23 20 23 27 30 30]'; StepNum = length(x) ...
- 用MMCls训练手势模型
import os import json import mmcv import time from mmcv import Config from mmdet.apis import inferen ...
- 使用Github Action实现构建、发布到 nuget.org
使用Github Action实现构建.发布到 nuget.org GitHub Actions是GitHub提供的持续集成和持续部署(CI/CD)工具,它能够自动化构建.测试和部署你的项目.在这篇教 ...
- OpenWrt主题在菜单中不显示
问题: 路径中有对应的主题,但是make menuconfig中不显示 原因: 需要建立软连接 1. 在路径 SDK-DR232-20221220/package/feeds/luci 中运行 ls ...
- Mysql [Show global status] 命令 参数详解(转)
Aborted_clients:由于客户端没有正确关闭连接导致客户端终止而中断的连接数. Aborted_connects:试图连接到MySQL服务器而失败的连接数. Binlog_cache_dis ...
- NEFU OJ Problem1356 帽儿山奇怪的棋盘 题解
帽儿山奇怪的棋盘 题目: Time Limit:1000ms | Memory Limit:65535K Description 军哥来到了帽儿山,发现有两位神人在顶上对弈.棋盘长成下图的模样: 每个 ...
- [Python急救站课程]获取星期字符串
如何获取星期字符串 weekstr = "星期一星期二星期三星期四星期五星期六星期日" weekid = eval(input("请输入星期数字(1~7):") ...
- Azure Data Factory(十)Data Flow 组件详解
一,引言 随着大数据技术的不断发展,数据处理和分析变得越来越重要.为了满足企业对数据处理的需求,微软推出了 Azure Data Factory (ADF),它是一个云端的数据集成服务,用于创建.安排 ...
- OpenGL 基础光照详解
1. 光照 显示世界中,光照环境往往是相对复杂的.因为假设太阳作为世界的唯一光源,那么太阳光照在物体A上A将阳光进行反射后,A又做为一个新的光源共同作用于另一个物体B.所以于B来讲光源是复杂的.然而这 ...
- 一张图搞懂sql执行顺序
冲浪时发现一张很有意思的图,细分了一个长sql语句的执行顺序