Game Dev/Unity ECS & DOTS

3. IJobEntity 예제, 관련 Docs 파헤치기 - ECS & DOTS 완전 정복하기. Feat. 안 반가운 큐브 프로젝트

Septentrions 2024. 11. 5. 11:21

*해당 글은 Entities 1.3.5 버전, Unity 6 LTS (0.25)  버전을 기준으로 작성되었습니다.

 

큐브 예제도 보고, Docs 파헤치기도 하면서 글을 작성하려다가

너무 난잡해질 것 같아서 분리하려고 한다.

 

처음에는 큐브 예제와 함께 관련 Docs 찾아보기

두번째는 Docs 쪼개기로 번갈아가며 작성 예정이다.

 

 

우선, 코드부터 살펴보자.

Entities Sample 프로젝트의 HelloCube이다.

 

Authoring 코드 (+ 텅 빈 Component )

    public class ExecuteAuthoring : MonoBehaviour
    {
        public bool MainThread;
        public bool IJobEntity;
        public bool Aspects;
        public bool Prefabs;
        public bool IJobChunk;
        public bool Reparenting;
        public bool EnableableComponents;
        public bool GameObjectSync;
        public bool CrossQuery;
        public bool RandomSpawn;
        public bool FirstPersonController;
        public bool FixedTimestep;
        public bool StateChange;
        public bool ClosestTarget;

        class Baker : Baker<ExecuteAuthoring>
        {
            public override void Bake(ExecuteAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.None);

				...

				// 해당 Scene에서 SubScene을 열어보면, Authoring 코드에 해당 bool값만 체크되어 있다.
                if (authoring.IJobEntity) AddComponent<ExecuteIJobEntity>(entity);
                
                ...
            }
        }
    }

 

System 코드

    public partial struct RotationSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<ExecuteIJobEntity>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var job = new RotateAndScaleJob
            {
                deltaTime = SystemAPI.Time.DeltaTime,
                elapsedTime = (float)SystemAPI.Time.ElapsedTime
            };
            job.Schedule();
        }
    }

	// 현재는 LEGACY 된 것 같지만, FOREACH 문과 동일한 동작이며 재사용 가능한 함수 같은 느낌이다.
    [BurstCompile]
    partial struct RotateAndScaleJob : IJobEntity
    {
        public float deltaTime;
        public float elapsedTime;

        // In source generation, a query is created from the parameters of Execute().
        // Here, the query will match all entities having a LocalTransform, PostTransformMatrix, and RotationSpeed component.
        // (In the scene, the root cube has a non-uniform scale, so it is given a PostTransformMatrix component in baking.)
        void Execute(ref LocalTransform transform, ref PostTransformMatrix postTransform, in RotationSpeed speed)
        {
            transform = transform.RotateY(speed.RadiansPerSecond * deltaTime);
            postTransform.Value = float4x4.Scale(1, math.sin(elapsedTime), 1);
        }
    }

 

첫 코드 쪼개기이므로, 구조 자체부터 설명을 한다.

 

(1)  public partial struct RotationSystem : ISystem

 

구현부, 로직을 담당하는 System은 여러 타입이 있다.

- Burst Compile에 적합하며, Unmanaged Type 인터페이스인  "ISystem"

- 느리지만, Managed Type을 다룰 수 있는 추상화 클래스 "SystemBase" 로 구분된다.

 

Docs에는 불친절하게도 Unmanaged / Managed 라고만 딱 써있어서 직접 조사하지 않으면 무슨 의미인지 알 수 없다.

List / Queue / Dict / Class 처럼 Nullable 한 데이터를 가질 수 있는 데이터를 Managed 라고 표현하고

int / float / double 처럼 blittable Type 데이터를 Unmanaged 라고 표현하는 것 같다.

 

 

 

(2) OnCreate

 

[BurstCompile] 부분은 일단은 무시하자.

Burst Compile 은 ISystem 에서 동작하며, 데코레이팅 되고 있는 함수가 Unmanaged Type 일 경우, Burst Compiler가 대신 동작하여 엄청난 성능 효율을 보여준다고 한다.  

* Docs를 보고 Unmanaged Type 을 이해 하고나서 쓰자.

OnCreate 는 게임을 시작 할 때, 한번 동작하는 일종의 Awake 코드라고 생각하자.
SystemState state 는 Raw Entity Struct 라고 하는데 (정말 애매모호한 표현이다..)

해당 ISystem이 존재하는 World의 상태를 저장한다고 이해하자

 

RequireForUpdate 문은 해당 Component가 존재하면 Update 문을 동작한다는 뜻이다.

Component가 동작하는 World 내에서 매번 해당 System의 Update가 동작한다.


(3) OnUpdate

 

이번엔 IJobEntity 라는 인터페이스를 이용하고 있다.

우선, IJobEntity Struct의 Docs를 읽어보자.

- Any type which implements this interface and also contains an Execute()  method (with any number of parameters) will trigger source generation of a corresponding IJobChunk or IJobEntity type.

어떤 타입이든 Execute()를 포함한 이 인터페이스를 구현하고 있으면, IJobChunk나 IJobEntity Type과 동일한 형태의 Source Generation (컴파일링 단에서 실행 가능한 소스 생성) 을 Trigger한다.

- The generated job in turn invokes the Execute() method on the IJobEntity type with the appropriate arguments.

이 유형들을 생성한 job 은 적합한 Arguments와 함께 Execute() 를 Invoke한다.

 

어려운 말 같지만, 일종의 자동화된 함수라고 생각하면 이해가 갈 것이다.

다음은 예제 코드에 있는 주석문을 이해해보자.

// In source generation, a query is created from the parameters of Execute().

소스 생성 단계에서 우리가 준 파라미터 대로 쿼리가 생성된다. (이거 정말 중요한 포인트다.)

// Here, the query will match all entities having a LocalTransform, PostTransformMatrix, and RotationSpeed component.

이제 그 쿼리는 우리가 준 파라미터를 가지고 있는 모든 엔티티들을 매칭 할 것이다.

// (In the scene, the root cube has a non-uniform scale, so it is given a PostTransformMatrix component in baking.)

씬 내부에서 root cube는 uniform 하지 않은  스케일을 가지고 있다. 그래서 이 친구는 PostTransformMatrix 컴포넌트를 베이킹 단계에서 주어진다.

 

이해가 가는가?

Docs와 주석을 읽고나면 팍 와닿을 것이다.

 

우리가 어떤 IJobEntity를 여러 파라미터들과 함께 구현 해놓으면, 알아서 쿼리가 생성되며

해당 쿼리는 모든 엔티티들로부터 파라미터들과 같은 조합을 가진 Archtype 들만을 가져와서 Execute를 실행한다.

 

그리고 주의해야 하며 중요한 것!, 어떤 시스템이던  해당 IJobEntity를 재사용 할 수 있다.

여러 시스템들은 플레이 내내 계속 업데이트 될텐데

같은 엔티티들을 여러 시스템이 계속 IJobEntity를 이용해 맛보고 뜯고 씹고 할 것이다.

이건 즉, 경쟁 상태에 놓인다는 뜻이다.

 

그러면 다시 System을 돌아가서 job.Schedule() 이 보인다.

함수 이름에서부터 알 수 있듯이, job을 실행 시키기 적합한 프레임일 때 실행 할 수 있도록 도와주는 동기식 코드이다.

 

이 불친절한 헬로 큐브 02는 시스템 꼴랑 하나 박아놓고 경쟁 상태에 놓인 큐브 처리를 보여주는 예제인 것이다.

 

 

(4) PostTransformMatrix

 

이 컴포넌트는 너무 뜬금없이 나왔는데

예제 Scene 의 큐브가 Scale이 동일하지 않았을 때, 자동으로 Baking 할 때 엔티티에 부착되는 컴포넌트라 한다.

PostTransformMatrix

An optional transformation matrix used to implement non-affine transformation effects such as non-uniform scale.

 

이건 그냥 그래픽스 적인 이야기인데

3D 오브젝트의 Translate / Rotate / Scaling 작업을 하기 위해서는 Affine Transformation이 필요하다.

 

그래서 예제 코드의 Execute에는 float4x4 (affine 변환에 사용되는 행렬 타입)이 있던 것이였다.

 

오늘은 기본적인 System 쪽 구조,

그리고 IJobEntity에 대해서 알아봤다.

중간에 IChunkEntity라던가 Baking 같은 알 수 없는 이야기도 나왔는데..

예제도 몸소 있으니 나중에 다루도록 하겠다.