Unity - Vector2.Dot으로 시야각 구하기

2024. 5. 17. 13:02Unity/Unity 학습정리

잠입을 해야하는 게임같은 경우 적 AI들이 시야각을 가지고 있다. 그 시야각에 플레이어가 들어가게 된다면 적발이 되고 쫓겨나는 그런 로직이 구현되어있는 경우다. 그럼 이 시야각을 어떻게 구현 할 수 있을까?

 

이번 챌린지 과제 Rocket 발사 프로젝트에서 로켓이 날아가는 도중, 소행성(Asteroid) 레이어가 시야각에 들어오게 된다면 우주선이 깜빡이는 애니메이션을 통해 경고하는 로직을 구현했다.

 

목차

  1. Mathf.Cos 
  2. Physics.2D.CircleCastAll()
  3. Vector2.Dot
  4. 마무리

     



    Mathf.Cos

    Mathf.Cos(float f) 메서드는 f에 값을 넣어주면 해당값을 Cos값으로 반환해준다 (-1,1)범위 

    우리가 알고 있는 Degree로 계산한다면 22.5º에 해당하는 코사인 값이 들어가 있을 것이다.

    float fov = 45f;
    alertThreshold = Mathf.Cos(fov * Mathf.Deg2Rad / 2f);

     

    Degree 45를 라디안으로 변경 후 2를 나누어 Cos값으로 반환해주어 alertThreshold에 대입해준다.

    alertThreshold는 로켓과 소행성 사이의 각도를 기준으로 삼는다.

     

    차후에 구할 소행성과 로켓 사이의 각도를 θ라고한다면

    Cos θ >= alertThreshold이면  θ가 fov각도보다 작다는 뜻이다.

    Cos함수는 라디안 각도가 작을수록 값이 커지기 때문이다.


     

    Physics.2D.CircleCastAll()

    Physics.2D.CircleCastAll()

    로켓이 비행을 할텐데 소행성이 여러개가 존재할텐데 소행성의 위치는 미리 알 수 없다. 직접 충돌이 아니라 로켓 기준으로 일정 범위내에서 충돌한 소행성들의 정보만 가져와서 해당 소행성들과의 시야각을 계산해주는 방식의 로직을 구현 한다.

     

    로켓 기준 원 범위만큼 탐지하여 소행성의 정보를 가져온다.

    public static RaycastHit2D[] CircleCastAll(Vector2 origin, float radius, Vector2 direction, float distance, int layerMask)

    RaycastHit2D 자료형으로 반환한다.

    • Vector2 origin : 원이 시작되는 중심.
    • float radius : origin을 중심으로 radius 반지름 만큼의 원 범위를 탐지한다.
    • float direction : 원의 방향을 나타낸다.
    • float distance : 원을 던지는 거리이다.
    • int layerMaks : 특정 레이어에서만 탐지할 수 있도록하는 필터이다.
    int layerMask = LayerMask.GetMask("Asteroid");
    var hits = Physics2D.CircleCastAll(transform.position, radius, Vector2.up, 0f, layerMask);

     

    Unity 에디터에서 "Asteroid" 레이어를 등록하고 해당 layerMask를 인자로 넘겨 소행성만 탐지할 수 있도록 필터링한다.

    radius = 10f 라서 반지름이 10인 원이고, Vector2.up을 통해 원의 방향은 위쪽이다. 원을 날릴필요 없이 로켓에 붙어있게 하면 되므로 0f를 넣어 준다.

     

    두 벡터 구하기

    RayCastHit2D [] hits 리스트에 담겨있는 소행성의 정보를 가져와 벡터를 구해준다.

    foreach (var hit in hits)
    {
        Vector2 directionToTarget = (hit.transform.position - transform.position).normalized;
        ...
    }

    A에서 B로 향하는 벡터를 구하고 싶다면 B-A 계산을 해주어야 한다. 위 코드는 hit벡터에서 로켓벡터를 빼주었기 때문에 로켓에서 hit(소행성)으로 향하는 벡터를 구할 수 있게 된다. normalized하는 이유는 추후 내적 파트에서 얘기하게 될 것이다.

     

    로켓의 정면 벡터도 구해준다.

    transform.up.normalized

     

    ※ Vector2.up 과 transform.up의 차이는 Vector2.up은 월드기준으로 위를 향하는 벡터이고, transform.up은 오브젝트의 회전을 고려한 정면 벡터를 반환해준다. 

     

    드디어 로켓이 바라보는 벡터와 로켓과 소행성사이의 벡터를 모두 구하게 되었다.


     

    Vector2.Dot()

    이제부터 로켓에서 소행성사이의 벡터를 u벡터, 로켓이 바라보는 정면 벡터를 v라고 한다. 로켓을 A라고한다.

     

    해당 그림과 같이 벡터가 그려졌다면 B가 소행성 일 것이다. 그럼 A가 바라보는 v벡터와 u벡터 사이의 각도 α를 알 수 있다면, 맨 처음 구해놨던 alertThreshold와 비교해 로켓의 시야 45도 이내에 소행성이 들어왔다는 정보를 알 수 있다.

     

    그럼 α를 구하기 위해선 벡터의 내적을 이용해야하는데 내적부터 알아보자면

    두 벡터의 내적은 두 벡터의 크기의 곱과 그 사이의 각도의 코사인 값으로 나타낸다.

    만약 두 벡터의 크기가 모두 1이라면 두 벡터의 내적은 두 벡터 사이의 각도의 코싸인 값 (cos θ )이다.

     

    그래서 우리가 아까 구했던 두 개의 벡터를 모두 normalized해 방향만 구해놨던 것이다. 두 벡터의 방향만 알면 크기를 알 필요 없이 각도를 바로 구할 수 있기 때문이다.

    코드로 표현하면 다음과 같다.

    float cos = Vector2.Dot(directionToTarget, transform.up.normalized);

    해당 코드는 이미 방향만 가진 (크기가 1인) 벡터 두개를 내적하기 때문에 코싸인 값 (cos θ ) 을 반환하게 된다. 해당 값은 cos함수에 따라 (-1,1)사이에 해당하는 값일 것이다.

    Cos값 비교하기

    //float fov = 45f;
    //alertThreshold = Mathf.Cos(fov * Mathf.Deg2Rad / 2f);
    if (cos >= alertThreshold)
    {
        needAlert = true;
        break;
        
    }

    cos 값이 우리가 맨처음 설정했던 45º를 코싸인값으로 만든 alertThreshold화 시킨 값보다 크다면 근처에 소행성이 존재한다고 알리는 애니메이션을 작동시킨다.

     

    설정해놓은 45º보다 작아야지 시야각에 들어오는건데 if(cos <= alertThreshold)

    왜 코드에는 if(cos >= alertThreshold)로 되어있는지 처음에 헷갈렸다. 하지만 다시 코드를 곰곰히 살펴보면서 생각해봤더니 두 변수 모두 Cos함수에 각도를 집어넣어서 탄생한 변수들이기 때문에 각도가 작을수록 Cos값이 커지는 Cos함수의 특성에 의하여 cos>= alerthreshold 는 cos에 들어가있는 각도가 alerthreshold에 들어가있는 각도 보다 작다는 뜻이 된다.

     

    전체소스코드

    public class AlertSystem : MonoBehaviour
    {
        // fov가 45라면 45도 각도안에 있는 aesteriod를 인식할 수 있음.
        [SerializeField] private float fov = 45f;
        // radius가 10이라면 반지름 10 범위에서 aesteriod들을 인식할 수 있음.
        [SerializeField] private float radius = 10f;
        private float alertThreshold;
    
        private Animator animator;
        private static readonly int blinking = Animator.StringToHash("isBlinking");
    
        private void Start()
        {
            animator = GetComponent<Animator>();
            // FOV를 라디안으로 변환하고 코사인 값을 계산
            alertThreshold = Mathf.Cos(fov * Mathf.Deg2Rad / 2f);
        }
    
        private void Update()
        {
            CheckAlert();
            
        }
    
        private void CheckAlert()
        {
            int layerMask = LayerMask.GetMask("Asteroid");
            var hits = Physics2D.CircleCastAll(transform.position, radius, Vector2.up, 0f, layerMask);
    
            bool needAlert = false;
            foreach (var hit in hits)
            {
                Vector2 directionToTarget = (hit.transform.position - transform.position).normalized;
                
                float cos = Vector2.Dot(directionToTarget, transform.up.normalized);
                Debug.Log("cos : " + cos + "alertThreshold : " + alertThreshold);
    
                //cos값은 각이 작을수록 크다
                if (cos >= alertThreshold)
                {
                    needAlert = true;
                    break;
        
                }
            }
            animator.SetBool(blinking, needAlert);
            // 주변 반경의 소행성들을 확인하고 이를 감지하여 Alert를 발생시킴(isBlinking -> true)
        }
    }

     


     

    마무리

    수학적인 내용을 이용해서 시야각을 탐지하는 방법은 처음 해봤던 방법이라 신기하면서 어려웠다. 특히 라디안은 우리가 직관적으로 단번에 이해하기엔 어려운 부분이라서 라디안과 Degree를 왔다 갔다하는것도 체크를 잘 하면서 구현해야한다는 생각이 들었다. 벡터의 내적같은 경우는 앞으로 게임을 만들면서 자주 쓰일 것 같다는 생각이 들어 이번 기회에 열심히 정리를 해보았다.

     

     

    'Unity > Unity 학습정리' 카테고리의 다른 글

    Unity - Object Pool  (0) 2024.05.23
    Unity - 싱글톤 패턴  (0) 2024.05.21
    Unity - 각도 구하기  (0) 2024.05.16
    Unity - LayerMask  (0) 2024.05.09
    Unity - Coroutine2  (0) 2024.04.22