Unreal Engine 的 Tick 机制

在前面的章节中,我们介绍了Unreal Engine中的各种使用注册回调来驱动逻辑的机制,包括计时器回调、事件分发回调和异步任务回调。但是在UE里这些回调逻辑并不是业务逻辑的大头,真正的大头在引擎提供的各种Tick函数里,我们日常使用最多的Actor/ActorComponent的主要逻辑基本都是通过Tick来管理的。在UEMain函数中,开头会执行引擎的一些初始化工作,在完成初始化之后,就开始使用while循环不断的来执行EngineTick:

/**
 * Static guarded main function. Rolled into own function so we can have error handling for debug/ release builds depending
 * on whether a debugger is attached or not.
 */
int32 GuardedMain( const TCHAR* CmdLine )
{
	// 省略很多初始化代码

	// Don't tick if we're running an embedded engine - we rely on the outer
	// application ticking us instead.
	if (!GUELibraryOverrideSettings.bIsEmbedded)
	{
		while( !IsEngineExitRequested() )
		{
			EngineTick();
		}
	}

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

#if WITH_EDITOR
	if( GIsEditor )
	{
		EditorExit();
	}
#endif
	return ErrorLevel;
}

EngineTick里会调用GEngineLoop.Tick(),这个函数是UE引擎的主循环函数,负责驱动逻辑层的帧Frame的更新。在FEngineLoop::Tick函数中,会先更新时间相关的变量,比如FApp::CurrentTimeFApp::DeltaTime等,然后根据最大帧率限制MaxTickRate来判断是否需要Sleep一段时间来避免帧数过高,然后调用GEngine->Tick()来驱动World->Level这两个层级的Tick,并在最后更新一下全局帧号GFrameCounter:

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++;

    // 省略很多代码
}

LevelTick里调用FTickTaskManager::Tick(),而FTickTaskManager::Tick()会在内部调用Actor/ActorComponentTick()函数。因此从引擎的TickActor/ActorComponentTick中间会经过以下几个步骤:

ue tick 调用链

上面提到的Tick调用链只是一个概览,其实还有很多非Actor/ActorComponentTick函数。在这一章节中,我们将详细介绍Unreal Engine中的Tick机制,包括Tick的注册、排序、执行等,同时还会介绍一下一些通用的优化策略。

tickfunction基类

Unreal Engine中提供了一个FTickFunction作为常规的可Tick对象的基类:

/** 
* Abstract Base class for all tick functions.
**/
USTRUCT()
struct FTickFunction
{
	GENERATED_USTRUCT_BODY()

public:
	// The following UPROPERTYs are for configuration and inherited from the CDO/archetype/blueprint etc

	/**
	 * Defines the minimum tick group for this tick function. These groups determine the relative order of when objects tick during a frame update.
	 * Given prerequisites, the tick may be delayed.
	 *
	 * @see ETickingGroup 
	 * @see FTickFunction::AddPrerequisite()
	 */
	UPROPERTY(EditDefaultsOnly, Category="Tick", AdvancedDisplay)
	TEnumAsByte<enum ETickingGroup> TickGroup;

	/**
	 * Defines the tick group that this tick function must finish in. These groups determine the relative order of when objects tick during a frame update.
	 *
	 * @see ETickingGroup 
	 */
	UPROPERTY(EditDefaultsOnly, Category="Tick", AdvancedDisplay)
	TEnumAsByte<enum ETickingGroup> EndTickGroup;

public:
	/** Bool indicating that this function should execute even if the game is paused. Pause ticks are very limited in capabilities. **/
	UPROPERTY(EditDefaultsOnly, Category="Tick", AdvancedDisplay)
	uint8 bTickEvenWhenPaused:1;

	/** If false, this tick function will never be registered and will never tick. Only settable in defaults. */
	UPROPERTY()
	uint8 bCanEverTick:1;

	/** If true, this tick function will start enabled, but can be disabled later on. */
	UPROPERTY(EditDefaultsOnly, Category="Tick")
	uint8 bStartWithTickEnabled:1;

	/** If we allow this tick to run on a dedicated server */
	UPROPERTY(EditDefaultsOnly, Category="Tick", AdvancedDisplay)
	uint8 bAllowTickOnDedicatedServer:1;

	/** True if we allow this tick to be combined with other ticks for improved performance */
	uint8 bAllowTickBatching:1;

	/** Run this tick first within the tick group, presumably to start async tasks that must be completed with this tick group, hiding the latency. */
	uint8 bHighPriority:1;

	/** If false, this tick will run on the game thread, otherwise it will run on any thread in parallel with the game thread and in parallel with other "async ticks" **/
	uint8 bRunOnAnyThread:1;
};

目前的ETickingGroup有七个取值范围:

/** Determines which ticking group a tick function belongs to. */
UENUM(BlueprintType)
enum ETickingGroup : int
{
	/** 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,
};

这七个取值相当于把一帧切分为了七个连续不相交的部分,用于控制Tick执行时机的分组设置:

  1. TG_PrePhysics:物理模拟执行,一般用于位置更新等需要作为物理运算的前置条件的逻辑
  2. TG_StartPhysics:启动物理模拟。一般仅内部用于`FStartPhysicsTickFunction``
  3. TG_DuringPhysics:和物理模拟无关的逻辑。通常用于位置、速度、加速度无关的逻辑
  4. TG_EndPhysics:阻塞等待物理模拟完成,分发物理碰撞回调。用于`EndPhysicsTickFunction``
  5. TG_PostPhysics:物理模拟结束后的Tick。任何依赖Rigidbody结果的逻辑都应该在此之后
  6. TG_PostUpdateWorkUpdate 结束时的Tick
  7. TG_LastDemotable:帧末尾的回调。

这里的TickGroupEndTickGroup代表了这个tick函数所能执行的ETickingGroup闭区间。这里之所以使用区间而不是直接指定一个TickGroup,是因为当前的FTickFunction可以指定前置依赖:

/** Prerequisites for this tick function **/
TArray<struct FTickPrerequisite> Prerequisites;
	/** 
	* Adds a tick function to the list of prerequisites...in other words, adds the requirement that TargetTickFunction is called before this tick function is 
	* @param TargetObject - UObject containing this tick function. Only used to verify that the other pointer is still usable
	* @param TargetTickFunction - Actual tick function to use as a prerequisite
	**/
ENGINE_API void AddPrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction);

/** 
	* Removes a prerequisite that was previously added.
	* @param TargetObject - UObject containing this tick function. Only used to verify that the other pointer is still usable
	* @param TargetTickFunction - Actual tick function to use as a prerequisite
	**/
ENGINE_API void RemovePrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction);

一个FTickFunction可以设置很多依赖,当前FTickFunction不得早于这些依赖的FTickFunction执行。这里的FTickFunction的依赖一般是用来处理SkeletalMeshComponentTick,特别是ActorA挂载在ActorB身上的情况,此时要求ActorB的骨骼和位置先执行更新,然后再更新ActorA的骨骼和位置。

如果A的执行依赖于B,但是BTickGroup晚于A,这样在执行Tick调度的时候就会出现失败。为了尽可能的降低调度失败的情况,这里就给每个FTickFunction都指定了一个可以执行的TickGroup区间,这样就有更多的空间来执行FTickFunction之间的拓扑排序,避免调度失败。

默认情况下FTickFunction会每帧都执行一次,不过可以通过修改TickInterval为正值来指定tick间隔,从而来达到降低Tick频率的目的

/** The frequency in seconds at which this tick function will be executed.  If less than or equal to 0 then it will tick every frame */
UPROPERTY(EditDefaultsOnly, Category="Tick", meta=(DisplayName="Tick Interval (secs)"))
float TickInterval;

当这个FTickFunction被选中执行的时候,基类上声明的ExecuteTick函数就会被执行,这个是一个纯虚函数,具体内容依赖于子类的定义:

/** 
	* Abstract function actually execute the tick. Batched tick managers should use ExecuteNestedTick
	* @param DeltaTime - frame time to advance, in seconds
	* @param TickType - kind of tick for this frame
	* @param CurrentThread - thread we are executing on, useful to pass along as new tasks are created
	* @param MyCompletionGraphEvent - completion event for this task. Useful for holding the completetion of this task until certain child tasks are complete.
	**/
ENGINE_API virtual void ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) PURE_VIRTUAL(,);

Actor/ActorComponent tick

Actor上有一个继承自FTickFunctionPrimaryActorTick类型的UProperty来控制tick的执行与注册,FActorTickFunction::ExecuteTick会首先调用到AActor::TickActor,然后这个TickActor函数会调用到AActor::Tick(float DeltaSeconds):

/** 
* Tick function that calls AActor::TickActor
**/
USTRUCT()
struct FActorTickFunction : public FTickFunction
{
	GENERATED_USTRUCT_BODY()

	/**  AActor  that is the target of this tick **/
	class AActor*	Target;

	/** 
		* Abstract function actually execute the tick. 
		* @param DeltaTime - frame time to advance, in seconds
		* @param TickType - kind of tick for this frame
		* @param CurrentThread - thread we are executing on, useful to pass along as new tasks are created
		* @param MyCompletionGraphEvent - completion event for this task. Useful for holding the completetion of this task until certain child tasks are complete.
	**/
	ENGINE_API virtual void ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override;
	/** Abstract function to describe this tick. Used to print messages about illegal cycles in the dependency graph **/
	ENGINE_API virtual FString DiagnosticMessage() override;
	ENGINE_API virtual FName DiagnosticContext(bool bDetailed) override;
};

void FActorTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
	if (IsValid(Target))
	{
		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 )
{
	// Actor validity was checked before this
	if (GetWorld())
	{
		Tick(DeltaSeconds);	// perform any tick functions unique to an actor subclass
	}
}

/**
	* Primary Actor tick function, which calls TickActor().
	* Tick functions can be configured to control whether ticking is enabled, at what time during a frame the update occurs, and to set up tick dependencies.
	* @see https://docs.unrealengine.com/API/Runtime/Engine/Engine/FTickFunction
	* @see AddTickPrerequisiteActor(), AddTickPrerequisiteComponent()
	*/
UPROPERTY(EditDefaultsOnly, Category=Tick)
struct FActorTickFunction PrimaryActorTick;

/** 
	*	Function called every frame on this Actor. Override this function to implement custom logic to be executed every frame.
	*	Note that Tick is disabled by default, and you will need to check PrimaryActorTick.bCanEverTick is set to true to enable it.
	*
	*	@param	DeltaSeconds	Game time elapsed during last frame modified by the time dilation
	*/
ENGINE_API virtual void Tick( float DeltaSeconds );

类似的ActorComponent上也有一个继承自FTickFunctionFActorComponentTickUProperty来控制Tick的执行与注册,通过这个FActorComponentTickExecuteTick中转到UActorComponent::TickComponent函数上:

/** 
* Tick function that calls UActorComponent::ConditionalTick
**/
USTRUCT()
struct FActorComponentTickFunction : public FTickFunction
{
	GENERATED_USTRUCT_BODY()

	/**  AActor  component that is the target of this tick **/
	class UActorComponent*	Target;

	/** 
		* Abstract function actually execute the tick. 
		* @param DeltaTime - frame time to advance, in seconds
		* @param TickType - kind of tick for this frame
		* @param CurrentThread - thread we are executing on, useful to pass along as new tasks are created
		* @param MyCompletionGraphEvent - completion event for this task. Useful for holding the completetion of this task until certain child tasks are complete.
	**/
	ENGINE_API virtual void ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override;
};

void FActorComponentTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
	TRACE_CPUPROFILER_EVENT_SCOPE(FActorComponentTickFunction::ExecuteTick);
	ExecuteTickHelper(Target, Target->bTickInEditor, DeltaTime, TickType, [this, TickType](float DilatedTime)
	{
		Target->TickComponent(DilatedTime, TickType, this);
	});
}

/** Main tick function for the Component */
UPROPERTY(EditDefaultsOnly, Category="ComponentTick")
struct FActorComponentTickFunction PrimaryComponentTick;

/**
	* Function called every frame on this ActorComponent. Override this function to implement custom logic to be executed every frame.
	* Only executes if the component is registered, and also PrimaryComponentTick.bCanEverTick must be set to true.
	*	
	* @param DeltaTime - The time since the last tick.
	* @param TickType - The kind of tick this is, for example, are we paused, or 'simulating' in the editor
	* @param ThisTickFunction - Internal tick function struct that caused this to run
	*/
ENGINE_API virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction);

注册时机是AActor::BeginPlay, 这里会通过RegisterAllActorTickFunctions将当前actortick函数执行注册,然后顺带的把所有componenttick都注册过去:

void AActor::BeginPlay()
{
	TRACE_OBJECT_LIFETIME_BEGIN(this);

	ensureMsgf(ActorHasBegunPlay == EActorBeginPlayState::BeginningPlay, TEXT("BeginPlay was called on actor %s which was in state %d"), *GetPathName(), (int32)ActorHasBegunPlay);
	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());
		}
		else
		{
			// When an Actor begins play we expect only the not bAutoRegister false components to not be registered
			//check(!Component->bAutoRegister);
		}
	}
}

void AActor::RegisterAllActorTickFunctions(bool bRegister, bool bDoComponents)
{
	if(!IsTemplate())
	{
		// Prevent repeated redundant attempts
		if (bTickFunctionsRegistered != bRegister)
		{
			FActorThreadContext& ThreadContext = FActorThreadContext::Get();
			check(ThreadContext.TestRegisterTickFunctions == nullptr);
			RegisterActorTickFunctions(bRegister);
			bTickFunctionsRegistered = bRegister;
			checkf(ThreadContext.TestRegisterTickFunctions == this, TEXT("Failed to route Actor RegisterTickFunctions (%s)"), *GetFullName());
			ThreadContext.TestRegisterTickFunctions = nullptr;
		}

		if (bDoComponents)
		{
			for (UActorComponent* Component : GetComponents())
			{
				if (Component)
				{
					Component->RegisterAllComponentTickFunctions(bRegister);
				}
			}
		}
		// 省略一些代码
	}
}

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!
}

FTickableGameObject

Actor/ActorComponent是一套非常复杂的框架,上面拥有了太多的功能,如果我们只是想要一个跟随着引擎的TickTick的对象,UE也提供了很方便的基类去继承,这就是FTickableGameObject:

/**
 * Base class for tickable objects
 */
class FTickableObjectBase
{
	// 这里省略很多代码
public:
	/**
	 * Pure virtual that must be overloaded by the inheriting class. It will
	 * be called at different times in the frame depending on the subclass.
	 *
	 * @param DeltaTime	Game time passed since the last call.
	 */
	virtual void Tick( float DeltaTime ) = 0;
};
/**
 * This class provides common registration for gamethread tickable objects. It is an
 * abstract base class requiring you to implement the Tick() and GetStatId() methods.
 * Can optionally also be ticked in the Editor, allowing for an object that both ticks
 * during edit time and at runtime.
 */
class FTickableGameObject : public FTickableObjectBase
{
	/** Returns the tracking struct for this type */
	static ENGINE_API FTickableStatics& GetStatics();

public:
	/** Tickable objects cannot be copied safely due to the auto registration */
	UE_NONCOPYABLE(FTickableGameObject);

	/**
	 * Registers this instance with the static array of tickable objects.	
	 */
	ENGINE_API FTickableGameObject();

	/**
	 * Removes this instance from the static array of tickable objects.
	 */
	ENGINE_API virtual ~FTickableGameObject();

	/**
	 * Used to determine if an object should be ticked when the game is paused.
	 * Defaults to false, as that mimics old behavior.
	 *
	 * @return true if it should be ticked when paused, false otherwise
	 */
	virtual bool IsTickableWhenPaused() const
	{
		return false;
	}
};

FTickableGameObjectTick函数是声明在其基类FTickableObjectBase上的虚接口,当前没有任何实现,需要具体的子类去提供重载。UE源代码中有很多这样的子类,下面就是一个非常简单的子类实现:

struct FTestTickHelper : FTickableGameObject
{
	TWeakObjectPtr<class UMockAI> Owner;

	FTestTickHelper() : Owner(nullptr) {}
	virtual void Tick(float DeltaTime) override;
	virtual bool IsTickable() const override { return Owner.IsValid(); }
	virtual bool IsTickableInEditor() const override { return true; }
	virtual TStatId GetStatId() const override;
};
void FTestTickHelper::Tick(float DeltaTime)
{
	if (Owner.IsValid())
	{
		Owner->TickMe(DeltaTime);
	}
}
UCLASS()
class UMockAI : public UObject
{
	GENERATED_UCLASS_BODY()

	virtual ~UMockAI() override;

	FTestTickHelper TickHelper;
};
void UMockAI::SetEnableTicking(bool bShouldTick)
{
	if (bShouldTick)
	{
		TickHelper.Owner = this;
	}
	else
	{
		TickHelper.Owner = nullptr;
	}
}

这样就可以通过一个简单的FTestTickHelper类型将一个不是Actor/Component体系的对象UMockAI加上了Tick接口。实际中使用最多的子类是UTickableWorldSubsystem:

UCLASS(Abstract, MinimalAPI)
class UTickableWorldSubsystem : public UWorldSubsystem, public FTickableGameObject
{
	GENERATED_BODY()

public:
	ENGINE_API UTickableWorldSubsystem();

	// FTickableGameObject implementation Begin
	ENGINE_API UWorld* GetTickableGameObjectWorld() const override;
	ENGINE_API virtual ETickableTickType GetTickableTickType() const override;
	ENGINE_API virtual bool IsAllowedToTick() const override final;
	ENGINE_API virtual void Tick(float DeltaTime) override;
	ENGINE_API virtual TStatId GetStatId() const override PURE_VIRTUAL(UTickableWorldSubsystem::GetStatId, return TStatId(););
};

这些Tick函数的注册时机是在FTickableGameObject的构造函数中:

FTickableGameObject::FTickableGameObject()
{
	FTickableStatics& Statics = GetStatics();

	// Queue for creation, this can get called very early in startup
	Statics.QueueTickableObjectForAdd(this);
}

void FTickableObjectBase::FTickableStatics::QueueTickableObjectForAdd(FTickableObjectBase* InTickable)
{
	// This only needs to lock the new object queue
	FScopeLock NewTickableObjectsLock(&NewTickableObjectsCritical);
	NewTickableObjects.Add(InTickable, ETickableTickType::NewObject);
}

这里的实现非常简单,就是先临时的加到NewTickableObjects这个数组之中,起到一个暂存的作用,后面会选择时机将这个数组中的元素转移到TickableObjects数组中去。

/** Implementation struct for internals of ticking, there should be one instance of this for each direct subclass */
struct FTickableStatics
{
	/** This critical section should be locked during entire tick process */
	FCriticalSection TickableObjectsCritical;

	/** List of objects that are fully ticking */
	TArray<FTickableObjectBase::FTickableObjectEntry> TickableObjects;

	/** Lock for modifying new list, this is automatically acquired by functions below */
	FCriticalSection NewTickableObjectsCritical;

	/** Set of objects that have not yet been queried for tick type */
	TMap<FTickableObjectBase*, ETickableTickType> NewTickableObjects;
};

这些Tick的调用时机则是在FTickableGameObject::TickObjects这个静态函数中,这个函数开头的Statics.StartTicking负责将Statics.NewTickableObjects中存储的新添加的Tick函数填充到Statics.TickableObjects数组中,然后再遍历这个Statics.TickableObjects来执行TickableObject->Tick(DeltaSeconds):

void FTickableGameObject::TickObjects(UWorld* World, const ELevelTick LevelTickType, const bool bIsPaused, const float DeltaSeconds)
{
	SCOPE_CYCLE_COUNTER(STAT_TickableGameObjectsTime);
	CSV_SCOPED_TIMING_STAT_EXCLUSIVE(Tickables);

	FTickableStatics& Statics = GetStatics();

	check(IsInGameThread());

	{
		// It's a long lock but it's ok, the only thing we can block here is the GC worker thread that destroys UObjects
		FScopeLock LockTickableObjects(&Statics.TickableObjectsCritical);

		Statics.StartTicking();

		for (const FTickableObjectEntry& TickableEntry : Statics.TickableObjects)
		{
			if (FTickableGameObject* TickableObject = static_cast<FTickableGameObject*>(TickableEntry.TickableObject))
			{
				// If it is tickable and in this world
				if (TickableObject->IsAllowedToTick()
					&& ((TickableEntry.TickType == ETickableTickType::Always) || TickableObject->IsTickable())
					&& (TickableObject->GetTickableGameObjectWorld() == World))
				{
					// If tick type is All because at least one game world ticked, this will treat the null world as a game world
					const bool bIsGameWorld = LevelTickType == LEVELTICK_All || (World && World->IsGameWorld());

					// If we are in editor and it is editor tickable, always tick
					// If this is a game world then tick if we are not doing a time only (paused) update and we are not paused or the object is tickable when paused
					if ((GIsEditor && TickableObject->IsTickableInEditor()) ||
						(bIsGameWorld && ((!bIsPaused && LevelTickType != LEVELTICK_TimeOnly) || (bIsPaused && TickableObject->IsTickableWhenPaused()))))
					{
						SCOPE_CYCLE_COUNTER_STATID(TickableObject->GetStatId());
						TickableObject->Tick(DeltaSeconds);
					}
				}
			}
		}

		Statics.FinishTicking();
	}
}

而这个TickObjects的调用时机则是在UWorld::Tick中, 这里传入了this对应的Uworld来对TickableObjects进行筛选:

// void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
// 省略很多代码
{
	SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TickObjects"), 5);
	FTickableGameObject::TickObjects(this, TickType, bIsPaused, DeltaSeconds);
}

tick的注册

FTickFunction的注册其实就是往FTickTaskManager执行注册,注册的时候需要带上其对应的Level

/**
* Adds the tick function to the primary list of tick functions.
* @param Level - level to place this tick function in
**/
void FTickFunction::RegisterTickFunction(ULevel* Level)
{
	if (!IsTickFunctionRegistered())
	{
		// Only allow registration of tick if we are are allowed on dedicated server, or we are not a dedicated server
		const UWorld* World = Level ? Level->GetWorld() : nullptr;
		if(bAllowTickOnDedicatedServer || !(World && World->IsNetMode(NM_DedicatedServer)))
		{
			if (InternalData == nullptr)
			{
				InternalData.Reset(new FInternalData());
			}
			FTickTaskManager::Get().AddTickFunction(Level, this);
			InternalData->bRegistered = true;
		}
	}
	else
	{
		check(FTickTaskManager::Get().HasTickFunction(Level, this));
	}
}

这个FTickTaskManager继承自FTickTaskManagerInterfaceFTickTaskManagerInterface是一个纯虚类,负责提供接口声明:

/** 
 * Interface for the tick task manager
 **/
class FTickTaskManagerInterface
{
public:
	virtual ~FTickTaskManagerInterface()
	{
	}

	/** Allocate a new ticking structure for a ULevel **/
	virtual FTickTaskLevel* AllocateTickTaskLevel() = 0;

	/** Free a ticking structure for a ULevel **/
	virtual void FreeTickTaskLevel(FTickTaskLevel* TickTaskLevel) = 0;

	/**
	 * Queue all of the ticks for a frame
	 *
	 * @param World	- World currently ticking
	 * @param DeltaSeconds - time in seconds since last tick
	 * @param TickType - type of tick (viewports only, time only, etc)
	 */
	virtual void StartFrame(UWorld* InWorld, float DeltaSeconds, ELevelTick TickType, const TArray<ULevel*>& LevelsToTick) = 0;

	/**
		* Run a tick group, ticking all actors and components
		* @param Group - Ticking group to run
		* @param bBlockTillComplete - if true, do not return until all ticks are complete
	*/
	virtual void RunTickGroup(ETickingGroup Group, bool bBlockTillComplete ) = 0;

	/** Finish a frame of ticks **/
	virtual void EndFrame() = 0;

	/**
	 * Singleton to retrieve the GLOBAL tick task manager
	 *
	 * @return Reference to the global cache tick task manager
	 */
	static ENGINE_API FTickTaskManagerInterface& Get();

};

这里的Get()目前只有一个实现FTickTaskManager,这也算是一种PIMPL模式,解耦了强依赖:

/**
 * Singleton to retrieve the global tick task manager
 * @return Reference to the global tick task manager
**/
FTickTaskManagerInterface& FTickTaskManagerInterface::Get()
{
	return FTickTaskManager::Get();
}

/** Class that aggregates the individual levels and deals with parallel tick setup **/
class FTickTaskManager : public FTickTaskManagerInterface
{
public:
	/**
	 * Singleton to retrieve the global tick task manager
	 * @return Reference to the global tick task manager
	**/
	static FTickTaskManager& Get()
	{
		static FTickTaskManager SingletonInstance;
		return SingletonInstance;
	}
};

这个FTickTaskManager内部通过Get函数提供了一个全局的单例,负责了所有的FTickFunction的管理。

不过FTickTaskManager其实并不是FTickFunction的容器,仍然是中转方。真正的容器是FTickTaskLevel,这个TickTaskLevel每个Level都有一个:

/** Add the tick function to the primary list **/
void FTickTaskManager::AddTickFunction(ULevel* InLevel, FTickFunction* TickFunction)
{
	check(TickFunction->TickGroup >= 0 && TickFunction->TickGroup < TG_NewlySpawned); // You may not schedule a tick in the newly spawned group...they can only end up there if they are spawned late in a frame.
	FTickTaskLevel* Level = TickTaskLevelForLevel(InLevel);
	Level->AddTickFunction(TickFunction);
	TickFunction->InternalData->TickTaskLevel = Level;
}

/** Find the tick level for this actor **/
FTickTaskLevel* FTickTaskManager::TickTaskLevelForLevel(ULevel* Level, bool bCreateIfNeeded = true)
{
	check(Level);

	if (bCreateIfNeeded && Level->TickTaskLevel == nullptr)
	{
		Level->TickTaskLevel = AllocateTickTaskLevel();
	}

	check(Level->TickTaskLevel);
	return Level->TickTaskLevel;
}

这个FTickTaskLevel内部逻辑还是比较复杂,要了解这个Level->AddTickFunction之前,需要先介绍一下FTickTaskLevel里主要成员变量的意义:

/** 所有注册过来的tick函数中enable了的 **/
TSet<FTickFunction*>						AllEnabledTickFunctions;
/** 所有在coolingdown状态下的tick函数列表 **/
FCoolingDownTickFunctionList				AllCoolingDownTickFunctions;
/** 所有注册过来的tick函数中disable了的 **/
TSet<FTickFunction*>						AllDisabledTickFunctions;
/** 等待重新排列的tick函数数组 **/
TArrayWithThreadsafeAdd<FTickScheduleDetails>				TickFunctionsToReschedule;
/** 在tick调度过程中新添加的tick函数集合  **/
TSet<FTickFunction*>						NewlySpawnedTickFunctions;

了解了这些成员变量之后,再来介绍一下AddTickFunction的实现,就是简单的加入到AllEnabledTickFunctions之中,如果bTickNewlySpawnedtrue也就是正在调度执行所有的FTickFunction的话,还需要往NewlySpawnedTickFunctions中添加一个备份,等待后续处理:

/** Add the tick function to the primary list **/
void AddTickFunction(FTickFunction* TickFunction)
{
	check(!HasTickFunction(TickFunction));
	if (TickFunction->TickState == FTickFunction::ETickState::Enabled)
	{
		AllEnabledTickFunctions.Add(TickFunction);
		if (bTickNewlySpawned)
		{
			NewlySpawnedTickFunctions.Add(TickFunction);
		}
	}
	else
	{
		check(TickFunction->TickState == FTickFunction::ETickState::Disabled);
		AllDisabledTickFunctions.Add(TickFunction);
	}
}

默认情况下每个FTickFunction都会跟着UWorldTick执行一次,但是如果一个FTickFunction自定义了Tick Interval的话,就需要计算这个FTickFunction目前是否已经可以调度执行。这些自定义了Tick IntervalFTickFunction会被AllCoolingDownTickFunctions管理, 这个AllCoolingDownTickFunctions类型有点特殊,是一个链表结构的头节点:

struct FCoolingDownTickFunctionList
{
	FCoolingDownTickFunctionList()
		: Head(nullptr)
	{
	}

	bool Contains(FTickFunction* TickFunction) const
	{
		FTickFunction* Node = Head;
		while (Node)
		{
			if (Node == TickFunction)
			{
				return true;
			}
			Node = Node->InternalData->Next;
		}
		return false;
	}

	FTickFunction* Head;
};

这个链表其实是一个有序链表,排序依据是下一次Tick的剩余过期时间,这个时间存储在TickFunction->InternalData->RelativeTickCooldown中。值得注意的是这个RelativeTickCooldown并不是相对于当前时间计算出来的时间差,而是相对于这个FCoolingDownTickFunctionList中前一个结点的过期时间点的时间差。举个例子来说,FCoolingDownTickFunctionList中有三个TickFunction, TickA0.2s之后执行,TickB0.3s后执行,TickC0.5s后执行,因此链表中的顺序是TickA->TickB->TickC。则TickA对应的RelativeTickCooldown0.2sTickB对应的RelativeTickCooldown0.1sTickC对应的RelativeTickCooldown0.2s

tick的排序

在一帧的开头,会调用到FTickTaskLevel::StartFrame,这个函数会遍历这个链表中的所有结点,寻找其中已经过期的来执行调度,如果可以调度则把这个TickFunctionState切换为FTickFunction::ETickState::EnabledAllEnabledTickFunctions基本都是每次都需要执行Tick的函数,所以这里的调度判定主要处理的是AllCoolingDownTickFunctions链表。为了判定链表中的某个Tick节点是否已经到了调度时间,这里使用了一个CumulativeCooldown来执行累加。从链表头部开始遍历,每调度一个节点则将这个CumulativeCooldown加上当前节点的TickFunction->InternalData->RelativeTickCooldown

int32 FTickTaskLevel::StartFrame(const FTickContext& InContext)
{
	check(!NewlySpawnedTickFunctions.Num()); // There shouldn't be any in here at this point in the frame
	Context.TickGroup = ETickingGroup(0); // reset this to the start tick group
	Context.DeltaSeconds = InContext.DeltaSeconds;
	Context.TickType = InContext.TickType;
	Context.Thread = ENamedThreads::GameThread;
	Context.World = InContext.World;
	bTickNewlySpawned = true;

	int32 CooldownTicksEnabled = 0;
	{
		// Make sure all scheduled Tick Functions that are ready are put into the cooling down state
		ScheduleTickFunctionCooldowns();

		// Determine which cooled down ticks will be enabled this frame
		float CumulativeCooldown = 0.f;
		FTickFunction* TickFunction = AllCoolingDownTickFunctions.Head;
		while (TickFunction)
		{
			if (CumulativeCooldown + TickFunction->InternalData->RelativeTickCooldown >= Context.DeltaSeconds)
			{
				TickFunction->InternalData->RelativeTickCooldown -= (Context.DeltaSeconds - CumulativeCooldown);
				break;
			}
			CumulativeCooldown += TickFunction->InternalData->RelativeTickCooldown;

			TickFunction->TickState = FTickFunction::ETickState::Enabled;
			TickFunction = TickFunction->InternalData->Next;
			++CooldownTicksEnabled;
		}
	}

	return AllEnabledTickFunctions.Num() + CooldownTicksEnabled;
}

而这里的ScheduleTickFunctionCooldowns的作用则是维持这个AllCoolingDownTickFunctions链表有序:

/* Puts a TickFunction in to the cooldown state*/
void ScheduleTickFunctionCooldowns()
{
	if (TickFunctionsToReschedule.Num() > 0)
	{
		SCOPE_CYCLE_COUNTER(STAT_ScheduleCooldowns);

		TickFunctionsToReschedule.Sort([](const FTickScheduleDetails& A, const FTickScheduleDetails& B)
		{
			return A.Cooldown < B.Cooldown;
		});

		int32 RescheduleIndex = 0;
		float CumulativeCooldown = 0.f;
		FTickFunction* PrevComparisonTickFunction = nullptr;
		FTickFunction* ComparisonTickFunction = AllCoolingDownTickFunctions.Head;
		while (ComparisonTickFunction && RescheduleIndex < TickFunctionsToReschedule.Num())
		{
			const float CooldownTime = TickFunctionsToReschedule[RescheduleIndex].Cooldown;
			if ((CumulativeCooldown + ComparisonTickFunction->InternalData->RelativeTickCooldown) > CooldownTime)
			{
				FTickFunction* TickFunction = TickFunctionsToReschedule[RescheduleIndex].TickFunction;
				check(TickFunction->InternalData->bWasInterval);
				if (TickFunction->TickState != FTickFunction::ETickState::Disabled)
				{
					TickFunction->TickState = FTickFunction::ETickState::CoolingDown;
					TickFunction->InternalData->RelativeTickCooldown = CooldownTime - CumulativeCooldown;

					if (PrevComparisonTickFunction)
					{
						PrevComparisonTickFunction->InternalData->Next = TickFunction;
					}
					else
					{
						check(ComparisonTickFunction == AllCoolingDownTickFunctions.Head);
						AllCoolingDownTickFunctions.Head = TickFunction;
					}
					TickFunction->InternalData->Next = ComparisonTickFunction;
					PrevComparisonTickFunction = TickFunction;
					ComparisonTickFunction->InternalData->RelativeTickCooldown -= TickFunction->InternalData->RelativeTickCooldown;
					CumulativeCooldown += TickFunction->InternalData->RelativeTickCooldown;
				}
				++RescheduleIndex;
			}
			else
			{
				CumulativeCooldown += ComparisonTickFunction->InternalData->RelativeTickCooldown;
				PrevComparisonTickFunction = ComparisonTickFunction;
				ComparisonTickFunction = ComparisonTickFunction->InternalData->Next;
			}
		}
		for ( ; RescheduleIndex < TickFunctionsToReschedule.Num(); ++RescheduleIndex)
		{
			FTickFunction* TickFunction = TickFunctionsToReschedule[RescheduleIndex].TickFunction;
			checkSlow(TickFunction);
			if (TickFunction->TickState != FTickFunction::ETickState::Disabled)
			{
				const float CooldownTime = TickFunctionsToReschedule[RescheduleIndex].Cooldown;

				TickFunction->TickState = FTickFunction::ETickState::CoolingDown;
				TickFunction->InternalData->RelativeTickCooldown = CooldownTime - CumulativeCooldown;

				TickFunction->InternalData->Next = nullptr;
				if (PrevComparisonTickFunction)
				{
					PrevComparisonTickFunction->InternalData->Next = TickFunction;
				}
				else
				{
					check(ComparisonTickFunction == AllCoolingDownTickFunctions.Head);
					AllCoolingDownTickFunctions.Head = TickFunction;
				}
				PrevComparisonTickFunction = TickFunction;

				CumulativeCooldown += TickFunction->InternalData->RelativeTickCooldown;
			}
		}
		TickFunctionsToReschedule.Reset();
	}
}

这个函数首先将TickFunctionsToReschedule里面存储的TickFunction按照Cooldown从小到大排序,然后里面的while循环就是将这个有序数组合并到AllCoolingDownTickFunctions这个有序链表中,最后的for循环负责将TickFunctionsToReschedule中没有合并到AllCoolingDownTickFunctions的元素直接拼接到AllCoolingDownTickFunctions的末尾。这个函数会执行一次TickFunctionsToReschedule快排,一次TickFunctionsToReschedule数组遍历,以及可能出现的AllCoolingDownTickFunctions链表全遍历,有些时候的开销会变得非常恐怖:

ue schedule cooldown

不过这种开销大的情况主要出现在有很多自定义了TickIntervalTickFunction中,如果TickInterval0也就是每次WorldTick都会执行的TickFunction,则不参与这个链表。但是之前FTickFunction的注册代码表明,即使自定义了TickInterval,也会存储在AllEnabledTickFunctions集合中,而不是AllCoolingDownTickFunctions链表中。因此在每帧的开头还需要将AllEnabledTickFunctions中有TickInterval的从集合中移除,并转移到AllCoolingDownTickFunctions。这部分逻辑在FTickTaskLevel::QueueAllTicks中,他会在FTickTaskLevel::StartFrame之后执行:

/**
	* Ticks the dynamic actors in the given levels based upon their tick group. This function
	* is called once for each ticking group
	*
	* @param World	- World currently ticking
	* @param DeltaSeconds - time in seconds since last tick
	* @param TickType - type of tick (viewports only, time only, etc)
	* @param LevelsToTick - the levels to tick, may be a subset of InWorld->Levels
	*/
virtual void FTickTaskManager::StartFrame(UWorld* InWorld, float InDeltaSeconds, ELevelTick InTickType, const TArray<ULevel*>& LevelsToTick) override
{
	// 省略很多代码
	int32 TotalTickFunctions = 0;
	for( int32 LevelIndex = 0; LevelIndex < LevelList.Num(); LevelIndex++ )
	{
		TotalTickFunctions += LevelList[LevelIndex]->StartFrame(Context);
	}
	INC_DWORD_STAT_BY(STAT_TicksQueued, TotalTickFunctions);
	CSV_CUSTOM_STAT(Basic, TicksQueued, TotalTickFunctions, ECsvCustomStatOp::Accumulate);
	TickTaskSequencer.SetupBatchedTicks(TotalTickFunctions);
	for( int32 LevelIndex = 0; LevelIndex < LevelList.Num(); LevelIndex++ )
	{
		LevelList[LevelIndex]->QueueAllTicks();
	}
	TickTaskSequencer.FinishBatchedTicks(Context);
}

QueueAllTicks会遍历当前AllEnabledTickFunctions集合,将其中自定义了TickIntervalTickFunction通过RescheduleForInterval函数转移到TickFunctionsToReschedule数组中:


void RescheduleForInterval(FTickFunction* TickFunction, float InInterval)
{
	TickFunction->InternalData->bWasInterval = true;
	TickFunctionsToReschedule.Add(FTickScheduleDetails(TickFunction, InInterval));
}

/* Queue all tick functions for execution */
void QueueAllTicks()
{
	FTickTaskSequencer& TTS = FTickTaskSequencer::Get();
	for (TSet<FTickFunction*>::TIterator It(AllEnabledTickFunctions); It; ++It)
	{
		FTickFunction* TickFunction = *It;
		TickFunction->QueueTickFunction(TTS, Context);

		if (TickFunction->TickInterval > 0.f)
		{
			It.RemoveCurrent();
			RescheduleForInterval(TickFunction, TickFunction->TickInterval);
		}
	}
	int32 EnabledCooldownTicks = 0;
	float CumulativeCooldown = 0.f;
	while (FTickFunction* TickFunction = AllCoolingDownTickFunctions.Head)
	{
		if (TickFunction->TickState == FTickFunction::ETickState::Enabled)
		{
			CumulativeCooldown += TickFunction->InternalData->RelativeTickCooldown;
			TickFunction->QueueTickFunction(TTS, Context);
			RescheduleForInterval(TickFunction, TickFunction->TickInterval - (Context.DeltaSeconds - CumulativeCooldown)); // Give credit for any overrun
			AllCoolingDownTickFunctions.Head = TickFunction->InternalData->Next;
		}
		else
		{
			break;
		}
	}
}

这个QueueAllTicks还会顺带的将AllCoolingDownTickFunctions里面可以执行的TickFunction从链表中摘除,并重新添加到TickFunctionsToReschedule数组中。这里新的Cooldown设置为了TickFunction->TickInterval - (Context.DeltaSeconds - CumulativeCooldown),注意到如果DeltaSeconds比较大的情况下,这个值会计算出负值,不过这里不会像TimerManager一样去做补偿重复执行多次Tick

TickFunction->QueueTickFunction的作用是将当前的TickFunction真正的加入到执行队列中。这里的逻辑主要是根据当前TickFunction的所有前置函数来计算出应该放在哪个ETickGroup中去调度,

void FTickFunction::QueueTickFunction(FTickTaskSequencer& TTS, const struct FTickContext& TickContext)
{
	checkSlow(TickContext.Thread == ENamedThreads::GameThread); // we assume same thread here
	check(IsTickFunctionRegistered());

	// Only compare the 32bit part of the frame counter
	uint32 CurrentFrameCounter = (uint32)GFrameCounter;
	if (InternalData->TickVisitedGFrameCounter.load(std::memory_order_relaxed) != CurrentFrameCounter)
	{
		InternalData->TickVisitedGFrameCounter.store(CurrentFrameCounter, std::memory_order_relaxed);
		if (TickState != FTickFunction::ETickState::Disabled)
		{
			ETickingGroup MaxPrerequisiteTickGroup =  ETickingGroup(0);

			TArray<FTickFunction*> RawPrerequisites;
			for (int32 PrereqIndex = 0; PrereqIndex < Prerequisites.Num(); PrereqIndex++)
			{
				FTickFunction* Prereq = Prerequisites[PrereqIndex].Get();
				if (!Prereq)
				{
					// stale prereq, delete it
					Prerequisites.RemoveAtSwap(PrereqIndex--);
				}
				else if (Prereq->IsTickFunctionRegistered())
				{
					// recursive call to make sure my prerequisite is set up so I can use its completion handle
					Prereq->QueueTickFunction(TTS, TickContext);
					if (Prereq->InternalData->TickQueuedGFrameCounter.load(std::memory_order_relaxed) != CurrentFrameCounter)
					{
						// this must be up the call stack, therefore this is a cycle
						UE_LOG(LogTick, Warning, TEXT("While processing prerequisites for %s, could use %s because it would form a cycle."),*DiagnosticMessage(), *Prereq->DiagnosticMessage());
					}
					else if (Prereq->InternalData->TaskState == ETickTaskState::NotQueued)
					{
						//ok UE_LOG(LogTick, Warning, TEXT("While processing prerequisites for %s, could use %s because it is disabled."),*DiagnosticMessage(), *Prereq->DiagnosticMessage());
					}
					else if (TTS.ShouldConsiderPrerequisite(this, Prereq))
					{
						MaxPrerequisiteTickGroup =  FMath::Max<ETickingGroup>(MaxPrerequisiteTickGroup, Prereq->InternalData->ActualStartTickGroup.GetValue());
						RawPrerequisites.Add(Prereq);
					}
				}
			}

			// tick group is the max of the prerequisites, the current tick group, and the desired tick group
			ETickingGroup MyActualTickGroup =  FMath::Max<ETickingGroup>(MaxPrerequisiteTickGroup, FMath::Max<ETickingGroup>(TickGroup.GetValue(),TickContext.TickGroup));
			if (MyActualTickGroup != TickGroup)
			{
				// if the tick was "demoted", make sure it ends up in an ordinary tick group.
				while (!CanDemoteIntoTickGroup(MyActualTickGroup))
				{
					MyActualTickGroup = ETickingGroup(MyActualTickGroup + 1);
				}
			}
			InternalData->ActualStartTickGroup = MyActualTickGroup;
			InternalData->ActualEndTickGroup = MyActualTickGroup;
			if (EndTickGroup > MyActualTickGroup)
			{
				check(EndTickGroup <= TG_NewlySpawned);
				ETickingGroup TestTickGroup = ETickingGroup(MyActualTickGroup + 1);
				while (TestTickGroup <= EndTickGroup)
				{
					if (CanDemoteIntoTickGroup(TestTickGroup))
					{
						InternalData->ActualEndTickGroup = TestTickGroup;
					}
					TestTickGroup = ETickingGroup(TestTickGroup + 1);
				}
			}

			if (TickState == FTickFunction::ETickState::Enabled)
			{
				TTS.QueueOrBatchTickTask(RawPrerequisites, this, TickContext);
			}
		}
		InternalData->TickQueuedGFrameCounter.store(CurrentFrameCounter, std::memory_order_relaxed);
	}
}

计算出ActualStartTickGroupActualEndTickGroup之后,最终会调用到QueueOrBatchTickTask去生成一个FTickGraphTask来封装一个TickFunction的执行,并放到处理多线程任务框架GraphTask的队列中:

FTickBatchInfo* QueueOrBatchTickTask(TArray<FTickFunction*>& Prerequisites, FTickFunction* TickFunction, const FTickContext& TickContext)
{
	// No batching, create a single task
	// FGraphEventArray array has some inline members so it is faster to not explicitly reserve space
	FGraphEventArray PrerequisiteEvents;
	for (FTickFunction* Prereq : Prerequisites)
	{
		PrerequisiteEvents.Add(Prereq->GetCompletionHandle());
	}

	QueueTickTask(&PrerequisiteEvents, TickFunction, TickContext);

	return nullptr;
}

/**
	* Start a tick task and add the completion handle
	*
	* @param	InPrerequisites - prerequisites that must be completed before this tick can begin
	* @param	TickFunction - the tick function to queue
	* @param	Context - tick context to tick in. Thread here is the current thread.
	*/
FORCEINLINE void QueueTickTask(const FGraphEventArray* Prerequisites, FTickFunction* TickFunction, const FTickContext& TickContext)
{
	FTickContext UseContext = SetupTickContext(TickFunction, TickContext);
	FTickGraphTask* Task = TGraphTask<FTickFunctionTask>::CreateTask(Prerequisites, ENamedThreads::GameThread).ConstructAndHold(TickFunction, &UseContext);
	TickFunction->SetTaskPointer(FTickFunction::ETickTaskState::HasTask, Task);

	AddTickTaskCompletion(TickFunction->InternalData->ActualStartTickGroup, TickFunction->InternalData->ActualEndTickGroup, Task, TickFunction->bHighPriority);
}

这里不去直接执行FTickFunction而是封装一个FTickGraphTask的好处是可以复用GraphTask系统自带的多线程和前置任务设计。

至此整个FTickFunction的调度处理QueueAllTicks流程结束,所有需要调度的FTickFunction都生成了一个对应的FTickGraphTask,内部逻辑还是非常复杂的,如果TickFunction数量非常大,且自定义间隔和前置条件设置的比较多的情况下,这个函数的执行时间会非常明显的长,达到ms级别:

queue ticks时间

tick的执行

上一个章节在介绍FTickableGameObject的时候提到其TickObjects接口会在UWorld::Tick中被调用,实际上TickFunctionExecuteTick也是在UWorld::Tick中被调用的,不过其调用关系隐藏的比较深,不是那么直白,需要了解一个中间函数RunTickGroup:

// void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
// 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);
	CSV_SCOPED_TIMING_STAT_EXCLUSIVE(TickActors);
	{
		SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_PrePhysics"), 10);
		SCOPE_CYCLE_COUNTER(STAT_TG_PrePhysics);
		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_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_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_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_SET_WAIT_STAT(PostPhysics);
		RunTickGroup(TG_PostPhysics);
	}
}

可以看到这里的RunTickGroup执行了五次,分别使用了TG_PrePhysics,TG_StartPhysics,TG_DuringPhysics,TG_EndPhysics,TG_PostPhysics这五个参数来依次驱动RunTickGroup的执行,这个枚举值刚好对应我们在介绍TickFunction时提到的ETickingGroup

这里的的RunTickGroup其实时一个非常轻度的封装,中转到FTickTaskManagerInterface::RunTickGroup上:

/**
	* Run a tick group, ticking all actors and components
	* @param Group - Ticking group to run
	* @param bBlockTillComplete - if true, do not return until all ticks are complete
	*/
void UWorld::RunTickGroup(ETickingGroup Group, bool bBlockTillComplete = true)
{
	check(TickGroup == Group); // this should already be at the correct value, but we want to make sure things are happening in the right order
	FTickTaskManagerInterface::Get().RunTickGroup(Group, bBlockTillComplete);
	TickGroup = ETickingGroup(TickGroup + 1); // new actors go into the next tick group because this one is already gone
}

由于目前FTickTaskManagerInterface只有一个子类FTickTaskManager,所以最终执行的是FTickTaskManager::RunTickGroup:

/**
	* Run a tick group, ticking all actors and components
	* @param Group - Ticking group to run
	* @param bBlockTillComplete - if true, do not return until all ticks are complete
*/
virtual void FTickTaskManager::RunTickGroup(ETickingGroup Group, bool bBlockTillComplete ) override
{
	check(Context.TickGroup == Group); // this should already be at the correct value, but we want to make sure things are happening in the right order
	check(bTickNewlySpawned); // we should be in the middle of ticking
	TickTaskSequencer.ReleaseTickGroup(Group, bBlockTillComplete);
	Context.TickGroup = ETickingGroup(Context.TickGroup + 1); // new actors go into the next tick group because this one is already gone
	if (bBlockTillComplete) // we don't deal with newly spawned ticks within the async tick group, they wait until after the async stuff
	{
		QUICK_SCOPE_CYCLE_COUNTER(STAT_TickTask_RunTickGroup_BlockTillComplete);

		bool bFinished = false;
		for (int32 Iterations = 0;Iterations < 101; Iterations++)
		{
			int32 Num = 0;
			for( int32 LevelIndex = 0; LevelIndex < LevelList.Num(); LevelIndex++ )
			{
				Num += LevelList[LevelIndex]->QueueNewlySpawned(Context.TickGroup);
			}
			if (Num && Context.TickGroup == TG_NewlySpawned)
			{
				SCOPE_CYCLE_COUNTER(STAT_TG_NewlySpawned);
				TickTaskSequencer.ReleaseTickGroup(TG_NewlySpawned, true);
			}
			else
			{
				bFinished = true;
				break;
			}
		}
		if (!bFinished)
		{
			// this is runaway recursive spawning.
			for( int32 LevelIndex = 0; LevelIndex < LevelList.Num(); LevelIndex++ )
			{
				LevelList[LevelIndex]->LogAndDiscardRunawayNewlySpawned(Context.TickGroup);
			}
		}
	}
}

这里的TickTaskSequencer.ReleaseTickGroup的作用就是把之前创建好的GraphTask根据ETickingGroup来分批的执行,不同的ETickingGroup不能并行执行。由于TickFunction执行的时候可能会导致其他TickFunction的注册,如果通过QueueNewlySpawned发现有新的TickFunction注册过来,则再执行一遍TickTaskSequencer.ReleaseTickGroup

ReleaseTickGroup依然是一个中转函数,内部会调用到DispatchTickGroup来执行符合条件的FTickFunction:

/**
 * Release the queued ticks for a given tick group and process them.
 * @param WorldTickGroup - tick group to release
 * @param bBlockTillComplete - if true, do not return until all ticks are complete
**/
void ReleaseTickGroup(ETickingGroup WorldTickGroup, bool bBlockTillComplete)
{
	if (bLogTicks)
	{
		UE_LOG(LogTick, Log, TEXT("tick %6llu ---------------------------------------- Release tick group %d"),(uint64)GFrameCounter, (int32)WorldTickGroup);
	}
	checkSlow(WorldTickGroup >= 0 && WorldTickGroup < TG_MAX);

	{
		SCOPE_CYCLE_COUNTER(STAT_ReleaseTickGroup);
		if (SingleThreadedMode() || CVarAllowAsyncTickDispatch.GetValueOnGameThread() == 0)
		{
			DispatchTickGroup(ENamedThreads::GameThread, WorldTickGroup);
		}
		else
		{
			// dispatch the tick group on another thread, that way, the game thread can be processing ticks while ticks are being queued by another thread
			FTaskGraphInterface::Get().WaitUntilTaskCompletes(
				TGraphTask<FDipatchTickGroupTask>::CreateTask(nullptr, ENamedThreads::GameThread).ConstructAndDispatchWhenReady(*this, WorldTickGroup));
		}
	}
	// 省略一些代码
}

这个DispatchTickGroup负责遍历当前WorldTickGroup里计算好的TickTask,一个一个的去激活,真正的投递到多线程任务框架GraphTask中,这里还有一个处理HiPriTickTasks高优先级任务的逻辑,我们这里就不去做介绍了,主要关注TickArray

void DispatchTickGroup(ENamedThreads::Type CurrentThread, ETickingGroup WorldTickGroup)
{
	QUICK_SCOPE_CYCLE_COUNTER(STAT_DispatchTickGroup);
	for (int32 IndexInner = 0; IndexInner < TG_MAX; IndexInner++)
	{
		TArray<FTickGraphTask*>& TickArray = HiPriTickTasks[WorldTickGroup][IndexInner]; //-V781
		if (IndexInner < WorldTickGroup)
		{
			check(TickArray.Num() == 0); // makes no sense to have and end TG before the start TG
		}
		else
		{
			for (int32 Index = 0; Index < TickArray.Num(); Index++)
			{
				TickArray[Index]->Unlock(CurrentThread);
			}
		}
		TickArray.Reset();
	}
	for (int32 IndexInner = 0; IndexInner < TG_MAX; IndexInner++)
	{
		TArray<FTickGraphTask*>& TickArray = TickTasks[WorldTickGroup][IndexInner]; //-V781
		if (IndexInner < WorldTickGroup)
		{
			check(TickArray.Num() == 0); // makes no sense to have and end TG before the start TG
		}
		else
		{
			for (int32 Index = 0; Index < TickArray.Num(); Index++)
			{
				TickArray[Index]->Unlock(CurrentThread);
			}
		}
		TickArray.Reset();
	}
}

可以看出TickTasks是一个二维数组,每个维度的索引都是ETickingGroupTickTasks[A][B]存储的是StartGroupAEndGroupBFTickFunction,由于B永远是不小于A的,所以上面的第一层遍历里会有一个check(TickArray.Num() == 0)

tick优化

tick调度优化

每帧执行的UWorld::StartFrame需要做如下的两件事:

  1. 调用FTickTaskLevel::StartFrame,这个函数负责为所有带TickIntervalFTickFunction维护好有序链表AllCoolingDownTickFunctions
  2. 调用FTickTaskLevel::QueueAllTicks,这个函数为所有的FTickFunction根据其Prerequisites计算出ActualStartTickGroupActualEndTickGroup,并生成带依赖的GraphTask,投递到TickTasks这个二维数组中

这两个过程都会对所有的FTickFunction执行遍历。由于每个Actor/ActorComponent都会注册FTickFunction,在Actor/ActorComponent的数量和种类都变多的情况下,这个过程就会变得非常的耗时。同时我们根据代码逻辑可以看出,在没有FTickFunction的增加和删除,没有Prerequisites的修改,没有TickInterval的修改的情况下,最终生成的TickTasks里的内容应该是不怎么变动的。在这种情况下没必要每帧都重新生成每个FTickFunction对应的GraphTask,基本可以复用之前的TickTasks。由于Actor/ActorComponent的创建和删除是一个非常低频的操作,TickInterval的修改与Prerequisites的修改也是非常罕见的,此时执行TickTasks的复用就可以省下来非常多的计算资源。如果我们能高效的处理TickInterval的修改与Prerequisites的修改,Tick的调度性能就会得到一个非常大的提升。

现在先来假设一种简化情况,如果所有的FTickFunctionPrerequisites都是空数组,我们应该如何实现UWorld::StartFrame。此时最简单直白的实现就是根据注册FTickFunction的时候,根据每个FTickFunction声明的StartGroupEndGroup直接投递到TickTasks[StartGroup][EndGroup]中,同时不再生成GraphTask。然后UWorld::RunTickGroup的时候直接对TickTasks存储的FTickFunction执行遍历:

  1. 如果这个FTickFunction没有设置TickInterval,则直接执行这个FTickFunction
  2. 如果这个FTickFunction设置了TickInterval,则根据这个FTickFunction上记录的LastTickTs判定是否已经超时,如果超时则执行这个FTickFunction

每个FTickFunction在执行之后,都会设置LastTickTs为当前时间戳。 在这种设定下UWorld::StartFrame就不需要做任何维护性的内容,所有的FTickFunctionTickInterval修改都不需要做额外的后处理。同时UWorld::RunTickGroup的性能也得到了很大程度的提升,因为我们跳过了GraphTask::DoTask这个虚函数的调用。这个GraphTask::DoTask自带的开销对于一些小的Tick函数来说还是比较明显的,以下图为例,UPathFollowingComponent::TickComponent在没有寻路任务的时候基本就是空跑,总共400ns一次,但是GraphTask::DoTask部分的消耗大概为500ns,已经大于ExecuteTickTickComponent两个虚函数的总和了:

GraphTask的overhead

接下来考虑复杂一些的情况,我们允许FTickFunction声明其Prerequisites,但是这个Prerequisites数组需要在注册的时候就固定,且要求Prerequisites中的所有FTickFunction都已经注册。此时我们只需要对FTickFunction的注册逻辑做修改,照搬一下原来的FTickTaskLevel::QueueAllTicks里计算ActualStartTickGroupActualEndTickGroup部分,然后投递到TickTasks[ActualStartTickGroup][ActualEndTickGroup]数组中,省下的UWorld::RunTickGroup则不需要做任何修改,依然可以脱离TaskGraph去执行,这样每帧的UWorld::RunTickGroup效率依然不变,只是UWorld::StartFrame里要处理一下新添加的FTickFunctionActualStartTickGroupActualEndTickGroup计算部分,这个依然是很轻量的,由于是低频操作,所以带来的额外影响可以忽略不记。

最后考虑一种最复杂的情况:FTickFunctionPrerequisites允许动态修改。此时由于Prerequisites的修改会导致FTickFunctionActualStartTickGroupActualEndTickGroup被修改,需要重新计算所有相关的FTickFunctionTickGroup值之后,再在TickTasks中对这些FTickFunction做调整。为了高效的应对这些调整,需要在UWorld::StartFrame创建一个拓扑排序系统,将受影响的FTickFunction都计算出来,从TickTasks中进行删除,然后更新TickGroup,最后重新向TickTasks的投递。由于拓扑排序是线性时间复杂度,且修改Prerequisites是低频操作,所以这里的额外影响依然可控。

批量tick优化

使用TaskGraph系统来管理大量的Tick函数还有一个显著的性能损耗,由于Tick函数的执行顺序除了PrerequisitesTickGroup之外基本没有其他限制,所以会频繁的出现不同的Tick函数交错执行的情况。在当代的计算机体系结构下,对于内存的访问都需要经过L1,L2,L3这三级缓存。执行一个函数的时候需要将这个函数对应的指令内存区域全都加载到这三级缓存里。

三级缓存

由于指令缓存的容量是有限的,不同函数交错执行的情况会导致指令缓存被不断的换入换出。而每一级缓存的读取速度相差是很大的,这样就导致了ABCABCABC这样的执行顺序相对于AAABBBCCC这样的执行顺序速度慢很多。特别是在函数体比较小的时候,这种缓存交换的惩罚就会越大。针对这个问题,在Unreal Fest 2019Sea of Thieves的开发商提出了将同一个Tick函数聚合起来批量执行的优化方式,效果还不错,视频在Aggregating Ticks to Manage Scale in Sea of Thieves

批量执行的优化效果

UE5.5中,也加入了批量执行的选项bAllowTickBatching。不过UE为了避免修改太多的代码,这里的优化就没有Sea of Thieves中那么的激进。这个批量执行的调整在原来的QueueTickFunctions,之前调用的是QueueTickTask,现在替换为了QueueOrBatchTickTask

/** This will add to an existing batch, create a new batch, or just spawn a single task and return null */
FTickBatchInfo* QueueOrBatchTickTask(TArray<FTickFunction*>& Prerequisites, FTickFunction* TickFunction, const FTickContext& TickContext)
{
	// Batching is not supported with the old frontend
#if TASKGRAPH_NEW_FRONTEND
	if (bAllowBatchedTicksForFrame)
	{
		FTickGroupCondition Condition = FTickGroupCondition(TickFunction);

		if (CanBatchCondition(Condition))
		{
			// 处理聚合tick
		}
	}
#endif //TASKGRAPH_NEW_FRONTEND

	// No batching, create a single task
	// FGraphEventArray array has some inline members so it is faster to not explicitly reserve space
	FGraphEventArray PrerequisiteEvents;
	for (FTickFunction* Prereq : Prerequisites)
	{
		PrerequisiteEvents.Add(Prereq->GetCompletionHandle());
	}

	QueueTickTask(&PrerequisiteEvents, TickFunction, TickContext);

	return nullptr;
}

QueueOrBatchTickTask这里会尝试执行聚合Tick,如果尝试失败就会走到默认的QueueTickTask函数。这里的FTickGroupCondition是一个非常简单的结构体,用来判定一个TickFunction是否能否聚合执行:


/** This is an integer that represents the conditions for which ticks can be grouped together */
struct FTickGroupCondition
{
	union
	{
		uint32 IntVersion;
		struct {
			TEnumAsByte<ETickingGroup> StartGroup;
			TEnumAsByte<ETickingGroup> EndGroup;
			bool bHighPriority;
			bool bIsBatch;
		};
	};
	
	FTickGroupCondition()
		: IntVersion(0)
	{
	}

	FTickGroupCondition(const FTickFunction* TickFunction)
		: StartGroup(TickFunction->GetActualTickGroup())
		, EndGroup(TickFunction->GetActualEndTickGroup())
		, bHighPriority(TickFunction->bHighPriority)
		, bIsBatch(TickFunction->bAllowTickBatching)
	{
	}
};

/** Return true if this tick condition is safe to batch */
FORCEINLINE bool CanBatchCondition(FTickGroupCondition Condition)
{
	// Don't batch high priority ticks or ones that last more than a single tick group
	return Condition.bIsBatch && !Condition.bHighPriority && Condition.StartGroup == Condition.EndGroup;
}

这里的聚合执行条件其实挺简单的,开启了允许批量执行,不是高优先级,且StartGroup等于EndGroup。如果可以聚合的话,先查询目前能否与其他TickFunction合并在一个BatchGroup里执行:

// Look for an appropriate batch
FTickBatchInfo* BatchInfo = nullptr;
for (int32 BatchIndex = 0; BatchIndex < TickBatchesNum; BatchIndex++)
{
	if (Condition == TickBatches[BatchIndex].Key)
	{
		FTickBatchInfo* PossibleBatch = TickBatches[BatchIndex].Value.Get();
		bool bPrerequisitesMatch = true;

		for (FTickFunction* Prereq : Prerequisites)
		{
			// Ignore prerequisites that are already in this batch
			if (Prereq->GetTaskPointer(FTickFunction::ETickTaskState::HasTask) != PossibleBatch->TickTask && !PossibleBatch->TickPrerequisites.Contains(Prereq))
			{
				bPrerequisitesMatch = false;
				break;
			}
		}
		if (bPrerequisitesMatch)
		{
			BatchInfo = PossibleBatch;
			break;
		}
	}
}

TickBatches是目前已经创建好的批量执行组,这里会遍历已有的批量执行组来查看其BatchCondition是否一样,如果一样的话还要查询当前TickFunction的任何一个Prerequisites都不能在这个组里,且这个组不能与当前TickFunction包含同样的前置依赖。如果没有依赖冲突的话就会将当前TickFunction放入到这个TickBatch中,如果没有合适的TickBatch则需要创建一个新的TickBatch:

if (!BatchInfo)
{
	// Create a new batch, resizing array if needed
	check(TickBatchesNum <= TickBatches.Num());
	if (TickBatchesNum == TickBatches.Num())
	{
		TickBatches.Emplace(FTickGroupCondition(), MakeUnique<FTickBatchInfo>());
		check(TickBatches.IsValidIndex(TickBatchesNum));
	}

	TickBatches[TickBatchesNum].Key = Condition;
	BatchInfo = TickBatches[TickBatchesNum].Value.Get();
	TickBatchesNum++;

	BatchInfo->TickPrerequisites = Prerequisites;
	check(BatchInfo->TickTask == nullptr);

	// Create the batched task now
	FGraphEventArray PrerequisiteEvents;
	for (FTickFunction* Prereq : Prerequisites)
	{
		PrerequisiteEvents.Add(Prereq->GetCompletionHandle());
	}

	FTickContext UseContext = SetupTickContext(TickFunction, TickContext);
	BatchInfo->TickTask = TGraphTask<FBatchTickFunctionTask>::CreateTask(&PrerequisiteEvents, ENamedThreads::GameThread).ConstructAndHold(BatchInfo, &UseContext);

	AddTickTaskCompletion(Condition.StartGroup, Condition.EndGroup, BatchInfo->TickTask, Condition.bHighPriority);
}
// Add this tick function to batch, which could be the first one
BatchInfo->TickFunctions.Add(TickFunction);
TickFunction->SetTaskPointer(FTickFunction::ETickTaskState::HasTask, BatchInfo->TickTask);

return BatchInfo;

这里的每个TickBatch都会生成一个FBatchTickFunctionTask,这样TaskGraph的调度与执行单位从面向单个TickFunctionFTickFunctionTask变为了面向多个TickFunctionFBatchTickFunctionTask。在一个FBatchTickFunctionTask::DoTask被调度执行时,内部的FTickFunction直接在For循环内依次执行,这样就省略了很多FTickFunctionTask::DoTask引入的虚函数执行开销,同时也增加了指令缓存的局部性:

/**
* Actually execute the tick.
* @param	CurrentThread; the thread we are running on
* @param	MyCompletionGraphEvent; my completion event. Not always useful since at the end of DoWork, you can assume you are done and hence further tasks do not need you as a prerequisite.
* However, MyCompletionGraphEvent can be useful for passing to other routines or when it is handy to set up subsequents before you actually do work.
*/
void FBatchTickFunctionTask::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
	check(TickBatch && TickBatch->TickFunctions.Num() > 0);
	for (FTickFunction* Target : TickBatch->TickFunctions)
	{
		if (Context.bLogTick)
		{
			Target->LogTickFunction(CurrentThread, Context.bLogTicksShowPrerequistes);
		}
		if (Target->IsTickFunctionEnabled())
		{
#if DO_TIMEGUARD
			FTimerNameDelegate NameFunction = FTimerNameDelegate::CreateLambda( [&]{ return FString::Printf(TEXT("Slowtick %s "), *Target->DiagnosticMessage()); } );
			SCOPE_TIME_GUARD_DELEGATE_MS(NameFunction, 4);
#endif
			LIGHTWEIGHT_TIME_GUARD_BEGIN(FBatchTickFunctionTask, GTimeguardThresholdMS);
			Target->ExecuteTick(Target->CalculateDeltaTime(Context.DeltaSeconds, Context.World), Context.TickType, CurrentThread, MyCompletionGraphEvent);
			LIGHTWEIGHT_TIME_GUARD_END(FBatchTickFunctionTask, Target->DiagnosticMessage());
		}
		Target->ClearTaskInformation();  // This is stale and a good time to clear it for safety
	}
}

FBatchTickFunctionTask内部拥有的TickFunction其实还是有些杂乱的,开启了批量执行之后还是可能会出现ABCABCABC这样的执行序列。如果想要进一步对缓存局部性进行提升的话,可能需要对TickFunction自动加上名字,然后FTickGroupCondition根据名字的Hash来补充没有使用14bit,这样一个大的FBatchTickFunctionTask就可以进一步细分为多个更小的FBatchTickFunctionTask,指令缓存的局部性就更高了。

struct FTickGroupCondition
{
	union
	{
		uint32 IntVersion;
		struct {
			TEnumAsByte<ETickingGroup> StartGroup;
			TEnumAsByte<ETickingGroup> EndGroup;
			bool bHighPriority;
			bool bIsBatch;
			// 还有14个bit没有使用
		};
	};
};