진위여부 확인 안됨

유니티에서의 TMP_InputField와 TMP_Text의 text 반환값은 모두 string 타입이지만, InputField에서 반환되는 text는 사용자가 입력한 값만을 포함하고, TMP_Text의 text는 추가적으로 포매팅이나 스타일에 대한 정보를 포함할 수 있습니다. 이는 TMP_Text에서 텍스트를 처리하는 방식과 관련되어 있습니다.

사용자가 "5004"라는 값을 입력했더라도, TMP_Text의 text 값을 가져오면 "\u200B5004" 또는 "5004\u200B"와 같이 보이지 않는 특수 문자가 포함된 문자열을 얻을 수 있습니다. 이 경우, int로 변환하는 과정에서 FormatException이 발생할 수 있습니다. 이는 int.Parse 또는 int.TryParse 메서드가 숫자 이외의 문자를 포함한 문자열을 int로 변환할 수 없기 때문입니다.

이 문제를 해결하려면, TMP_Text의 text에서 숫자만 추출하여 int로 변환하는 것이 필요합니다. 이를 위해서는 정규 표현식 등을 사용하여 숫자만 추출할 수 있습니다.

string inputString = _text.text;
string cleanedString = System.Text.RegularExpressions.Regex.Replace(inputString, "[^0-9]", "");

int stageIDX;
if (int.TryParse(cleanedString, out stageIDX))
{
    // stageIDX 사용 가능
}
else
{
    Debug.LogError($"Failed to parse {_text.text} to int");
}

이 코드는 _text.text에서 숫자 이외의 문자를 모두 제거한 후, 그 결과 문자열을 int로 변환하려고 시도합니다. 숫자 이외의 문자를 제거하는 것이 필요한 경우에 유용하게 사용될 수 있습니다.

블로그 이미지

RIsN

,

[System.Flags]는 C#(.NET 프레임워크의 일부)에서 열거형(enum)을 플래그(flag)의 모음으로 처리해야 함을 나타내는 속성입니다. 이를 통해 비트 연산을 사용하여 enum 값을 결합할 수 있으며, 이는 코드에서 옵션 또는 상태 조합을 효율적으로 나타내는 데 도움이 됩니다.

[System.Flags]의 주요 사용 사례는 다양한 방식으로 결합될 수 있는 독립적인 옵션 또는 속성 집합이 있을 때입니다. 플래그 속성을 사용하면 단일 정수 값으로 이러한 옵션의 모든 조합을 나타낼 수 있습니다.

다음은 이 개념을 설명하는 예입니다:

[System.Flags]를 사용하지 않는 경우:

public enum Colors { Red, Green, Blue }

이 경우 변수에 한 번에 하나의 색상만 할당할 수 있습니다.

[System.Flags]를 사용하는 경우:

[System.Flags] public enum Colors { None = 0, Red = 1, Green = 2, Blue = 4 }

이제 비트 OR 연산을 사용하여 색상을 결합할 수 있습니다:

Colors combinedColors = Colors.Red | Colors.Green;

combinedColors는 이제 Red와 Green 플래그를 모두 가지게 됩니다. 비트 AND 연산을 사용하여 특정 플래그가 설정되어 있는지 확인할 수도 있습니다:

bool isRedSet = (combinedColors & Colors.Red) == Colors.Red;

이를 통해 단일 정수 값으로 여러 옵션, 상태 또는 속성을 효율적으로 저장하고 조작할 수 있습니다. 그러나 플래그를 결합할 때 비트가 겹치지 않도록 열거 값으로 2의 거듭제곱(2^n)을 할당하는 것이 중요합니다.

블로그 이미지

RIsN

,

Mono를 계속 설치하라고 나오고, 설치해도  처리가 안될 때

  • C# Extension의 버전을 v1.25.0로 변경해볼 것
    • v1.25.1 ~ v1.25.4까지 문제 발생하는 경우 있음

블로그 이미지

RIsN

,

코드

  • enum 데이터
private enum CharacterVoice
{
    LJ8 = 15,
    POKI = 16,
    CHARASI = 65
}
  • 실행 코드
// :: enum 안의 모든 데이터를 가져옴
var values = System.Enum.GetValues(typeof(CharacterVoice));
// :: 데이터의 길이 만큼 랜덤 획득
int random = Random.Range(0, values.Length);
// :: 해당 enum의 수치값을 획득
Debug.Log((int)values.GetValue(random));

// >> 결과: 15 or 16 or 65

 

블로그 이미지

RIsN

,

원본: https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/DOTS_Guide/ecs_tutorial/README.md

Step 9 - 카메라 추적(Camera follow)

런타임에서 ECS와 GameObjects의 간단한 상호작용.

  1. "Scripts/MonoBehaviours" 폴더에 "CameraSingleton.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다:
  2. // 메인 카메라에 접근하는 방법은 여러 가지가 있지만, 
    // 싱글톤(여기서 사용하는 방식)을 사용하는 접근 방식은 모든 종류의 MonoBehaviour에서 작동됩니다.
    class CameraSingleton : UnityEngine.MonoBehaviour
    {
        public static UnityEngine.Camera Instance;
    
        void Awake()
        {
            Instance = GetComponent<UnityEngine.Camera>();
        }
    }
  3. "SampleScene"의 "Main Camera" 게임 오브젝트에 "CameraSingleton" MonoBehavior를 추가합니다.
  4. "Scripts/Systems" 폴더에 "CameraSystem.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다:
  5. using Unity.Collections;
    using Unity.Entities;
    using Unity.Mathematics;
    using Unity.Transforms;
    
    // 이 시스템은 변환 시스템이 업데이트된 후 실행되어야 합니다. 
    // 그렇지 않으면 카메라가 탱크보다 한 프레임 뒤로 처져 흔들림이 발생합니다.
    [UpdateInGroup(typeof(LateSimulationSystemGroup))]
    partial class CameraSystem : SystemBase
    {
        Entity Target;
        Random Random;
        EntityQuery TanksQuery;
    
        protected override void OnCreate()
        {
            Random = Random.CreateFromIndex(1234);
            TanksQuery = GetEntityQuery(typeof(Tank));
            RequireForUpdate(TanksQuery);
        }
    
        protected override void OnUpdate()
        {
            if (Target == Entity.Null || UnityEngine.Input.GetKeyDown(UnityEngine.KeyCode.Space))
            {
                var tanks = TanksQuery.ToEntityArray(Allocator.Temp);
                Target = tanks[Random.NextInt(tanks.Length)];
            }
    
            var cameraTransform = CameraSingleton.Instance.transform;
            var tankTransform = GetComponent<LocalToWorld>(Target);
            cameraTransform.position = tankTransform.Position - 10.0f * tankTransform.Forward + new float3(0.0f, 5.0f, 0.0f);
            cameraTransform.LookAt(tankTransform.Position, new float3(0.0f, 1.0f, 0.0f));
        }
    }
  6. 플레이 모드로 들어가서 게임 뷰(이전과 같이 씬 뷰가 아님)를 보면 카메라가 탱크 중 하나를 따라가고 있음을 확인할 수 있습니다.  게임 뷰에 입력 포커스가 되어 있는지 확인하고(클릭해서) 스페이스바를 반복적으로 누릅니다. 카메라는 그 때마다 매번 다른 임의의 탱크로 전환되어야 합니다.
  7. 플레이 모드에서 나오세요.

축하합니다!

이 튜토리얼을 끝냈습니다! 가서 케이크 좀 사 와서 축하하세요!

블로그 이미지

RIsN

,

원본: https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/DOTS_Guide/ecs_tutorial/README.md

Step 8 - 세이프 존(Safe zone)

싱글턴 구성, 실시간 변환.

  1. "Scripts/Components" 폴더에 "Shooting.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다:
  2. using Unity.Entities;
    
    // 이 태그 컴퍼넌트는 "사용 가능한 컴퍼넌트"이기도 합니다.
    // 이러한 컴퍼넌트들은 엔티티에 존재하는 동안 온/오프 될 수 있습니다.
    // 이렇게 하는 것이 컴퍼넌트를 추가하거나 제거하는 것보다 훨씬 효율적입니다.
    struct Shooting : IComponentData, IEnableableComponent
    {
    }
  3. "Scripts/Authoring" 폴더에 있는 "TurretAuthoring.cs" 파일의 내용을 다음과 같이 수정합니다:
  4.  using Unity.Entities;
     
     class TurretAuthoring : UnityEngine.MonoBehaviour
     {
         public UnityEngine.GameObject CannonBallPrefab;
         public UnityEngine.Transform CannonBallSpawn;
     }
     
     class TurretBaker : Baker<TurretAuthoring>
     {
         public override void Bake(TurretAuthoring authoring)
         {
             AddComponent(new Turret
             {
                 CannonBallPrefab = GetEntity(authoring.CannonBallPrefab),
                 CannonBallSpawn = GetEntity(authoring.CannonBallSpawn)
             });
     
    +        // 활성화된 컴퍼넌트들은 항상 초기에 활성화됩니다.
    +        AddComponent<Shooting>();
         }
     }
  5. "Scripts/Systems" 폴더에 "SafeZoneSystem.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다.
  6. using Unity.Burst;
    using Unity.Collections;
    using Unity.Entities;
    using Unity.Mathematics;
    using Unity.Transforms;
    
    // Turret type을 처리없이 사용해야 합니다(Execute 메서드에 포함되지 않음).
    [WithAll(typeof(Turret))]
    [BurstCompile]
    partial struct SafeZoneJob : IJobEntity
    {
        // 이 작업을 병렬로 실행할 경우, 서로 다른 스레드에서 동일한 엔티티에 액세스하면 문제가 발생하여 
        // 세이프티 시스템에서 TurretActiveFromEntity와의 잠재적인 경합 상태에 대해 항의합니다.
        // 그러나 이 작업의 코드는 현재 처리 중인 엔티티만 TurretActiveFromEntity에서 조회되도록 작성되어 
        // 이 프로세스를 안전하게 만듭니다.
        // 그래서 우리는 병렬 세이프티 체크를 비활성화할 수 있습니다.
        [NativeDisableParallelForRestriction] public ComponentLookup<Shooting> TurretActiveFromEntity;
    
        public float SquaredRadius;
    
        void Execute(Entity entity, TransformAspect transform)
        {
            // 슈팅 태그 컴퍼넌트는 탱크가 지정된 범위를 벗어나는 경우에만 활성화됩니다.
            TurretActiveFromEntity.SetComponentEnabled(entity, math.lengthsq(transform.Position) > SquaredRadius);
        }
    }
    
    [BurstCompile]
    partial struct SafeZoneSystem : ISystem
    {
        // ComponentLookup 랜덤 접근자는 즉석에서 생성해서는 안 됩니다.
        // EntityQuery처럼, 한 번 만들고 필드에 저장해야 합니다.
        ComponentLookup<Shooting> m_TurretActiveFromEntity;
    
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<Config>();
    
            m_TurretActiveFromEntity = state.GetComponentLookup<Shooting>();
        }
    
        [BurstCompile]
        public void OnDestroy(ref SystemState state)
        {
        }
    
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            float radius = SystemAPI.GetSingleton<Config>().SafeZoneRadius;
            const float debugRenderStepInDegrees = 20;
    
            // Debug rendering (the white circle).
            for (float angle = 0; angle < 360; angle += debugRenderStepInDegrees)
            {
                var a = float3.zero;
                var b = float3.zero;
                math.sincos(math.radians(angle), out a.x, out a.z);
                math.sincos(math.radians(angle + debugRenderStepInDegrees), out b.x, out b.z);
                UnityEngine.Debug.DrawLine(a * radius, b * radius);
            }
    
            m_TurretActiveFromEntity.Update(ref state);
            var safeZoneJob = new SafeZoneJob
            {
                TurretActiveFromEntity = m_TurretActiveFromEntity,
                SquaredRadius = radius * radius
            };
            safeZoneJob.ScheduleParallel();
        }
    }
  7. 다음과 같이 "Scripts/Systems" 폴더에 있는 "TurretShootingSystem.cs" 파일의 내용을 수정합니다:
  8. +// 슈팅 태그 컴퍼넌트의 요구로 인해 안전 영역에 있는 탱크에 대해 이 작업이 실행되는 것을 사실상 방지합니다.
    +[WithAll(typeof(Shooting))]
     [BurstCompile]
     partial struct TurretShoot : IJobEntity
     {
         [ReadOnly] public ComponentLookup<LocalToWorldTransform> LocalToWorldTransformFromEntity;
         public EntityCommandBuffer ECB;
     
         void Execute(in TurretAspect turret)
         {
             var instance = ECB.Instantiate(turret.CannonBallPrefab);
             var spawnLocalToWorld = LocalToWorldTransformFromEntity[turret.CannonBallSpawn];
             var cannonBallTransform = UniformScaleTransform.FromPosition(spawnLocalToWorld.Value.Position);
     
             cannonBallTransform.Scale = LocalToWorldTransformFromEntity[turret.CannonBallPrefab].Value.Scale;
             ECB.SetComponent(instance, new LocalToWorldTransform
             {
                 Value = cannonBallTransform
             });
             ECB.SetComponent(instance, new CannonBall
             {
                 Speed = spawnLocalToWorld.Value.Forward() * 20.0f
             });
     
             // 아래 라인은 포탑에서 대포알까지 색을 전파합니다.
             ECB.SetComponent(instance, new URPMaterialPropertyBaseColor { Value = turret.Color });
         }
     }
  9. 플레이 모드로 들어가면 탱크가 세이프 존 밖으로 나온 후에만 사격합니다. 보기 옵션에서 gizmos가 활성화 되었는지 확인하세요, 그렇지 않으면 흰색 원이 표시되지 않습니다.
  10.  
  11. 여전히 플레이 모드에서, 제작된 "Config" GameObject를 선택하고 "Safe Zone Radius"를 수정합니다. "Live Baking" 덕분에 변경 사항이 실시간으로 반영됩니다.
  12. 플레이 모드에서 나오세요.
블로그 이미지

RIsN

,

원본: https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/DOTS_Guide/ecs_tutorial/README.md

Step 7 - 색이 입혀진 탱크들과 대포알들(Colored tanks and cannon balls)

고급 베이킹, 베이킹 시스템들에 대해 소개

ECS 컴퍼넌트들은 렌더링에 사용되는 쉐이더들에 대한 입력들을 제어할 수 있습니다. 자체 쉐이더를 만들고(쉐이더 그래프를 통해) 사용자 지정 ECS 컴퍼넌트를 입력에 맵핑하는 것은 이 튜토리얼의 범위를 벗어나지만, URPMaterialPropertyBaseColor라는 기존 컴퍼넌트를 사용할 예정입니니다. 이름에서 알 수 있듯이 표준 URP 머티리얼의 기본 색상을 제어할 수 있습니다.

우리의 탱크는 3개의 원시 요소(탱크, 터렛, 대포)로 구성되어 있으며, 각 엔티티에는 URPMaterialPropertyBaseColorcomponent가 추가되어야 합니다.

그렇게 하려면 Tank 프리팹을 열고 Hierarchy(탱크, 포탑, 대포)에서 SpawnPoint 트랜스폼을 제외하고 세 가지 기본 요소를 모두 선택하합니다. 렌더러가 없기 때문에 색상은 필요하지 않습니다.

세 가지 기본 요소를 선택한 상태에서 인스펙터의 "컴퍼넌트 추가" 단추를 사용하여 URPMaterialPropertyBaseColorAuthoringcomponent를 추가합니다.

  1. 플레이 모드로 전환하면 탱크가 완전히 검은색이 됩니다(방금 추가한 컴퍼넌트 요소의 기본값이 0,0,0,0입니다.).
  2.  
  3. 플레이 모드에서 나가세요.
  4. 📝 NOTE
    아래 시스템에서 사용하는 EntityCommandBuffer에는 SetComponentForLinkedEntityGroup에서 대상으로 지정할 엔티티를 지정하는 쿼리가 필요합니다.
    엔티티 쿼리의 핵심은 컴퍼넌트 타입 집합들로 구성되며, 쿼리는 해당 집합과 일치하는 엔티티에 대한 필터링된 보기만 제공합니다.
    엔티티 쿼리에 대한 자세한 내용은 package documentation를 참조하세요.
    다음과 같이 "Scripts/Systems" 폴더에 있는 "TankSpawningSystem.cs" 파일의 내용을 수정합니다.
  5.  using Unity.Burst;
     using Unity.Collections;
     using Unity.Entities;
     using Unity.Mathematics;
     using Unity.Rendering;
     
     [BurstCompile]
     partial struct TankSpawningSystem : ISystem
     {
    +    // 쿼리는 OnUpdate의 즉석에서 만들어져서는 안 되므로 필드에 캐시됩니다.
    +    EntityQuery m_BaseColorQuery;
     
         [BurstCompile]
         public void OnCreate(ref SystemState state)
         {
    +        // Config(구성) 싱글톤이 로드되기 전에는 이 시스템을 실행하면 안 됩니다.
    +        state.RequireForUpdate<Config>();
     
    +        m_BaseColorQuery = state.GetEntityQuery(ComponentType.ReadOnly<URPMaterialPropertyBaseColor>());
         }
     
         [BurstCompile]
         public void OnDestroy(ref SystemState state)
         {
         }
     
         [BurstCompile]
         public void OnUpdate(ref SystemState state)
         {
             var config = SystemAPI.GetSingleton<Config>();
     
    +        // 이 시스템은 한 번만 실행되므로 랜덤 시드를 하드 코딩할 수 있습니다.
    +        // 임의 상수 시드를 사용하면 동작이 결정론적으로 진행됩니다.
    +        var random = Random.CreateFromIndex(1234);
    +        var hue = random.NextFloat();
     
    +        // 가능한 한 서로 다른 색상을 만듭니다.
    +        // 이 접근법의 논리는 다음 주소에서 자세히 설명합니다:
    +        // https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
    +        URPMaterialPropertyBaseColor RandomColor()
    +        {
    +            // Note: 이 개념에 익숙하지 않은 경우, 이것은 "로컬 함수"입니다.
    +            // 더 많은 정보를 얻기 위해 인터넷에서 그 용어를 검색할 수 있습니다.
     
    +            // 0.618034005f == 2 / (math.sqrt(5) + 1) == 황금비율의 반대
    +            hue = (hue + 0.618034005f) % 1;
    +            var color = UnityEngine.Color.HSVToRGB(hue, 1.0f, 1.0f);
    +            return new URPMaterialPropertyBaseColor { Value = (UnityEngine.Vector4)color };
    +        }
     
             var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
             var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
     
             var vehicles = CollectionHelper.CreateNativeArray<Entity>(config.TankCount, Allocator.Temp);
             ecb.Instantiate(config.TankPrefab, vehicles);
     
    +        // EntityQueryMask는 EntityQuery에 의해 특정 엔티티가 선택 되었는지 여부를 효율적인 테스트를 제공합니다.
    +        var queryMask = m_BaseColorQuery.GetEntityQueryMask();
     
    +        foreach (var vehicle in vehicles)
    +        {
    +            // 모든 프리팹의 근본에는 해당 엔티티의 모든 목록인 LinkedEntityGroup이 포함됩니다.
    +            ecb.SetComponentForLinkedEntityGroup(vehicle, queryMask, RandomColor());
    +        }
     
             state.Enabled = false;
         }
     }
  6. 플레이 모드로 들어가면 탱크들이 무작위 색상으로 지정됩니다.
  7. 플레이 모드에서 나가세요.
  8. "Scripts/Authoring" 폴더에 있는 "CannonBallAuthoring.cs" 파일의 내용을 다음과 같이 수정합니다:
  9.  using Unity.Entities;
     using Unity.Rendering;
     
     class CannonBallAuthoring : UnityEngine.MonoBehaviour
     {
     }
     
     class CannonBallBaker : Baker<CannonBallAuthoring>
     {
         public override void Bake(CannonBallAuthoring authoring)
         {
             AddComponent<CannonBall>();
    +        AddComponent<URPMaterialPropertyBaseColor>();
         }
     }
  10. 다음과 같이 "Scripts/Aspects" 폴더에 있는 "TurretAspect.cs" 파일의 내용을 수정합니다:
  11.  using Unity.Entities;
     using Unity.Mathematics;
     using Unity.Rendering;
     
     readonly partial struct TurretAspect : IAspect
     {
         readonly RefRO<Turret> m_Turret;
    +    readonly RefRO<URPMaterialPropertyBaseColor> m_BaseColor;
     
         public Entity CannonBallSpawn => m_Turret.ValueRO.CannonBallSpawn;
         public Entity CannonBallPrefab => m_Turret.ValueRO.CannonBallPrefab;
    +    public float4 Color => m_BaseColor.ValueRO.Value;
     }
  12. 다음과 같이 "Scripts/Systems" 폴더에 있는 "TurretShootingSystem.cs" 파일의 내용을 수정합니다:
  13.  [BurstCompile]
     partial struct TurretShoot : IJobEntity
     {
         [ReadOnly] public ComponentLookup<LocalToWorldTransform> LocalToWorldTransformFromEntity;
         public EntityCommandBuffer ECB;
     
         void Execute(in TurretAspect turret)
         {
             var instance = ECB.Instantiate(turret.CannonBallPrefab);
             var spawnLocalToWorld = LocalToWorldTransformFromEntity[turret.CannonBallSpawn];
             var cannonBallTransform = UniformScaleTransform.FromPosition(spawnLocalToWorld.Value.Position);
     
             cannonBallTransform.Scale = LocalToWorldTransformFromEntity[turret.CannonBallPrefab].Value.Scale;
             ECB.SetComponent(instance, new LocalToWorldTransform
             {
                 Value = cannonBallTransform
             });
             ECB.SetComponent(instance, new CannonBall
             {
                 Speed = spawnLocalToWorld.Value.Forward() * 20.0f
             });
     
    +        // 아래의 줄은 포탑에서 대포알까지 색을 전파합니다.
    +        ECB.SetComponent(instance, new URPMaterialPropertyBaseColor { Value = turret.Color });
         }
     }
  14. 플레이 모드로 들어가면 대포알이 생성된 탱크와 같은 색을 갖게 됩니다.
  15. 플레이 모드에서 나가세요.

Reference

  • 맵핑에 관해서
    • https://www.ibm.com/docs/ko/developer-for-zos/14.2?topic=pli-introduction-mapping-concepts
블로그 이미지

RIsN

,

원본: https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/DOTS_Guide/ecs_tutorial/README.md

Step 6 - 많은 탱크들을 소환(Spawning many tanks)

한 번만 실행되어야 하는 초기화 시스템을 처리.

  1. "Tank" Game Object를 "EntityScene"에서 "Assets/Prefabs" 폴더로 드래그 앤 드롭합니다.
  2. "EntityScene"에서 "Tank" GameObject(현재는 프리팹 인스턴스)를 삭제합니다.
  3. "Scripts/Components" 폴더에 "Config.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다:
  4. using Unity.Entities;
    
    struct Config : IComponentData
    {
        public Entity TankPrefab;
        public int TankCount;
        public float SafeZoneRadius;
    }
  5. "Scripts/Authoring" 폴더에 "ConfigAuthoring.cs"이라는 이름의 새 C# 소스 파일을 만들고 다음 내용을 저장합니다:
  6. using Unity.Entities;
    
    class ConfigAuthoring : UnityEngine.MonoBehaviour
    {
        public UnityEngine.GameObject TankPrefab;
        public int TankCount;
        public float SafeZoneRadius;
    }
    
    class ConfigBaker : Baker<ConfigAuthoring>
    {
        public override void Bake(ConfigAuthoring authoring)
        {
            AddComponent(new Config
            {
                TankPrefab = GetEntity(authoring.TankPrefab),
                TankCount = authoring.TankCount,
                SafeZoneRadius = authoring.SafeZoneRadius
            });
        }
    }
  7. Hierarchy 창에서 "EntityScene"을 마우스 오른쪽 단추로 클릭하고 Create Empty를 선택한 다음 새 GameObject의 이름을 "Config"로 지정합니다.
  8. "ConfigAuthoring" 컴퍼넌트를 "Config" GameObject에 추가합니다.
  9. "Config" GameObject를 선택하고, 프로젝트 폴더에서 새 필드 "TankPrefab"을 "Tank" 프리팹(드래그 & 드롭)으로 설정하고, "TankCount"를 20으로 설정하고, "SafeZoneRadius"를 15로 설정합니다.
    📝 NOTE
    "SafeZone Radius"는 아직 사용되지 않지만 이후 진행될 튜토리얼과 관련이 있습니다.
  10. "Scripts/Systems" 폴더에 "TankSpawningSystem.cs"이라는 이름의 새 C# 스크립트 파일을 만들고 다음 내용을 저장합니다.
    ⚠ WARNING
    이 시점에서 플레이 모드로 들어가면 탱크가 하나밖에 없다는 사실에 놀랄 수 있습니다. 실제로 20개의 탱크가 있지만, 그들은 모두 같은 위치에서 스폰되고 정확히 같은 방식으로 움직입니다.
  11. using Unity.Burst;
    using Unity.Collections;
    using Unity.Entities;
    using Unity.Mathematics;
    using Unity.Rendering;
    
    [BurstCompile]
    partial struct TankSpawningSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
        }
    
        [BurstCompile]
        public void OnDestroy(ref SystemState state)
        {
        }
    
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var config = SystemAPI.GetSingleton<Config>();
    
            var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
            var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
    
            var vehicles = CollectionHelper.CreateNativeArray<Entity>(config.TankCount, Allocator.Temp);
            ecb.Instantiate(config.TankPrefab, vehicles);
    
            // 이 시스템은 시작할 때 한 번만 실행해야 합니다. 따라서 한 번 업데이트하면 자동으로 비활성화됩니다.
            state.Enabled = false;
        }
    }
  12. 다음과 같이 "Scripts/Systems" 폴더에 있는 "TankMovementSystem.cs" 파일의 내용을 수정합니다:
  13.  using Unity.Entities;
     using Unity.Mathematics;
     using Unity.Transforms;
     
     partial class TankMovementSystem : SystemBase
     {
         protected override void OnUpdate()
         {
             var dt = SystemAPI.Time.DeltaTime;
     
             Entities
                 .WithAll<Tank>()
    -            .ForEach((TransformAspect transform) =>
    +            .ForEach((Entity entity, TransformAspect transform) =>
                 {
                     var pos = transform.Position;
     
    +                // 이는 탱크의 실제 위치를 수정하는 것이 아니라 3D 노이즈 기능을 샘플링하는 지점만 수정합니다. 
    +                // 이 방식으로, 모든 탱크는 다른 파츠를 사용하고 있으며 고유한 랜덤 플로우 필드를 따라 이동합니다.
    +                pos.y = entity.Index;
                     var angle = (0.5f + noise.cnoise(pos / 10f)) * 4.0f * math.PI;
     
                     var dir = float3.zero;
                     math.sincos(angle, out dir.x, out dir.z);
                     transform.Position += dir * dt * 5.0f;
                     transform.Rotation = quaternion.RotateY(angle);
     
                 }).ScheduleParallel();
         }
     }
  14. 플레이 모드로 들어가면, 20개의 탱크가 각각의 흐름 필드를 따라 이동하기 시작합니다.
  15. 플레이 모드를 종료하세요.
블로그 이미지

RIsN

,

원본: https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/DOTS_Guide/ecs_tutorial/README.md

Step 5 - 대포알 이동(Cannon ball movement)

병렬 job에 대한 소개

  1. "Scripts/Aspects" 폴더에 "CannonBallAspect.cs"이라는 이름의 새 C# 소스 파일을 만들고 다음 내용을 저장합니다.
  2. using Unity.Entities;
    using Unity.Mathematics;
    using Unity.Transforms;
    
    readonly partial struct CannonBallAspect : IAspect
    {
        // 외관(aspect)의 엔티티 필드는 엔티티 자체에 대한 액세스를 제공합니다.
        // 이것은 예를 들어 EntityCommandBuffer에 명령을 등록하는 데 필요합니다.
        public readonly Entity Self;
    
        // 외관(aspect)들은 다른 외관(aspect)을 격납할 수 있습니다.
        readonly TransformAspect Transform;
    
        // RefRW 필드는 컴퍼넌트에 대한 읽기 쓰기 액세스 권한을 제공합니다.
        // 외관(aspect)이 "in" 매개 변수로 사용되는 경우 필드는 RefRO인 것처럼 동작하며 쓰기 시도 시 예외를 발생시킵니다.
        readonly RefRW<CannonBall> CannonBall;
    
        // 이와 같은 프로퍼티는 필수가 아니고, 대신 트랜스폼 필드를 공개할 수도 있습니다.
        // 그러나 "aspect.aspect.aspect.component.value.value" 같은 체인을 피함으로써 가독성을 향상시킵니다.
        public float3 Position
        {
            get => Transform.Position;
            set => Transform.Position = value;
        }
    
        public float3 Speed
        {
            get => CannonBall.ValueRO.Speed;
            set => CannonBall.ValueRW.Speed = value;
        }
    }
  3. "Scripts/Systems" 폴더에 "CannonBallSystem.cs"이라는 이름의 새 C# 소스 파일을 만들고 다음 내용을 저장합니다.
  4. using Unity.Burst;
    using Unity.Entities;
    using Unity.Mathematics;
    
    [BurstCompile]
    // IJobEntity는 소스 생성에 의존하여 실행 함수의 서명 시에 암시적으로 쿼리를 정의합니다. 
    partial struct CannonBallJob : IJobEntity
    {
        // 일반 EntityCommandBuffer는 병렬로 사용할 수 없으며, ParallelWriter를 명시적으로 사용해야 합니다.
        public EntityCommandBuffer.ParallelWriter ECB;
        // job에서 직접 시간에 액세스할 수 없으므로 DeltaTime을 매개 변수로 전달해야 합니다.
        public float DeltaTime;
    
        // ChunkIndexInQuery 특성은 청크 인덱스를 int 매개 변수에 매핑합니다.
        // 각 chunk는 단일 스레드에서만 처리할 수 있으므로 이러한 인덱스는 각 스레드마다 고유합니다.
        // 또한 병렬 처리의 양에 관계없이 완전히 결정론적입니다.
        // 따라서 이러한 인덱스는 EntityCommandBuffer에서 명령을 기록할 때 정렬 키로 사용되므로,
        // 명령 재생이 항상 결정론적임을 보장합니다.
        void Execute([ChunkIndexInQuery] int chunkIndex, ref CannonBallAspect cannonBall)
        {
            var gravity = new float3(0.0f, -9.82f, 0.0f);
            var invertY = new float3(1.0f, -1.0f, 1.0f);
    
            cannonBall.Position += cannonBall.Speed * DeltaTime;
            if (cannonBall.Position.y < 0.0f)
            {
                cannonBall.Position *= invertY;
                cannonBall.Speed *= invertY * 0.8f;
            }
    
            cannonBall.Speed += gravity * DeltaTime;
    
            var speed = math.lengthsq(cannonBall.Speed);
            if (speed < 0.1f) ECB.DestroyEntity(chunkIndex, cannonBall.Self);
        }
    }
    
    [BurstCompile]
    partial struct CannonBallSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
        }
    
        [BurstCompile]
        public void OnDestroy(ref SystemState state)
        {
        }
    
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();
            var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
            var cannonBallJob = new CannonBallJob
            {
                // EntityCommandBuffer의 병렬 기록기를 가져오는 데 필요한 함수 호출을 기록합니다.
                ECB = ecb.AsParallelWriter(),
                // job에서 직접 시간에 액세스할 수 없으므로 DeltaTime을 매개 변수로 전달해야 합니다.
                DeltaTime = SystemAPI.Time.DeltaTime
            };
            cannonBallJob.ScheduleParallel();
        }
    }
  5. 플레이 모드로 들어가면 대포알이 탱크에서 떨어져 땅 위로 튀어오릅니다.
블로그 이미지

RIsN

,

원본: https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/DOTS_Guide/ecs_tutorial/README.md

Step 4 - 대포알

다른 엔터티의 엔터티를 참조하는 프리팹을 만드는 중입니다.

  1. "Scripts/Components" 폴더에 "CannonBall.cs"이라는 이름의 새 C# 소스 파일을 만들고 다음 내용을 저장합니다.
  2. using Unity.Entities;
    using Unity.Mathematics;
    
    // 대포알에 대한 동일한 접근 방식으로, 우리는 엔티티를 식별하기 위한 구성요소를 만들고 있습니다.
    // 그러나 이번에는 속도 필드라는 데이터가 포함되어 있기 때문에 태그 컴퍼넌트(비어 있는 컴퍼넌트)가 아닙니다.
    // 그것은 즉시 사용되지는 않겠지만, 우리가 모션을 구현할 때 관련성이 있을 것입니다.
    struct CannonBall : IComponentData
    {
        public float3 Speed;
    }
  3. "Scripts/Authoring" 폴더에 "CannonBallAuthoring.cs"이라는 이름의 새 C# 소스 파일을 만들고 다음 내용을 저장합니다.
  4. using Unity.Entities;
    using Unity.Rendering;
    
    class CannonBallAuthoring : UnityEngine.MonoBehaviour
    {
    }
    
    class CannonBallBaker : Baker<CannonBallAuthoring>
    {
        public override void Bake(CannonBallAuthoring authoring)
        {
    		// 기본적으로 컴퍼넌트들은 0으로 초기화됩니다.
            // 따라서 이 경우 대포알의 스피드 필드는 float3.zero가 됩니다.
            AddComponent<CannonBall>();
        }
    }
  5. Hierarchy 창에서 "SampleScene"을 마우스 오른쪽 버튼으로 클릭하고 GameObject > 3D Object > Sphere를 선택한 후 새로운 GameObject의 이름을 "CannonBall"로 지정합니다. Position을 (0,0,0), Rotation을 (0,0,0), Scale을 (0.2,0.2,0.2)로 설정합니다.
  6. "CannonBallAuthoring" 컴퍼넌트를 "CannonBall" GameObject에 추가합니다.
  7. "CannonBall" 게임 오브젝트에서 "Sphere Collider" 컴퍼넌트를 제거합니다.
  8. "CannonBall" GameObject를 Project 창의 "Prefabs" 폴더로 드래그 앤 드롭합니다.
  9. "SampleScene"에서 "CannonBall" GameObject(현재는 프리팹 인스턴스)를 삭제합니다.
  10. 다음과 같이 "Scripts/Components" 폴더에 있는 "Turret.cs" 파일의 내용을 수정합니다.
  11.  using Unity.Entities;
     
     struct Turret : IComponentData
     {
    +    // 이 엔티티는 대포알이 생성되어야 하는 대포의 노즐을 참조할 것입니다.
    +    public Entity CannonBallSpawn;
     
    +    // 이 엔티티는 대포알이 발사될 때마다 생성될 프리팹을 참조할 것입니다.
    +    public Entity CannonBallPrefab;
     }
  12. "Scripts/Authoring" 폴더에 있는 "TurretAuthoring.cs" 파일의 내용을 다음과 같이 수정합니다.
  13.  using Unity.Entities;
     
     class TurretAuthoring : UnityEngine.MonoBehaviour
     {
    +    public UnityEngine.GameObject CannonBallPrefab;
    +    public UnityEngine.Transform CannonBallSpawn;
     }
     
     class TurretBaker : Baker<TurretAuthoring>
     {
         public override void Bake(TurretAuthoring authoring)
         {
    -        AddComponent<Turret>();
    +        AddComponent(new Turret
    +        {
    +            // 기본적으로 각 GameObject 작성은 엔티티로 바뀝니다.
    +            // GameObject(또는 컴퍼넌트 작성)가 지정되면 GetEntity는 결과 엔티티를 검색합니다.
    +            CannonBallPrefab = GetEntity(authoring.CannonBallPrefab),
    +            CannonBallSpawn = GetEntity(authoring.CannonBallSpawn)
    +        });
         }
     }
  14. "Turret" GameObject를 선택하고, "Turret Authoring" 컴퍼넌트 요소의 새 필드 "CannonBallPrefab"과 "CannonBallSpawn"을 각각 Project 폴더의 "CannonBall" 프리팹(드래그 & 드롭)과 "SpawnPoint" GameObject(Hierarchy 창에서 드래그 & 드롭)로 설정합니다.
  15. "Scripts/Aspects" 폴더에 "TurretAspect.cs"이라는 이름의 새 C# 소스 파일을 만들고 다음 내용을 저장합니다.
  16. using Unity.Entities;
    using Unity.Mathematics;
    using Unity.Rendering;
    
    // Turret component에 직접 접근하는 것이 아니라 외관(Aspect)을 만들고 있습니다.
    // 외관(Aspect)을 사용하면 구성 요소에 액세스하기 위한 사용자 지정 API를 제공할 수 있습니다.
    readonly partial struct TurretAspect : IAspect
    {
        // 이 참조는 Turret 구성 요소에 대한 읽기 전용 액세스를 제공합니다.
        // 읽기 전용 참조에서 ValueRW(ValueRO 대신)를 사용하려고 하면 오류가 발생합니다.
        readonly RefRO<Turret> m_Turret;
    
        // 다음 속성들의 ValueRo값의 사용에 유의하십시오.
        public Entity CannonBallSpawn => m_Turret.ValueRO.CannonBallSpawn;
        public Entity CannonBallPrefab => m_Turret.ValueRO.CannonBallPrefab;
    }
  17. 📝 NOTE
    다음 단계에서는 타입이 지정된 구성 요소에 임의로 접근할 수 있는 ComponentDataFromEntity<T>를 사용합니다. 이 기능에 대한 자세한 내용은 API Documentation를 참조하십시오.
    "Scripts/Systems" 폴더에 "TurretShootingSystem.cs"이라는 이름의 새 C# 소스 파일을 만들고 다음 내용을 저장합니다.
  18. using Unity.Burst;
    using Unity.Collections;
    using Unity.Entities;
    using Unity.Rendering;
    using Unity.Transforms;
    
    [BurstCompile]
    partial struct TurretShootingSystem : ISystem
    {
        // ComponentLookup은 컴퍼넌트(엔티티 검색)에 대한 임의 접근을 제공합니다.
        // 우리는 이것을 사용하여 스폰 포인트(대포 노즐)의 월드 공간 좌표와 방향을 추출할 것입니다.
        ComponentLookup<LocalToWorldTransform> m_LocalToWorldTransformFromEntity;
    
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            // ComponentLookup 구조체는 한 번 초기화해야 합니다.
            // 매개 변수는 검색들을 읽기 전용으로 할지 또는 쓰기를 허용할지 여부를 지정합니다.
            m_LocalToWorldTransformFromEntity = state.GetComponentLookup<LocalToWorldTransform>(true);
        }
    
        [BurstCompile]
        public void OnDestroy(ref SystemState state)
        {
        }
    
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // ComponentLookup 구조체들은 모든 프레임마다 업데이트 되어야 합니다.
            m_LocalToWorldTransformFromEntity.Update(ref state);
    
            // 인스턴스화에 필요한 구조 변경을 지연시키기 위해 EntityCommandBuffer를 제작합니다.
            var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
            var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
    
            // Job의 인스턴스를 만드는 중입니다.
            // 그것을 통과하면서 ComponentLookup은 스폰 포인트의 월드 트랜스폼을 요구합니다.
            // 또한 Job이 쓸 수 있는 엔티티 명령 버퍼도 있습니다.
            var turretShootJob = new TurretShoot
            {
                LocalToWorldTransformFromEntity = m_LocalToWorldTransformFromEntity,
                ECB = ecb
            };
    
            // 싱글 스레드에서 실행을 예약하고 메인 스레드를 차단하지 않습니다.
            turretShootJob.Schedule();
        }
    }
    
    [BurstCompile]
    partial struct TurretShoot : IJobEntity
    {
        [ReadOnly] public ComponentLookup<LocalToWorldTransform> LocalToWorldTransformFromEntity;
        public EntityCommandBuffer ECB;
    
        // TurretAspects 매개변수는 읽기 전용으로 선언하는 "in"입니다.
        // 이 경우 "ref"(읽기-쓰기)로 만들면 차이가 없지만 
    	// 잠재적으로 경합 상태(Race conditions)가 안전 시스템(Safety System)을 건드는 상황에 직면하게 됩니다.
        // 그래서 일반적으로 가능한 모든 곳에서 "in"을 사용하는 것이 좋은 원칙입니다.
        void Execute(in TurretAspect turret)
        {
            var instance = ECB.Instantiate(turret.CannonBallPrefab);
            var spawnLocalToWorld = LocalToWorldTransformFromEntity[turret.CannonBallSpawn];
            var cannonBallTransform = UniformScaleTransform.FromPosition(spawnLocalToWorld.Value.Position);
    
            // 새 인스턴스의 트랜스폼을 덮어쓰려고 합니다. 
    		// 비율을 명시적으로 복사하지 않으면 1로 재설정되고 크기가 너무 큰 캐논 볼을 갖게 됩니다.
            cannonBallTransform.Scale = LocalToWorldTransformFromEntity[turret.CannonBallPrefab].Value.Scale;
            ECB.SetComponent(instance, new LocalToWorldTransform
            {
                Value = cannonBallTransform
            });
            ECB.SetComponent(instance, new CannonBall
            {
                Speed = spawnLocalToWorld.Value.Forward() * 20.0f
            });
        }
    }
  19. 플레이 모드로 들어가면 탱크 뒤에 대포알의 흔적이 남아 있는 것을 볼 수 있습니다.

블로그 이미지

RIsN

,