Tick
Tick이란 코드나 블루프린트 스크립트를 프레임과 같은 일정한 주기로 실행시키는 것을 의미한다. 게임의 Actor, Component들이 Tick을 수행하는 상대적인 순서를 이해하는 것은 게임 실행 방식의 일관성을 보장하고, 또 엔진에서 따로 수행되는 작업과의 Tick이 어긋나며 생기는 문제를 피할 수 있다.
Actor와 Component는 설정된 최소 시간 간격으로 프레임 Tick을 설정할 수도 있고, Tick을 전혀 하지 않도록 할 수도 있다. 또한 엔진의 프레임 별 업데이트 루프에서 서로 다른 단계에서 Grouping이 가능하며, 특정 Tick이 완료될 때까지 기다리도록 설정도 가능하다.
TickFunction
언리얼 엔진에서 Tick을 어떻게 발생시키는지 알아보자. 가장 기본적으로는 FTickFunction 이라는 객체가 있다. 이 객체는 언리얼에서 생기는 모든 Tick에 대해 정의하고, Tick을 요구하는 객체에 Tick 이벤트를 발생시키도록 하며, 각 Tick이 어떤 순서 및 형태로 발생해야하는지에 대해 명세할 수 있다. 가장 많이 사용되는 Actor 객체와 ActorComponent는 Tick을 어떻게 등록하였을까?
Actor와 Component은 Tick을 수행하기 위해서 각자 FActorTickFunction, FActorComponentTickFunction이라는 객체를 PrimaryActorTick, PrimaryComponentTick이라는 이름으로 갖고 있다.
Actor의 경우 다음 Tick 함수와 TickFunction 객체를 갖고있다.
// Tick 함수
virtual void Tick( float DeltaSeconds );
// Tick Function 객체
FActorTickFunction PrimaryActorTick;
Component의 경우 다음 Tick 함수와 TickFunction 객체를 갖고있다.
// Tick 함수
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction);
// Tick Function 객체
FActorComponentTickFunction PrimaryComponentTick;
TickFunction은 Tick 처리를 위한 설정값과, TickGroup 지정, Tick Dependency와 같은 데이터를 갖고 있는 객체인데 BeginPlay 시점에 등록된다. (Actor.cpp의 RegisterAllActorTickFunctions 함수 참고) 등록된 TickFunction 객체들은 TickGroup과 Tick Dependency에 맞게 Target의 Tick 함수를 호출시킨다.
엔진코드 분석
등록된 Tick들은 어떤 원리로 동작할까? 먼저 TickFunction의 RegisterTickFunction 함수를 추적해보면 FTickTaskManager에 등록되는 것을 확인할 수 있다.
FTickTaskManager::Get().AddTickFunction(Level, this);
그리고 이 함수는 인자로 레벨을 받는데, 이를 통해 Tick이 레벨에 종속적이라는 것을 확인할 수 있다.
최종적으로 TickFunction이 자리잡는 위치는 FTickTaskLevel이라는 객체의 AddTickFunction이다.
호출 스택은 다음과 같다.
TGraphTask:: ExecuteTask
TickFunctionTask::DoTask (GameThread)
TickFunction::ExecuteTick
Actor::TickActor
TickGroup
TickGroup을 통해 Tick의 호출 순서에 대한 명세를 세팅할 수 있다. 이러한 TickGroup은 물리 시뮬레이션과 같은 엔진 내의 여러 작업들 사이 어느 프레임 위치에서 실행시킬지를 결정하기 위해 사용된다.
각 TickGroup은 그룹 내 모든 Actor와 Component들의 Tick이 끝나고 나서야 다음 TickGroup을 실행시킨다.
또한 하나의 TickGroup 내에서도 특정 Actor와 Component들 사이의 Tick Dependency를 결정할 수 있다.
TickGroup을 이용하는 것과 Tick Dependency를 이용하는 것은 게임 내의 물리 의존적인 작업이나 여러 Actor와 Component간 연계된 작업을 처리하기 위해 필수적이다.
(LevelTick.cpp 1544 line 참고)
(Link 참고)
설정값 및 사용방법
TickFunction에는 몇 가지 자주 이용하는 설정값들이 있다.
1. bCanEverTick
TickFunction을 이용할 것인지에 대한 설정값이다.
이 값이 false이면 TickFunction의 등록 자체를 막아버리기 때문에,
절대로 Tick을 사용하지 않을 객체에 대해서만 false로 처리하도록 한다.
2. bStartWithTickEnabled
선택적으로 Tick을 이용하는 상황에 대한 설정값이다.
이 값이 false로 설정되면 TickFunction은 등록이 되지만,
실질적인 Tick 함수는 호출이 되지 않는다.
상황에 맞게 Tick을 껐다 켰다 해야하는 객체에 대해서 사용하면 좋을 것이다.
3. bTickEvenWhenPaused
게임이 paused 되었어도 Tick을 호출해야하는 객체에 대해 사용된다.
4. TickGroup
TickGroup을 지정한다.
/** Determines which ticking group a tick function belongs to. */
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,
};
5. Tick Dependency 설정
Actor와 Component에는
AddTickPrerequisiteComponent(SomeComponent);
AddTickPrerequisiteActor(SomeActor);
와 같은 함수들이 있는데, 이 함수는 내부적으로 TickFunction에 Tick Dependency를 추가하도록 구현되어 있다.
A라는 Actor와 C1, C2라는 Component가 있다고 가정해보자.
여기서 C2는 반드시 A와 C1이 끝난 후에 처리가 되어야 기능이 올바르게 동작한다면,
다음과 같이 추가해주면 된다.
C2.AddTickPrerequisiteComponent(C1);
C2.AddTickPrerequisiteActor(A);
Actor Tick 설정 예제
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
PrimaryActorTick.bTickEvenWhenPaused = true;
// TickGroup
PrimaryActorTick.TickGroup = TG_PrePhysics;
// Tick Dependency
AddTickPrerequisiteComponent(SomeComponent);
AddTickPrerequisiteActor(SomeActor);
Component Tick 설정 예제
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = true;
PrimaryComponentTick.bTickEvenWhenPaused = true;
// TickGroup
PrimaryComponentTick.TickGroup = TG_PrePhysics;
// Tick Dependency
AddTickPrerequisiteComponent(SomeComponent);
AddTickPrerequisiteActor(SomeActor);
심화 예제
하나의 Component에서 물리 이전, 물리 이후 두 번에 걸쳐 처리해야할 작업이 있는 경우를 가정해보자.
기본적으로 제공되는 TickFunction으로는 PrimaryComponentTick이 있다.
이 객체를 물리 이전으로 TickGroup을 설정한다.
PrimaryComponentTick.TickGroup = TG_PrePhysics;
물리 이후에 처리해야할 Tick을 위해 새로운 TickFunction을 멤버변수로 추가하고 물리 이후로 TickGroup을 설정한다.
// header
FComponentTickFunction PostPhysicsComponentTick;
// source
PostPhysicsComponentTick.TickGroup = TG_PostPhysics;
PostPhysicsComponentTick은 PrimaryComponentTick과 달리 따로 관리를 받는 객체가 아니므로 직접 등록을 해주어야 한다. TickFunction 등록을 위해 이미 Super 클래스에서 호출해 주는 RegisterComponentTickFunctions라는 가상함수가 있다. 이 함수를 override 해준다.
// header
virtual void RegisterComponentTickFunctions(bool bRegister) override;
// source
void UMyComponent::RegisterComponentTickFunctions(bool bRegister)
{
Super::RegisterComponentTickFunctions(bRegister);
if(bRegister)
{
if (SetupActorComponentTickFunction(&PostPhysicsComponentTick))
{
PostPhysicsComponentTick.Target = this;
}
}
else
{
if(PostPhysicsComponentTick.IsTickFunctionRegistered())
{
PostPhysicsComponentTick.UnRegisterTickFunction();
}
}
}
이렇게 작업을 하면 TickComponent 함수가 한번은 PrimaryComponentTick을 통해서 호출되고,
한번은 PostPhysicsComponentTick을 통해서 호출될 것이다.
그러므로 함수 내부에서 이것을 ThisTickFunction을 통해 구분해주어야 한다.
void UMyComponent::TickComponent(
float DeltaTime,
enum ELevelTick TickType,
FActorComponentTickFunction *ThisTickFunction
{
if(ThisTickFunction->TickGroup == ETickingGroup::TG_PrePhysics)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// PrePhysics 처리
}
else if(ThisTickFunction->TickGroup == ETickingGroup::TG_PostPhysics)
{
// PostPhysics 처리
}
}
'게임 엔진 > Unreal' 카테고리의 다른 글
[Unreal] 언리얼로 Google Play 결제 시스템 이용하기 (3) | 2021.03.04 |
---|---|
[Unreal] 언리얼 안드로이드 프로젝트 Google Play에 출시하기 (0) | 2021.02.09 |
[Unreal] 언리얼 프로젝트 소스코드를 옮기는 방법 (Redirect) (0) | 2021.01.19 |
[Unreal] 언리얼 메모리 관리 시스템 (Smart Pointer, GC) (5) | 2021.01.19 |
[Unreal] 언리얼 엔진 디버깅 (0) | 2021.01.12 |