楚一帆 发表于 2021-1-5 09:48

Unreal源码学习(一)Tick逻辑总结

本篇文章主要明确以下几大类问题:
C++ 中的 Tick 和蓝图中的 Tick 的关系?Tick 的分组有何作用?不同 Tick 函数之间的如何处理依赖关系?Tick,TickableObject,Timer 在同一帧中的以何种顺序执行?


我们随便创建一个蓝图的时候都会看到有个 Tick 节点。而使用UE4在C++中创建一个Actor的时候也会自动帮你把 Tick 方法重写好。
蓝图中的Tick
void ATestActor::Tick(float DeltaTime)
{
        Super::Tick(DeltaTime);
}简单翻看AActor的源码就能得知其实蓝图中的 Tick 是实现了C++中的 ReceiveTick:
UFUNCTION(BlueprintImplementableEvent, meta=(DisplayName = "Tick"))
void ReceiveTick(float DeltaSeconds);只不过为了统一逻辑用DisplayName在蓝图中把它显示成了Tick。而此方法在AActor的Tick方法中被调用:
void AActor::Tick( float DeltaSeconds )
{
        if (GetClass()->HasAnyClassFlags(CLASS_CompiledFromBlueprint) || !GetClass()->HasAnyClassFlags(CLASS_Native))
        {
                // Blueprint code outside of the construction script should not run in the editor
                // Allow tick if we are not a dedicated server, or we allow this tick on dedicated servers
                if (GetWorldSettings() != nullptr && (bAllowReceiveTickEventOnDedicatedServer || !IsRunningDedicatedServer()))
                {
                        ReceiveTick(DeltaSeconds);
                }

                //....
        }
}在这里我们还能看到是有判断的,如果你是一个纯C++的AActor是不会走ReceiveTick的。


虚幻中Tick是可以指定分组的。从源码中我们能看出这个分组主要针对的问题就是我们自己的Tick逻辑和物理模拟如何相处的问题:
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我们能够选择的分组主要就是四个:
TG_PrePhysics 在物理模拟前运行TG_DuringPhysics 和物理模拟一起运行TG_PostPhysics 在物理模拟之后运行TG_PostUpdateWork 在所有Tick 运行之后(包括Timer和Tickableobject)


光看注释当然不能说明什么,具体执行时的代码在UWorld::Tick中(此处只截取有关部分):
void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
{
        //...

        for (int32 i = 0; i < LevelCollections.Num(); ++i)
        {
                //...
                // If caller wants time update only, or we are paused, skip the rest.
                if (bDoingActorTicks)
                {
                        // Actually tick actors now that context is set up
                        SetupPhysicsTickFunctions(DeltaSeconds);
                        TickGroup = TG_PrePhysics; // reset this to the start tick group
                        FTickTaskManagerInterface::Get().StartFrame(this, DeltaSeconds, TickType, LevelsToTick);

                        SCOPE_CYCLE_COUNTER(STAT_TickTime);
                        {
                                SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_PrePhysics"), 10);
                                SCOPE_CYCLE_COUNTER(STAT_TG_PrePhysics);
                                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(PrePhysicsMisc);
                                CSV_SCOPED_SET_WAIT_STAT(PrePhysics);
                                RunTickGroup(TG_PrePhysics);
                        }
                        bInTick = false;
                        EnsureCollisionTreeIsBuilt();
                        bInTick = true;
                        {
                                SCOPE_CYCLE_COUNTER(STAT_TG_StartPhysics);
                                SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_StartPhysics"), 10);
                                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StartPhysicsMisc);
                                CSV_SCOPED_SET_WAIT_STAT(StartPhysics);
                                RunTickGroup(TG_StartPhysics);
                        }
                        {
                                SCOPE_CYCLE_COUNTER(STAT_TG_DuringPhysics);
                                SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_DuringPhysics"), 10);
                                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(DuringPhysicsMisc);
                                CSV_SCOPED_SET_WAIT_STAT(DuringPhysics);
                                RunTickGroup(TG_DuringPhysics, false); // No wait here, we should run until idle though. We don't care if all of the async ticks are done before we start running post-phys stuff
                        }
                        TickGroup = TG_EndPhysics; // set this here so the current tick group is correct during collision notifies, though I am not sure it matters. 'cause of the false up there^^^
                        {
                                SCOPE_CYCLE_COUNTER(STAT_TG_EndPhysics);
                                SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_EndPhysics"), 10);
                                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(EndPhysicsMisc);
                                CSV_SCOPED_SET_WAIT_STAT(EndPhysics);
                                RunTickGroup(TG_EndPhysics);
                        }
                        {
                                SCOPE_CYCLE_COUNTER(STAT_TG_PostPhysics);
                                SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_PostPhysics"), 10);
                                CSV_SCOPED_TIMING_STAT_EXCLSIVE(PostPhysicsMisc);
                                CSV_SCOPED_SET_WAIT_STAT(PostPhysics);
                                RunTickGroup(TG_PostPhysics);
                        }
       
                }
                else if( bIsPaused )
                {
                        FTickTaskManagerInterface::Get().RunPauseFrame(this, DeltaSeconds, LEVELTICK_PauseTick, LevelsToTick);
                }
               
                // We only want to run the following once, so only run it for the source level collection.
                if (LevelCollections.GetType() == ELevelCollectionType::DynamicSourceLevels)
                {
                         //...

                        {
                                SCOPE_CYCLE_COUNTER(STAT_TickableTickTime);

                                if (TickType != LEVELTICK_TimeOnly && !bIsPaused)
                                {
                                        SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TimerManager"), 5);
                                        STAT(FScopeCycleCounter Context(GetTimerManager().GetStatId());)
                                        GetTimerManager().Tick(DeltaSeconds);
                                }

                                {
                                        SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TickObjects"), 5);
                                        FTickableGameObject::TickObjects(this, TickType, bIsPaused, DeltaSeconds);
                                }
                        }
                }

                if (bDoingActorTicks)
                {
                        SCOPE_CYCLE_COUNTER(STAT_TickTime);
                        {
                                SCOPE_CYCLE_COUNTER(STAT_TG_PostUpdateWork);
                                SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - PostUpdateWork"), 5);
                                CSV_SCOPED_SET_WAIT_STAT(PostUpdateWork);
                                RunTickGroup(TG_PostUpdateWork);
                        }
                        {
                                SCOPE_CYCLE_COUNTER(STAT_TG_LastDemotable);
                                SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_LastDemotable"), 5);
                                CSV_SCOPED_SET_WAIT_STAT(LastDemotable);
                                RunTickGroup(TG_LastDemotable);
                        }

                        FTickTaskManagerInterface::Get().EndFrame();
                }
        }
}通过 RunTickGroup 来按顺序跑每组的 tick。需要注意的是在跑 TG_DuringPhysics 与其他的不一样通过传入第二个参数来表示不需要等待 TG_DuringPhysics 组中的 Tick 完成直接开始下一组的 tick。
在一系列的 RunTickGroup 执行顺序里面穿插了两个我们也比较熟悉的需要 tick 的功能:
GetTimerManager().Tick(DeltaSeconds);

FTickableGameObject::TickObjects(this, TickType, bIsPaused, DeltaSeconds);一个是 Timer 的 tick。这个就是字面意思很好理解。
TickableGameObject是为了那些实现 FTickableGameObject 的对象准备的。
通过我们最常用的 AController 和 APawn 就能对 Tick 的这些逻辑有很好的理解。
与其他单独的Actor不同,AController 和 APawn 有明显的关联关系。
void AController::AddPawnTickDependency(APawn* NewPawn)
{
        if (NewPawn != NULL)
        {
                bool bNeedsPawnPrereq = true;
                UPawnMovementComponent* PawnMovement = NewPawn->GetMovementComponent();
                if (PawnMovement && PawnMovement->PrimaryComponentTick.bCanEverTick)
                {
                        PawnMovement->PrimaryComponentTick.AddPrerequisite(this, this->PrimaryActorTick);

                        // Don't need a prereq on the pawn if the movement component already sets up a prereq.
                        if (PawnMovement->bTickBeforeOwner || NewPawn->PrimaryActorTick.GetPrerequisites().Contains(FTickPrerequisite(PawnMovement, PawnMovement->PrimaryComponentTick)))
                        {
                                bNeedsPawnPrereq = false;
                        }
                }
               
                if (bNeedsPawnPrereq)
                {
                        NewPawn->PrimaryActorTick.AddPrerequisite(this, this->PrimaryActorTick);
                }
        }
}在 AController 的 AddPawnTickDependency 方法中我们可以明确的看到如何来处理这种关系。
PawnMovement 将 AController 作为前置依赖,而 PawnMovement 又是 NewPawn 的前置依赖。这样就组成了一条依赖链。我们可以通过这种方式来组织具有关联的 Tick 逻辑。
如果我们想为一个 UObject 的子类添加 Tick 我们还有另一种选择,就是利用 FTickableGameObject。这里我们可以拿虚幻中的 AI 子系统作为参考:
class AIMODULE_API UAISubsystem : public UObject, public FTickableGameObject
{
        GENERATED_BODY()
        //...
public:
        //...

        // FTickableGameObject begin
        virtual UWorld* GetTickableGameObjectWorld() const override { return GetWorldFast(); }
        virtual void Tick(float DeltaTime) override {}
        virtual ETickableTickType GetTickableTickType() const override;
        virtual TStatId GetStatId() const override;
        // FTickableGameObject end

};UAISubsystem 继承 FTickableGameObject 并实现几个需要的方法就可以了。
不过此种方式实现的 Tick 不能配置上面所讲的 TickGroup 等参数。只是能把你的 Tick 逻辑注册到引擎中。然后在 TG_PostPhysics 后,TG_PostUpdateWork 前执行。
总结

蓝图中的 Tick 实质上是 C++ Tick 中所调用的一部分。Tick 分组可以用来控制 Tick 在当前帧的什么阶段执行。具有依赖关系的 Tick 之间可以通过 AddPrerequisite 来设置先后顺序。除了像 Actor 那样使用 FTickFunction (PrimaryActorTick)还可以直接继承 FTickableGameObject 来向 UObject 添加 Tick 能力。
页: [1]
查看完整版本: Unreal源码学习(一)Tick逻辑总结