아래는 플레이어블 캐릭터의 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 & 오류 및 해결방법
시스템 로직 도는 순서 변경하는 방법
기능별로 시스템이 돌기 때문에 간혹 순서상에 문제가 발생할 수도 있습니다.
물론 이런경우를 발생 안시키는게 베스트이긴 하지만, 부득이할 경우 로직 도는 순서를 변경 시킬 수 있습니다.
// 타겟 시스템이 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으로 걸러낼 수 있죠.