게임엔진/DOTS

DOTS 1.0 - 2 (생성, MonoBehavour 연계)

tsyang 2023. 2. 4. 01:31

2023.01.29 - [유니티/DOTS] - DOTS 1.0 - 기본 (Component,System,Aspect,Job)

 

DOTS 1.0 - 기본 (Component,System,Aspect,Job)

2023.01.01 - [유니티/DOTS] - DOTS 1.0 나온 기념 세팅법 DOTS 1.0 나온 기념 세팅법 ECS 1.0 pre-release 버전이 나왔다. 설치해보자 우선 유니티 2022.2.0b 이상을 깔아야 한다. IDE도 최신으로 바꿔야 함. 아래 링

tsyang.tistory.com

 

Webp사용

 

이번에는 ECS에서 Entity를 생성해보고, 기존 MonoBehvaiour코드에서 Entity의 데이터에 접근하도록 하는 걸 해보자.

 

 

ECS Instantiate

 

우선 Spawner 역할을 할 컴포넌트를 만든다. 간단한게 prefab과 몇 개를 만들지를 지정해준다.

public struct PlayerSpawnerComponent : IComponentData
{
    public Entity Prefab;
    public int SpawnAmount;
}
public class PlayerSpawnerAuthoring : MonoBehaviour
{
    public GameObject prefab;
    public int spawnAmount;
}

public class PlayerSpawnerBaker : Baker<PlayerSpawnerAuthoring>
{
    public override void Bake(PlayerSpawnerAuthoring authoring)
    {
        AddComponent(new PlayerSpawnerComponent()
        {
            Prefab = GetEntity(authoring.prefab),
            SpawnAmount = authoring.spawnAmount,
        });
    }
}

 

그 다음에 지금 생성된 엔티티들 중 Player인 엔티티들을 파악할 필요가 있는데, 이를 위해서 빈 컴포넌트인 PlayerTag를 추가해줄 수 있다.

 

public struct PlayerTag : IComponentData
{
}
public class PlayerTagAuthoring : MonoBehaviour
{
}

public class PlayerTagBaker : Baker<PlayerTagAuthoring>
{
    public override void Bake(PlayerTagAuthoring authoring)
    {
        AddComponent(new PlayerTag());
    }
}

 

이제 만든 Prefab에 PlayerTagAuthoring을 추가해주면 끝난다. PlayerTag를 Query하는 것 만으로 Player 엔티티만 모아올 수 있다.

 

이제 SystemBase를 상속한 시스템에서 prefab을 Instantiate해주면 된다.

 

그런데 주의할 점이 있는 게, 엔티티 리스트를 얻어오고 이걸 시스템 내에서 사용하게 될 탠데, 시스템 내에서 엔티티 리스트에 엔티티를 추가하거나 제거한다면? 컬렉션 반복 중에 컬렉션 목록을 수정하는 것과 똑같은 오류가 발생할 것이다.

 

따라서 이런 건 다른 시스템들이 수행되기 전이나 후에 실행되어야 한다. 

 

시스템 목록을 보면 Begin Simulation Entity Command Buffer System이 있는데 이 시스템은 다른 시스템이 돌아가기 전에 호출되는 시스템이다. 

 

여기서 커맨드 버퍼란 엔티티 리스트의 수정을 위해 추가나 제거 명령을 모아두는 버퍼이다. 

 

 

아무튼 이 시스템을 이용해서 생성 시스템을 만든다.

public partial class PlayerSpawnSystem : SystemBase
{
    protected override void OnUpdate()
    {
        var query = EntityManager.CreateEntityQuery(typeof(PlayerTag));

        var playerSpawnerComponent = SystemAPI.GetSingleton<PlayerSpawnerComponent>();
        var spawnAmount = playerSpawnerComponent.SpawnAmount;

        if (query.CalculateEntityCount() >= spawnAmount) return;
        
        var randomComponent = SystemAPI.GetSingletonRW<RandomComponent>();
        
        var entityCommandBuffer = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>()
            .CreateCommandBuffer(World.Unmanaged);
        
        var newEntity = entityCommandBuffer.Instantiate(playerSpawnerComponent.Prefab);
        entityCommandBuffer.SetComponent(newEntity, 
            new Speed(){Value = randomComponent.ValueRW.Random.NextFloat(1f,5f)});
    }
}

 

 

그럼 끝.

 

 

 

MonoBehaviour에서 Entity 읽기

 

마우스 클릭으로 캡슐 오브젝트를 클릭하면 아래와 같이 선택되었다는 느낌을 주는 오브젝트를 추가하고 싶다고 하자.

 

 

대충 동그란 띠 모양의 오브젝트가 있는 프리팹을 만들고 

 

public class SelectedObjectVisual : MonoBehaviour
{
    public GameObject Circle;
    public float Distance;
    
    private Entity _targetEntity;
    private Transform _transform; //Transform 캐싱

    private void Awake()
    {
        _transform = GetComponent<Transform>();
    }
    
    private void LateUpdate()
    {
        //엔티티 위치를 얻어 업데이트 해준다.
    }

    private Entity SelectEntity(Vector3 mousePosition)
    {
        //마우스 클릭 위치로 엔티티를 선택한다.
    }
}

위와 같은 스크립트를 일단 추가해주자. 

 

circle은 실제 초록 동그라미 오브젝트로, 선택한 개체가 없을 경우 on/off해주기 위해 참조를 받는다.

distance는 선택 기준이 되는 거리

targetEntity는 현재 오브젝트가 따라다닐 entity를 말한다.

 

우선 Entity를 얻어오려면 EntityManager를 알아야 하는데 ECS에서는 World라는 개념이 있어서 World마다 EntityManager가 존재할 수 있다. 우리가 원하는 건 DefaultGameObjectInjectionWorld라는 곳에 있다. 

 

아무튼 다음과 같이 마우스 클릭 지점으로부터 거리가 Distance 이내인 가장 가까운 엔티티를 선택하는 메서드를 구현한다.

private Entity SelectEntity(Vector3 mousePosition)
{
    if(MainCamera.Instance == null) return Entity.Null;
    var mainCam = MainCamera.Instance.Camera;
    var ray = mainCam.ScreenPointToRay(mousePosition);
    if (!Physics.Raycast(ray, out RaycastHit hit, 50f, LayerMask.GetMask("Ground"))) return Entity.Null;

    var planePos = hit.point;
    var query = World.DefaultGameObjectInjectionWorld.EntityManager.CreateEntityQuery(typeof(PlayerTag));
    Entity closestEntity = Entity.Null;
    var minSqrDist =  distance * distance;
    foreach (var entity in query.ToEntityArray(Unity.Collections.Allocator.Temp))
    {
        var entityPos = World.DefaultGameObjectInjectionWorld.EntityManager.GetComponentData<LocalTransform>(entity).Position;
        var dist = (planePos - (Vector3)entityPos).sqrMagnitude;
        if (dist < minSqrDist)
        {
            minSqrDist = dist;
            closestEntity = entity;
        }
    }
    return closestEntity;
}

 

그리고 LateUpdate에 위치를 업데이트 해준다.

 

private void LateUpdate()
{
    if (Input.GetMouseButtonUp(0))
    {
        var mousePosition = Input.mousePosition;
        _targetEntity = SelectEntity(mousePosition);
    }

    if (_targetEntity == Entity.Null)
    {
        cursor.SetActive(false);
        return;
    }
    
    cursor.SetActive(true);
    
    Vector3 targetPosition = World.DefaultGameObjectInjectionWorld.EntityManager
    				.GetComponentData<LocalTransform>(_targetEntity).Position;
    _transform.position = targetPosition;
}

 

 

여태껏 구현한 내용을 실행해주면 다음과 같다.