엔티티에 Fragment를 추가했습니다. 하지만, Fragment는 단순히 데이터를 추가해줄 뿐 이 데이터를 변경하거나, 검사하는 작업을 수행해야합니다. 이 작업들을 Subsystem이나 Processor에서 수행합니다.

Processor

먼저, 프로세서입니다. 프로세서의 구현은 UMassProcessor를 상속하여 만들 수 있습니다. 이 프로세서는 UObject기반으로 틱마다 요구조건에 충족하는 아키텍처/청크를 탐색하여 작업을 수행합니다. 이전의 트레이트처럼 부모 클래스를 살펴보기에는 꽤나 함수량이 많습니다. 따라서, 필수구현 부분만 참고해봅시다.
  • Constructor : 먼저 생성자입니다. 생성자에서는 Processor의 변수를 수정합니다.
├ ExecutionFlags - 프로세서 실행 주체 (Standalone, Client, Server ...) ├ ProcessingPhase - PrePhysics, PostPhysics 등.. Tick 실행 Phase ├ ExecutionOrder - 특정 프로세서 보다 먼저 또는 이전에 실행되야하는지 지정. │ ExecuteAfter와 ExecuteBefore, ExecuteInGroup이 있다. └ bRequiresGameThreadExecution - 게임 스레드에서 실행되어야 하는 프로세서?※ Constructor() : EntityQuery(*this) 로 쿼리의 소유권을 프로세서에 둡니다.
  • ConfigureQueries : EntityQuery에 쿼리 조건들을 작성합니다.
각 조건들에 대해 ReadOnly인지 ReadWrite인지, 태그가 있어야하는지 없어야하는지 등에 대해 검사합니다.
  • Execute : 실제로 프로세서가 엔티티쿼리를 통해 대상 엔티티들을 선별하고 청크별로 처리하는 곳입니다.
마지막 Iterator 부분의 it은 ArrayView 등에서 인덱스로 활용해서 각 엔티티를 구분하는 역할을 수행합니다.추가적으로, 프로세서는 UObject에서 Tickable하게 작동하므로 꽤 무겁습니다. 또한, 알아서 EntityQuery를 하는 만큼 다른 Trait이나 Fragment처럼 필요한곳에만 사용되는 것이 아니라, 틱마다 모든 엔티티에 대해 쿼리를 돌린 후, 쿼리에 걸린 청크 내부의 모든 엔티티에 대해 작업을 수행하기 때문입니다. 따라서, 사용하지 않는 프로세서들에 대해 관리를 해주면 최적화에 도움이 됩니다. 이런 내용에 관련해서는 Mass Processor CDO 관련하여 검색해볼 수 있습니다.
image

Subsystem

프로세서는 틱마다 쿼리하면서 동작합니다. 굳이 틱마다 처리하지 않기를 원할수도있고, 전역적으로 접근하거나, 저장된 데이터 등에 자유롭게 접근하고 싶을 수 있습니다. 그럴 때 Subsystem을 사용합니다. 서브시스템은 싱글톤처럼 동작하므로 자유롭게 여러군데에서 접근할 수 있습니다. 서브시스템은 UMassSubsystemBase, 틱이 필요한 경우 UMassTickableSubsystemBase를 상속하여 구현합니다. 프로세서와 서브시스템의 특징은 어떤 매니저 클래스나 월드에 직접 등록할 필요없이, 자동으로 엔진에서 등록시켜준다는 것입니다.프로세서와 마찬가지로, 상속하는 클래스가 Tickable일 경우 무거워질 수 있으니 사용에 주의가 필요합니다.서브시스템은 전역적으로 동작하기 때문에 저는 개발 중에 데이터 컨테이너로 많이 활용했습니다.
image
1번이 직접참조, 2번이 서브시스템을 컨테이너로 활용한 간접참조라고 볼 수 있습니다. 지금 보니 2번은 이미지가 살짝 잘못된게 TMap이 아니라 TArray를 사용했습니다. 아무튼, Handle은 Index와 Serial을 가지고 있으며, Handles[Index]에서 가져온 FTargetInfo의 Serial이 Query에 사용된 Serial과 일치하는 경우, Target을 반환합니다.기본적으로 이 구조는 TWeakObjectPtr<Target>과 크게 다르지 않습니다. 다만, 구조를 좀 복잡하게 잡아둔 이유는 위 예시처럼 Target*만 저장하는 것이 아니라, Target의 마지막 위치, 각 엔티티들의 타겟간 비교 등 작업을 위해서 이 방식으로 설계했습니다.
Handle은 기본적으로 Index(uint32), Serial(int32) 를 가지므로 8바이트, Ptr도 64비트 환경에서는 8바이트이므로, 엔티티 프래그먼트 크기에는 차이가 없습니다. 다만, 처리 방식에서 목표 오브젝트에 대한 여러 프로퍼티가 필요하고, 그 값의 관리/엔티티별 처리 일관성 확보 등에 있어서, 하나의 중앙 통제되는 컨테이너에서 관리하는 것이 구조상 좋아보여서 사용했습니다. 특히, 뱀서라이크/핵슬 처럼 수백+ 마리의 엔티티들과 하나의 공유된 타겟(플레이어)의 경우라 이렇게 사용합니다. (나중에 멀티 또는 디코이 등 타겟이 늘어날 가능성을 두고). 만약, 타겟 수가 고정됐거나 타겟의 수가 아주 많아서 중앙관리가 의미가 없는 수준이라면 프래그먼트에 직접 저장하는게 더 용이할 수 있습니다.