- Unity DOTS - 클릭한 장소로 이동하기2025년 02월 09일
- 개양반
- 작성자
- 2025.02.09.:27
개요
Unity DOTS에서 클릭한 좌표로 이동시키는 방법에 대해 알아보자.
Scene 작업
1. 서브씬 만들기
게임오브젝트를 Entity로 변환하려면 SubScene에서 게임오브젝트를 생성해야 한다.
미리 Entity로 만들어서 런타임에서 랙걸리지 않기 위함이라고 생각하면 된다.
2. Ground 생성
SubScene 의 자식으로 Plane을 만든다. Ground로 이름을 변경하고 LayerMask에 Ground를 추가해서 해당 오브젝트의 레이어를 Ground로 변경한다.
3. 캐릭터 생성
SubScene의 자식오브젝트로 빈게임오브젝트를 만들고 이름을 Player로 변경한다. Rigidbody와 Capsule Collider를 추가한다.
Player의 자식오브젝트로 Capsule을 추가하고 Collider를 삭제한다.
Capsule의 자식오브젝트로 Cube를 추가하고 Transform을 아래와 같이 수정한다.
Position Rotation Scale x 0 0 0.5 y 0.25 0 0.3 z 0.5 0 0.3 아래와 같이 만들면 된다.
이동 구현하기
원하는 좌표로 이동하기 구현을 하기 전에 특정 좌표로 이동하는 기능을 구현해봅시다.
1. 컴포넌트 만들기
UnitMoverAuthoring.cs를 만들고 아래의 코드를 작성합니다.
using UnityEngine; using Unity.Entities; using Unity.Mathematics; public class UnitMoverAuthoring : MonoBehaviour { public float moveSpeed; public float rotationSpeed; public class Baker : Baker<UnitMoverAuthoring> { public override void Bake(UnitMoverAuthoring authoring) { var entity = GetEntity(TransformUsageFlags.Dynamic); AddComponent(entity, new UnitMover { moveSpeed = authoring.moveSpeed, rotationSpeed = authoring.rotationSpeed }); } } } public struct UnitMover : IComponentData { public float moveSpeed; public float rotationSpeed; }
위의 스크립트를 Player 게임오브젝트에 추가합니다.
2. System 만들기
UnitMoverSystem.cs를 만들고 아래의 코드를 작성합니다.
using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.Physics; using Unity.Transforms; partial struct UnitMoverSystem : ISystem { [BurstCompile] public void OnUpdate(ref SystemState state) { var deltaTime = SystemAPI.Time.DeltaTime; new UnitMoverJob { deltaTime = deltaTime }.ScheduleParallel(); } [BurstCompile] partial struct UnitMoverJob : IJobEntity { public float deltaTime; public void Execute(ref LocalTransform localTransform, ref PhysicsVelocity physicsVelocity, in UnitMover unitMover) { float3 moveDirection = localTransform.Position - new float3(10, 1, 0); float reachedTargetDistanceSq = 1f; // 목적지에 도착했다면. if (math.lengthsq(moveDirection) < reachedTargetDistanceSq) { physicsVelocity.Linear = float3.zero; physicsVelocity.Angular = float3.zero; return; } moveDirection = math.normalize(moveDirection); localTransform.Rotation = math.slerp( localTransform.Rotation, quaternion.LookRotation(moveDirection, math.up()), deltaTime * unitMover.rotationSpeed ); physicsVelocity.Linear = moveDirection * unitMover.moveSpeed; physicsVelocity.Angular = float3.zero; } } }
physicsVelocity.Angular = float3.zero;를 한 이유는 localTransform.Rotation으로만 캐릭터의 방향 전환을 하기 위함입니다. 이동 중에 physicsVelocity에 의한 캐릭터 방향이 변할 수 있기 때문입니다. 이것이 버그인지 몰라도 에디터에서 Rigidbody의 Freeze Rotation을 체크해도 DOTS에서는 동작하지 않습니다. 그래서 physicsVelocity.Angular = float3.zero;로 Freeze Rotation 효과를 만든 겁니다.
3. 중간 확인
재생 버튼을 누르면 Player가 좌측으로 이동합니다.
클릭으로 월드 좌표 얻기
클릭한 위치의 월드 좌표를 얻는 방법에 대해 알아봅시다.
기존의 GameObject 방식에서는 Camera.main.ScreenToWorldPoint(Input.mousePosition)로 카메라의 월드 좌표를 구한다음에 RayCast로 그라운드를 체크해서 바닥의 좌표를 얻었습니다. DOTS에서도 똑같이 구현할 수도 있지만 Job과 BurstCompile에서 해당 기능이 동작하려면 조금 다른 방식으로 접근해야 합니다.
1. 월드 좌표 구하기
1-1 마우스 클릭이 발생하면 RayCast 쏘기
GetWorldPostionSystem.cs를 만들고 아래의 코드를 작성한다.
using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.Physics; using UnityEngine; partial struct GetWorldPostionSystem : ISystem { // [BurstCompile] Camera, Input 는 관리되는 컴포넌트로 BurstCompile을 사용할 수 없다. public void OnUpdate(ref SystemState state) { if (Input.GetMouseButtonDown(0)) { var physicsWorldSingleton = SystemAPI.GetSingleton<PhysicsWorldSingleton>(); var colliderWorld = physicsWorldSingleton.CollisionWorld; float3 mouseWorldPostion = Camera.main.ScreenToWorldPoint(Input.mousePosition); if (colliderWorld.CastRay(new RaycastInput { Start = mouseWorldPostion, End = mouseWorldPostion + new float3(0, -100, 0), Filter = new CollisionFilter { BelongsTo = ~0u, CollidesWith = 1u << 6, // 6번 레이어만 충돌. }, }, out Unity.Physics.RaycastHit raycastHit)) { } } } }
아래의 코드는 시스템에서 RayCast를 사용하기 위한 템플릿이라고 보면 된다. RayCast를 사용하려면 ColliderWorld가 필요하다고 외우면 된다. CollisionFilter는 충돌이 가능한 레이어마스크를 설정하는 옵션이다.
BelongsTo 레이캐스트를 쏘는 엔티티가 속한 레이어 CollidesWith 충돌할 레이어 1-2 월드 좌표 저장하기
raycastHit에는 raycast가 충돌한 좌표 정보가 있다. 이 좌표를 어딘가에 저장해야 UnitMoverSystem에서 사용할 수있다. 저장하는 방법으로는 SystemHandle을 활용하는 방법과 ComponentData를 활용하는 방법이 있다.
먼저 SystemHandle을 활용하는 방법에 대해 알아보자.using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.Physics; using UnityEngine; partial struct GetWorldPostionSystem : ISystem { public float3 m_WorldPosition; // [BurstCompile] public void OnUpdate(ref SystemState state) { // if문 코드 생략 }, out Unity.Physics.RaycastHit raycastHit)) { m_WorldPosition = raycastHit.Position; } //if (colliderWorld.CastRay(new RaycastInput } // if (Input.GetMouseButtonDown(0)) } }
전역 변수로 m_WorldPosition을 만들고 바닥의 월드 좌표를 얻을 때마다 m_WorldPosition의 값을 갱신한다.
그럼, 다른 시스템에서 어떻게 m_WorldPosition에 접근할 수 있는지 살펴보자. SetMoveTargetSystem.cs을 만들고 아래의 코드를 작성한다.
using Unity.Burst; using Unity.Entities; using Unity.Mathematics; partial struct SetMoveTargetSystem : ISystem { [BurstCompile] public void OnUpdate(ref SystemState state) { foreach (var (worldPosition, entity) in SystemAPI.Query<RefRO<WorldPosition>>().WithEntityAccess()) { var getWorldPositionSystemHandle = state.World.GetExistingSystem<GetWorldPostionSystem>(); if (getWorldPositionSystemHandle != SystemHandle.Null) { var getWorldPositionSystem = state.WorldUnmanaged .GetUnsafeSystemRef<GetWorldPostionSystem>(getWorldPositionSystemHandle); var worldPosition = getWorldPositionSystem.worldPostion; } } } }
하지만 위의 방법은 클릭 여부와 상관없이 매프레임마다 m_WorldPosition에 접근한다는 문제가 발생한다. 그럼, 클릭할 때만 m_WorldPosition에 접근하려면 어떻게 해야할까? 방법은 IEnableableComponent를 활용해서 활성될 때만 접근하는 방법이다.
이번에는 IComponentData로 WorldPosition을 저장하고 사용하는 방법에 대해 알아보자.
WorldPositionAuthoring.cs를 만들고 아래의 코드를 작성한다.
using Unity.Entities; using Unity.Mathematics; using UnityEngine; public class WorldPositionAuthoring : MonoBehaviour { public class Baker : Baker<WorldPositionAuthoring> { public override void Bake(WorldPositionAuthoring authoring) { var entity = GetEntity(TransformUsageFlags.Dynamic); AddComponent(entity, new WorldPosition { Value = float3.zero }); SetComponentEnabled<WorldPosition>(entity, false); } } } public struct WorldPosition : IComponentData, IEnableableComponent { public float3 Value; }
SubScene의 자식오브젝트로 빈 게임오브젝트를 만들고 이름을 WorldPosition으로 변경한다. WorldPosition에 WorldPositionAuthoring.cs를 추가한다.
GetWorldPostionSystem.cs를 아래와 같이 수정한다.
using Unity.Entities; using Unity.Mathematics; using Unity.Physics; using UnityEngine; partial struct GetWorldPostionSystem : ISystem { private EntityQuery m_Query; public void OnCreate(ref SystemState state) { m_Query = state.GetEntityQuery(new EntityQueryDesc { All = new ComponentType[] { typeof(WorldPosition) }, Options = EntityQueryOptions.IgnoreComponentEnabledState // 활성 여부를 무시한다. }); } // [BurstCompile] public void OnUpdate(ref SystemState state) { if (Input.GetMouseButtonDown(0)) { var physicsWorldSingleton = SystemAPI.GetSingleton<PhysicsWorldSingleton>(); var colliderWorld = physicsWorldSingleton.CollisionWorld; float3 mouseWorldPostion = Camera.main.ScreenToWorldPoint(Input.mousePosition); if (colliderWorld.CastRay(new RaycastInput { Start = mouseWorldPostion, End = mouseWorldPostion + new float3(0, -100, 0), Filter = new CollisionFilter { BelongsTo = ~0u, CollidesWith = 1u << 6, // 6번 레이어만 충돌. }, }, out Unity.Physics.RaycastHit raycastHit)) { var entity = m_Query.GetSingletonEntity(); var worldPosition = SystemAPI.GetComponentRW<WorldPosition>(entity); SystemAPI.SetComponentEnabled<WorldPosition>(entity, true); worldPosition.ValueRW.Value = raycastHit.Position; } } } }
WorldPosition을 저장하고 Enable을 true로 변경한다. WorldPosition은 1개만 존재하므로 위와 같이 처리했지만 반복문이 필요한 경우에는 아래와 같이 처리할 수 있다.
// 쿼리를 사용하여 WorldPosition 컴포넌트를 가진 엔티티를 찾음 foreach (var (worldPosition, entity) in SystemAPI.Query<RefRW<WorldPosition>>() .WithEntityAccess() .WithOptions(EntityQueryOptions.IgnoreComponentEnabledState)) { worldPosition.ValueRW.value = raycastHit.Position; SystemAPI.SetComponentEnabled<WorldPosition>(entity, true); }
.WithOptions(EntityQueryOptions.IgnoreComponentEnabledState)) 을 추가하여 비활성된 IEnableableComponent도 쿼리에 조회되도록 만들었다.
이번에는 WorldPosition을 활용해서 유닛이 이동하도록 만들어보자. UnitMoverAuthoring.cs에서 UnitMover에 TargetPosition을 추가한다.
public struct UnitMover : IComponentData { public float moveSpeed; public float rotationSpeed; public float3 TargetPosition; // << 추가 }
SetMoveTargetSystem.cs를 아래와 같이 수정한다.
using Unity.Burst; using Unity.Entities; using Unity.Mathematics; partial struct SetMoveTargetSystem : ISystem { [BurstCompile] public void OnUpdate(ref SystemState state) { foreach (var (worldPosition, entity) in SystemAPI.Query<RefRO<WorldPosition>>().WithEntityAccess()) { var jobHandle = new SetMoveTargetJob { TargetPosition = worldPosition.ValueRO.Value }.ScheduleParallel(state.Dependency); state.Dependency = jobHandle; SystemAPI.SetComponentEnabled<WorldPosition>(entity, false); } } partial struct SetMoveTargetJob : IJobEntity { public float3 TargetPosition; void Execute(ref UnitMover unitMover) { unitMover.TargetPosition = TargetPosition; } } }
IEnableableComponent는 OnCreate 함수에서 state.RequireForUpdate로 사용할 수 없다. 그래서 foreach에서 쿼리로 조회를 했다. 이 방법 외에도 쿼리에 조회된 Entity의 개수를 활용하는 방법 등 다양한 방법으로 조건문을 걸 수 있다. 그리고 foreach의 쿼리를 사용한 이유는 IEnableableComponent는 GetSingleton을 호출할 수 없다. EntityQueryOptions.IgnoreComponentEnabledState를 활용해서 쿼리를 만들어서 GetSingleton를 IEnableableComponent 가 호출하게 할 수 있지만 내 기준에서는 foreach를 사용하는게 코드가 더 깔끔하다고 판단했다.
아래의 코드는 종속성에 대한 처리다.
}.ScheduleParallel(state.Dependency); state.Dependency = jobHandle;
ScheduleParallel는 멀티스레드에서 동작하므로 Job이 완료되지 않아도 그 아래의 코드가 실행될 수 있다. Job에서는 WorldPosition의 데이터를 다른 엔티티에서 활용하는데 다른 스레드에서 WorldPosition의 데이터가 변경되면 에러가 발생한다. 그래서 state.Dependency를 활용해서 Job이 모두 완료되면 SystemAPI.SetComponentEnabled(entity, false);가 실행되도록 만들었다.
마무리
유니티 에디터에서 실행 버튼을 누르고 바닥을 클릭해보자. 클릭한 방향으로 이동하는 것을 볼 수 있다.
'Unity DOTS > TIP' 카테고리의 다른 글
Unity DOTS - IEnableableComponent을 반복문없이 변경하기 (0) 2025.02.10 UNITY DOTS TIP - ComponentData를 세분화 해라 (0) 2022.12.01 UNITY DOTS - JobHandle.CombineDependencies (0) 2022.11.29 UNITY DOTS - Unity.Mathematics.Random 랜덤 사용하기 (0) 2022.11.28 다음글이전글이전 글이 없습니다.댓글