Unity ECS - Hybrid 에 대한 스크립팅 및 사용방법 기록 포스팅입니다.

 

ECS에 대한 설명및 기본세팅은 이전 포스팅으로 - https://zprooo915.tistory.com/66

아래 포스팅은 Unity ECS 샘플 프로젝트의 스크립트를 기준으로 작성하였습니다.

 

이번 포스팅은 아래순서로 진행됩니다.

  1. Hybrid ECS란?
  2. 기본적인 사용방법
  3. 사용 Tip & 오류 및 해결방법

 

1. Hybrid ECS란?

  • 간단하게 기존 Unity의 컴포넌트 방식과 ECS 방식을 융합된 방법입니다.
    • Unity Editor 상에서는 기존 Unity와 동일하게 하이러키와 컴포넌트를 사용하고, 스크립트는 ECS 방식입니다.
  • ECS 방법 자체는 기존 사용자가 바로 사용하기에는 무리가 있을 정도로 에디팅 방식이 많이 바뀌지만, Hybrid 방식으로 둘을 적당히 섞음으로써 ECS 방식을 좀 더 쉽게 접근할 수 있습니다.
  • 장점
    • 진입장벽이 낮다    
    • 기존의 유니티에서 제공하는 시스템을 함께 사용할 수 있다. (ECS만 사용한다면, 새로 만들어야 할 수도 있습니다.) 
  • 단점

 

2. 기본적인 사용방법

 편의상 component - Entity - System 순으로 진행하겠습니다.

 순서는 간단한 설명 후 플레이어블 캐릭터를 예시로 드는 식으로 진행합니다. 

 

Component

 - 하나의 정의에 필요한 데이터 집합

 

  • 아래는 플레이어블 캐릭터의 playerInput 데이터의 집합(컴포넌트) 입니다. 일반 클래스 생성하듯 만듭니다.
public class PlayerInput : MonoBehaviour 
{ 
    public float2 move; 

    public bool jump; 
    public bool seat; 

    public bool attack; 
}

 

  • 그리고 밑은 캐릭터/ 몬스터 누구나 가지고 있을법한 hp에 대한 데이터 집합(컴포넌트) 입니다.
public class Health : MonoBehaviour 
{ 
    public float value;

    public bool isDead;
}

 

  • 위와 같이 하나의 정의에 필요한 데이터 집합들이 component입니다.
    • input에 관련된 요소, Health에 관련된 요소 hp값과 죽었는지 여부입니다.

 

Entity

 - 컴포넌트들의 집합

  •  위의 컴포넌트들을 조합해서 만들어집니다. 기존의 게임오브젝트와 유사하게 생각하시면 됩니다.
  •  위에서 만든 컴포넌트들을 에디터창에서 원하는 오브젝트에 붙이면 됩니다.
    • 각 오브젝트에 필요한 특성과 정의를 부여한다고 생각할 수 있겠네요.
  •  반드시 지켜야 할 것이 ECS의 관리를 받는 오브젝트를 만들려면 Game Object Entity라는 클래스도 오브젝트에 추가해야합니다. (이것을 안하면 System에서 아예 인지를 하지 못합니다.)
  • 아래는 플레이어블 캐릭터의 인스펙터 창입니다.

  • 아래는 체력만 가진 몬스터의 인스펙터 창입니다. ( PlayerInput 빼고는 위와 동일합니다.)

 

 

 System

 - Entity들의 행동로직 

  • 데이터들을 가공하는 곳으로 일반 객체지향의 method와 비슷합니다.
  • 시스템은 방식이 두가지가 있습니다.
    • 두 방식이 어떨 때 좋을지는 정확히는 잘 모르겠네요.
    • 차이점이라면 두번째는 개체수를 얻을 수 있다는 점과 개인적으로 사용했을 때는 여러 엔티티를 가져와 로직을 돌려야 할 경우 두번째 방식이 더 편하고, 하나만 사용할 경우에는 첫번째가 편하더군요.
  • 기본 템플릿

1. 

using UnityEngine; 
using Unity.Entities; 

public class ClassName : ComponentSystem 
{ 
    public struct StructName
    { 
        public ComponentClass name; 
         
    } 
    protected override void OnUpdate() 
    { 
        foreach(var entity in GetEntities<StructName>()) 
        {

            // 로직 진행
            // ex ) entity.name.property = 0;
        } 
    } 
}

 

2.

using Unity.Entities; 
using UnityEngine; 

public class ClassName : ComponentSystem 
{ 
    public struct StructName
    { 
        public readonly int Length; 
        public ComponentArray<ComponentClass > name; 
    } 
    [Inject] 
    private StructName m_data; 

    protected override void OnUpdate() 
    {         
        for(int i = 0; i < m_data.Length; i++) 
        {

            // 로직 진행
            // ex ) m_data.name[i].property = 0; 
             
        } 
    } 
}

 

  • 사용예시

  • 아래는 인풋제어 컴포넌트를 가진 엔티티들을 불러와 키맵핑을 해주는 시스템입니다.
    • 다른 컴포넌트를 가지고 있든말든 인풋제어만 있으면 다 로직이 돕니다.
using UnityEngine; 
using Unity.Entities; 

//사용자의 입력을 input component에 배정하는 시스템 
// 이동 점프 앉기 공격 등 키입력 맵핑 
public class PlayerInputSystem : ComponentSystem 
{ 
    public struct PlayerData 
    { 
        public PlayerInput input; 
         
    } 
    protected override void OnUpdate() 
    { 
        foreach(var entity in GetEntities<PlayerData>()) 
        { 
            entity.input.move.x = Input.GetAxis("Horizontal"); 

            entity.input.seat = false; 
            entity.input.jump = false; 

            if (Input.GetKey(KeyCode.DownArrow)) entity.input.seat = true; 
            if (Input.GetKey(KeyCode.UpArrow)) entity.input.jump = true; 

        } 
         
    } 
}

 

 

  • 아래는 플레이어블 캐릭터의 컴포넌트들을 가진 엔티티들을 불러와 이동시켜주는 시스템입니다.
    • 마찬가지로 PlayerData에 있는 컴포넌트를 가진 모든 엔티티를 불러옵니다.
using Unity.Entities; 
using UnityEngine; 

// 입력된 input에 맞춰 캐릭터 이동  
public class PlayerMoveSystem : ComponentSystem 
{ 
    public struct PlayerData 
    { 
        public readonly int Length; 
        public ComponentArray<PlayerInput> input; 
        public ComponentArray<Health> health; 
        public ComponentArray<Rigibody2D> rigid; 
    } 
    [Inject] 
    private PlayerData m_data; 

    protected override void OnUpdate() 
    { 
        // 병렬로 전체 캐릭터들이 모두 별도로 이동하고, 나중에 한꺼번에 적용 
        float dt = Time.deltaTime; 
        for(int i = 0; i < m_data.Length; i++) 
        { 
            m_data.rigid[i].AddForce(new Vector2(m_data.input[i].move.x * 4, 0)); 
             
        } 
    } 
}

 

  • 아래는 Health를 가진 모든 오브젝트에 대해 죽음 판별하는 시스템입니다.
    • 캐릭터와 몬스터 모두 Health를 가졌으니 모두를 대상으로 돌겠죠
using UnityEngine; 
using Unity.Entities; 

public class HealthCheckSystem : ComponentSystem 
{ 
    public struct PlayerData 
    { 
        public Health health; 
         
    } 
    protected override void OnUpdate() 
    { 
        foreach(var entity in GetEntities<PlayerData>()) 
        { 
            if(entity.health.value <=0) entity.health.isDead = true;

        } 
         
    } 
} 

 

  • 그래서 필요 기능별로 컴포넌트들을 구분지어야 나중에 시스템을 돌릴때 편해지고 직관적이게 됩니다. 오브젝트를 사용할 때도 컴포넌트만으로 제어하기 편합니다.
    • 예를들어 이 캐릭터는 무적인 특성이 있다 했을 때, 예외처리를 하는게 아니라 단지 그 캐릭터에게 Health 컴포넌트만 제거하면 Health에 관련된 모든 로직을 무시할 수 있게되죠.
    • 마찬가지로, 스킬이나 특성들도 컴포넌트만으로 제어할 수 있겠죠.

 

 

3. 사용 Tip & 오류 및 해결방법

  • 시스템 로직 도는 순서 변경하는 방법

    •  기능별로 시스템이 돌기 때문에 간혹 순서상에 문제가 발생할 수도 있습니다.
      •  물론 이런경우를 발생 안시키는게 베스트이긴 하지만, 부득이할 경우 로직 도는 순서를 변경 시킬 수 있습니다.
      • (디폴트로는 생성 순으로 진행되는 것으로 압니다.)
    • 참고 사이트 - https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/system_update_order.html
    • 방법 
// 타겟 시스템이 SystemClass 이후에 실행되도록 설정
// 같은 시스템 그룹 내에서 동작
[UpdateAfter(typeof(SystemClass))]

// 타겟 시스템이 SystemClass 이전에 실행되도록 설정
// 같은 시스템 그룹 내에서 동작.
[UpdateBefore(typeof(SystemClass))]

// 타겟 시스템을 SystemGroup에 귀속시킴.
// 디폴트로 개발자가 만든 System은 모두 SimulationSystemGroup에 속함.
[UpdateInGroup(typeof(SystemGroup))]


// 사용 예시
// PlayerInputSystem 실행 이전에 TestSystem (타켓시스템)을 실행하도록 설정.
[UpdateBefore(typeof(PlayerInputSystem))]
public class TestSystem : ComponentSystem
{
    public struct PlayerData
    {
        public PlayerInput input;
    }
    protected override void OnUpdate()
    {
        foreach (var entity in GetEntities<PlayerData>())
        {
        }
    }
}

 

  • 여러 엔티티를 하나의 시스템에서 써야할 때

    • 그냥 두개 inject 해오면 된다.
    public struct PlayerData 
    { 
        public readonly int Length; 
        public ComponentArray<PlayerInput> input; 
        public ComponentArray<Transform> transform; 

    } 
    [Inject] 
    private PlayerData p_data; 

    public struct MinimapData 
    { 
        public readonly int Length; 
        public ComponentArray<Minimap> minimap; 

    } 
    [Inject] 
    private MinimapData m_data;

 

  • Tag처럼 쓰기

    • Tag도 하나의 컴포넌트로써 사용하면 된다.
      • ex) Component로 Human클래스를 만들고 아무 내용없이 오브젝트에 붙여만 놓으면, 시스템에서 Human으로 걸러낼 수 있죠.

 

 

 

 

 

 

 

 

Posted by 검은거북

* 작년부터 개인 프로젝트를 진행하며 공부한 내용을 정리하는 포스팅입니다.

 

ECS란 무엇인가

Entity - Component - System 의 줄임말로, 각 용어에 대해 간단히 설명하자면 아래와 같습니다.

 

  • Component  - 데이터 집합
    • 요소를 이루는 데이터 집합
    • ex
      • position - 위치의 데이터만 가짐 (x,y,z)
      • rotation - 방향의 데이터만 가짐 (x,y,z)
  • Entity  - 컴포넌트 집합
    • 하나의 오브젝트를 이루는 데이터 집합의 집합 
    • ex
      • character - position, rotation 컴포넌트를 가짐.
  • System - 데이터들을 가공하는 작업 ( method 개념)
    • ex
      • MoveSystem - position, rotation 컴포넌트를 가진 엔티티들을 대상으로 키 입력시 position의 데이터를 변경하기만 한다.

 

기존의 객체 지향 개념(OOP) 이 아닌 데이터 지향 방식(DOD)의 개념으로, 데이터 지향은 저장되는 데이터의 위치를 집적시켜 캐시 히트율을 높여 속도를 끌어올리는 목적의 개념으로 알고있습니다.

 

 * 데이터 지향 부가 설명 및 운영체제 지식

  - 프로그램에서 데이터 필요시 CPU 캐시공간을 먼저 검사하고, 거기 없으면 메모리(램)에서 해당 데이터를 가져와서 사용합니다. (메모리에서 가져올 때는 필요 데이터 주변의 데이터를 뭉태기로 캐시에 가져옵니다. 그 주변 데이터를 다음에 사용할 확률이 크기때문이죠) 이때 CPU 캐시에서 바로 가져오느냐, 메모리에서 가져와서 사용하느냐의 속도차이는 아마 몇 십배일거에요. 즉, 캐시에 미리 사용할 데이터가 없을 수록 속도는 몇십배씩 느려진다는 거죠.  그래서 데이터 지향 방식은 데이터들을 한 곳에 몰아 넣어서 캐시 적중률을 늘려 속도를 올릴 수 있는거죠. (객체 지향은 객체에 따라 데이터가 위치가 분산될겁니다.) 

 

Unity Scripting 방법

* 현재 unity에서 제공하는 방식은 3가지 입니다.

  1. Classic ( 현재 일반적으로 사용하는 컴포넌트 방식입니다.)
  2. Hybrid ECS 
    • Component + ECS 방식입니다.
    • 에디터상의 작업은 기존의 Classic 방식을 따르고, 코딩은 ECS 방식으로 하는 형식입니다. 아마 ECS의 진입장벽을 낮추고, ECS의 속도면 이점도 어느정도 갖추기 위해 만들어졌을거 같네요.
  3. Pure ECS
    • 완전한 ECS 방식입니다.
    • 기존의 Classic 방식과는 객체 생성및 등록 방식이 확 바뀝니다. 거의 모든 걸 스크립트상에서 처리하기에 진입장벽이 많이 높습니다.

 

장점 & 단점

  • 장점

    • 재사용성이 높다. / 상호 의존도가 낮다
      • 위 부분은 객체지향의 장점에도 포함되지만 개인적으로 사용 경험상, 데이터 지향 방식이 더 활용도가 높다고 느껴집니다.
      • 데이터 지향의 시스템은 자기가 필요한 엔티티 집합을 검색해 로직을 돌리는 방식이고, 엔티티는 시스템의 검색에 찾아지도록 필요 컴포넌트들을 포함시켜 만들어진 집합입니다. 즉, 특정 엔티티가 특정 시스템에 돌아가게 하고싶다면, 시스템이 필요로하는 컴포넌트만 엔티티에 추가만 하면 끝인거죠.
        • 이런 방식으로 시스템과 컴포넌트를 구성해놓으면 엔티티는 자기가 원하는 것만 골라 재사용하기 용이하게 느껴집니다.
        •  그리고 실제 설계 / 코딩을 할 때도 필요한 컴포넌트를 개별로 시스템에서 가져다 사용하기때문에 다른 시스템/ 엔티티에 영향도가 적습니다. (상호 의존성 낮음)
    • 유지보수가 용이하다.
      • 위의 장점과 일맥상통합니다.
        • 시스템 별로 필요 엔티티만 집약되어있기에 시스템별로 찾고 고치기가 쉽습니다. 
    • 실행 속도가 빠르다
      • 네, 엄청 빠릅니다. (이건 다음 포스팅에서 검증하도록 하겠습니다)
  • 단점

    •  진입장벽이 많이 크다.
      • Pure가 특히 높습니다. Hybrid는 기존의 방식을 어느정도 쓰기때문에 사실 ECS 개념만 좀 숙달되면 금방하는데, Pure는 스크립트로 거의 모든 걸 진행하여 에디터 UI상의 이점을 많이 뺏깁니다.
    •  디버깅이 힘들다.
      • Pure의 단점으로, 위의 단점과 일맥상통합니다.
      • Pure는 에디터상의 하이러키창에 오브젝트가 안나옵니다. 그래서 하이러키창을 자주 활용하여 디버깅하시는 분은 꽤 당황스러울 수 있습니다.
    • 베타 버전 (개인적으로 최고 단점으로 뽑습니다.)
      • Unity에서 기존에 제공하던 컴포넌트들을 직접 구현해야 할 수 있습니다. 
        • 일부 컴포넌트가 ECS형태로 아직 제공이 안됩니다 ㅠㅠ  자주 사용하는 Physics, animation관련 컴포넌트도 없는게 꽤 있습니다.

 

저는 아직 Pure ECS는 많이 어색하고 어렵더군요. 그래서 앞으로의 포스팅은 Hybrid ECS 위주로 진행하겠습니다. 애초에 개인 프로젝트도 Hybrid ECS를 이용해 만들기도 했거든요.

 

다음 포스팅은 유니티에서 제공해주는 샘플 프로젝트를 이용해 실험해본 간단한 성능 벤치마크를 하도록 하겠습니다.

대체 얼마나 기존에 비해 성능이 빨라질지 간단하게 실험한 결과입니다.

 

* 개인이 공부하며 하는 포스팅이기에 잘못된 부분이 있을 수 있습니다.

 혹시 잘못되거나 문제시 되는 부분이 있으면 지적 부탁드립니다.

Posted by 검은거북
이전버튼 1 이전버튼

블로그 이미지
프로그래밍 공부 요약 및 공부하며 발생한 궁금증과 해결과정을 포스팅합니다.
검은거북

공지사항

Yesterday
Today
Total

달력

 « |  » 2025.1
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

최근에 올라온 글

최근에 달린 댓글

글 보관함