1. 前情提要

因工作需要,有在编辑器模式下执行Actor的Tick函数的需求。经过查阅资料,了解到重载Actor::ShouldTickIfViewportOnly函数可以实现在编辑器视口下也可以执行Tick函数。

已知Actor和ActorComponent都有自己的Tick函数,并且进入游戏并执行BeginPlay后才会开始Tick。

出于好奇心,产生了一系列的疑问:

  1. Actor和组件的Tick函数是由谁管理和统一调用的?
  2. 在执行BeginPlay后才会开始Tick,但是编辑器模式下默认并不会调用BeginPlay,那么为什么重载了ShouldTickIfViewportOnly()后照样开始了Tick?

    以此衍生出新的问题

    1. UE是如何控制Actor的Tick的开始的?
    2. 有哪些变量或者函数与之相关?

本文将从这些问题出发,简要地探究一下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 时的路径。

  1. SpawnActor 被调用。
  2. PostSpawnInitialize
  3. PostActorCreated - 创建后即被生成的 Actor 调用,构建函数类行为在此发生。PostActorCreated 与 PostLoad 互斥。
  4. ExecuteConstruction:
    • OnConstruction - Actor 的构建。蓝图 Actor 的组件在此处创建,蓝图变量在此处初始化
  5. PostActorConstruction:
    1. PreInitializeComponents - 在 Actor 的组件上调用 InitializeComponent 之前进行调用。
    2. InitializeComponent - Actor 上定义的每个组件的创建辅助函数。
    3. PostInitializeComponents - Actor 的组件初始化后调用。
  6. OnActorSpawned 在 UWorld 上播放。
  7. 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的注册与执行的更多相关文章

  1. 源码简析XXL-JOB的注册和执行过程

    一,前言 XXL-JOB是一个优秀的国产开源分布式任务调度平台,他有着自己的一套调度注册中心,提供了丰富的调度和阻塞策略等,这些都是可视化的操作,使用起来十分方便. 由于是国产的,所以上手还是比较快的 ...

  2. xmanagr 注册机执行ubuntu 桌面程序,ubuntu无需安装 桌面环境

    Xshell 5 注册码: 690313-111999-999313Xftp 5 注册码:101210-450789-147200 Xmanager 5 注册码:101210-450789-14720 ...

  3. WebShell代码分析溯源(十一)

    WebShell代码分析溯源(十一) 一.一句话变形马样本 <?php $e = $_REQUEST['e'];declare(ticks=1);register_tick_function ( ...

  4. 剖析虚幻渲染体系(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 编辑 ...

  5. Scala学习笔记--Actor和并发

    感谢博主lyrebing  博文地址:http://blog.csdn.net/lyrebing/article/details/20446061 1.  Actor用法 1.1 Actor的基本使用 ...

  6. 追源索骥:透过源码看懂Flink核心框架的执行流程

    li,ol.inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-bottom:20px}dt, ...

  7. 一个用于实现并行执行的 Java actor 库

    即使 Java 6 和 Java 7 中引入并发性更新,Java 语言仍然无法让并行编程变得特别容易.Java 线程.synchronized 代码块.wait/notify 和java.util.c ...

  8. 透过源码看懂Flink核心框架的执行流程

    前言 Flink是大数据处理领域最近很火的一个开源的分布式.高性能的流式处理框架,其对数据的处理可以达到毫秒级别.本文以一个来自官网的WordCount例子为引,全面阐述flink的核心架构及执行流程 ...

  9. Spring笔记(8) - @EventListener注解探究

    在上文中讲了Spring的事件监听机制,流程是:定义事件.监听器,发布事件,控制台输出监听到的事件内容. 在上文的扩展中 使用 @EventListener 注解来自定义监听器,监听指定的事件,比如下 ...

  10. Dapr + .NET Core实战(五)Actor

    什么是Actor模式 Actors 为最低级别的"计算单元" 以上解释来自官方文档,看起来"晦涩难懂".大白话就是说Actors模式是一段需要单线程执行的代码块 ...

随机推荐

  1. Java-全网最详细反射

    Java-反射 前言 Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象 ...

  2. QAction常用接口总结

    目录 public (一)构造函数 (二)setShortcut (三)setStatusTip Signals (一)trigger() public (一)构造函数 1.QAction(const ...

  3. Web Woeker和Shared Worker的使用以及案例

    目录 1.前言 2.介绍 Web Worker 3.使用须知及兼容性 3.1.使用须知 3.2.兼容性 4.使用 Web Worker 4.1.创建 Web Worker 4.2.与主线程通信 4.3 ...

  4. (转)用GPU做DeepLearning要比CPU快40~80倍

    The speed difference of CPU and GPU can be significant in deep learning. But how much? Let's do a te ...

  5. excel对比两个文档,判断范围内的取值是否在另一个列表内存在(vlookup函数的使用)

    背景: sheet1表为原始数据: sheet2表为新的数据副本, 目标是查询sheet2列表中是否存在sheet1表的数据,并且标记出来,且获取sheet2列表的一些数据至sheet1列表中,补充D ...

  6. python之继承的方法重写

    目录 普通方法 方法重写super().xxx() 我们在python之继承及其实现方法中已经学会了打印子类在父类继承的属性 那么我们怎么打印出子类中特有的属性呢 普通方法 我们当然可以在子类中添加新 ...

  7. 三菱PLC 轻松数采

    目前市面上数采的软件有很多,但是用的最为省力最为简单的就是kepserver了,在kepserver的应用中,有对应的三菱驱动针对于三菱PLC,三菱驱动支持多个Mitsubishi 协议,包括 MEL ...

  8. String.trim()含义

    就是去除两端空格,目前只用到了这个.

  9. 31. 干货系列从零用Rust编写正反向代理,HTTP限流的实现(limit_req)

    wmproxy wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代 ...

  10. Macbook磁盘系统结构/文件/目录介绍分析

    1. 系统磁盘根目录详解: 1.1 磁盘根目录结构 / (根目录)|-- Applications # 存放应用程序|-- Users # 存放用户文件和设置|-- cores # 存放核心转储文件, ...