top of page
Search
  • Bob

Unreal - Tick Functions, Delta Time, and the Task Graph

Updated: Oct 20, 2021



The Tick

All video games need to execute code before rendering a frame. The Tick() function is the core mechanism in Unreal that allows your game to execute code every single frame.

Ticking in UE is more complex than you might think, so let's go on a bit of deep dive into how it works. To follow along, you should have a basic understanding of what Tick() is and how it's used in Unreal. If you're unsure about the basics of ticking, or are a little rusty, Epic has a great overview right here . https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Actors/Ticking/


There are actually two different ticking implementations you can leverage in UE.

The first uses FTickFunction and supports features like TickGroups,TickIntervals,TickDependencies. This is what AActor uses.

The second is is for gamethread tickable objects. To use, just inherit from FTickableObjectBase and override tick. It is ticked by UWorld::Tick after all the other groups fire. It is often used by object managers like Niagara, AI, Sequencer, and NetworkReplayStreaming.

Understanding UE ticking also helps immensely when using profiling tools, as they are typically the top level code you drill into when you are cpu bound.


The Tick and Game Performance

It is worth emphasizing at this point that anything you do in Tick() can potentially impact your game's performance. Tick functions are executed many many times every second. Anything you do in the tick needs to be done fast, otherwise you can hurt your game's framerate. Because of this, while it is common to prototype your tick in bp, it is often the first thing to consider moving to c++ in a shipping game.

Getting a handle on ticking is critical for defining the behavior and performance of your game. UE4 gives you a couple of ways to wrangle your ticks through bp or code:


  • Tick Interval This controls the frequency of the tick. e.g. How often it occurs. It is probably worth mentioning that this isn't exact, and that this is the minimum amount of time that will pass between ticks. (The actual time between ticks will depend on other factors)

  • Tick Groups This controls when during the frame the tick is executed.

  • Tick Dependencies This lets you to define prerequisites for your tick and control the order of execution of different tick code.

Tick Groups

Tick Groups are predefined by UE4 and each one is intended to execute at a specific point in the rendering of a single frame.

  • TG_PrePhysics

  • TG_DuringPhysics

  • TG_PostPhysics

  • TG_PostUpdateWork

All actors in a given Tick Group are executed before any of the actors in the following group. In code, Actors are typically assigned to a TickGroup in the constructor


Tick Dependencies

Tick dependencies give a more granular control of the ordering of Tick calls by letting you specify dependencies between actors within a group. Both AddTickPrerequisiteActor and AddTickPrerequisiteComponent can be used to require a seperate Actor(or Component) to tick first.


Tick Interval

You can control the frequency of a tick function using the TickInterval. This check will verify that amount of time has elapsed before allowing the task to execute.


Tick Tasks

Since the order that tick functions are called can change every frame, UE creates a task for each tick function and adds it to a TSet for that TickGroup, then each set of tasks is subsequently executed in the correct order. This is done every frame.

This leverages the built-in TGraphTask system to schedule and execute tasks for the associated tick functions. More on this later...


Delta Time

A critical piece of information for many tick functions is DeltaTime. Delta Time represents the amount of time that has occured since the last tick. This is particularly important for any behavior that is to be framerate independant.

Because your game runs at different speeds on different machines, the number of ticks executed during any given second of game play can vary greatly from machine to machine.

For example, if you created a moving platform by simply adding 10 units to it's x location every frame, you may be surprised to find that the platform moves slower on slower machines. This is because the slower machine executes fewer ticks per second.

Instead, you should add SPEED * DeltaTime to determine the platform's location. This will result in the platform moving at the same rate on every machine.

Calculating Delta Time

Delta time is calculated in the main loop before any ticking is done. The main game loop is defined in LaunchEngineLoop.cpp. Specifically, in the function void FEngineLoop::Tick(), which calls void UEngine::UpdateTimeAndHandleMaxTickRate(), which calculates how much time has passed since the previous frame was rendered (aka DeltaTime). It also does some other things, such as optionally applying framerate clamping, smoothing, etc..

On Windows, QueryPerformanceCounter is used to get the current time.

FApp Delta Time

Ultimately, FApp::SetCurrentTime(CurrentRealTime) is called to set a global variable (double FApp::DeltaTime) with the elapsed time between frames.

E.g. FApp::SetDeltaTime(FApp::GetCurrentTime() - LastRealTime); Although this DeltaTime global can be read directly using FApp::GetDeltaTime(), it is much more common to access it through the World().

UWorld Delta Time

The UWorld object also stores DeltaTimeSeconds and several useful variants like RealTimeSeconds for easy access.

After setting FApp::DeltaTime, FEngineLoop::Tick() calls GEngine->Tick(FApp::GetDeltaTime(), bIdleMode) Depending on whether you are running in PIE or as a game (as there are 2 possible types of engine instances) - but both inherit from UEngine: class UNREALED_API UEditorEngine : public UEngine class ENGINE_API UGameEngine : public UEngine Both implement the Tick() function: void UEditorEngine::Tick( float DeltaSeconds, bool bIdleMode ) void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode ) And both ultimately tick the world object, passing in the calculated DeltaSeconds ...World()->Tick( LEVELTICK_All, DeltaSeconds ) Ultimately, delta time is stored in a member variable DeltaTimeSeconds of the UWorld.

/** Frame delta time in seconds adjusted by e.g. time dilation. */
float DeltaTimeSeconds;

As a result, DeltaTimeSeconds is set in UWorld::Tick, which gets called before any other tick functions get called, meaning that the DeltaTime value will be the same in any Tick() method in any object within a single frame. The World's DeltaTimeSeconds is passed to each actors tick function when that function is called. (It is passed through a context object that is attached to the wrapped TickTask. More on this later).

It is a common idiom in unreal source code and unreal projects to access this value directly using the GetWorld()->GetDeltaSeconds() accessor, rather than have a long chain of nested function calls passing the value from Tick(). This facilitates more loosely coupled code that is easier to read and modify.

It is also worth noting that many game engines follow a similar idiom, e.g. Unity has Time.DeltaTime. "The time in seconds it took to complete the last frame (Read Only)."

Tick Logging

A very useful tool for understanding ticking is to enable the console command Tick.LogTicks. This will dump out the following: UE_LOG(LogTick, Log, TEXT("tick %s [%1d, %1d] %6llu %2d %s"), Target->bHighPriority ? TEXT("*") : TEXT(" "), (int32)Target->GetActualTickGroup(), (int32)Target->GetActualEndTickGroup(), (uint64)GFrameCounter, (int32)CurrentThread, *Target->DiagnosticMessage());

Here is some example output:

LogTick: tick 250846 ---------------------------------------- Start Frame
LogTick: tick 250846 ---------------------------------------- Release tick group 0
LogTick: tick * [0, 0] 250846  3 PlayerController /Temp/UEDPIE_0_Untitled_1.Untitled_1:PersistentLevel.PlayerController_0[TickActor]
LogTick: tick * [0, 0] 250846  3 CharacterMovementComponent /Temp/UEDPIE_0_Untitled_1.Untitled_1:PersistentLevel.ThirdPersonCharacter_C_0.CharMoveComp[TickComponent]

More Tick Details

Tick functions are defined as UStructs with an abstract ExecuteTick() function. Taken from EngineBaseTypes.h:

/** 
* Abstract Base class for all tick functions.
**/
USTRUCT()
struct ENGINE_API FTickFunction
{
	GENERATED_USTRUCT_BODY()
...skipping 
/** 
* 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.
**/
virtual void ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
	check(0); // you cannot make this pure virtual in script because it wants to create constructors.
}
} 

It is worth noting there is also a child struct of FTickFunction specifically created for Actor tick functions:

/** 
* 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;
   ...
}

Every AActor has an FActorTickFunction UProperty. You can see this, for example, in Actor.h:

/**
* 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/latest/INT/API/Runtime/Engine/Engine/FTickFunction/
* @see AddTickPrerequisiteActor(), AddTickPrerequisiteComponent()
*/
UPROPERTY(EditDefaultsOnly, Category=Tick)
struct FActorTickFunction PrimaryActorTick;

High Level Implementation Summary

UWorld is ticked first. The FTickTaskManager maintains a TSet of all tick functions in AllEnabledTickFunctions (this includes the tick functions for all actors spawned in a level). Every tick, TickTasks are created for each Tick function that needs to get called. These tasks are then added in the correct order to a task map - organized by TickGroup. Any tick dependenices are resolved when adding these tasks. Finally, the respective tick functions are executed via the ExecuteTask function.

The Task Graph

Unreal has a built-in task graph system designed for running many short-lived tasks.(For long running tasks, have a look at FRunnableThread). Not only can the task graph spread this work over multiple threads, but it also lets you set up pre-requisites for any given task and will ensure that tasks don't fire before their dependencies have.

For an example of this in action, check out this blog post

Some example uses of the task graph in the engine include: FParallelAnimationEvaluationTask, FParticleFinalizeTask,FParallelBlendPhysicsTask,FSimpleDelegateGraphTask,FDelegateGraphTask

As an example task, suppose that you have some work that needs to be done later in the frame, then you could use the built-in CreateAndDispatchWhenReady to create a FDelegateGraphTask and execute it on an available worker thread, like so:

FGraphEventRef DoThisTask = FFunctionGraphTask::CreateAndDispatchWhenReady([SomeCaptureVariable]()
        {
            //Do Some Work,
            GRandomVariable = SomeCaptureVariable;
        }, TStatId(), NULL, ENamedThreads::AnyThread);

How are Tasks Scheduled for executation?

After Prerequisites are determined, QueueTask is called to schedule the tasks on a thread. Each FNamedTaskThread has a Queue of work that it processes every frame.

FFunctionGraphTask

You can easily crate your own tasks by implementing DoTask(). Any class that implements a DoTask() function can be used as a templated argument for a TGraphTask<> (typically created using CreateTask()). This user defined class (TTask) is wrapped by TGraphTask.

TGraphTask has an ExecuteTask that calls the DoTask on the wrapped task.

Other key apsects of TGraphTask include:

  • ConstructAndDispatchWhenReady: Passthrough internal task constructor and dispatch. Note! Generally speaking references will not pass through; use pointers

  • ConstructAndHold: Passthrough internal task constructor and hold. Within the engine, the TickTask is the only one that uses ConstructAndHold. ConstructAndHold does not execute the task until Release is called.

  • CreateTask:Factory to create a task and return the helper object to construct the embedded task and set it up for execution.

You can see an example user defined task in TaskGraphInterfaces.h:

//The user defined task type can take arguments to a constructor. These arguments //(unfortunately) must not be references.
//The API required of TTask:

class FGenericTask
{
	TSomeType	SomeArgument;
public:
	FGenericTask(TSomeType InSomeArgument) // CAUTION!: Must not use references in the constructor args; use pointers instead if you need by reference
		: SomeArgument(InSomeArgument)
	{
		// Usually the constructor doesn't do anything except save the arguments for use in DoWork or GetDesiredThread.
	}
	~FGenericTask()
	{
		// you will be destroyed immediately after you execute. Might as well do cleanup in DoWork, but you could also use a destructor.
	}
	FORCEINLINE TStatId GetStatId() const
	{
		RETURN_QUICK_DECLARE_CYCLE_STAT(FGenericTask, STATGROUP_TaskGraphTasks);
	}

	[static] ENamedThreads::Type GetDesiredThread()
	{
		return ENamedThreads::[named thread or AnyThread];
	}
	void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
	{
		// The arguments are useful for setting up other tasks. 
		// Do work here, probably using SomeArgument.
		MyCompletionGraphEvent->DontCompleteUntil(TGraphTask<FSomeChildTask>::CreateTask(NULL,CurrentThread).ConstructAndDispatchWhenReady());
	}
};

Task Graph And Ticking

The task graph is particularly useful for TickFunctions, since ticks represent a bunch of short-lived units of work that need to be executed in different order every frame. By bundling up the TickFunctions properly sorted into some TSets, UE has a mechanism to store Tick Tasks and execute them at the proper time.

The general approach for ticking is to loop over all the tick functions in all loaded levels, creating a TickTask for each TickFunction in that level, and adding that task to the appropriate TickTask queue. In addition to adding the task, any tick dependencies can be resolved and added into the queue first. This all occurs in FTickTaskManagerInterface::Get().StartFrame. After all TickTasks are added to the appropriate queues, each TickGroup's queue is executed sequentially in the correct order.

BTW - On windows, ticks are always processed on the gamethread. StartFrame:

#if !PLATFORM_WINDOWS
		// the windows scheduler will hang for seconds trying to do this algorithm, threads starve even though other threads are calling sleep(0)
		if (!FTickTaskSequencer::SingleThreadedMode())
		{
			bConcurrentQueue = !!CVarAllowConcurrentQueue.GetValueOnGameThread();
		}
#endif

		if (!bConcurrentQueue)
		{
			int32 TotalTickFunctions = 0;
			for( int32 LevelIndex = 0; LevelIndex < LevelList.Num(); LevelIndex++ )
			{
				TotalTickFunctions += LevelList[LevelIndex]->StartFrame(Context);
			}
			INC_DWORD_STAT_BY(STAT_TicksQueued, TotalTickFunctions);
			for( int32 LevelIndex = 0; LevelIndex < LevelList.Num(); LevelIndex++ )
			{
				LevelList[LevelIndex]->QueueAllTicks();
			}
		}
		else
		{
			for( int32 LevelIndex = 0; LevelIndex < LevelList.Num(); LevelIndex++ )
			{
				LevelList[LevelIndex]->StartFrameParallel(Context, AllTickFunctions);
			}
			INC_DWORD_STAT_BY(STAT_TicksQueued, AllTickFunctions.Num());
			FTickTaskSequencer& TTS = FTickTaskSequencer::Get();
			TTS.SetupAddTickTaskCompletionParallel(AllTickFunctions.Num());
			for( int32 LevelIndex = 0; LevelIndex < LevelList.Num(); LevelIndex++ )
			{
				LevelList[LevelIndex]->ReserveTickFunctionCooldowns(AllTickFunctions.Num());
			}
			ParallelFor(AllTickFunctions.Num(),
				[this](int32 Index)
				{
					FTickFunction* TickFunction = AllTickFunctions[Index];

					TArray<FTickFunction*, TInlineAllocator<8> > StackForCycleDetection;
					TickFunction->QueueTickFunctionParallel(Context, StackForCycleDetection);
				}
			);
			for( int32 LevelIndex = 0; LevelIndex < LevelList.Num(); LevelIndex++ )
			{
				LevelList[LevelIndex]->ScheduleTickFunctionCooldowns();
			}
			AllTickFunctions.Reset();
		}
	}```
    

### `FTickFunctionTask`
Defined in `TickTaskManager.cpp`, `FTickFunctionTask` is the user defined task created for Tick Functions.  It's `DoTask()` function calls the `ExecuteTick()` function on the wrapped `FTickFunction`.

### `FTickTaskManager`

**`FTickTaskLevel`** Maintains a list of all enabled Tick Functions:
```cpp
/** Master list of enabled tick functions **/
TSet<FTickFunction*>	AllEnabledTickFunctions;

Whenver an Actor is spawned, it registers its Tick() function (and its components' Tick() functions) with the level it is spawned into.

/**
* Adds the tick function to the master list of tick functions.
* @param Level - level to place this tick function in
**/
void FTickFunction::RegisterTickFunction(ULevel* Level)

This calls FTickTaskManager::Get().AddTickFunction(Level, this) which adds it to that level's AllEnabledTickFunctions.

Specifically:

/** Master list of enabled tick functions **/
TSet<FTickFunction*>  AllEnabledTickFunctions;

Every frame, this list of AllEnabledTickFunctions is traversed, and a task added for it. This occurs in FTickTaskManager::StartFrame, which is called from UWorld::Tick.

**
	 * 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 StartFrame(UWorld* InWorld, float InDeltaSeconds, ELevelTick InTickType, const TArray<ULevel*>& LevelsToTick) override
	{...

via FORCEINLINE void StartTickTask(const FGraphEventArray* Prerequisites, FTickFunction* TickFunction, const FTickContext& TickContext)

class FTickTaskSequencer is a class that handles the actual tick tasks and starting and completing tick groups. Two key members to store tasks:

/** HiPri Held tasks for each tick group. */
TArrayWithThreadsafeAdd<TGraphTask<FTickFunctionTask>*> HiPriTickTasks[TG_MAX][TG_MAX];

/** LowPri Held tasks for each tick group. */
TArrayWithThreadsafeAdd<TGraphTask<FTickFunctionTask>*> TickTasks[TG_MAX][TG_MAX];

DispatchTickGroup does the actual dispatch by calling unlock on the HiPriTickTasks and TickTasks arrays.

Physics Tick Example

As an example, let's deep dive into how Physics is ticked. Not only will this illustrate how some of the tick infrustructure works, it also helps in understanding the PhysX integration.

The Physics tick function is simply a member variable on UWorld:

FStartPhysicsTickFunction StartPhysicsTickFunction; // Tick function for starting physics												
FEndPhysicsTickFunction EndPhysicsTickFunction;//Tick function for ending physics												

Every frame UWorld registers these tick functions in SetupPhysicsTickFunctions(DeltaSeconds); called from UWorld::Tick. This is called before setting any other tick functions. SetupPhysicsTickFunctions registers the appropriate physics tick functions in the appropriate group. Thus, the Physics tick functions are the first in each respective group's task queue.

The StartPhysicsTickFunction ExecuteTick explicitly calls StartPhysicsSim.

void FStartPhysicsTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
	Target->StartPhysicsSim();
}

Finally, void UWorld::StartPhysicsSim() begins the physics for this frame by calling: PhysScene->StartFrame();

In summary, PhysScene->StartFrame is called via a TickFunction regestered in the TG_StartPhysics Tick Group. It is the first function registered every frame and is always called first.

Summary

The UE TaskGraph enables you to easily schedule short-running tasks. UE utilizes the TaskGraph to implement it's ticking system. The tick is a standard gamedev idiom for implementing behavior. UE exposes several features such as Tick Groups and Tick Dependencies to help you wrangle tick behavior and timing.


20,448 views0 comments

Recent Posts

See All

New Content On Epic

I have been posting new content directly to Epic:  https://dev.epicgames.com/community/profile/vElq/OptimisticMonkey#learning

bottom of page