前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unreal TickFunc调度

Unreal TickFunc调度

作者头像
JohnYao
发布2022-06-29 15:12:12
1K0
发布2022-06-29 15:12:12
举报
文章被收录于专栏:JohnYao的技术分享

一 Unreal Tick背景知识

Tick

Tick在计算机领域并没有很好的中文翻译,英汉词典里的解释是很短的一段时间,或者时钟的一次滴答。

在引擎里,tick对应的是引擎循环(EngineLoop)的单次执行,也可以认为是服务器的一次滴答;时钟的滴答是一秒滴答一次,但服务器一秒会滴答很多次。每秒的tick数越多,一般来说代表了服务器对客户端的响应可以更及时,一般意味着服务器的性能也就更高。

和Tick非常相关的一个概念是帧(Frame)。帧,最开始是衡量画面的刷新频率的;后面在帧同步的技术中,用于逻辑同步,一个逻辑帧是游戏逻辑执行的最小时间单位。在Unreal Engine的Dedicated Server中,由于和客户端共用了代码(比如Tick的计数器变量被命名为GFrameCounter),所以每次Tick执行,很多时候也被称为一帧。但为了突显Unreal原生使用的状态同步方式,我们用每秒128 tick,会比每秒128帧更准确。

Unreal Engine的Tick

我们看下Unreal Engine中的Tick:可以看到进程的主函数在完成初始化后,就会进入所谓的引擎循环,只要引擎没有被要求退出,就会一直执行Tick。

代码语言:javascript
复制
int32 GuardedMain( const TCHAR* CmdLine )
{
    <初始化代码>

    BootTimingPoint("Tick loop starting");
    DumpBootTiming();

    while( !IsEngineExitRequested() )
    {
        EngineTick();
    }

    TRACE_BOOKMARK(TEXT("Tick loop end"));

    <其他代码>
}

EngineTick执行过程的调用层级如下:

代码语言:javascript
复制
GuardedMain(const wchar_t * CmdLine)
  EngineTick()
    FEngineLoop::Tick()
      UGameEngine::Tick(float DeltaSeconds, bool bIdleMode)
        UWorld::Tick(ELevelTick TickType, float DeltaSeconds)

针对于本文要讲的内容,我们需要关注下调用层级中的第三层,FEngineLoop层的Tick函数。

代码语言:javascript
复制
void FEngineLoop::Tick()
{
    <其他代码>

    // set FApp::CurrentTime, FApp::DeltaTime and potentially wait to enforce max tick rate
    {
        QUICK_SCOPE_CYCLE_COUNTER(STAT_FEngineLoop_UpdateTimeAndHandleMaxTickRate);
        GEngine->UpdateTimeAndHandleMaxTickRate();
        GEngine->SetGameLatencyMarkerStart(CurrentFrameCounter);
    }

    <其他代码>  

    // main game engine tick (world, game objects, etc.)
    GEngine->Tick(FApp::GetDeltaTime(), bIdleMode);

    <其他代码>  

    // Increment global frame counter. Once for each engine tick.
    GFrameCounter++;

    <其他代码>  
}

这里主要关注如下三个执行逻辑即可:

1. UEngine::UpdateTimeAndHandleMaxTickRate() 顾名思义,就是更新引擎中使用的时间变量,并根据设置的MaxTickRate来休眠一定时间。

2. 调用UEngine::Tick()触发Gameplay中的Tick执行。

3. 在FEngineLoop::Tick()的最后,自增GFrameCounter。

对于Gameplay来讲,最重要的一步,则是调用层级中的第五层,调用当前游戏世界的Tick函数驱动游戏逻辑(UWorld::Tick)。

UWorld的Tick

Engine的Tick函数提供了Tick运行的框架逻辑,Tick的主要运行开销还是集中在UWorld的Tick函数。

探究UWorld的Tick函数之前,需要先对Unreal Gameplay的相关类(Actor,Componen等)的作用有所了解,读者可以自行探究,这里不再赘述。如有有需要可以阅读我的另一篇文章,johnyao:UE网络通信(一) 概述

UWorld Tick函数主要的开销有三处。

代码语言:javascript
复制
void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
{
    <其他代码>

    {
        SCOPE_CYCLE_COUNTER(STAT_NetWorldTickTime);
        CSV_SCOPED_TIMING_STAT_EXCLUSIVE(NetworkIncoming);
        CSV_SCOPED_SET_WAIT_STAT(NetworkTick);
        SCOPE_TIME_GUARD(TEXT("UWorld::Tick - NetTick"));
        LLM_SCOPE(ELLMTag::Networking);
        // Update the net code and fetch all incoming packets.
        BroadcastTickDispatch(DeltaSeconds);
        BroadcastPostTickDispatch();

        if( NetDriver && NetDriver->ServerConnection )
        {
            TickNetClient( DeltaSeconds );
        }
    }

    <调用Actor Component等的Tick函数>

    // Update net and flush networking.
    // Tick all net drivers
    {
        SCOPE_CYCLE_COUNTER(STAT_NetBroadcastTickTime);
        LLM_SCOPE(ELLMTag::Networking);
        BroadcastTickFlush(RealDeltaSeconds); // note: undilated time is being used here
    }
  1. RPC处理。这个是由BroadcastTickDispatch调用NetDriver的TickDispatch,最终由各个客户端连接的相关函数完成。
  2. Actor, Component等的Tick函数。这里的处理逻辑相对复杂,这篇文章后续内容主要介绍这块的实现。
  3. Actor的属性同步。这个是由BroadcastTickFlush调用NetDriver的TickFlush,最终将游戏世界中不同Actor的状态同步给各个客户端。

在我们先从第二部分,Gameplay层的TickFunction开始介绍。

二 Gameplay层的TickFunc

Actor TickFunc的配置

我们首先从Actor下面这个成员变量说起。

代码语言:javascript
复制
UPROPERTY(EditDefaultsOnly, Category=Tick)
    struct FActorTickFunction PrimaryActorTick;

FActorTickFunction是从FTickFunction继承的一个类,FTickFunction及其派生类负责TickFunc的配置和运行调度。大部分的配置和调度相关的成员变量都在基类FTickFunction中定义; 在派生类里一般只有一个Target成员变量,它是指向TickFunc所属的类对象的指针。(TickFunc在这里特指Gameplay层面,某个具体的Gameplay对象在引擎某一Tick中需要完成的任务)

下面是Actor初始化的部分代码。这里展示了FTickFunction部分成员变量的使用:

代码语言:javascript
复制
void AActor::InitializeDefaults()
{
    PrimaryActorTick.TickGroup = TG_PrePhysics;
    // Default to no tick function, but if we set 'never ticks' to false (so there is a tick function) it is enabled by default
    PrimaryActorTick.bCanEverTick = false;
    PrimaryActorTick.bStartWithTickEnabled = true;
    PrimaryActorTick.SetTickFunctionEnable(false); 
    <other-init-code>
  1. bCanEverTick是Tick是否开启的总开关
  2. bStartWithTickEnabled控制在Actor BeginPlay之后是否默认启动
  3. TickGroup规定了Tick的运行优先级,这个后面会单独介绍

除此之外,还需要重点了解的配置项有如下:

1. bAllowTickOnDedicatedServer是否在Dedicated Server中运行该Tick逻辑

2. TickInterval Tick运行的时间间隔,小于引擎Tick的运行时间, 则为每引擎Tick都执行

我们可以在蓝图内对如上属性进行配置:

Dedicated Server上常规Tick优化

上述两个配置项是DS上Tick优化的关键。很多的Tick逻辑只需要跑在客户端,甚至不需要跑tick,但由于unreal的代码生成工具生成的代码是把tick默认打开的(如下面代码所示),可能导致产生无谓的开销。

代码语言:javascript
复制
AMyActor::AMyActor()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;
}

这里就引出了Tick常规优化的第一个常见做法:关闭不必要的Tick逻辑

个人建议可以生成如下形式的代码,在必要的时候才打开客户端或者DS上的Tick执行。

代码语言:javascript
复制
#if !UE_SERVER  
    PrimaryActorTick.bCanEverTick = false;  // client tick. default disable
#else
    PrimaryActorTick.bCanEverTick = false;  // ds tick. default disble
#endif

Tick常规优化的另外一个做法是,设置合理的Tick间隔

很多Tick逻辑并不需要每帧都需要运行,比如AI补充刷新逻辑,我们可以将这部分Tick的TickInterval设置为秒级,甚至更长。

Actor TickFunc的注册和执行

在Actor的BeginPlay函数中,会调用RegisterActorTickFunctions将TickFunc向FTickTaskManager进行注册。

代码语言:javascript
复制
void AActor::RegisterActorTickFunctions(bool bRegister)
{
    check(!IsTemplate());

    if(bRegister)
    {
        if(PrimaryActorTick.bCanEverTick)
        {
            PrimaryActorTick.Target = this;
            PrimaryActorTick.SetTickFunctionEnable(PrimaryActorTick.bStartWithTickEnabled || PrimaryActorTick.IsTickFunctionEnabled());
            PrimaryActorTick.RegisterTickFunction(GetLevel());
        }
    }
    else
    {
        if(PrimaryActorTick.IsTickFunctionRegistered())
        {
            PrimaryActorTick.UnRegisterTickFunction();          
        }
    }

    FActorThreadContext::Get().TestRegisterTickFunctions = this; // we will verify the super call chain is intact. Don't copy and paste this to another actor class!
}

在World的Tick中,会调用FTickTaskManager的RunTickGroup,经过一系列调用,会执行到FActorTickFunction的ExecuteTick函数,这里调用Target的TickActor函数,最终会调用Tick函数。

代码语言:javascript
复制
void FActorTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
    if (Target && !Target->IsPendingKillOrUnreachable())
    {
        if (TickType != LEVELTICK_ViewportsOnly || Target->ShouldTickIfViewportsOnly())
        {
            FScopeCycleCounterUObject ActorScope(Target);
            Target->TickActor(DeltaTime*Target->CustomTimeDilation, TickType, *this);
        }
    }
}

void AActor::TickActor( float DeltaSeconds, ELevelTick TickType, FActorTickFunction& ThisTickFunction )
{
    //root of tick hierarchy

    // Non-player update.
    // If an Actor has been Destroyed or its level has been unloaded don't execute any queued ticks
    if (!IsPendingKill() && GetWorld())
    {
        Tick(DeltaSeconds); // perform any tick functions unique to an actor subclass
    }
}

Component的TickFunc

Component的TickFunc逻辑和Actor 90%类似,这里不再赘述,感兴趣的可以阅读相关代码。

三 TickFunc的调度

我们继续上一小节,UWorld的Tick函数调用Actor Component等的TickFunc这部分内容。在本节中,我们看下从UWorld的Tick函数如何完成对Gamplay层各游戏对象TickFunc的调度。

上一节,我们已经介绍了一部分内容。大家可以回忆下这两点:

1. 每个Tick函数会指定一个TickGroup

2. Tick函数会向FTickTaskManager注册自身

关于TickGroup, 读者可以简单的理解为Tick执行的优先级。列举如下:

代码语言:javascript
复制
UENUM(BlueprintType)
enum ETickingGroup
{
    /** Any item that needs to be executed before physics simulation starts. */
    TG_PrePhysics UMETA(DisplayName="Pre Physics"),

    /** Special tick group that starts physics simulation. */                           
    TG_StartPhysics UMETA(Hidden, DisplayName="Start Physics"),

    /** Any item that can be run in parallel with our physics simulation work. */
    TG_DuringPhysics UMETA(DisplayName="During Physics"),

    /** Special tick group that ends physics simulation. */
    TG_EndPhysics UMETA(Hidden, DisplayName="End Physics"),

    /** Any item that needs rigid body and cloth simulation to be complete before being executed. */
    TG_PostPhysics UMETA(DisplayName="Post Physics"),

    /** Any item that needs the update work to be done before being ticked. */
    TG_PostUpdateWork UMETA(DisplayName="Post Update Work"),

    /** Catchall for anything demoted to the end. */
    TG_LastDemotable UMETA(Hidden, DisplayName = "Last Demotable"),

    /** Special tick group that is not actually a tick group. After every tick group this is repeatedly re-run until there are no more newly spawned items to run. */
    TG_NewlySpawned UMETA(Hidden, DisplayName="Newly Spawned"),

    TG_MAX,
};

在Gameplay层面, 关于Tick我们只需要关注到FTickFunction。我们使用它进行Tick函数的配置。

但在底层,完成Actor,Component TickFunc的执行,还需要如下几部分功能的支持。其中一部分,负责TickFunc的管理调度,主要有如下类组成:FTickTaskManager,FTickTaskLevel,FTickTaskSequencer;另一部分,负责TickFunc的依赖关系梳理以及TickFunc的执行:主要由TGraphTask,FTaskGraphCompatibilityImplementation等组成。再有就是更底层的任务队列和执行线程。

在TickFunction加入游戏世界时,主要是任务管理调度类参与,具体时序如下:

简单讲,就是调用TickTaskManager的AddTickFunction,最终添加到TickTaskLevel中。

Tick的函数执行要复杂的多,但可以梳理为如下时序:

在world的Tick函数中,会遍历自己的LevelCollections,对于LevelCollections中的每个colletion执行后续操作

1. StartFrame阶段

  • 遍历该colletion中的所有level,放置在一个TArray中,并将该array作为参数,调用TickTaskManager的StartFrame函数。
  • TickTaskManager首先会调用FTickTaskSequencer的StartFrame,完成其执行前初始化的一些工作。
  • 调用自身的FillLevelList,填充自己类成员的level array。
  • 对于有TickInterval的TickFunction,TickTaskLevel维持了一个最近到期的队列。在这一步,会遍历所有level,并调整队列中TickFunction的等待时间,并将该tick内到期的TickFunction设置为Enable的。
  • 这一步会二次遍历所有level中的所有TickFunction,并调用其QueueTickFunction。这里会嵌套调用其依赖的TickFunction的QueueTickFunction,并根据依赖的TickFunc的优先级(TickGroup),调整自身。依赖的TickFunc的优先级为N,则自身会降级为N+1。这一步内,也会按顺序将自己及依赖的TickFunc添加到执行线程的队列中。

2. RunTickGroup阶段

这一步按本节开头给出的TickGroup的顺序,依次调用TickTaskManager的RunTickGroup。该函数的参数即具体的TickGroup。 这里最终会由FTaskGraphCompatibilityImplementation来调用具体的线程,来依次执行之前已经添加到队列中的Task(TickFunc)。

3. EndFrame阶段

该步会再次调用各个level的ScheduleTickFunctionCooldowns,将有TickIntervel,且刚执行完的TickFunc设置为CoolingDown状态。

总结

本文介绍了UE4中的Tick, TickFunction等功能,以及TickFunction的调度,希望对正在研究学习这块的朋友有帮助。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-02-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一 Unreal Tick背景知识
    • Tick
      • Unreal Engine的Tick
        • UWorld的Tick
        • 二 Gameplay层的TickFunc
          • Actor TickFunc的配置
            • Dedicated Server上常规Tick优化
              • Actor TickFunc的注册和执行
                • Component的TickFunc
                • 三 TickFunc的调度
                • 总结
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档