Unity DOTS/따라하며 배우기

UNITY DOTS - State Machine(상태 머신) 만들기 #4 Guard 순찰

개양반 2022. 8. 7.

오늘 알아볼 내용

Guard가 목적지에 도착하면 Idle 상태가 되고 CoolTime 동안 대기를 한 뒤에 다음 목적지로 이동하는 순찰을 구현할 예정입니다.


Idle 상태 만들기

1. ComponentData 추가

Idle 상태가 되면 Guard에게 얼마동안 휴식을 취했는지 저장하는 IdleTimer Data를 추가합니다. GuardAuthoring.cs에 아래의 코드를 추가합니다. 

public struct IdleTimer : IComponentData
{
    public float Value;
}

 

2. GuardAIUtility

Idle 상태가 되면 IdleTimer를 Guard에 추가하는 기능을 만듭니다. 아래의 코드를 GuardAIUtility.cs에 추가합니다.

    /// <summary>
    /// Idle 상태로 변경
    /// </summary>
    public static void TransitionToIdle(EntityCommandBuffer.ParallelWriter ecb, Entity e, int index)
    {
        ecb.AddComponent(index, e, new IdleTimer { Value = 0.0f });
    }

 

Idle 상태가 해제되면 Guard의 Idletimer를 제거하는 코드를 GuardAIUtility.cs에 추가합니다.

    /// <summary>
    /// Idle 상태 해제
    /// </summary>
    public static void TransitionFromIdle(EntityCommandBuffer.ParallelWriter ecb, Entity e, int index)
    {
        ecb.RemoveComponent<IdleTimer>(index, e);
    }

 

3. System 

목적지에 도착하면 GuardAIUtility.TransitionFromIdle를 호출하여 Guard에게 IdleTimer를 추가하는 기능을 구현합니다. CheckedReachedWaypointSystem.cs를 만들고 아래의 코드를 작성합니다.

public partial class CheckedReachedWaypointSystem : SystemBase
{
    private EndSimulationEntityCommandBufferSystem endSimECBSystem;

    protected override void OnCreate()
    {
        endSimECBSystem = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
    }

    [BurstCompile]
    protected override void OnUpdate()
    {
        // 멀티스레드 병렬처리에서 EntityCommandBuffer에 명령을 기록할 수 있도록 한다.
        var ecb = endSimECBSystem.CreateCommandBuffer().AsParallelWriter();

        Entities
            // Idle 상태를 테스트하려면 .WithNone<IdleTimer>() 필요.
            .ForEach((
                Entity e,       
                int entityInQueryIndex,
                in Translation currentPosition,         
                in TargetPosition targetPosition) =>
                {
                    // 남은 거리 측정
                    var distanceSq = math.lengthsq(targetPosition.Value - currentPosition.Value);

                    // 도착 여부 확인
                    if (distanceSq < GuardAIUtility.kStopDistanceSq)
                    {
                        // 상태 변경
                        GuardAIUtility.TransitionToIdle(ecb, e, entityInQueryIndex);
                    }

                }).ScheduleParallel();

        // EntityCommandBuffer 명령 실행 예약
        endSimECBSystem.AddJobHandleForProducer(Dependency);

    }
}

#코드 설명

멀티스레드에서는 쓰기 작업의 경우 여러 스레드가 하나의 데이터에 동시에 접근할 수 있으므로 경쟁 문제가 발생합니다. 그래서 멀티스레드에서는 EntityCommandBuffer를 이용해서 쓰기 작업을 예약한 뒤에 현재 프레임이 종료되면 쓰기 작업을 진행합니다. EntityCommandBuffer의 메뉴얼은 아래의 링크를 참고합니다.

[Unity DOTS/Dots Custom Manual] - UNITY DOTS - Entity Command Buffers 에 대해 알아보자.

 

math.lengthsq 는 대략적인 거리를 계산할 때 사용합니다. 정밀한 거리 계산은 안되지만 성능상의 이점이 있어 대략적인 거리 계산으로도 충분할때 사용합니다. Idle상태를 테스트하고 싶다면  .WithNone<IdleTimer>() 를 추가해야 합니다.

 

 endSimECBSystem.AddJobHandleForProducer(Dependency);는 쓰기 준비가 완료되면 endSimECBSystem에 기록된 쓰기 기록을 실행하라는 의미입니다. 매개변수로 종속성을 전달하는데  종속성은 시스템이 의존하는 모든 작업이 완료되었는지를 확인합니다.

 


순찰 구현하기

1. ComponentData

아래의 두개의 Data를 GuardAuthoring.cs에 추가합니다.

// 목적지 Index
public struct NextWaypointIndex : IComponentData
{
    public int Value;
}

// 휴식 시간 체크
public struct CooldownTime : IComponentData
{
    public float Value;
}

 

2. Authoring

GuardAuthoring.cs 의 GuardAuthoring Class에 전역변수를 추가합니다.

    public float IdleCooldownTime = 3.0f;

 

GuardAuthoring.Convert ()에 dstManager.AddComponents 에 위에서 작성한 NextWaypointIndex와 CooldownTime을 추가하는 코드를 작성합니다. 

        dstManager.AddComponents(entity, new ComponentTypes(
            new ComponentType[]
            {
                typeof(CooldownTime), // 추가
                typeof(NextWaypointIndex), // 추가
                typeof(TargetPosition),
                typeof(MovementSpeed),
                typeof(WaypointPosition)
            }));

 

GuardAuthoring.Convert()에 NextWaypointIndex와 CooldownTime의 초기값을 설정하는 코드를 추가합니다.

        dstManager.SetComponentData(entity, new CooldownTime { Value = IdleCooldownTime });
        dstManager.SetComponentData(entity, new NextWaypointIndex { Value = 0 });

 

3. GuardAIUtility

순찰 상태로 변경하거나, 해제하는 코드를 GuardAIUtility에 추가합니다.

    /// <summary>
    /// 순찰 상태 해제
    /// </summary>
    public static void TransitionFromPatrolling(EntityCommandBuffer.ParallelWriter ecb, Entity e, int index)
    {
        ecb.RemoveComponent<TargetPosition>(index, e);
    }

    /// <summary>
    /// 순찰 상태로 변경
    /// </summary>
    /// <param name="waypointPosition">다음 순찰지</param>
    public static void TransitionToPatrolling(EntityCommandBuffer.ParallelWriter ecb, Entity e, int index, float3 waypointPosition)
    {
        ecb.AddComponent(index, e, new TargetPosition { Value = waypointPosition });
    }

 

4. System

 

Idle 상태가 CooldownTime 동안 휴식을 취한 뒤에 순찰 상태로 변경하는 기능을 구현합니다. UpdateIdleTimerSystem.cs를 만들고 아래의 코드를 작성합니다.

using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;


public partial class UpdateIdleTimerSystem : SystemBase
{
    private EndSimulationEntityCommandBufferSystem endSimECBSystem;
    protected override void OnCreate()
    {
        endSimECBSystem = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
    }

    [BurstCompile]
    protected override void OnUpdate()
    {
        var ecb = endSimECBSystem.CreateCommandBuffer().AsParallelWriter();
        var deltaTime = Time.DeltaTime;

        Entities
            .ForEach((
                Entity e,
                int entityInQueryIndex,
                DynamicBuffer<WaypointPosition> waypoints,
                ref IdleTimer idleTimer,
                ref NextWaypointIndex index,
                in CooldownTime cooldownTime) =>
                {
                    idleTimer.Value += deltaTime;

                    if (idleTimer.Value >= cooldownTime.Value)
                    {
                        // Idle 상태 해제
                        GuardAIUtility.TransitionFromIdle(ecb, e, entityInQueryIndex);

                        // 이동해야 할 목적지의 index 값 변경
                        index.Value = (index.Value + 1) % waypoints.Length;
                        
                        // 순찰 상태로 변경
                        GuardAIUtility
                        .TransitionToPatrolling
                        (ecb, e, entityInQueryIndex, waypoints[index.Value].Value); 
                    }

                }).ScheduleParallel();
        
        endSimECBSystem.AddJobHandleForProducer(Dependency);

    }
}

#코드 설명

Idle 상태가 되면 몇초 동안 휴식을 취했는지 체크합니다. CooldownTime만큼 휴식을 취했다면 Idle 상태를 해제하고 목적지 Index를 변경해서 순찰 상태로 변경하는 코드입니다.

 

이번에는 목적지에 도착하면 순찰 상태를 해제하겠습니다.

CheckedReachedWaypointSystem의 if (distanceSq < GuardAIUtility.kStopDistanceSq) 안에 아래의 코드를 추가합니다.

 [BurstCompile]
    protected override void OnUpdate()
    {
        // 멀티스레드 병렬처리에서 EntityCommandBuffer에 명령을 기록할 수 있도록 한다.
        var ecb = endSimECBSystem.CreateCommandBuffer().AsParallelWriter();

        Entities
            .ForEach((
                Entity e,       
                int entityInQueryIndex,
                in Translation currentPosition,         
                in TargetPosition targetPosition) =>
                {
                    // 남은 거리 측정
                    var distanceSq = math.lengthsq(targetPosition.Value - currentPosition.Value);

                    // 도착 여부 확인
                    if (distanceSq < GuardAIUtility.kStopDistanceSq)
                    {
                        // 상태 변경
                        GuardAIUtility.TransitionFromPatrolling(ecb, e, entityInQueryIndex);
                        GuardAIUtility.TransitionToIdle(ecb, e, entityInQueryIndex);
                    }

                }).ScheduleParallel();

        // EntityCommandBuffer 명령 실행 예약
        endSimECBSystem.AddJobHandleForProducer(Dependency);

    }

#코드 설명

목적지에 도착하면  GuardAIUtility.TransitionFromPatrolling를 통해 순찰 상태를 제거하는 코드입니다. 이때, TargetPosition도 제거됩니다. 

 

 

재생 버튼을 눌러 Guard가 목적지를 순찰하는지 테스트를 합니다.

댓글

💲 추천 글