2021.09.26 - [이론/설계] - ECS (Entity Component System)
ECS는 위 내용 참고..
Unity ECS?
ECS는 아직 개발 중이라서 preview 버전만 쓸 수 있다. 업데이트할 때마다 클래스가 없어지거나 하는 경우도 많아서 좀 그렇긴 한데... 기본적인 원리는 배울 수 있으니까 그냥 시작해보자.
유니티는 ECS를 크게 두가지 PURE/HYBRID로 나누고 있다.
PURE
- ECS로 GameObject는 더 이상 없고 Entity가 이를 대체한다.
- MonoBehaviours도 없다. 데이터는 컴포넌트가 가지며 로직은 System이 업데이트해준다.
HYBRID
- PURE의 모든 특징을 갖는다.
- GameObject를 Entity로 , MonoBehaviours를 Component로 바꿔주는 Convert Helper Class들이 존재한다.
- 유니티의 native class에 대한 지원도 해준다.
PURE 한 방식은 개발이 어렵다. 특히 Prefab과 같은 작업이 불가능하기 때문에 Prefab을 ECS에 맞게 변환해주는 HYBRID 한 방법을 사용할 것이다.
세팅
패키지 매니저에서 +버튼을 누른 후 Add Package From Git URL 을 눌러주자.
그다음 com.unity.rendering.hybrid를 적고 Add를 누른다. (알아서 최신으로 깔아줌)
hybrid 안에는 기본적인 Unity ECS에 대한 dependency가 있어서 자동으로 다른 애들도 설치해준다.
참고로 이 글은 HyBrid Renderer 0.11.0-preview.44, com.unity.entities 0.17.0-preview42 버전을 사용했다.
https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/index.html
기타 패키지들은 위 글의 DOTS Packages에서 확인할 수 있다.
Convert to entity
위에서 Hybird ECS에서는 기존 Gameobject, Monobehaviour를 ECS에 맞게 변환시켜주는 헬퍼 클래스를 제공한다고 했다. ConvertToEntity가 바로 이 핵심 역할을 하는 클래스이다.
사용 방법은 매우 간단하다
그냥 변환할 게임 오브젝트에 스크립트를 붙여주면 된다.
물론 이건 기초에 해당하는 이야기이고... 찾아보니까 엄청나게 복잡한 Conversion Workflow가 존재한다.
아무튼 ConvertToEntity 클래스는 Monobehaviour이며, Awake() 에서 Convert System에 해당 게임 오브젝트를 변환할 오브젝트 리스트에 추가해준다.
Conversion Mode가 Convert And Destroy라면 기존 GameObject를 제거한다.
일단 여기까지 봤을 때, ConvertSystem이 Cube에 붙어있는 Transform, MeshFilter, MeshRenderer를 알아서 Component로 변환해 줄 것으로 기대할 수 있다. 이 과정은 어떻게 이뤄질까?
대표적으로 Transform의 변환을 알아보자.
TransformConversion
Transform 변환은 TransfromConversion에서 일어난다. 아래 코드를 보면 그 과정을 알 수 있다.
private void Convert(Transform transform)
{
var entity = GetPrimaryEntity(transform);
DstEntityManager.AddComponentData
(entity, new LocalToWorld { Value = transform.localToWorldMatrix });
if (DstEntityManager.HasComponent<Static>(entity))
return;
var hasParent = HasPrimaryEntity(transform.parent);
if (hasParent)
{
DstEntityManager.AddComponentData
(entity, new Translation { Value = transform.localPosition });
DstEntityManager.AddComponentData
(entity, new Rotation { Value = transform.localRotation });
if (transform.localScale != Vector3.one)
DstEntityManager.AddComponentData
(entity, new NonUniformScale { Value = transform.localScale });
DstEntityManager.AddComponentData
(entity, new Parent { Value = GetPrimaryEntity(transform.parent) });
DstEntityManager.AddComponentData
(entity, new LocalToParent());
}
else
{
DstEntityManager.AddComponentData
(entity, new Translation { Value = transform.position });
DstEntityManager.AddComponentData
(entity, new Rotation { Value = transform.rotation });
if (transform.lossyScale != Vector3.one)
DstEntityManager.AddComponentData
(entity, new NonUniformScale { Value = transform.lossyScale });
}
}
일단 TransformConversion은 Transform 을 LocalToWorld, Translation, Rotation으로 저장한다. 여기에 더해 Scale이 1이 아니라면 NonUnitformScale을 , 부모 오브젝트가 있으면 LocalToParent를 추가해준다.
당연히 Component의 조합이 다르면 다른 Chunk에 담긴다. (ECS 참고 글의 아키 타입 참고)
아래와 같은 파란 큐브가 있다.
큐브는 다음의 컴포넌트를 갖는다.
게임을 실행하여 Entity Debugger를 확인해보면 다음과 같은 entity를 확인할 수 있다.
Transform이 LocalToWorld와 Rotation, Translation으로 바뀌었다. MeshFilter와 MeshRenderer도 알아서 잘 변환된 것 같다. 그런데 BoxCollider는 없다. 왜 없을까?
BoxCollider에 대한 Conversion이 없기 때문이다. 따라서 이런 경우 BoxCollider를 Entity Component로 바꿔주는 Conversion System이 필요하다. 즉, Monobehaviour를 적절한 ECS Component로 바꾸기 위해서는 해당 Monobehaviour의 Conversion 클래스가 필요하다. (아마 Physics Package에 있지 않을까 싶음)
가령 RenderMeshConversion의 OnUpdate 부분을 주석처리 시켜보자...
그러면 ECS World에 마땅한 렌더러가 추가되지 않을 태니 게임 오브젝트가 렌더링 되지 않을 것이다.
당연히 큐브는 보이지 않게 되고..
RenderMesh와 그에 의존하는 다른 컴포넌트 역시 청크에서 사라진다.
ConversionWorld, Destination World
가끔 코드를 보면 DestinationWorld, DstManager 이런 이름들이 나오는데 Destination World란 Conversion이 모두 끝난 후의 World라고 생각하면 된다. Conversion 월드에서는 Monobehaviour와 GameObject를 추출하고 여기서 업데이트가 한 번 일어나면 Destination World에 변환된 Component들이 추가된다.
로직 업데이트
로직은 System에서 업데이트한다.
간단하게 큐브를 특정 궤도에서 움직이도록 해보자.
먼저 속도를 추가해야 할 것이다. 아래와 같은 구조체를 만들어준다. (Burst를 사용하기 위해서는 struct여야 하는 듯..? 아님 말고 ㅎ)
[GenerateAuthoringComponent]
public struct Velocity : IComponentData
{
[HideInInspector] public float4 Value;
}
위와 같은 스크립트를 만들어서 Cube의 Prefab에 붙여준다.
[GenerateAuthoringComponent] 어트리뷰트를 달면 아래와 같이 인스펙터에서 Prefab에 스크립트를 추가할 수 있도록 해준다.
[HideInInspector] 어트리뷰트와도 잘 작동한다!
게임을 실행해보면 Velocity라는 컴포넌트가 잘 들어간 것을 확인할 수 있다.
그러면 이번에는 이 속도를 기반으로 큐브를 움직여보자.
다음과 같이 SystemBase를 상속한 시스템을 만들어준다. (따로 등록할 필요는 없음)
public class GravityVelocitySystem : SystemBase
{
protected override void OnUpdate()
{
var deltaTime = Time.DeltaTime * CubeGM.SimulationSpeed;
var core = CubeCenter.Pos;
var gravityAccel = CubeCenter.G;
Entities
.WithName("CubeECS_GravityVelocitySystem")
.ForEach((ref Translation translation, ref Velocity velocity) =>
{
//TODO : deltaTime에 의한 오차 누적 있음.에너지보존 필요
var pos = core - translation.Value.xyz;
var gravity = gravityAccel * Normalize(pos) / SqrMagnitude(pos);
var laterVelocity = velocity.Value.xyz + gravity * deltaTime;
translation.Value.xyz += (velocity.Value + laterVelocity) * 0.5f * deltaTime;
velocity.Value = laterVelocity;
})
.ScheduleParallel(); // 멀티 Worker 쓰레드
// 아래 두 옵션도 가능하다.
// .Run() // 메인쓰레드(Single)에서 돌린다.
// .Schedule() // 싱글 Worker 쓰레드에서 돌린다.
}
}
WithName은 그냥 디버깅 or 프로파일 용이다.
ForEach에서는 인자로 넘긴 컴포넌트를 가진 엔티티들을 긁어모은 뒤 수행할 작업을 지정해준다.
ScheduleParallel에서 실제로 로직을 업데이트한다. 메인 스레드에서만 하고싶다면 Run(), 싱글 Worker 쓰레드에서 돌리고 싶다면 Schedule()을 대신 사용한다.
이제 게임을 실행해보자!
큐브가 잘 돌아간다.
'게임엔진 > DOTS' 카테고리의 다른 글
DOTS 1.0 - 2 (생성, MonoBehavour 연계) (0) | 2023.02.04 |
---|---|
DOTS 1.0 - 기본 (Component,System,Aspect,Job) (0) | 2023.01.29 |
DOTS 1.0 나온 기념 세팅법 (2) | 2023.01.01 |
일단 시작3 - Instantiate & Destory (0) | 2022.03.23 |
DOTS - 일단 시작2 (2) | 2022.03.16 |