UE5

UE5 멀티스레딩과 enum을 이용한 상태 변환

게임 개발 2023. 10. 22. 10:36

 

멀티스레딩을 이용하면 여러 함수가 동시에 호출되는 것이 가능하다.

 

 

해당 언리얼 엔진 문서를 참고하였음

https://docs.unrealengine.com/5.1/ko/animation-optimization-in-unreal-engine/

https://docs.unrealengine.com/5.3/ko/animation-optimization-in-unreal-engine/

 

애니메이션 최적화

애니메이션 블루프린트의 퍼포먼스를 높이는 최적화 기법에 대한 설명입니다.

docs.unrealengine.com

 

애니메이션 최적화에 가장 좋은 방법은 멀티스레드를 이용하는 것이다.

 

여기서 잠깐,

멀티 스레드의 개념 이전에 알아 두어야 할 것이 있는데,

스레드 개념이다.

 

스레드란 무엇인가?

일반적으로 많이 쓰는 운영체제에서 대부분 스레드라는 기능을 제공한다.

언리얼에서의 스레드 개념도 이 개념에서 크게 다르지 않다.

스레드는 프로세스처럼 명령어를 한 줄씩 수행하는 기본 단위이다.

 

기본적으로 큰 기능을 3가지로 요약하자면,

스레드는 한 프로세스 안에 여러 개가 있다.

한 프로세스 안에 있는 스레드는 프로세스 안의 메모리 공간을 같이 사용할 수 있다.

스레드마다 스택을 가지며,
이는 각 스레드에서 실행되는 함수의 로컬 변수들이 스레드마다 있다는 것이다.

 

프로젝트의 규모와 범위에 따라,

조금 더 심한 변화가 필요할 수도 있지만,
일반적으로는 최적화 작업을 할 때 따르면 좋을 몇 가지 지침을 참고하면

더욱 적절하게 사용이 가능하다.

 

  • Parallel Updates (병렬 업데이트) 조건이 만족되었는지 확인하는 것
    - 게임 스레드에서 실행중인 애니메이션의 업데이트 페이즈를 필하기 위해 만족해야하는 조건은
    UAnimInstance::NeedsImmediateUpdate 에서 확인 가능하다.
    캐릭터 루트 모션이 필요한 경우,
    캐릭터 이동은 멀티 스레드가 아니므로 병렬 업데이트가 불가능하다.
  • 블루프린트 가상 머신으로의 호출을 피한다.

    1)
    블루프린트 네이티브화 를 통해 C++ 코드로 변환하는 것을 고려해본다.

    2)
    애니메이션 블루프린트의 이벤트 그래프 를 비워둔다.

    3)
    애니메이션 블루프린트의 애님 그래프 내 노드가 Fast Path를 사용하도록 되어있는지 구조를 확인해본다.
    커스텀 UAnimInstance  FAnimInstanceProxy 파생 클래스를 사용하고
    프록시의 모든 작업은 워커 스레드에서 실행되는 
    FAnimInstanceProxy::Update  FAnimInstanceProxy::Evaluate 도중에 시도한다.

    4) 
    프로젝트 세팅
     
    Optimize Anim Blueprint Member Variable Access 
    ( = 애님 블루프린트 멤버 변수 최적화) 옵션이 켜져있는지 확인해본다.
    자기 클래스의 멤버 변수에 직접 접근하는 애니메이션 블루프린트 노드가
    블루프린트 가상 머신으로의 썽크를 피하는 최적화된 경로를 사용하는지 여부를 제어해주는 옵션이다.

    5)
    일반적으로 애님 그래프 실행에서 가장 큰 비용을 차지하는
    virtual machine으로의 호출을 피하는 것이
    애니메이션 블루프린트의 퍼포먼스를 최대로 뽑아내는 핵심이다.

  • 가능한 경우 업데이트 속도 최적화(URO = Update Rate Optimizations )를 사용한다.
    1)
    URO는 애니메이션이 너무 자주 틱되지 않도록 한다.
    URO 적용 방법은 프로젝트의 필요에 따라 다르지만,
    대부분의 캐릭터에 대해 적절한 거리에서 
    15Hz 이하로 수행되는 업데이트 속도를 목표로 하고 보간을 비활성화하는 것이 좋다.

    2)
    이 옵션을 켜려면,
    스켈레탈 메시 컴포넌트의 
    Enable Update Rate Optimizations (업데이트 속도 최적화 켜기)
    옵션을 설정하고 
    AnimUpdateRateTick() 을 참조한다.

    2-1)
    옵션으로, 
    Display Debug Update Rate Optimizations (업데이트 속도 최적화 디버그 표시)를 켜면
    적용중인 URO 디버깅 화면 표시를 사용할 수 있다.
  • 컴포넌트가 스켈 바운드 사용 옵션을 키는 것
    1) 스켈레탈 메시 컴포넌트에서, Component Use Skel Bounds (컴포넌트가 스켈 바운드 사용) 옵션을 킨다.
    2) 피직스 애셋 사용을 생략하는 대신 항상 스켈레탈 메시에 정의된 고정 바운드를 사용한다.
    3)
    모든 프레임의 컬링에 바운딩 볼륨을 재계산하는 것도 생략되어 퍼포먼스가 향상된다.

 

 

++ thunk 란?

 

컴퓨터 프로그래밍에서 썽크( thunk)는 
기존의 서브루틴에 추가적인 연산을 삽입할 때 사용되는 서브루틴이다.

썽크는 주로 연산 결과가 필요할 때까지 연산을 지연시키는 용도로 사용되거나,
기존의 다른 서브루틴들의 시작과 끝 부분에 연산을 추가시키는 용도로 사용되는데

컴파일러 코드 생성시와 모듈화 프로그래밍 방법론 등에서 좀 더 다양한 형태로 활용되기도 한다.

 

++

 

 

 

 

 

 

가령, 게임 개발 시 죽는 모션을 구현 할 때,

하나의 애니메이션을 호출하기보다

몽타주를 만들어 섹션을 나누어 만약 4개의 dying 모션이 있으면,
4개중 랜덤한 1개의 모션을 불러오는 식으로 짤 것이다.

 

이 때, 어떤 개발자는 enum클래스를 만들어서
블루프린트에서 해당 애니메이션을 랜덤 값을 돌려
enum클래스에 맞는 애니메이션을 호출하도록 구현 할 수 있다.

<예시>

 

이 방법도 나쁜 방법은 아니다.

 

하지만 애니메이션 최적화에 가장 좋은 방법을

언리얼 공식 문서에서는 제시하고 있다.

 

 

프로젝트의 애니메이션 시스템 퍼포먼스, 또는 각 프레임의 평가 효율성 의 기준은( = efficiently each frame is evaluated)
게임 스레드(Game Thread) 및 작업자 스레드(Worker Thereads)에서

틱마다 애니메이션 시스템 처리에 필요한 시간을 기반으로 한다.

 

애니메이션은 애니메이션 블루프린트에 추가되어
런타임 시 캐릭터에 대해 재생되면서 이에 대한 값을 받아온다.

애니메이션 블렌딩, IK계산, 피직스 시뮬레이션 등의 추가 프로세스는

각각의 수치 값 계산을 위해 그에 따라 프로젝트 예상 비용이 필요한 프로세스도 있다. 

( = your project's performance budget in order to be evaluated.)

따라서 모든 애니메이션 시스템 기능은 퍼포먼스 비용이 발생하게 된다.

 

일반적으로, 애니메이션 블루프린트에서 

퍼포먼스 비용이 가장 많이 소요되는 연산은 이벤트 그래프 로직이다.

따라서 애니메이션 블루프린트 최적화시 가장 좋은 방법은 다음과 같다.

 

애님그래프 로직은 Fast Path와 같은 시스템을 사용하여 최적화할 수 있지만,
최고의 퍼포먼스를 내기 위해서는 이벤트 그래프 로직을 최소화하는 것이 좋다.

이벤트 그리프는 틱마다 연산되어,

각 프로세스는 게임 스레드에서 순차적으로 발생하게 되기 때문이다.

 

아래의 그림은 단일 에니메이션 프레임을 추상화하여 분석한 것이다.

각 애니메이션 프레임에는 여러 개의 틱이 포함되어 있으며,
틱마다 이벤트 그래프가 연산된다.

출저 - 언리얼 공식 문서 발췌

위 그림에서 보는 바와 같이,

이벤트 그래프의 연산은 일반적으로 각 틱마다 연산을 수행하는데,

이 때 수행되는 연산이 가장 큰 비용을 지불해야 한다.

이벤트 그래프의 연산은 순차적으로 처리되므로,

이벤트 그래프의 모든 연산을 완료하는 데에는 시간이 많이 걸린다.

 

이벤트 그래프 로직에 대한 간략한 설명을 마쳤다.

 

이제 왜 런타임과 블루프린트 내에서

퍼포먼스 비용이 가장 많이 소요되는 연산이 이벤트 그래프 로직인지,

이벤트 그래프 로직을 최소화하는 것이 좋다는지 개념이 좀 잡혔다.

 

이 전 포스팅에서 언급했던 병렬형 컴퓨팅이 여기에 나오는 병렬 처리와 흡사하다.

다만, CPU가 아닌 스레드로 작업하는 병렬연산이다.

사용할 수 있는 작업자 스레드를 돌리면 해당 연산이 병렬처리되어

연산을 빨리 할 수 있게된다.

여기서 Thread Safe 함수에 이벤트 그래프 로직을 재배치하면

이 프로세스를 최적화 할 수 있다.

 

아래의 다이어그램은 해당 기능에 대해 추상화 한 것이다.

출저 - 언리얼 공식 문서 발췌

각 틱 완료 시간이 극적으로 감소한 것을 가시적으로 확인 가능하다.

모든 이벤트 그래프 연산을 Thread Safe 함수에 재배치하면,

연산을 동시에 수행할 수 있으므로 각 틱 평가에 필요한 시간이 상당히 감소하고,

애니메이션 연산에 할애되는 퍼포먼스가 개선된다.

 

 

자, 이제 개념적인 부분을 알고 갔으니,

해당 멀티 스레드 애니메이션 업데이트를 사용해보자.

 

 

멀티 스레드 애니메이션 업데이트 사용하기

애니메이션 블루프린트 이벤트 그래프는 하상 게임 스레드에서 실행된다.

멀티 스레드를 활용하기 위해 이벤트 그래프 내의 로직을 최적화 하려면,

Thread Safe 함수를 사용하여 로직을 빌드하면 된다.

Blueprint Thread Safe Update Animation

Blueprint Thread Safe Update Animation 오버라이드 함수를 사용하여
스레드 세이프 방식으로 애니메이션 블루프린트의 로직을 평가할 수 있다.

 

함수 목록에 마우스를 가져다대면 오버라이드라는 목록이 보인다.

드롭다운 메뉴에서 Blueprint Thread Safe Update Animation  함수를 선택하여

애니메이션 블루프린트에 추가 가능하다.

 

출저 - 언리얼 공식 문서 발췌

 

++ Warning ++

 

스레드의 안정성을 보장하면서 사용하려면

변수처럼 프로젝트 내의 다른 블루프린트 및 컴포넌트에서 파생된 데이터에 대한 모든

레퍼런스를 애니메이션 블루프린트로 불러오는(=Pushed) 것이 아니라

애니메이션 블루프린트를 통해 호출(= Called)해야 한다.

 

++

 

++ Plus ++

 

왜 이 부분이 Override 를 사용하는지 아는가?

오버라이딩(overriding)은 런타임 중에 발생하는 동적 다형성이고,

오버로딩(overloading)은 컴파일 중에 발생하는 정적 다형성이기 때문이다

 

애니메이션은 런타임중에 발생하는 일이니

오버라이딩을 사용하는 것이다.

 

++

 

Thread Safe 함수의 호출 방법

Thread Safe 함수는 블루프린트 함수로서,

이 함수를 사용해,

애니메이션 시스템이 사용할 수 있는 변수와 프로퍼티를 설정하는 로직을 수행 가능하다.

또한 이벤트 그래프에서 일반적으로 수행되는 다른 연산도 수행할 수 있다.

 

애니메이션 블루프린트에서 Thread Safe 함수를 생성하려면

My Blueprint 패널에서 + 버튼을 눌러 새 함수를 생성한 다음,

새 함수의 디테일(Details) 패널을 열고 스레드 세이프(Thread Safe) 프로퍼티를 활성화한다.

출저 - 언리얼 공식 문서 발췌

그러면 스레드 세이프가 활성화된 함수를 Blueprint Thread Safe Update Animation 오버라이드 함수에 추가하여,

틱마다 작업자 스레드를 사용할 수 있을 때 동시에 평가할 수 있다.

 

해당 함수는 블루프린트 업데이트 애니메이션처럼 매 프레임마다 호출된다.

해당 함수는 스레드 세이프 함수로 변수의 값에 접근해
다른 BP스레드와 병렬로 호출되거나 동시에 호출 가능하다.

안전하게 애니메이션을 업데이트할 수 있는 함수이이며,
여기서 스레드를 안전하게 이용하기 위해서 변수에 직접적으로 접근하지 않는다.

변수에 직접적으로 접근한다는 것은

해당 변수의 겟 혹은 셑을 이용하여 접근하는 것을 말한다.

변수에 직접적으로 접근하지 않는 이유는

동일한 캐릭터에 대해 동시에 호출되는 다양한 애니메이션 블루프린트에

병렬로 호출되는 버전 중 하나에서 이 버전과 일치하지 않는 내용으로

즉, 변경 사항이 될 수 있기 때문이다.

때문에 스레드 안전성이 깨진다.

 

이게 버전이라고 하면 좀 아리까리한데,

그냥 병렬로 호출 될 때 스레드가 여러번 호출되는데

관련 데이터에서 안전성이 깨질 수 있다는 소리다.

 

그 데이터 안전성이 깨진 것을 변경 사항이라고 생각하면 된다.

 

따라서 모든 관련 데이터는 그것들을 모두 저장하는 프록시 데이터 구조에서 캐시되고,

Enemy 변수와 관련되는 프록시 데이터 구조의 사본값에 접근하면 된다.

 

그렇게 스레드 안전성을 확보고하고 업데이트 프레임에 대한 
애니메이션 함수가 끝나면, 프록시 또한 업데이트 된다.

그렇게 되면 멀티스레드 기능이 다른 기능과 충돌하는 데이터에 접근하지 않는다.

 

++Plus++

 

proxy data 

proxy 는 대신이라는 뜻을 가진다.

프로토콜에 있어서 대리 응답 등에서 사용되는 개념이라고 생각하면 된다.

 

보안상의 문제로 직접 통신을 주고 받을 수 없는 사이에서

프록시를 이용하여 중계를 하는 개념이라고 볼 수 있다.

 

서버와 클라이언트 사이에서 중계기로서 대리로 통신을 수행하는 기능을 '프록시',

그 중계 기능을 하는 것을 '프록시 서버'라고 한다.

 

그럼

언리얼에서 사용하는 프록시 데이터는 

대리 응답하는 데이터 ( = 변수) 라고 이해하면 된다.

 

++

 

해당 기능에서 proxy data를 만드려면 다음과 같이 검색하면 된다. 

 

이렇게 해당 프로퍼티 ( = proxy data ) 에서 사본으로 만들고자 하는 데이터를 선택한다.

필자는 열거형으로 나눠서 애니메이션을 실행하기 위해서 위해서 열거형을 선택했다.

 

 

다음과 같이 연결해주면 이제 열거형을 응용하여 프록시 데이터를 만들어준 것이다.

 

 

완성된 enumy Death Pose Enum에서 무작위 추출 영상

 

'UE5' 카테고리의 다른 글

언리얼 내의 포인터  (0) 2023.11.02
언리얼 게임 모드 클래스  (0) 2023.10.27
UFUNCTION() 응용을 위한 가이드  (1) 2023.10.16
Physics Field System _ RPG 부서지는 물체  (1) 2023.10.16
Coding Standard  (0) 2023.10.14