Game Dev/Unity ECS & DOTS

[ECS&DOTS] NSprite 머리 쪼개기 (1) 우선, 샘플 프로젝트부터 공략하자.

Septentrions 2024. 11. 8. 15:33

미친 성능을 보여주는 NSprite....

 

 

https://github.com/Antoshidza/NSprites

 

GitHub - Antoshidza/NSprites: Unity DOTS Sprite Rendering Package

Unity DOTS Sprite Rendering Package. Contribute to Antoshidza/NSprites development by creating an account on GitHub.

github.com

https://github.com/Antoshidza/Age-of-Sprites

 

GitHub - Antoshidza/Age-of-Sprites: Sample project for NSprites package

Sample project for NSprites package. Contribute to Antoshidza/Age-of-Sprites development by creating an account on GitHub.

github.com

 

 

NSprite 프레임워크의 공식 예제 Age of Sprite 를 기준으로 코드 리뷰를 진행하는 문서이다.

 

유니티 ECS&DOTS에서 2D Sprite 를 다루는 공식 예제 코드를 보면

GameObject에 Sprite Renderer를 그냥 박아버린 채로, ECS에서 사용하고 있는 걸 볼 수 있다.

이건 그냥 Hybrid ECS로 쓰겠다는 의미이며

 

Sprite Renderer를 전혀 처리하지 않기 때문에,

그냥 GameObject만 잔뜩 생성한 것보다 약간 좋은 성능만 보여줄 뿐이다.

 

내 멘탈을 무너뜨린 문제의 공식 유니티 샘플

 

 

아래는 해당 방식으 직접 돌려본 영상

https://youtu.be/8QtJMG1XxZ8?feature=shared

 

 

직접 돌려봤을 때,  9,000-10,000개의 2D Entity 기준으로 30~40 fps 성능을 보여주기 때문에

이를 해결하기 위한 방법이 필요하다.

 

슬프게도 유니티 공식에서는 Animation 쪽에 더 집중하고 있어서인지

2D 지원 계획이 없다. (로드맵조차 안올라와있다.)

아직 DOTS가 확립된 것도 아니고, 인력 문제도 있겠지만.. 아쉬울 뿐이다.

 

2D Sprite 를 ECS에서 사용하기 위해서는 아주 다양한 방법이 제안되어 왔지만

그 중에서도 사용하기 쉽고,

최신 DOTS에서도 적용 가능 할 수 있는 NSprite를 내 프로젝트에도

적용하고 싶어서 포스팅을 준비했다.

 

처음에 좌절했지만.. 포기는 배추 셀 때나 쓰는 말.

잘근 잘근 씹어주기 위해,  자신을 전부 다 내려놓고,

코드 리뷰를 진행하려 한다.

 

우선, NSprite / NSprite-Foundation을 가져오고 있는 걸 확인

 

 

그 외에는 프로젝트 안에 별다를게 없으니

코드 구현 부분을 확인해보자.

(Sources - Rome)


 

이전 ECS & DOTS 포스팅에서 설명했던 워크 플로우 대로

우선, 프로젝트 머리를 쪼개려고 한다.

https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/ecs-workflow-intro.html

 

Understand the ECS workflow | Entities | 1.0.16

Understand the ECS workflow The workflow to create applications with Unity's entity component system (ECS) framework differs from the one you would use to create object-oriented Unity applications in both principle and implementation. It's useful to unders

docs.unity3d.com

 

https://lucid-boundary.tistory.com/99

 

4. ECS 워크플로 이해하기- ECS & DOTS 완전 정복하기. Feat. 안 반가운 큐브 프로젝트

*해당 글은 Entities 1.3.5 버전, Unity 6 LTS (0.25)  버전을 기준으로 작성되었습니다.  이번 포스팅은 Understand the ECS workflow 의 번역 및 소개를 하려고 한다.가장 기초적인 걸음이 중요한 법이다. 원문

lucid-boundary.tistory.com

 

 

 

 

 

(1) SubScene 부분부터 살펴보기

 

 

- SquadSettings

- MapSettings

- AnimationSettings

- Squad (#)

- Tent (#),  각 텐트안에는 Flag_0 / Flag_1 이라는, 지금은 이해할 수 없는 차일드 오브젝트들이 있다.

으로 구성된다.

 


 

 

각 Entity 시켜줄 Authoring 코드들이 존재한다.

해당 코드들은 Authroing 구현 부분에서 설명하겠다.

 

프로젝트를 실행해보면,

병사들이 튀어나오며 스쿼드로 정렬하게 되는데

SubScene에는 병사들에 대한 Entity들이 존재하지 않는다.

분명, 병사들을 따로 관리하는 엔티티가 존재할 것이다.

 

(2) Components 구현 부분

 

생각보다 적은 Component 들을 사용해서 다행이다.

 

AnimationSettings Animation 상태 값들을 Hash 형태로 저장 (IDLE/WALK)
Destination float2
FactoryData Entity (Soldier 프리팹)
count
duration
float2 (instantiate Position)
FactoryTimer float
IComponentData (Name) Tag 역할
InSquadSoldierTag Tag 역할
MapSettings float2x2 size
Entity rockCollectionLink
int rockCount
MoveSpeed float
MoveTimer float
MovingTag Tag 역할 + IEnableComponent
PrefabLink Enttiy link  + IBufferElementData
PrevWorldPosition2D float2
RequireSolider int count
SoldierLink Enttiy entity + IBufferElementData
SoldierTag Tag 역할
SquadDefaultSettings + IEquatable<SquadDefaultSettings> (Equal 체크용)
Entity soldierPrefab
SquadSettings defaultSettings
float2 SoldierMargin => defaultSettings.soldierMargin
int2 SquadResolution => defaultSettings.SquadResolution
int SoldierCount => SquadResoultion.x * SquadResolution.y

Equals / operator / GetSquadSize 함수
SquadSettings + IEquatable (SquadSettings 체크용)
float2 soldierMargin
int2 squadResolution;

Equals / operator / GetSquadSize 함수
WorldPosition2D float2

public WorldPosition2D(in float3 pos) => Value = pos.xy;
(3d -> 2d 변환)

 

컴포넌트 부분은 크게 어려운 건 없는것 같다.

Settings 부분에 먼가가 잔뜩 있는데

System 쪽에서 설명하겠다.

그래도 변수 명으로 대강 감은 잡힐 것이다.

 

(2) Authoring 구현 부분

 

다음은 Authoring 부분이다.

여기서부터는 정신 바짝 차리고 봐야 할 것 같다.

 

AnimationSettingsAuthoring

 

"더 보기" 를 누르면 스크립트를 확인 할 수 있다.

더보기
using Unity.Entities;
using UnityEngine;

namespace NSprites
{
    public class AnimationSettingsAuthoring : MonoBehaviour
    {
        private class AnimationSettingsBaker : Baker<AnimationSettingsAuthoring>
        {
            public override void Bake(AnimationSettingsAuthoring authoring)
            {
                AddComponent(GetEntity(TransformUsageFlags.None), new AnimationSettings
                {
                    IdleHash = Animator.StringToHash("idle"),
                    WalkHash = Animator.StringToHash("walk")
                });
            }
        }
    }
}

 

 

SubScene에서는 AnimationSettings GameObject가 컴포넌트로 가지고 있다.

아주 일반적인 형태의 Authroing 코드이다.

추가하는 컴포넌트는 AnimationSettings 하나 뿐이다.

AnimationSettings Animation 상태 값들을 Hash 형태로 저장 (IDLE/WALK)

 

Animator.StringToHash() 함수를 사용해

idle / walk 애니메이션을 Hash화 시켜준다.

Hash 컴포넌트만을 가지고 있는 엔티티라...

 


 

FactoryAuthoring

 

"더 보기" 를 누르면 스크립트를 확인 할 수 있다.

 

더보기
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Serialization;

public class FactoryAuthoring : MonoBehaviour
{
    private class FactoryBaker : Baker<FactoryAuthoring>
    {
        public override void Bake(FactoryAuthoring authoring)
        {
            var entity = GetEntity(TransformUsageFlags.None);
            AddComponent
            (
                entity,
                new FactoryData
                {
                    prefab = GetEntity(authoring.Prefab, TransformUsageFlags.None),
                    instantiatePos = new float2(authoring.transform.position.x, authoring.transform.position.y) + authoring.SpawnOffset,
                    count = authoring.SpawnCount,
                    duration = authoring.Duration
                }
            );
            AddComponent(entity, new FactoryTimer { value = authoring.RandomInitialDuration ? UnityEngine.Random.Range(0f, authoring.Duration) : authoring.Duration });
        }
    }

    [FormerlySerializedAs("_prefab")] public GameObject Prefab;
    [FormerlySerializedAs("_spawnOffset")] public float2 SpawnOffset;
    [FormerlySerializedAs("_duration ")] public float Duration = 1f;
    [FormerlySerializedAs("_spawnCount ")] public int SpawnCount = 1;
    [FormerlySerializedAs("_randomInitialDuration")] public bool RandomInitialDuration;
}

 

SubScene에서는 "Tent" GameObject들이 가지고 있다.

컴포넌트로는 FactoryData  / FactoryTimer 를 추가해주고 있다.

FactoryData Entity (Soldier 프리팹)
count
duration
float2 (instantiate Position)
FactoryTimer float

 

 

Factory Authoring에서 Factory Data 컴포넌트는 Legionnaire_View 라는 Prefab을 가져와 Entity 로 가지고

SpawnOffset / Duration / SpawnCount / RandomInitialDuration 에 대한 값을 받고 있다.

명칭부터 짐잠 가듯이, Soldier들을 생산 할 역할을 Tent가 맡을 것이다.

 

Factory Timer 는 고정적으로 생성할 것인지 정하는 컴포넌트이므로, RandomInitialDuration 에 의해

랜덤으로 혹은 1초로 고정 시킬 수 있다.

 

다음 Authoring을 보기전에 Prefab을 먼저 뜯어보자. 

(어처피 해당 Prefab에서 무엇을 먼저 봐야 할 지 알 수 있다.)

 

Legionnaire_View Prefab을 열면, Scene 내부에는 아무것도 보이지 않지만

NSprite 패키지에서 쓰이는 Authoring 코드가 존재한다.

 

SpriteAnimatedRendererAuthoring 코드는 NSprite 패키지에서 제공하는 것이므로, 

이번 포스팅에서는 다루지 않을 것이다.

 

추가로 SoldierAuthoring 스크립트도 컴포넌트로 가지고 있다.

SoldierAuthoring을 먼저 보자.

 


 

SoldierAuthoring

 

"더 보기" 를 누르면 스크립트를 확인 할 수 있다.

 

더보기
using Unity.Entities;
using UnityEngine;

public class SoldierAuthoring : MonoBehaviour
{
    private class SoldierBaker : Baker<SoldierAuthoring>
    {
        public override void Bake(SoldierAuthoring authoring)
        {
            var entity = GetEntity(TransformUsageFlags.None);
            AddComponent<SoldierTag>(entity);
            AddComponent<MovingTag>(entity);
            SetComponentEnabled<MovingTag>(entity, false);
            AddComponent<Destination>(entity);
            AddComponent<MoveTimer>(entity);
            AddComponent(entity, new MoveSpeed { value = authoring.MoveSpeed });
        }
    }

    public float MoveSpeed;
}

SoldierAuthoring은 여러개의 컴포넌트들을 가지고 있다.

 

Destination float2
MoveSpeed float
MoveTimer float
MovingTag Tag 역할 + IEnableComponent
SoldierTag Tag 역할

 

그렇게 어려울 것은 없는 Authoring이다.

Tent Entity 들은 일정 시간마다 해당 Soldier Entity를 생성 할 것이란 것만 기억해두고 있자.

 


 

MapAuthoring

 

"더 보기" 를 누르면 스크립트를 확인 할 수 있다.

더보기
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Serialization;

public class MapAuthoring : MonoBehaviour
{
    private class MapBaker : Baker<MapAuthoring>
    {
        public override void Bake(MapAuthoring authoring)
        {
            if (authoring.RockPrefabs == null)
                return;

            var rockCollectionEntity = CreateAdditionalEntity(TransformUsageFlags.None);
            var rockBuffer = AddBuffer<PrefabLink>(rockCollectionEntity);
            rockBuffer.Capacity = authoring.RockPrefabs.Length;
            for (int i = 0; i < authoring.RockPrefabs.Length; i++)
                _ = rockBuffer.Add(new PrefabLink { link = GetEntity(authoring.RockPrefabs[i], TransformUsageFlags.None) });

            AddComponent(GetEntity(TransformUsageFlags.None), new MapSettings
            {
                rockCollectionLink = rockCollectionEntity,
                rockCount = authoring.RockCount,
                size = authoring.Rect
            });
        }
    }

    [FormerlySerializedAs("_gizmoColor ")] public Color GizmoColor = Color.green;
    [Space]

    [FormerlySerializedAs("_rect")] public float2x2 Rect;
    [FormerlySerializedAs("_rockCount")] public int RockCount;
    [FormerlySerializedAs("_rockPrefabs")] public GameObject[] RockPrefabs;

#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        Utils.DrawRect(Rect, GizmoColor);
    }
#endif
}

*            // var rockCollectionEntity = CreateAdditionalEntity(TransformUsageFlags.None);
*           var rockCollectionEntity = GetEntity(TransformUsageFlags.None);

CreateAdditionalEntity 는 1.0 이후로 삭제되었으므로 GetEntity를 쓰자.

 

MapAuthoring 에서 엔티티에 들어갈 컴포넌트들은 다음과 같다.

 

MapSettings float2x2 size
Entity rockCollectionLink
int rockCount
PrefabLink Enttiy link  + IBufferElementData

 

 

rockCollectionEntity 라는 이름으로 엔티티를 만들고, PrefabLink : IBufferElementData 의 Entity로 만들어 준다.

 

에디터에서 등록된 RockPrefabs 는 17개이고

IBufferElementData 의 Capacity를 17개로 선언해주고,

각 버퍼에 Prefab들을 넣어주는 식으로 Buffer를 만들어줬다.

 

IBufferElementData 란?

Dynamic Buffer로써, 기존 컴포넌트에 사용 할 수 없는 List / Dictionary 같은 역할로 어느 정도 사용 할 수 있게 해주는 컴포넌트이다.

 

다음은 MapSetting 컴포넌트 부분을 보겠다.

 

17개의 다이나믹 버퍼 Preafab을 가진 Entity를 넣고,

75000 카운트, (-25, -25) ~ (25, 25) 값을 넣어준 채로 컴포넌트를 추가했다.

 

정리하자면,

맵 엔티티가 가지고 있는 컴포넌트는 에디터에서 등록한 RockPrefab들 만큼 Dynamic Buffer를 만들어진 컴포넌트

엔티티 자신을 컴포넌트의 값에 넣고, 맵 사이즈, 생성 개수를 가지고 있는 컴포넌트

2개를 갖고 있는 맵 세팅 엔티티가 되었다.

 

시스템에서 명확하게 사용되겠지만.. 일단 이렇게 이해를 하자. 얼마나 무시무시한 짓을 할 것인지...

 


SquadDefaultSettingAuthoring

 

"더 보기" 를 누르면 스크립트를 확인 할 수 있다.

더보기
using NSprites;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Serialization;

public class SquadDefaultSettingsAuthoring : MonoBehaviour
{
    private class SquadDefaultSettingsBaker : Baker<SquadDefaultSettingsAuthoring>
    {
        public override void Bake(SquadDefaultSettingsAuthoring authoring)
        {
            if (authoring.SoldierView == null)
                return;

            AddComponent(GetEntity(TransformUsageFlags.None), new SquadDefaultSettings
            {
                soldierPrefab = GetEntity(authoring.SoldierView, TransformUsageFlags.None),
                defaultSettings = new SquadSettings { soldierMargin = authoring.SoldierMargin, squadResolution = authoring.SquadResolution }
            });
        }
    }

    public SpriteAnimatedRendererAuthoring SoldierView;
    [FormerlySerializedAs("_squadResolution")] public int2 SquadResolution;
    [FormerlySerializedAs("_soldierMargin")] public float2 SoldierMargin;
}

본격적으로 NSprite가 나오기 시작한다.

 

이번 Authoring에서 사용되는 컴포넌트들은 다음과 같다.

 

SquadDefaultSettings + IEquatable<SquadDefaultSettings> (Equal 체크용)
Entity soldierPrefab
SquadSettings defaultSettings
float2 SoldierMargin => defaultSettings.soldierMargin
int2 SquadResolution => defaultSettings.SquadResolution
int SoldierCount => SquadResoultion.x * SquadResolution.y

Equals / operator / GetSquadSize 함수
SquadSettings + IEquatable (SquadSettings 체크용)
float2 soldierMargin
int2 squadResolution;

Equals / operator / GetSquadSize 함수

 

 

 

SoldierView 는 SpriteAnimatedRendererAuthoring 으로 되어 있지만,  실제로는 Tent 엔티티에 넣던 그 Prefab이다.

SquadSettings를 넣고, Squad Resolution 과 Margin 값을 넣어준다.

SquadSettings은 각 Soldier들에게 부여할 컴포넌트인데

디폴트 값이란 걸 알리는 용도인 것 같다. 시스템에 가야지 자세하게 알 수 있을 것이다.

 


SquadSettings

 

"더 보기" 를 누르면 스크립트를 확인 할 수 있다.

 

더보기
using NSprites;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Serialization;

public class SquadAuthoring : MonoBehaviour
{
    private class SquadBaker : Baker<SquadAuthoring>
    {
        public override void Bake(SquadAuthoring authoring)
        {
            var entity = GetEntity(TransformUsageFlags.None);
            var pos = new float3(authoring.transform.position).xy;
            AddComponent(entity, new WorldPosition2D { Value = pos });
            AddComponent(entity, new PrevWorldPosition2D { value = pos });
            AddComponent(entity, new SquadSettings { squadResolution = authoring.Resolution, soldierMargin = authoring.SoldierMargin });
            AddComponent(entity, new RequireSoldier { count = authoring.Resolution.x * authoring.Resolution.y });
            _ = AddBuffer<SoldierLink>(entity);
        }
    }

    [FormerlySerializedAs("_resolution")] public int2 Resolution;
    [FormerlySerializedAs("_soldierMargin")] public float2 SoldierMargin;
}

대망의 마지막 Authoring.

Squad GameObject들은 인 게임 상에서는 보이지 않고, 허공에 위치해 있는데

엔티티화 된 Soldier들을 정렬 시키는 줄로 쓰이는 것 같다.

 

추가하는 컴포넌트들은 다음과 같다.

 

PrevWorldPosition2D float2
RequireSolider int count
SquadSettings + IEquatable (SquadSettings 체크용)
float2 soldierMargin
int2 squadResolution;

Equals / operator / GetSquadSize 함수
WorldPosition2D float2

public WorldPosition2D(in float3 pos) => Value = pos.xy;
(3d -> 2d 변환)

 

이번 Authoring도 NSprite를 사용하지만 별개 없다.

좌표 관련 컴포넌트들을 추가시키는걸로 보아

이동 관련 엔티티인 것 같다고 짐작할 수 있을 것이다.

 

길었지만... Authoring 과 Components 스크립트를 한번 씩 훑어보았다.

 

다음 글에서는 드디어 System 부분을 다루려고 한다.

System 부분이 많이 어렵기 때문에

 

Authorung / Components 보다도 길어질 수 있을 것 같다..

 

 

다음 포스팅에서 System 을 설명하기 전에

System Window 창을 보면서 마치겠다.

구현한 System 코드가 어디에서 동작하는지를 알 수 있다.