Unity DOTS/따라하며 배우기

UNITY DOTS - State Machine(상태 머신) 만들기 #5 Guard 추격

개양반 2022. 8. 7.
728x90

오늘 알아볼 내용

Guard의 탐색 범위에 Player가 존재하면 추격상태로 변경하고 탐색범위 밖으로 도망가면 Idle 상태로 변경하는 것에 대해 다룹니다.


범위를 탐색하기

1. Authoring

Guard의 탐색범위를 저장할 ComponentData가 필요하다. GuardAuthoring.cs 에 아래의 코드를 추가한다.

public struct VisionCone : IComponentData
{
    public float AngleRadians;
    public float ViewDistanceSq;
}

 

위에서 만든 ComponentData를 GuardAuthoring class의 전역변수로 추가한다.

    // 탐색 범위와 거리
    public float VisionAngleDegrees = 45.0f;
    public float VisionMaxDistance = 5.0f;

#코드 설명

탐색 각도는 45이고 탐색 거리는 5이다.

 

Guard가 Entity로 변환될때 VisionCon을 가지도록 GuardAuthoring.Convert를 수정한다.

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponents(entity, new ComponentTypes(
            new ComponentType[]
            {          
                typeof(VisionCone),

                typeof(CooldownTime), 
                typeof(NextWaypointIndex),
                typeof(TargetPosition),
                typeof(MovementSpeed),
                typeof(WaypointPosition)
            }));


        var buffer = dstManager.GetBuffer<WaypointPosition>(entity);
        foreach (var waypointTransform in Waypoints)
        {
            buffer.Add(new WaypointPosition { Value = waypointTransform.position });
        }

        dstManager.SetComponentData(entity, new CooldownTime { Value = IdleCooldownTime });
        dstManager.SetComponentData(entity, new NextWaypointIndex { Value = 0 });
        dstManager.SetComponentData(entity, new TargetPosition { Value = buffer[0].Value });
        dstManager.SetComponentData(entity, new MovementSpeed { MetersPerSecond = MovementSpeedMetersPerSecond });

        dstManager.SetComponentData
            (entity,
            new VisionCone { 
                AngleRadians = math.radians(VisionAngleDegrees),

                // 거리 x 거리는 대략적인 거리를 계산할때 사용한다.
                ViewDistanceSq = VisionMaxDistance * VisionMaxDistance 
            });
    }

 

2. System

범위 내에 플레이어가 있는지 탐색하는 System을 만든다. LookForPlayerSystem.cs를 만들고 아래 코드를 작성한다.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

public partial class LookForPlayerSystem : SystemBase
{
    private EndSimulationEntityCommandBufferSystem endSimECBSystem;
    private EntityQuery playerQuery;

    protected override void OnCreate()
    {
        endSimECBSystem = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
        playerQuery = GetEntityQuery(ComponentType.ReadOnly<PlayerTag>(), ComponentType.ReadOnly<Translation>());
    }


    protected override void OnUpdate()
    {
    	// NativeArray 만들기
        var playerPosition
            = playerQuery.ToComponentDataArrayAsync<Translation>(Allocator.TempJob, out JobHandle getPositionHandle);

        var ecb = endSimECBSystem.CreateCommandBuffer().AsParallelWriter();

        // TargetPosition은 오직 Guard만 가지고 있다. false는 쓰기 가능 데이터
        var targetPositionFromEntity = GetComponentDataFromEntity<TargetPosition>(false);

        var lookHandle = Entities
            .WithName("LookForPlayer")
            .WithReadOnly(playerPosition)
            .WithNativeDisableParallelForRestriction(targetPositionFromEntity)                             
            .WithDisposeOnCompletion(playerPosition)
            .ForEach((
                Entity guardEntity,
                int entityInQueryIndex,
                in Translation guardPosition,
                in Rotation guardRotation,
                in VisionCone guardVisionCone) =>
            {


			// 코드가 길어서 둘로 나눔. 나머지는 아래에서

            }).ScheduleParallel(getPositionHandle);


        endSimECBSystem.AddJobHandleForProducer(lookHandle);

        Dependency = lookHandle;

    }

 #코드 설명

1. playerQuery.ToComponentDataArrayAsync<Translation>(Allocator.TempJob, out JobHandle getPositionHandle);

Query로 조회된 데이터 중에 Translation을 NativeArray로 만드는 코드이다. 첫번째 매개변수로 메모리를 할당하는데 멀티스레드에서 병렬로 처리할때 이용한다면 Allocator.TempJob를 넣으면 된다. 두번째 코드는 종속성이다. Player의 위치값이 먼저 업데이트 된 뒤에 탐색 범위를 체크해야 하므로 playerPosition 값이 변경되면 getPositionHandle로 업데이트 내역을 전달할 예정이다.

 

2. var targetPositionFromEntity = GetComponentDataFromEntity<TargetPosition>(false);

GetComponentDataFromEntity<TargetPosition>은 모든 Entities로부터 TargetPosition ComponentData를 얻어오는 코드이다. 매개변수로 읽기 가능인지 설정한다. false은 읽기 가능이다. 참고로 Guard가 Idle상태일 때는 TargetPosition가 없다.

 

3. WithNativeDisableParallelForRestriction

NatvieArray는 멀티스레드 병렬처리에서 쓰기 작업을 할 수 없다. WithNativeDisableParallelForRestriction를 통해 쓰기 제한을 해제해야만 멀티스레드 병렬처리에서 쓰기 작업을 할 수 있다. 만약, 쓰기 작업하려는 NatvieArray의 데이터가 Entities.ForEach의 Query에서 조회된 Entity의 데이터라면 경쟁 문제가 발생하여 에러가 발생한다. 여기서는 NatvieArray의 데이터가 Entities.ForEach에서 조회되는 Guard의 데이터가 아니라 Player의 위치 데이터 이므로 경쟁문제가 발생하지 않는다는 확신이 있으므로 사용한 것이다.

 

4. WithDisposeOnCompletion

NativeArray로 할당된 메모리는 해제를 해야 한다. WithDisposeOnCompletion는 해당 Entities.ForEach가 완료되면 할당된 메모리를 해제한다는 의미이다.

 

5. var lookHandle = Entities

종속성을 수동으로 설정한 것이다. getPositionHandle 이 먼저 업데이트 되면 Entities.ForEach가 실행되고 변경된 내역이 lookHandle 라는 이름의 종속성으로 전달된다. EntitiyCommandBuffer는 ForEach가 완료되고 실행되어야 하므로 EntitiyCommandBuffer를 실행 예약할때 종속성으로 lookHandle 을 전달한다. 평상시에는 자동으로 처리되나 위의 코드처럼 Entities.ForEach 외부에서 종속성이 변경될때는 수동으로 처리해줘야 한다.

 

위의 ForEach에 아래의 코드를 입력한다.

    if (playerPosition.Length <= 0) return;

    // Guard가 바라보는 방향
    var forwardVector = math.forward(guardRotation.Value);

    // Guard와 Player의 거리
    var vectorToPlayer = playerPosition[0].Value - guardPosition.Value;

    // Guard와 Player 사이의 벡터 방향
    var unitVecToPlayer = math.normalize(vectorToPlayer);

    // 내적 값 구하기(1 ~ -1를 반환한다.)
    var dot = math.dot(forwardVector, unitVecToPlayer);

    // Player가 탐색 범위에 존재하는지 체크
    var canSeePlayer =
        dot > 0.0f && // 바라보는 방향이 비슷한지 체크
        math.abs(math.acos(dot)) < guardVisionCone.AngleRadians && 
        math.lengthsq(vectorToPlayer) < guardVisionCone.ViewDistanceSq;

#코드 설명

1. var dot = math.dot(forwardVector, unitVecToPlayer);

두 벡터의 내적값을 구한다. Dot의 대한 설명은 https://blog.naver.com/unity3dman/220816707776 링크를 참고한다.

 

2.  math.abs(math.acos(dot)) 

내적값을 토대로 각도를 구하는 식이다. 고등학교 수학 내용인데 자세한 설명은 https://dallcom-forever2620.tistory.com/47 링크를 참고한다. 

 

탐색 범위 내에 Player가 존재하면  var canSeePlayer에 true가 반환된다.


쫓아가기 구현

1. GuardUtility 

추격 상태를 설정하거나 해제하는 코드를 추가한다.

    /// <summary>
    /// 추격 상태로 만든다.
    /// </summary>
    public static void TransitionFromChasing(EntityCommandBuffer.ParallelWriter ecb, Entity e, int index)
    {
        ecb.RemoveComponent<IsChasingTag>(index, e);
        ecb.RemoveComponent<TargetPosition>(index, e);
    }

    /// <summary>
    /// 추격 상태를 해제한다.
    /// </summary>
    public static void TransitionToChasing(EntityCommandBuffer.ParallelWriter ecb, Entity e, int index, float3 playerPosition)
    {
        ecb.AddComponent<IsChasingTag>(index, e);
        ecb.AddComponent(index, e, new TargetPosition { Value = playerPosition });
    }

 

2. System

LookForPlayerSystem의 Entities.ForEach에서 var canSeePlayer = 아래에 아래의 코드를 추가한다.

var isCurrentlyChasing = HasComponent<IsChasingTag>(guardEntity);

if (canSeePlayer)
{
    // 추격중 여부 체크
    if (isCurrentlyChasing)
    {
        // 추격 중에 Player의 위치 정보를 변경해준다.
        targetPositionFromEntity[guardEntity] = new TargetPosition { Value = playerPosition[0].Value };
    }

    else
    {
        // 추격 상태로 변경
        GuardAIUtility.TransitionToChasing(ecb, guardEntity, entityInQueryIndex, playerPosition[0].Value);
    }

    // Idle 상태였다면.
    if (HasComponent<IdleTimer>(guardEntity))
    {
        // Idle 상태를 해제한다.
        GuardAIUtility.TransitionFromIdle(ecb, guardEntity, entityInQueryIndex);
    }
}

// 추격 중에 Player가 탐색 범위를 벗어난다면.
else if (isCurrentlyChasing)
{
    // 추격 상태를 해제하고 Idle상태로 변경한다.
    GuardAIUtility.TransitionFromChasing(ecb, guardEntity, entityInQueryIndex);
    GuardAIUtility.TransitionToIdle(ecb, guardEntity, entityInQueryIndex);
}

#코드 설명

1. if (canSeePlayer)

탐색 범위 내에 Player가 있으면 true가 반환된다. 

 

2. if (isCurrentlyChasing)

Guard가 추격 중이라면 지속적으로 Player의 위치 정보를 업데이트한다. 추격 상태가 아니라면 추격 상태로 변경하고 IdleTimer를 해제한다. 

 

다음은 CheckedReachedWaypointSystem.cs에서 추격 상태이면 제외하는 코드를 추가한다.

 Entities
    .WithNone<IsChasingTag>() // 추격 상태 중에는 도착 여부를 확인하지 않는다. 계속 추격한다.
    .ForEach((
        Entity e,       
        int entityInQueryIndex,
        in Translation currentPosition,         
        in TargetPosition targetPosition) =>

#코드 설명

CheckedReachedWaypointSystem는 도착지에 도착하면 Idle 상태로 변경하는 시스템이다. 추격 중에는 Idle 상태가 되면 안된다.

 

유니티에서 재생버튼을 눌러 테스트한다.

댓글

💲 추천 글