본문 바로가기

UE4

[UE4] Actor의 Tick 관련 #2

문서가 너무 길어져서 1/2부로 내용을 나누었다.
1부 문서는 여기를 참고하기 바란다.
 

5. TickGroup

Actor와 Component의 Tick 발생은 개별의 TickInterval에 의해서 주기가 정해진다. 
이는 객체 차원에서의 주기 관점이고, 메인 루프 관점에서는 TickGroup의 순서에 따라 오브젝트들의 Tick을 처리한다.
앞선 TickGroup에 속한 오브젝트들의 Tick이 모두 실행된 이후에야 뒷 TickGroup에 속한 오브젝트들의 Tick이 실행되는 것이다.
 
ETickingGroup 열거형에는 8개의 아이템이 존재하지만, 
게임 플레이에서 지정할 수 있는 TickGroup은 아래의 4개이며, 아래의 번호 순서가 TickGroup의 실행 순서이다.
  1. TG_PrePhysics : 별도로 TickGroup을 지정하지 않으면, 이 그룹에 소속된다.
  2. TG_DuringPhysics
  3. TG_PostPhsysics
  4. TG_PostUpdateWork
(개별 TickGroup에 대한 설명은 UE4 문서 - 액터 틱#틱 그룹 을 참고하기 바란다)
 
FTickFunction 구조체에 TickGroup 멤버 변수가 존재하며, Actor의 경우 이에 대한 Setter 함수가 존재한다.
 
// FTickFunction의 멤버 변수
USTRUCT()
struct ENGINE_API FTickFunction
{
public:
    // Tick 실행이 시작되어야 할 최소 TickGroup
    // TickGroup은 frame update 과정에서의 Tick 처리의 상대적인 순서를 결정짓는다
    UPROPERTY(EditDefaultsOnly, Category="Tick", AdvancedDisplay)
    TEnumAsByte<enum ETickingGroup> TickGroup;

    // Tick 실행이 완료되어야 하는 TickGroup
    UPROPERTY(EditDefaultsOnly, Category="Tick", AdvancedDisplay)
    TEnumAsByte<enum ETickingGroup> EndTickGroup;

protected:
    // Tick 실행이 실제 시작될 TickGroup
    // 의존성을 설정한 Actor/Component에 의해 결정된다
    // A가 TG_PrePhysics이고, B가 TG_PostPhysics인 상황에서
    // A->B로의 의존 관계가 설정되면, A의 ActualStartTickGroup은 TG_PostPhysics로 Demote 된다
    TEnumAsByte<enum ETickingGroup> ActualStartTickGroup;

    // Tick 실행이 실제 완료될 TickGroup
    TEnumAsByte<enum ETickingGroup> ActualEndTickGroup;
}


// AActor의 Setter 멤버 함수
UCLASS(...생략...)
class ENGINE_API AActor : public UObject
{
    // NewTickGroup 열거항목으로 TickGroup 설정
    UFUNCTION(BlueprintCallable, Category="Utilities", meta=(Keywords = "dependency"))
    void SetTickGroup(ETickingGroup NewTickGroup);
}
 
 
Tick 실행이 시작/완료되기로 설정한 Start/EndTickGroup 변수와 별도로 ActualStartTickGroup/ActualEndTickGroup 변수가 존재하는 것을 확인할 수 있다.
 
이는 Actor/Component간의 의존 관계에 의해 본디 설정된 TickGroup이 아닌, 의존하는 Actor/Component의 TickGroup으로 demote 되어야 하기 때문이다. 조금만 생각을 해 봐도 A->B 즉, B의 Tick 실행이 완료된 다음에 A의 Tick 실행을 해야하는 의존 관계가 있다면, TickGroup 역시 그 관계에 맞게 설정되는 것이 이치에 맞다는 것을 이해할 수 있다.
 
정리하면, 초기 설정된 TickGroup은 뒤에 살펴볼 의존성에 의해 변경될 수 있으니 6. Dependency에서 주의깊게 살펴보도록 하자.
 
 

6. Dependency

Actor/Component는 AddTickPrerequisiteActor/AddPreprequisiteComponent 함수를 통해 의존(종속) 대상의 Tick 실행이 완료된 이후에 나의 Tick 실행이 시작되는 것에 대한 의존(종속) 관계를 설정할 수 있다. 동일 프레임에 처리되지만, 데이터 처리의 순서 의존성이 있을 경우 유용하다 할 수 있다.
 
앞서 5. TickGroup 챕터에서도 보았겠지만, Dependency는 TickGroup보다 높은 우선순위를 가진다.
앞선 TickGroup에 속한 Actor가 뒤에 실행되는 TickGroup에 속한 Actor에 의존할 경우 해당 Actor의 실질 TickGroup은 의존 대상의 TickGroup 이하 순서로 설정(demote)되는 것이다.
 
위 이야기를 뒤집으면, 이미 설정된 TickGroup 순서가 두 언리얼 오브젝트 간 의존 흐름에 문제가 없다면, 굳이 Actor/Component간 의존 관계를 명시적으로 설정할 필요가 없다는 이야기가 된다. (자연스레 먼저 처리 될테니 말이다)
 
Dependency에 관련된 정보는 별도로 FTickPrerequisite라는 구조체로 저장되며, FTickFunction에서는 TArray로 관리한다.
우선, FTickPrerequisite 구조체는 다음과 같이 선언/정의되어 있다.
 
// Prerequisite 정보만 따로 모은 구조체
USTRUCT()
struct FTickPrerequisite
{
    GENERATED_USTRUCT_BODY()

public:
    // 의존 대상 (약참조 사용에 주목)
    TWeakObjectPtr<class UObject> PrerequisiteObject;
    // 나보다 먼저 실행이 완료되어야 할 FTickFunction 참조 포인터
    struct FTickFunction*       PrerequisiteTickFunction;

public:
    // 빈 생성자
    FTickPrerequisite()
    : PrerequisiteTickFunction(nullptr)
    {
    }
    // 대상 Actor 객체와 FTickFunction으로 초기화되는 생성자
    FTickPrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction)
    : PrerequisiteObject(TargetObject)
    , PrerequisiteTickFunction(&TargetTickFunction)
    {
        check(PrerequisiteTickFunction);
    }

public:
    // 비교 연산자
    // FTickFunction에서 Prerequisite를 Array로 관리하는데, 중복 방지용(for "AddUnique")으로 사용
    bool operator==(const FTickPrerequisite& Other) const
    {
        return PrerequisiteObject == Other.PrerequisiteObject &&
            PrerequisiteTickFunction == Other.PrerequisiteTickFunction;
    }

public:
    // Getters
    struct FTickFunction* Get()
    {
        return PrerequisiteObject.IsValid(true) ? PrerequisiteTickFunction : nullptr;
    }  
    const struct FTickFunction* Get() const
    {
        return PrerequisiteObject.IsValid(true) ? PrerequisiteTickFunction : nullptr;
    }
};
 
 
FTickPrerequisite 구조체는 의존하는 대상 언리얼 오브젝트와 그 오브젝트가 가지는 FTickFunction에 대한 참조 정보를 가진다.
의존 대상에 대한 약참조 타입이 AActor가 아닌 UObject인 이유는, Component도 의존 대상으로 설정할 수 있고, Component는 Actor가 아닌 UObject이기 때문이다.
 
FTickFunction 구조체에 FTickPrerequisite 관련 멤버 변수가 존재하며, Actor의 경우 이에 대한 Setter 함수가 존재한다.
(Actor의 Prerequisite에 대한 멤버 함수는 예외처리 이후 FTickFunction의 대응되는 함수를 호출하는 방식으로 구현되어 있다)
 
// FTickFunction의 멤버 변수
USTRUCT()
struct ENGINE_API FTickFunction
{
public:
    // TArray로 관리 (추가시 AddUnique로... 그래서 FTickPrerequisite에 operator == 오버라이딩)
    TArray<struct FTickPrerequisite> Prerequisites;
}


// AActor의 Setter 멤버 함수
UCLASS(...생략...)
class ENGINE_API AActor : public UObject
{
    // 해당 Actor에 대한 의존관계 설정
    // Actor의 FTickFunction에만 영향을 주며, Actor의 Component들은 별도의 의존관계를 가질 수 있다
    UFUNCTION(BlueprintCallable, Category="Utilities", meta=(Keywords = "dependency"))
    virtual void AddTickPrerequisiteActor(AActor* PrerequisiteActor);

    // 해당 Actor에 대한 의존관계 제거
    UFUNCTION(BlueprintCallable, Category="Utilities", meta=(Keywords = "dependency"))
    virtual void RemoveTickPrerequisiteActor(AActor* PrerequisiteActor);

    // 해당 Component에 대한 의존관계 설정
    UFUNCTION(BlueprintCallable, Category="Utilities", meta=(Keywords = "dependency"))
    virtual void AddTickPrerequisiteComponent(UActorComponent* PrerequisiteComponent);

    // 해당 Component에 대한 의존관계 제거
    UFUNCTION(BlueprintCallable, Category="Utilities", meta=(Keywords = "dependency"))
    virtual void RemoveTickPrerequisiteComponent(UActorComponent* PrerequisiteComponent);
}
 
 
이제 TickGroup이 상이한 두 Actor간 의존 관계가 맺어지는 경우에 대한 예를 조금 더 살펴보도록 하자.
  • A : TickGroup = PG_PrePhysics 
  • B : TickGroup = PG_PostPhysics
 
A의 TickGroup이 B의 TickGroup보다 앞선 상대 순서를 가지기에, 의존 관계가 없을 경우 A의 Tick 함수가 B의 Tick 함수보다 먼저 수행된다. 
이 상황에서 A가 B에 의존하도록 설정하면, A의 TickGroup은 다음과 같이 변경된다.
  • TickGroup = PG_PrePhysics
  • ActualTickStartGroup = PG_PostPhycis
 
이렇게 의존 관계가 설정될 때, 의존 관계에 적합한 TickGroup으로 demote(promote 되는 경우는 없다)되는 내용은 engine의 다음 파일에서 실제 구현을 확인할 수 있다.
  • Runtime/Engine/Private/TickTaskManager.cpp
  • void FTickFunction::QueueTickFunction(FTickTaskSequencer& TTS, const struct FTickContext& TickContext);
 
의존성을 A의 B에 대한 의존성을 제거하면, 본디의 TickGroup을 다시 ActualTickStartGroup으로 설정하게 된다.
 
 

7. Component Tick

Actor를 다른 TickGroup으로 분리시킬 수 있듯이, Component 역시 다른 TickGroup으로 분리시킬 수 있다.
Actor의 Tick 실행 도중 소유한 모든 Component의 Tick 콜백 함수를 적용시키나, 다른 TickGroup으로 분리된 Component는 별도로 처리된다.
또한, Actor가 그러하듯 Component 역시 다른 Component를 대상으로 의존(종속) 관계를 설정할 수 있다.
 
다음은 지금까지 Actor 기준(AActor)에서 설명했던 요소들의 명칭이 Component 기준(UActorComponent)에서 어떻게 대응되는지를 보여준다.
  • FActorTickFunction PrimaryActorTick 변수 -> FActorComponentTickFunction PrimaryComponentTick 변수
  • Tick 콜백 함수 -> TickComponent 콜백 함수
  • SetActorTickEnabled 함수 -> SetComponentTickEnabled 함수
  • SetActorTickInterval 함수 -> SetComponentTickInterval 함수
  • 기타 "Actor" -> "Component" 치환 ...