안녕하세요.
이번 Hello Cube 샘플 프로젝트는 13번 : Custom Transform 만 리뷰할 예정입니다.
아마도, Hello Cube 샘플 중에서는 이 예제가 제일 어렵고 난해한 것 같네요.
순서대로 리뷰하지 않고 주제에 맞게 샘플을 선정하다보니 좀 정리가 필요할 것 같군요.
앞으로 남은 샘플은 ( 9, 11, 14, 15 ). 총 4개이지만, 11번 FirstPersonController 는 안 반가운 큐브 프로젝트에서는 다루지 않을 예정입니다.
해당 샘플은 인풋 시스템 관련 포스팅에서 다룰려고 합니다.
1. EntityQuery 파헤치기
2. ECS에서 Transform 은 뭐가 달라?
3. 13번 Custom Transfrom Sample 프로젝트 리뷰
1. EntityQuery 파헤치기
이번 프로젝트는 코드가 아니라 Docs 뽀개기로 먼저 시작해야 할 것 같습니다.
Custom Transform 에서는 EntityQuery / Transfrom 2가지에 대한 명확한 이해가 있어야 코드 리뷰가 수월할 것이거든요.
EntityQuery 는 진작부터 열심히 써먹던 Query (질의) 하는 API인데
그냥 엔티티나 엔티티가 가진 데이터를 찾는 용도로 쓴다고 해버리기엔 내용이 좀 더 있습니다.
명확한 이해를 하기위해서는 기존에 알던 지식을 모두 내려다놓고 다시 처음으로 돌아가는게 최고의 방법이라고 생각합니다.
해당 내용은 Docs 문서를 보고 분석하는 내용이므로, Sample 프로젝트 부분으로 넘어가셔도 됩니다.
https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/systems-entityquery.html
Query data with EntityQuery | Entities | 1.0.16
Query data with EntityQuery An EntityQuery finds archetypes that have a specified set of component types. It then gathers the archetype's chunks into an array which a system can process. Additional resources
docs.unity3d.com
EntityQuery overview | 엔티티 쿼리란 무엇인가? |
EntityQuery filters | 정보를 정렬하기 위한 필터링 |
Write groups | 시스템 덮어쓰기? |
Version numbers (change filtering) |
버전 관리 (세대 관리)를 통해 설계 최적화 |
EntityQuery Overview
An EntityQuery finds archetypes that have a specified set of component types. It then gathers the archetype's chunks into an array which a system can process. 엔티티 쿼리는 특정한 컴포넌트들을 가지고 있는 아키타입 (Archetype)들을 찾는다. 그러고, 그 아키타입들의 청크들을 시스템이 처리할 수 있는 배열 (array)로 모은다. For example, if a query matches component types A and B, then the query gathers the chunks of all the archetypes that have those two component types, regardless of whatever other component types those archetypes might have. Therefore, an archetype with component types A, B, and C would match the query. 예를 들면, 쿼리가 A 컴포넌트와 B 컴포넌트를 매칭 했다면, 쿼리는 두 컴포넌트를 가지고 있는 모든 아키타입들의 청크를 모은다. 이 때, A와 B 컴포넌트를 포함하는 아키타입들이, 가지고 있는 다른 컴포넌트들이 있더라도 신경쓰지 않는다. 그래서, 만약 특정 아키타입이 A, B, C라는 컴포넌트가 있다면, 예시의 A,B 쿼리에게 매칭되어진다. You can use EntityQuery to do the following: - Run a job to process the selected entities and components 선택된 엔티티들과 컴포넌트들을 처리하기 위한 잡을 실행 한다. - Get a NativeArray that contains all the selected entities 모든 선택된 엔티티들이 포함된 NativeArray를 받는다. - Get a NativeArray of the selected entities by component type 컴포넌트 타입에 의해 선택된 엔티티들의 Native Array를 받는다. The entity and component arrays that EntityQuery returns are parallel. This means that the same index value always applies to the same entity in any array. 엔티티 쿼리가 준 엔티티와 컴포넌트 배열들은 병렬이다. 배열의 인덱스 자체가 같은 엔티티로부터 나온 값임을 의미한다. (이래서 Sorting Information 이라는 표현이?!) |
쿼리는 단순히 엔티티를 찾아서 사용하는게 아니라
병렬로 사용하기 위해 Sorting 을 하고 있다고 표현하고 있군요.
컨셉 자체는 어렵진 않습니다!
EntityQuery Filters
단순히 특정 컴포넌트를 이용해서 엔티티를 찾는 것만으로는 복잡한 프로그램 설계에서 한계가 있을 겁니다.
(무식하게 Tag를 붙여가며 하나하나씩 엔티티들을 찾으려고 하면...)
엔티티 쿼리에 대해서 엔티티를 좀더 필터링 할 수 있는 방법이 여러가지 있습니다.
Shared component filter |
Change filter |
Enableable components |
참고로, 필터를 한번 설정하고나면 계속 필터 설정값이 남아있게 됩니다.
ResetFilter() 를 이용해서 초기화 할 수 있습니다.
한 시스템안에서 여러 필터를 사용 할 때 주의하시길 바랍니다.
(1) Shared Component Filter
public void SetSharedComponentFilter<SharedComponent>(SharedComponent sharedComponent)
where SharedComponent : unmanaged, ISharedComponentData
SharedComponent 를 이용해서, 해당 Shared Component가 갖고 있는 값을 비교해서 필터링을 하는 방식입니다.
*Shared Component
https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/components-shared.html
Shared components | Entities | 1.0.16
Shared components Shared components group entities in chunks based on the values of their shared Component, which helps with the de-duplication of data.
docs.unity3d.com
Docs를 정독해도 빡세다..
쉽게 생각하면, 이름 그대로 공유 되는 컴포넌트 입니다.
Shared Component를 가지고 있는 모든 엔티티들은 동일한 Shared Component의 값을 가지고 있죠.
일종의 컴포넌트 싱글톤이라고 생각하면 편할까요?
유니티는 이 Shared Component가 가지고 있는 값을 ECS 청크와는 개별로 저장하고 있다고 합니다.
다시 EntityQuery로 넘어와서 예제 코드를 한번 봅시다.
struct SharedGrouping : ISharedComponentData
{
public int Group;
}
[RequireMatchingQueriesForUpdate]
partial class ImpulseSystem : SystemBase
{
EntityQuery query;
protected override void OnCreate()
{
query = new EntityQueryBuilder(Allocator.Temp)
.WithAllRW<ObjectPosition>()
.WithAll<Displacement, SharedGrouping>()
.Build(this);
}
protected override void OnUpdate()
{
// By default (without a filter), count all entities that have the required components.
query.ResetFilter();
int unfilteredCount = query.CalculateEntityCount();
// With a filter, only entities in chunks that have SharedGrouping=1 will be counted.
query.SetSharedComponentFilter(new SharedGrouping { Group = 1 });
int filteredCount = query.CalculateEntityCount();
// Many query methods include a variant that ignores any active filters. These variants are generally
// more efficient, and should be used when conservative upper-bound results are acceptable.
int ignoreFilterCount = query.CalculateEntityCountWithoutFiltering();
}
}
OnUpdate 문에서 Query를 통해 엔티티의 개수를 카운팅 하고 있는데요.
SharedGrouping이라는 Shared Component 를 이용해서 필터링을 다르게 할 수 있는 걸 보여주고 있습니다.
(2) Change Filter
필터링을 하더라도, 값이 수정 된 엔티티만 필터링 하는 용도로도 사용 할 수 있습니다.
public void SetChangedVersionFilter(ComponentType componentType)
(3) Enableable components
아니면, 간단하게 enableable components 를 이용해서도 필터링 가능합니다.
Enum EntityQueryOptions | Entities | 1.0.16
Enum EntityQueryOptions The bit flags to use for the Options field. Assembly: solution.dll Syntax [Flags] public enum EntityQueryOptions Fields Name Description Default No options specified. FilterWriteGroup The query filters selected entities based on the
docs.unity3d.com
https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/systems-entityquery-crea
Create an EntityQuery | Entities | 1.0.16
Create an EntityQuery To create an entity query, you can pass component types to the EntityQueryBuilder helper type. The following example defines an EntityQuery that finds all entities with both ObjectRotation and ObjectRotationSpeed components: EntityQue
docs.unity3d.com
WithAny<T>() |
WithAll<T>() |
WithNone<T>() |
WithAspect<T>() |
그 외에는 기본적으로 With 로 이루어진 함수로부터 엔티티를 필터링 하는데요.
public EntityQueryBuilder WithAll<T1>()
필터에서 다시 쿼리로 나오고 있어서 딱히 순서에 영향은 없는 것으로 보입니다.
Write groups
https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/systems-write-groups.html
Write groups | Entities | 1.0.16
Write groups Write groups provide a mechanism for one system to override another, even when you can't change the other system. A common ECS pattern is for a system to read one set of input components and write to another component as its output. However, y
docs.unity3d.com
이 녀석도 설명이 너무 난잡해서 Docs를 여러번 읽어봐야....
쓰기 그룹은 한마디로 override 라고 생각하면 편할 것 같습니다만...
생각보다 많이 까다롭습니다.
(이 문서 영문 구조 자체가 해석하기에 너무 극혐 ㅆㅃ) Write groups provide a mechanism for one system to override another, even when you can't change the other system. 쓰기 그룹은 어떤 시스템이 또다른 시스템을 Override 하는 메카니즘을 제공한다. 또한, 다른 시스템을 바꿀 수 없어도 사용 가능하다. A common ECS pattern is for a system to read one set of input components and write to another component as its output. However, you might want to override the output of a system, and use a different system based on a different set of inputs to update the output component. 일반적인 ECS 패턴은 입력 컴포넌트들을 읽고, 다른 컴포넌트를 출력으로 사용하도록 하는 시스템이다. 그러나, 여러분은 시스템의 출력을 override하고 싶어할지도 모르고, 출력 컴포넌트를 업데이트하기 위한 다른 입력 컴포넌트를 위한 시스템을 사용하고 싶어 할 지 모른다. The write group of a target component type consists of all other component types that ECS applies the WriteGroup attribute to, with that target component type as the argument. As a system creator, you can use write groups so that your system's users can exclude entities that your system would otherwise select and process. This filtering mechanism lets system users update components for the excluded entities based on their own logic, while letting your system operate as usual on the rest. 타겟 컴포넌트 타입의 쓰기 그룹은 모두 ,ECS가 쓰기 그룹 Attribute으로 구성되는, 타겟 컴포넌트를 인자로 받는, 다른 컴포넌트 타입들로 구성된다. 시스템 설계자로서, 사용자들이 그 엔티티들을 필터링 (exclude) 하거나, 그렇지 않으면 쓸 수 있게 하기 위한 쓰기 그룹을 사용 할 수 있다. 시스템 사용자들이 그들의 로직에 의해 제외되는 엔티티들을 업데이트 하면서 다른 엔티티들은 일반적으로 동작하도록 할 수 있다. |
내가 영어를 못하는건가, 문서가 고약한건가
쉽게 말하면..
// 쓰기 그룹 컴포넌트들
[WriteGroup(typeof(a))] <-- Docs에서 장황하게 설명하는 내용이 요 Attribute
public structu CompB : IComponentData
{ ... }
[WriteGroup(typeof(a))]
public structu CompB : IComponentData
{ ... }
// 이 녀석이 타겟 컴포넌트
public structu a : IComponentData
{ ... }
attribute, 즉, [WriteGroup(typeof("a")] 형식으로 감싼 컴포넌트들이 타겟 컴포넌트 ("a")의 쓰기 그룹에 해당한다는 뜻 입니다.
쓰기 그룹을 기준으로 필터링 하든 그냥 쓰게 하는 식으로 분기를 줘서 좀 더 유연하게 설계 할 수 있다는 말이죠.
그리고, 해당 컴포넌트들에게 쓰기 그룹이 할당 된다면, 앞으로 일반적인 쿼리들에 의해 매칭 되지 않도록 수정도 해줍니다.
public class AddingSystem : SystemBase
{
protected override void OnUpdate() {
Entities
// support write groups by setting EntityQueryOptions
.WithEntityQueryOptions(EntityQueryOptions.FilterWriteGroup)
.ForEach((ref W w, in B b) => {
// perform computation here
}).ScheduleParallel();}
}
쓰기 그룹 필터링을 활성화 하기 위한 예제 입니다.
WithEntityQueryOptions에서 필터링을 걸어서 쓰기 그룹에 해당하는 컴포넌트가 있다면, 해당 엔티티는 제외 시켜버립니다.
이번엔 필터링 말고, override를 하고 싶을땐?
예제를 보면 쉽게 이해가 됩니다.
using System;
using Unity.Collections;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
[Serializable]
[WriteGroup(typeof(Rotation))]
public struct RotationAngleAxis : IComponentData
{
public float Angle;
public float3 Axis;
}
using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Transforms;
public class RotationAngleAxisSystem : SystemBase
{
protected override void OnUpdate()
{
Entities.ForEach((ref Rotation destination, in RotationAngleAxis source) =>
{
destination.Value
= quaternion.AxisAngle(math.normalize(source.Axis), source.Angle);
}).ScheduleParallel();
}
}
Rotation 쓰기 그룹을 가진 RoationAngleAxis 컴포넌트는 OnUpdate 안에서
Quaternion.AxisAngle로 처리 하는 걸 볼 수 있습니다. (람다 같다 먼가..)
Version numbers (change filtering)
ECS에서는 각 부분에 세대 번호를 저장하고 있습니다. (Generation / Version Number)
세대는 항상 양으로 증가 하고 있으며, 값이 변화가 없다면 굳이 처리 안해도 된다는 느낌으로 소개하고 있습니다.
bool VersionBIsMoreRecent == (VersionB - VersionA) > 0;
해당 조건을 만족하면 스킵 아니면 처리 하도록 정할 수 있음.
말 그대로 위에서 소개 했던 Change Filter의 추가 관련 내용이니 한번 읽어보시면 될 것 같네요.
https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/systems-version-numbers.html
Version numbers | Entities | 1.0.16
Version numbers You can use the version numbers (also known as generations) of the parts of the ECS architecture to detect potential changes and to implement efficient optimization strategies, such as skipping processing when data hasn't changed since the
docs.unity3d.com
2. Transform
정확히는 Transform in Entities 라고 표현하는게 맞겠죠.
일반적인 Transform 과 뭐가 다른지를 살펴봅시다.
*엔티티 Transform을 다루는 모듈들은 네임스페이스에서 Unity.Transforms 를 통해서 불러와야 합니다.
LocalToWorld | LocalTransform | PostTransformMatrix | Parent | Child | ParentSystem | LocalToWorldSystem |
(1) LocalToWorld
LocalToWorld 는 float4x4 자료형인 컴포넌트 입니다.
특이하게도 엔티티 인스펙터를 보면, Transform이 다르게 나오는데요.
Local Space에서 World Space로 변환하기 위한 matrix라고 합니다.
정확히는 렌더링 할 때 사용되는 Matrix를 따른다고 생각하시면 될 것 같아요.
Matrix4x4.TRS(coordsss[idx], Quaternion.identity, new Vector3(1f, 1f, 1f))
GPU Instancing 할 때, 4x4 자료형을 받아서 Mesh 드로잉을 하게 되는데
이와 비슷한 원리로 파악하시면 되겠습니다.
LocalToWorld는 저희가 오브젝트를 옮기거나 회전하거나 스케일을 바꿀 때, 자동으로 LocalToWorldSystem이 업데이트 해줍니다. 그래서 해당 컴포넌트로 머리 싸매고 바꿔줄 필요는 없습니다.
만약, 커스터마이징하기 위해 LocalToWorldSystem이 업데이트 하지 않도록 하려면 쓰기 그룹을 사용하면 된다고 합니다.
[WriteGroup(typeof(LocalToWorld))]
쓰기 그룹 필터링은 위에서 한번 다뤄봤었죠.. 와 바로 나오네
그렇다고 막 바꾸면 안되고 주의할 점이 있습니다!!
Transform은 기본적으로 System Flow에서 TransformSystemGroup 단에서 동작합니다.
실제 로직이 흘러가는 SimulationSystemGroup 단에서 다루려면 주의해야 할 점입니다.
이렇게 하는 이유는 그래픽적으로 스무딩 시키기 위해서라는데..
좀 더 정확하게 다루고 싶다면 ComputeWorldTransformMatrix를 이용하라고 안내하고 있습니다.
(2) LocalTransform
public struct LocalTransform : IComponentData
{
public float3 Position;
public float Scale;
public quaternion Rotation;
}
로컬 트랜스폼 컴포넌트는 간단합니다.
포지션, 스케일, 쿼터니온을 프로퍼티로 갖고 있고
해당 컴포넌트를 통해 시스템에서 LocalToWorld 매트릭스를 다룹니다.
(3) PostTransformMatrix
LocalTransform 의 Scale 값이 왜 float인가 궁금할까요?
LocalTransform의 Scale값은 Uniform 할 때만 동작하게 되어 있다고 합니다.
XYZ 크기가 다른 엔티티는 해당 컴포넌트가 부착됩니다.
(4) Parent
Parent 컴포넌트는.. 해당 엔티티가 자식이 있다면 자동으로 추가되는 컴포넌트 입니다.
(5) Child
Child는 지난 포스팅에서도 언급 했습니다.
Parent System에 의해 Child 다이나믹 버퍼가 자동으로 제어됩니다.
개발자는 Parent 컴포넌트만 사용하면 된다고 합니다.
https://lucid-boundary.tistory.com/107
7. 비슷해서 쉬울 줄 알았지? - ECS & DOTS 완전 정복하기. Feat. 안 반가운 큐브 프로젝트
반갑습니다.지난 번 포스팅 이후로 텀이 좀 있었군요.그동안 밀렸던 프로젝트 개발이랑 GPU 인스턴싱과 렌더링에 깊게 빠져 있었네요.ECS 정복한다더니 갑자기 딴 (더욱 깊고 어두운) 길로 샜
lucid-boundary.tistory.com
(6) Parent System, LocalToWorldSystem
두 시스템은 시스템에 자동으로 추가되는 시스템들입니다.
Transoform 관련 혹은 부모 자식 계층이 있는 엔티티들을 자동으로 관리 해줍니다.
기본적인 Transform 소개는 이걸로 끝입니다.
https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/transforms-using.html
Using transforms | Entities | 1.0.16
Using transforms To use transforms in your project, use the Unity.Transforms namespace to control the position, rotation, and scale of any entity in your project. LocalTransform represents the relative position, rotation, and scale of the entity. If there
docs.unity3d.com
Transform 을 이용해 위치 변경이나 회전 같은 걸 할 때는 위 API 문서를 읽어보면 됩니다.
*Transform usage flags
트랜스폼 컴포넌트들은 엔티티화 과정 (Baking Process)에서 자동으로 추가 되는 컴포넌트입니다.
근데 모든 엔티티들이 트랜스폼 컴포넌트가 필요할까요?
ECS는 엔티티가 가진 데이터를 기준으로 필터링하고 사용하는건데, 트랜스폼 컴포넌트도 결국은 필터링을 위한 용도로 사용 할 수있습니다. 그래서 Transform Usage Flags가 필요합니다.
None | 어떤 트랜스폼 컴포넌트도 필요로 하지 않을 때 (매니저, 정적인 엔티티 등) 다만, 다른 베이커로부터 Authoring 할 때, 트랜스폼 컴포넌트가 붙을 수 있음. |
Renderable | 렌더링 되어질 트랜스폼 컴포넌트가 필요할 때 (렌더링은 되지만, 런타임 동안 정적인 모델) |
Dynamic | 런 타임동안 움직이는 엔티티들을 사용 할 때 |
WorldSpace | 엔티티가 월드 스페이스에 있어야 하는 엔티티일 때 (용도가 뭘까..) |
NonUniformScale | Scale이 일정하지 않은 엔티티들을 사용 할 때 |
ManualOverride | 다른 베이커들의 모든 플래그 조차 무시하고 어떤 트랜스폼 컴포넌트도 없는 상태로 엔티티가 필요 할 때 (커스터마이징 하기 위한 엔티티) |
이런 부분까지 고려해서 설계되어지다니..
*Custom Transform
그러면, 이번 포스팅의 하이라이트입니다.
앞의 기나긴 설명의 이유는 해당 섹션을 위한 리뷰였거든요.
Transform 컴포넌트들은 전부 자동으로 제공되고 있어, 그대로 사용해도 되지만..
Write Group ! Entity Query에서 사용했던 쓰기 그룹을 이용하면 Transform Override를 할 수 있습니다.
- LocalTransform 을 대체하기.
LocalTransform 의 내부 코드는 정확히 모르지만,
Position / Quaternion / Scale 값을 가지고 있는 컴포넌트라고 말씀 드렸습니다.
컴포넌트에 여러가지 static으로 이루어진 Helper 함수들 (LookRotation / Rotate 등)을
커스터마이징 할 컴포넌트 안에다가 Override 해서 사용 할 수 있습니다.
예제를 볼까요?
using System.Globalization;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Properties;
using Unity.Transforms;
[WriteGroup(typeof(LocalToWorld))]
public struct LocalTransform2D : IComponentData
{
[CreateProperty]
public float2 Position;
[CreateProperty]
public float Scale;
[CreateProperty]
public float Rotation;
public override string ToString()
{
return $"Position={Position.ToString()} Rotation={Rotation.ToString()} Scale={Scale.ToString(CultureInfo.InvariantCulture)}";
}
/// <summary>
/// Gets the float4x4 equivalent of this transform.
/// </summary>
/// <returns>The float4x4 matrix.</returns>
public float4x4 ToMatrix()
{
quaternion rotation = quaternion.RotateZ(math.radians(Rotation));
var matrixTRS = float4x4.TRS(new float3(Position.xy, 0f), rotation, Scale);
return matrixTRS;
}
}
쓰기 그룹을 통해 LocalWorld에 필터링을 걸었군요.
그리고 익숙한 Position / Scale / Quaternion이 다른 자료형으로 구현되어 있습니다.
또, ToString 함수도 원하는 식으로 변경되어 있네요?
ToMatrix() 구현 부를 보면, 2D를 타겟으로 한 LocalTransform2D 컴포넌트를
기존 3D 환경의 ECS에 맞게 변환해서 구현되어 있습니다.
EntityQuery와 Transform에 대한 명확한 이해를 하고나니 해당 예제가 빠르게 이해가 됩니다..!
참고로 컴포넌트 말고 System 자체를 커스터마이징 하고 싶다면,
Packages > Entities > Unity.Transforms in your project, copy the LocalToWorldSystem.cs file, and rename it.
방법을 따르라고 합니다.
성의없네..
이걸 보니까.. 왜 NSprite가 어렵게 구현되어있는지 알만하네요..
그냥 존경심만 생기던 그 NSprite 너드 개발자가 어떤 식으로 프레임워크를 만들어왔는지 감이 옵니다.
그러면 실전으로 가봅시다.
드디어 Hello Cube 끝판왕
13번 샘플 : CustomTransform을 리뷰할 차례 입니다.
샘플 프로젝트는 매우 간단하게 구성되어 있습니다.
SubScene에는 Triangle을 자식으로 갖고 있는 Circle 이구요.
둘 다 Sprite Renderer를 갖고 있습니다. 컴포넌트로 Transform 2D Authoring 코드가 추가되어 있군요.
코드는 총 3개 입니다.
Transform2DAuthoring.cs
using System.Globalization;
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace HelloCube.CustomTransforms
{
public class Transform2DAuthoring : MonoBehaviour
{
class Baker : Baker<Transform2DAuthoring>
{
public override void Bake(Transform2DAuthoring authoring)
{
// Ensure that no standard transform components are added.
var entity = GetEntity(TransformUsageFlags.ManualOverride);
AddComponent(entity, new LocalTransform2D
{
Scale = 1
});
AddComponent(entity, new LocalToWorld
{
Value = float4x4.Scale(1)
});
var parentGO = authoring.transform.parent;
if (parentGO != null)
{
AddComponent(entity, new Parent
{
Value = GetEntity(parentGO, TransformUsageFlags.None)
});
}
}
}
}
// By including LocalTransform2D in the LocalToWorld write group, entities with LocalTransform2D
// are not processed by the standard transform system.
[WriteGroup(typeof(LocalToWorld))]
public struct LocalTransform2D : IComponentData
{
public float2 Position;
public float Scale;
public float Rotation;
public override string ToString()
{
return
$"Position={Position.ToString()} Rotation={Rotation.ToString()} Scale={Scale.ToString(CultureInfo.InvariantCulture)}";
}
public float4x4 ToMatrix()
{
quaternion rotation = quaternion.RotateZ(math.radians(Rotation));
return float4x4.TRS(new float3(Position.xy, 0f), rotation, Scale);
}
}
}
이걸 진짜 모르고 봤다면.. 숨이 턱턱 막힙니다.
우선 부모가 되는 Circle이 authoring 이겠죠?
TransformUsageFlags.ManualOverride로 엔티티화 하고 (아무런 Transform도 넣지 않음)
LocalTransform2D 라는 컴포넌트를 추가하였습니다.
그리고, 자식을 불러와서 Parent 컴포넌트를 추가해줬는데요.
왜 이랬을까? 고민 해봤는데,
ManualOverride 때문에 Parent 컴포넌트도 안들어가게 되어서, 임의로 추가한 것 같습니다.
그러면 LocalTransform2D 컴포넌트를 볼까요?
// By including LocalTransform2D in the LocalToWorld write group, entities with LocalTransform2D
// are not processed by the standard transform system.
[WriteGroup(typeof(LocalToWorld))]
쓰기 그룹이 LocalToWorld로 들어가있군요.
LocalTransform2D를 컴포넌트로 사용하면, 해당 엔티티는 더 이상 LocalToWorld는 사용하지 않게 됩니다.
MovementSystem.cs
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace HelloCube.CustomTransforms
{
public partial struct MovementSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<LocalTransform2D>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float rotation = SystemAPI.Time.DeltaTime * 180f; // Half a rotation every second (in degrees)
float elapsedTime = (float) SystemAPI.Time.ElapsedTime;
float xPosition = math.sin(elapsedTime) * 2f - 1f;
float scale = math.sin(elapsedTime * 2f) + 1f;
scale = scale <= 0.001f ? 0f : scale;
foreach (var localTransform2D in
SystemAPI.Query<RefRW<LocalTransform2D>>()
.WithNone<Parent>())
{
localTransform2D.ValueRW.Position.x = xPosition;
localTransform2D.ValueRW.Rotation = localTransform2D.ValueRO.Rotation + rotation;
localTransform2D.ValueRW.Scale = scale;
}
}
}
}
코드 자체는 무진장 쉽습니다.
무언가 움직이고 회전하고 작아지고 하는 로직이 준비가 되구요.
그 밑에 동작 처리를 위해 쿼리를 하고 있습니다.
foreach (var localTransform2D in
SystemAPI.Query<RefRW<LocalTransform2D>>()
.WithNone<Parent>())
{
localTransform2D.ValueRW.Position.x = xPosition;
localTransform2D.ValueRW.Rotation = localTransform2D.ValueRO.Rotation + rotation;
localTransform2D.ValueRW.Scale = scale;
}
" LocalTransform2D를 가진 엔티티를 찾아줘, 대신에 Parent 가 없는 엔티티들을 찾아줘 "
Triangle 자식 엔티티는 제외하고 Circle 엔티티가 쿼리되어서 나옵니다.
쿼리된 엔티티의 프로퍼티를 수정 해주고 있습니다.
실제로 자식인 Triangle은 아무런 프로퍼티가 변경하지 않고 있습니다.
부모인 Circle을 따라서 움직이고 회전 할 뿐이죠.
LocalToWorld2DSystem.cs
해당 코드는
Package - Entities - Unity.Transforms 에 있는 LocalToWorld 스크립트를 기반으로
재작업된 문서입니다.
솔직히.. 이 문서 리뷰하려면 글 하나 다시 파야 할 것 같은데... 죄송합니다..
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Assertions;
using Unity.Burst.Intrinsics;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Mathematics;
using Unity.Transforms;
namespace HelloCube.CustomTransforms
{
// This system computes a transform matrix for each entity with a LocalTransform2D.
// For root-level / world-space entities with no Parent, the LocalToWorld can be
// computed directly from the entity's LocalTransform2D.
// For child entities, each unique hierarchy is traversed recursively, computing each child's LocalToWorld
// by composing its LocalTransform with its parent's transform.
[WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)]
[UpdateInGroup(typeof(TransformSystemGroup))]
[UpdateAfter(typeof(ParentSystem))]
[BurstCompile]
public partial struct LocalToWorld2DSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<LocalTransform2D>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var rootsQuery = SystemAPI.QueryBuilder().WithAll<LocalTransform2D>().WithAllRW<LocalToWorld>()
.WithNone<Parent>().Build();
var parentsQuery = SystemAPI.QueryBuilder().WithAll<LocalTransform2D, Child>()
.WithAllRW<LocalToWorld>()
.WithNone<Parent>().Build();
var localToWorldWriteGroupMask = SystemAPI.QueryBuilder()
.WithAll<LocalTransform2D, Parent>()
.WithAllRW<LocalToWorld>().Build().GetEntityQueryMask();
// compute LocalToWorld for all root-level entities
var rootJob = new ComputeRootLocalToWorldJob
{
LocalTransform2DTypeHandleRO = SystemAPI.GetComponentTypeHandle<LocalTransform2D>(true),
PostTransformMatrixTypeHandleRO = SystemAPI.GetComponentTypeHandle<PostTransformMatrix>(true),
LocalToWorldTypeHandleRW = SystemAPI.GetComponentTypeHandle<LocalToWorld>(),
LastSystemVersion = state.LastSystemVersion,
};
state.Dependency = rootJob.ScheduleParallelByRef(rootsQuery, state.Dependency);
// compute LocalToWorld for all child entities
var childJob = new ComputeChildLocalToWorldJob
{
LocalToWorldWriteGroupMask = localToWorldWriteGroupMask,
ChildTypeHandle = SystemAPI.GetBufferTypeHandle<Child>(true),
ChildLookup = SystemAPI.GetBufferLookup<Child>(true),
LocalToWorldTypeHandleRW = SystemAPI.GetComponentTypeHandle<LocalToWorld>(),
LocalTransform2DLookup = SystemAPI.GetComponentLookup<LocalTransform2D>(true),
PostTransformMatrixLookup = SystemAPI.GetComponentLookup<PostTransformMatrix>(true),
LocalToWorldLookup = SystemAPI.GetComponentLookup<LocalToWorld>(),
LastSystemVersion = state.LastSystemVersion,
};
state.Dependency = childJob.ScheduleParallelByRef(parentsQuery, state.Dependency);
}
[BurstCompile]
unsafe struct ComputeRootLocalToWorldJob : IJobChunk
{
[ReadOnly] public ComponentTypeHandle<LocalTransform2D> LocalTransform2DTypeHandleRO;
[ReadOnly] public ComponentTypeHandle<PostTransformMatrix> PostTransformMatrixTypeHandleRO;
public ComponentTypeHandle<LocalToWorld> LocalToWorldTypeHandleRW;
public uint LastSystemVersion;
public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask,
in v128 chunkEnabledMask)
{
Assert.IsFalse(useEnabledMask);
LocalTransform2D* chunk2DLocalTransforms =
(LocalTransform2D*)chunk.GetRequiredComponentDataPtrRO(ref LocalTransform2DTypeHandleRO);
if (chunk.DidChange(ref LocalTransform2DTypeHandleRO, LastSystemVersion) ||
chunk.DidChange(ref PostTransformMatrixTypeHandleRO, LastSystemVersion))
{
LocalToWorld* chunkLocalToWorlds =
(LocalToWorld*)chunk.GetRequiredComponentDataPtrRW(ref LocalToWorldTypeHandleRW);
PostTransformMatrix* chunkPostTransformMatrices =
(PostTransformMatrix*)chunk.GetComponentDataPtrRO(ref PostTransformMatrixTypeHandleRO);
if (chunkPostTransformMatrices != null)
{
for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i)
{
chunkLocalToWorlds[i].Value = math.mul(chunk2DLocalTransforms[i].ToMatrix(),
chunkPostTransformMatrices[i].Value);
}
}
else
{
for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i)
{
chunkLocalToWorlds[i].Value = chunk2DLocalTransforms[i].ToMatrix();
}
}
}
}
}
[BurstCompile]
unsafe struct ComputeChildLocalToWorldJob : IJobChunk
{
[NativeDisableContainerSafetyRestriction]
public ComponentLookup<LocalToWorld> LocalToWorldLookup;
[ReadOnly] public EntityQueryMask LocalToWorldWriteGroupMask;
[ReadOnly] public BufferTypeHandle<Child> ChildTypeHandle;
[ReadOnly] public BufferLookup<Child> ChildLookup;
public ComponentTypeHandle<LocalToWorld> LocalToWorldTypeHandleRW;
[ReadOnly] public ComponentLookup<LocalTransform2D> LocalTransform2DLookup;
[ReadOnly] public ComponentLookup<PostTransformMatrix> PostTransformMatrixLookup;
public uint LastSystemVersion;
public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask,
in v128 chunkEnabledMask)
{
Assert.IsFalse(useEnabledMask);
bool updateChildrenTransform = chunk.DidChange(ref ChildTypeHandle, LastSystemVersion);
BufferAccessor<Child> chunkChildBuffers = chunk.GetBufferAccessor(ref ChildTypeHandle);
updateChildrenTransform = updateChildrenTransform ||
chunk.DidChange(ref LocalToWorldTypeHandleRW, LastSystemVersion);
LocalToWorld* chunkLocalToWorlds =
(LocalToWorld*)chunk.GetRequiredComponentDataPtrRO(ref LocalToWorldTypeHandleRW);
for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++)
{
var localToWorld = chunkLocalToWorlds[i].Value;
var children = chunkChildBuffers[i];
for (int j = 0, childCount = children.Length; j < childCount; j++)
{
ChildLocalToWorldFromTransformMatrix(localToWorld, children[j].Value, updateChildrenTransform);
}
}
}
void ChildLocalToWorldFromTransformMatrix(in float4x4 parentLocalToWorld, Entity childEntity,
bool updateChildrenTransform)
{
updateChildrenTransform = updateChildrenTransform
|| PostTransformMatrixLookup.DidChange(childEntity, LastSystemVersion)
|| LocalTransform2DLookup.DidChange(childEntity, LastSystemVersion);
float4x4 localToWorld;
if (updateChildrenTransform && LocalToWorldWriteGroupMask.MatchesIgnoreFilter(childEntity))
{
var localTransform2D = LocalTransform2DLookup[childEntity];
localToWorld = math.mul(parentLocalToWorld, localTransform2D.ToMatrix());
if (PostTransformMatrixLookup.HasComponent(childEntity))
{
localToWorld = math.mul(localToWorld, PostTransformMatrixLookup[childEntity].Value);
}
LocalToWorldLookup[childEntity] = new LocalToWorld { Value = localToWorld };
}
else
{
localToWorld = LocalToWorldLookup[childEntity].Value;
updateChildrenTransform = LocalToWorldLookup.DidChange(childEntity, LastSystemVersion);
}
if (ChildLookup.TryGetBuffer(childEntity, out DynamicBuffer<Child> children))
{
for (int i = 0, childCount = children.Length; i < childCount; i++)
{
ChildLocalToWorldFromTransformMatrix(localToWorld, children[i].Value, updateChildrenTransform);
}
}
}
}
}
}
참고로 해당 코드는 Native 쪽을 건드는 거라서 Unsafe 체크를 해주셔야 합니다.
대충 그냥 넘길까 했는데... 그래도 살짝만 건드려볼까요?
어쨌든 커스터마이징한 2D 컴포넌트값은 결국은 ECS에 맞는,
System의 일부로 포함시켜야 합니다.
Authoring 코드의 ToMatrix() 함수가 기억나시나요?
2D 연산을 한 다음에 4x4 형태로 재작업하여 일반적인 LocalTransform 처럼 적용 시키겠단 의미로 받아들이시면 될 것 같습니다.
공부하면 할 수록 새로운게 계속 튀어나오고
다시 되돌아보는 시간이 많군요..
그래도 모르고 쓰는 것보다는 부딪혀보는게 최고 아닐까요?
제일 어려운 13번이 끝났으니
거의 다 끝나갑니다..!
다음 포스팅에서 뵙겠습니다..
'Game Dev > Unity ECS & DOTS' 카테고리의 다른 글
7. 비슷해서 쉬울 줄 알았지? - ECS & DOTS 완전 정복하기. Feat. 안 반가운 큐브 프로젝트 (5) | 2024.12.05 |
---|---|
6. 원하는 위치에 프리팹 생성 해보기- ECS & DOTS 완전 정복하기. Feat. 안 반가운 큐브 프로젝트 (0) | 2024.11.20 |
5. 뭐가 다른데? IJobEntity, IJobChunk, IASpect - ECS & DOTS 완전 정복하기. Feat. 안 반가운 큐브 프로젝트 (4) | 2024.11.19 |
[ECS&DOTS] NSprite 머리 쪼개기 (3) 우선, 샘플 프로젝트부터 공략하자. (3, 마무리) (2) | 2024.11.12 |
[ECS&DOTS] NSprite 머리 쪼개기 (2) 우선, 샘플 프로젝트부터 공략하자. (2) (8) | 2024.11.11 |