안녕하세요. 플라네타리움에서 나인 크로니클을 개발하고 있는 현승민입니다. 해당 프로젝트는 아직 Unity DOTS를 사용하고 있지 않지만, 차기작에 적용하기 위해 열심히 공부 중인데요. 앞으로 공부한 내용을 꾸준히 공유해보려고 해요.
이번에는 DynamicBuffer<T>
에 대해서 알아 볼게요. 엔티티에 동적 버퍼를 설정하고 이를 사용하는 방법에 대한 것인데요. DOTS와 관련한 첫 번째 글 치고는 몇 단계를 넘어 오기는 했지만 그 양이 적으니 관련한 내용을 함께 보시면 바로 이해하실 수 있을 것이라 생각해요.
이 글은 Unity 공식 문서와 튜토리얼 영상을 참고했어요.
개발 환경
- Unity
- 2019.3.12f1
com.unity.entities
- 0.10.0-preview.6
IBufferElementData
구현하기
엔티티에 더하는 컴포넌트가 IComponentData
인터페이스를 구현해야 하는 것과 마찬가지로, DynamicBuffer<T>
또한 IBufferElementData
인터페이스를 구현해야 해요.
-
IBufferElementData
를 구현하는IntBufferElement
구조체를 만들었어요.IComponentData
와 같은 흐름이죠?using Unity.Entities; namespace DOTS_DynamicBuffer { public struct IntBufferElement : IBufferElementData { public int Value; } }
EntityManager.AddBuffer<T>()
사용하기
엔티티에 컴포넌트를 더하는 방법과 같이 버퍼를 더할 때도 EntityManager
를 활용해요. 아래에서는 게임 오브젝트에 더해서 사용할 PlayModeTest
라는 컴포넌트를 작성하고 플레이 모드에서 Entity Debugger를 확인해 볼게요.
-
엔티티에
IntBufferElement
버퍼를 더하고, 그 버퍼에 값을 좀 넣어 볼게요.using UnityEngine; using Unity.Entities; namespace DOTS_DynamicBuffer { public class PlayModeTest : MonoBehaviour { private void Awake() { var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; var entity = entityManager.CreateEntity(); var dynamicBuffer = entityManager.AddBuffer<IntBufferElement>(entity); dynamicBuffer.Add(new IntBufferElement { Value = 1 }); dynamicBuffer.Add(new IntBufferElement { Value = 2 }); dynamicBuffer.Add(new IntBufferElement { Value = 3 }); } } }
-
DOTS_DynamicBufferScene을 만들고
PlayModeTest
스크립트를 같은 이름의 게임 오브젝트에 추가했어요. -
플레이 모드에서 Entity Debugger 를 통해서
PlayModeTest.Awake()
메서드에서 생성한 엔티티를 확인할 수 있어요.IntBufferElement
버퍼에 값이 세 개인 것이 보이시죠?
DynamicBuffer<T>.Reinterpret<U>()
사용하기
버퍼에 담긴 구조체가 포함하는 값을 직접 수정하는 방법을 알아 볼게요.
-
PlayModeTest.Awake()
메서드를 조금 수정해서 재해석이라는 의미의DynamicBuffer<T>.Reinterpret<U>()
메서드를 사용해 봤어요. 12번 줄에서와 같이 인덱스로 접근한 구조체는 변수로 분류되지 않는 임시 값이기 때문에 변경할 수 없는데, 14–15번 줄에서와 같은 방법을 사용하면 값을 수정할 수 있어요.private void Awake() { var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; var entity = entityManager.CreateEntity(); var dynamicBuffer = entityManager.AddBuffer<IntBufferElement>(entity); dynamicBuffer.Add(new IntBufferElement {Value = 1}); dynamicBuffer.Add(new IntBufferElement {Value = 2}); dynamicBuffer.Add(new IntBufferElement {Value = 3}); // ERROR: Indexer access returns temporary value. // Cannot modify struct member when accessed struct is not classified as a variable // dynamicBuffer[0].Value *= 10; var intDynamicBuffer = dynamicBuffer.Reinterpret<int>(); intDynamicBuffer[0] *= 10; }
-
값이 바뀌었는지 플레이 모드에서 확인해 볼게요. 잘 바뀌었네요! 15번 줄에서 변경한
intDynamicBuffer[0]
의 값을dynamicBuffer[0]
에 다시 넣지 않았는데 버퍼의 값이 바뀐 것이 중요한 점으로 보여요.
EntityManager.GetBuffer<T>()
사용하기
엔티티의 버퍼에 접근하는 방법도 필요하겠죠?
-
PlayModeTest
클래스를 수정했어요.Awake()
메서드에서 생성한 엔티티와 이것에 추가한 버퍼를Start()
메서드에서 불러와서 값을 수정했어요.public class PlayModeTest : MonoBehaviour { private Entity _entity; private void Awake() { var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; _entity = entityManager.CreateEntity(); var dynamicBuffer = entityManager.AddBuffer<IntBufferElement>(_entity); dynamicBuffer.Add(new IntBufferElement { Value = 1 }); dynamicBuffer.Add(new IntBufferElement { Value = 2 }); dynamicBuffer.Add(new IntBufferElement { Value = 3 }); // ERROR: Indexer access returns temporary value. // Cannot modify struct member when accessed struct is not classified as a variable // dynamicBuffer[0].Value *= 10; var intDynamicBuffer = dynamicBuffer.Reinterpret<int>(); intDynamicBuffer[0] *= 10; } private void Start() { var entityManger = World.DefaultGameObjectInjectionWorld.EntityManager; var dynamicBuffer = entityManger.GetBuffer<IntBufferElement>(_entity); var intDynamicBuffer = dynamicBuffer.Reinterpret<int>(); for (var i = 0; i < intDynamicBuffer.Length; i++) { intDynamicBuffer[i]++; } } }
-
잘 동작하는지 확인할게요. 버퍼 내의 모든 값이 1씩 증가한 것이 보이네요! 여전히 신기한
Reinterpret<T>()
.
Authoring
GenerateAuthoringComponentAttribute
를 적용하면 게임 오브젝트에 Authoring Component를 더해서 엔티티로 만들 수 있죠. IBufferElementData
도 같은 방법을 사용할 수 있어요.
-
IntBufferElement
를 수정해서GenerateAuthoringComponentAttribute
를 적용할게요.[GenerateAuthoringComponent] public struct IntBufferElement : IBufferElementData { public int Value; }
-
그리고 Scene을 수정해서 자동으로 생성된
IntBufferElementAuthoring
컴포넌트를 게임 오브젝트에 더하고 값을 넣어 봤어요. 그리고 게임 오브젝트의 엔티티화를 위해서ConvertToEntity
컴포넌트를 더했어요. -
Entity Debugger로 보면 Authoring 컴포넌트가 더해져 있던 게임 오브젝트와 같은 이름의 엔티티가 생성된 것을 확인할 수 있어요.
-
이후 과정을 위해
UnitTag
와PlayerTag
,EnemyTag
컴포넌트를 작성해서 각 컴포넌트를 포함하는 엔티티에IntBufferElement
버퍼를 더해 볼게요.using Unity.Entities; namespace DOTS_DynamicBuffer { [GenerateAuthoringComponent] public struct UnitTag : IComponentData { } [GenerateAuthoringComponent] public struct PlayerTag : IComponentData { } [GenerateAuthoringComponent] public struct EnemyTag : IComponentData { } }
ComponentSystem
에서 사용하기
ComponentSystem
을 상속하는 시스템을 작성해서 UnitTag
컴포넌트를 포함하는 엔티티의 IntBufferElement
DynamicBuffer
에 접근해 볼게요.
-
TestBufferFromEntitySystem
을 작성했어요.UnitTag
를 포함하는 엔티티들의IntBufferElement
형DynamicBuffer
에 접근해서 값을 변경하는 로직이에요. 20번 줄과 같이 사용하는 것은 안 되니 23–28번 줄과 같이 사용해요. 물론Reinterpret<T>()
도 사용할 수 있겠죠?using Unity.Entities; namespace DOTS_DynamicBuffer { public class TestBufferFromEntitySystem : ComponentSystem { protected override void OnUpdate() { var bufferFromEntity = GetBufferFromEntity<IntBufferElement>(); Entities .WithAll<UnitTag>() .ForEach(entity => { if (bufferFromEntity.Exists(entity)) { var dynamicBufferFromUnitTag = bufferFromEntity[entity]; foreach (var intBufferElement in dynamicBufferFromUnitTag) { // Foreach iteration variable 'intBufferElement' is immutable. // Cannot modify struct member when accessed struct is not classified as a variable // intBufferElement.Value++; } for (var i = 0; i < dynamicBufferFromUnitTag.Length; i++) { var intBufferElement = dynamicBufferFromUnitTag[i]; intBufferElement.Value++; dynamicBufferFromUnitTag[i] = intBufferElement; } } }); } } }
-
플레이 모드에서 Entity Debugger를 보면
UnitTag
컴포넌트를 포함하는 엔티티의IntBufferElement
DynamicBuffer
의 값이 변하는 것을 확인할 수 있어요.
JobComponentSystem
에서 사용하기
JobComponentSystem
을 상속하는 시스템을 작성해서 PlayerTag
컴포넌트를 포함하는 엔티티의 IntBufferElement
DynamicBuffer
에 접근해 볼게요.
-
TestBufferFromEntityJobSystem
을 작성했어요.PlayerTag
를 포함하는 엔티티들의IntBufferElement
형DynamicBuffer
에 접근해서 값을 변경하는 로직이에요. 이번에는Reinterpret<T>()
를 사용해 봤어요.using Unity.Entities; using Unity.Jobs; namespace DOTS_DynamicBuffer { public class TestBufferFromEntityJobSystem : JobComponentSystem { protected override JobHandle OnUpdate(JobHandle inputDeps) { return Entities .WithAll<PlayerTag>() .ForEach((ref DynamicBuffer<IntBufferElement> dynamicBuffer) => { var intDynamicBuffer = dynamicBuffer.Reinterpret<int>(); for (var i = 0; i < intDynamicBuffer.Length; i++) { intDynamicBuffer[i]++; } }) .Schedule(inputDeps); } } }
-
잘 동작하네요! 값이 쭉쭉 올라가고 있어요.
팁들
InternalBufferCapacityAttribute
엔티티는 기본적으로 청크에 포함되는데, IBufferElementData
를 구현하는 구조체에 InternalBufferCapacityAttribute
를 적용하면 청크 내 존재할 수 있는 최대 요소 수를 지정할 수 있어요. 지정한 요소 수를 넘어서면 해당 버퍼는 힙 메모리로 이동해요. 물론 이때에도 이전과 같이 DynamicBuffer
API로 해당 버퍼에 접근할 수 있어요.
-
요소 수를 2개로 설정해 봤어요.
// InternalBufferCapacity specifies how many elements a buffer can have before // the buffer storage is moved outside the chunk. [InternalBufferCapacity(2)] [GenerateAuthoringComponent] public struct IntBufferElement : IBufferElementData { public int Value; }
-
그리고 같은 청크에서 테스트하기 위해서
EnemyTag
를 포함하는 Enemy 게임 오브젝트를 두 개 더 복재했어요. -
Entity Debugger를 확인해 봤어요. 그런데
IntBufferElement
가 여전히 청크에 남아 있는 것 처럼 보이네요. 힙 메모리로 이동됐어도 편의를 위해서 이렇게 보여주는 것인지는 확인이 필요하겠어요.
implicit
연산자
편의를 위해서 이렇게 작성해서 사용할 수도 있겠죠?
using Unity.Entities;
namespace DOTS_DynamicBuffer
{
// InternalBufferCapacity specifies how many elements a buffer can have before
// the buffer storage is moved outside the chunk.
[InternalBufferCapacity(2)]
[GenerateAuthoringComponent]
public struct IntBufferElement : IBufferElementData
{
public int Value;
// The following implicit conversions are optional, but can be convenient.
public static implicit operator int(IntBufferElement e)
{
return e.Value;
}
public static implicit operator IntBufferElement(int e)
{
return new IntBufferElement { Value = e };
}
}
}
마치며
IBufferElementData
와 DynamicBuffer<T>
를 가볍게 알아 봤어요.
게임을 만들 때 오브젝트 풀링에 대해서 수도 없이 많이 들어 보셨을 거예요. 1회용 객체를 생성하는 것은 쓰레기를 만드는 것이기에 풀링해서 재사용하면 잦은 쓰레기 수집을 줄이고 인스턴스 생성 타이밍을 관리할 수 있어서 더욱 부드러운 게임을 만들 수 있죠.
다음에는 이 기능을 게임에 어떻게 적용하는지 알아보고, 적용하기 전과 후를 비교하면서 어느정도 효과를 얻을 수 있는지 확인해 볼게요.