Chapter 7-3. AI : 동물 AI 의 시야각

Date:     Updated:

카테고리:

태그:

인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

Chapter 7. 동물들의 공격형 AI, 도망형 AI 구현

🚖 돼지의 시야각

돼지의 시야각에 플레이어가 들어서면 돼지는 플레이어를 피해 도망치게 한다.

📜FieldOfViewAngle.cs

Pig 에 붙인다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FieldOfViewAngle : MonoBehaviour
{
    [SerializeField] private float viewAngle;  // 시야 각도 (130도)
    [SerializeField] private float viewDistance; // 시야 거리 (10미터)
    [SerializeField] private LayerMask targetMask;  // 타겟 마스크(플레이어)

    private Pig thePig; // 📜Pig.cs

    void Start()
    {
        thePig = GetComponent<Pig>();
    }

    void Update()
    {
        View();  // 매 프레임마다 시야 탐색
    }

    private Vector3 BoundaryAngle(float _angle)
    {
        _angle += transform.eulerAngles.y;
        return new Vector3(Mathf.Sin(_angle * Mathf.Deg2Rad), 0f, Mathf.Cos(_angle * Mathf.Deg2Rad));
    }

    private void View()
    {
        Vector3 _leftBoundary = BoundaryAngle(-viewAngle * 0.5f);  // z 축 기준으로 시야 각도의 절반 각도만큼 왼쪽으로 회전한 방향 (시야각의 왼쪽 경계선)
        Vector3 _rightBoundary = BoundaryAngle(viewAngle * 0.5f);  // z 축 기준으로 시야 각도의 절반 각도만큼 오른쪽으로 회전한 방향 (시야각의 오른쪽 경계선)

        Debug.DrawRay(transform.position + transform.up, _leftBoundary, Color.red);
        Debug.DrawRay(transform.position + transform.up, _rightBoundary, Color.red);

        Collider[] _target = Physics.OverlapSphere(transform.position, viewDistance, targetMask);

        for (int i = 0; i < _target.Length; i++)
        {
            Transform _targetTf = _target[i].transform;
            if (_targetTf.name == "Player")
            {
                Vector3 _direction = (_targetTf.position - transform.position).normalized;
                float _angle = Vector3.Angle(_direction, transform.forward);

                if (_angle < viewAngle * 0.5f)
                {
                    RaycastHit _hit;
                    if(Physics.Raycast(transform.position + transform.up, _direction, out _hit, viewDistance))
                    {
                        if (_hit.transform.name == "Player")
                        {
                            Debug.Log("플레이어가 돼지 시야 내에 있습니다.");
                            Debug.DrawRay(transform.position + transform.up, _direction, Color.blue);

                            thePig.Run(_hit.transform.position);
                        }
                    }
                }
            }
        }
    }
}

image

삼각 함수 개념! 각도와 삼각 함수를 사용하여 좌표를 구할 수 있다.

image

y 축으로 회전하면 x, z 축도 함께 움직인다.

  • 돼지의 z축(앞뒤), x축(좌우)를 기준으로 한 삼각 함수
    • 돼지의 z축과 x위치는 돼지가 바라보고 있는 방향, 즉 돼지가 y축 중심으로 회전한 방향을 기준으로 뻗어 위치하게 된다.

3 D 에서 적용 한다면, 돼지의 시야각은 돼지가 바라보는 방향(z 축이자, y 축을 중심으로 회전한 방향)을 중심으로 한다. 돼지가 y 축을 중심으로 회전한 값이 돼지 기준에서의 앞 방향인 z축이 되고, 돼지의 시야 각은 이 돼지 기준의 z축을 중심으로 한 돼지의 시야 각도의 절반만큼 왼쪽(-), 오른쪽(+)으로 더한 각도 사이의 범위와 같다!

image

image

  • 시야각도 오른쪽 경계 좌표는
    • 삼각 함수에게 인자로 넘길 각도는 [돼지가 y축 중심으로 회전한 값(돼지가 보는 앞 방향인 z축 위치) + 시야각의 절반] 이 되야 한다.
    • sin 삼각 함수의 결과는 시야각도 오른쪽 경계 좌표의 x 값이 되고
    • cos 삼각 함수의 결과는 시야각도 오른쪽 경계 좌표의 z 값이 된다.
  • 시야각도 왼쪽 경계 좌표는
    • 삼각 함수에게 인자로 넘길 각도는 [돼지가 y축 중심으로 회전한 값(돼지가 보는 앞 방향인 z축 위치) - 시야각의 절반] 이 되야 한다.
    • sin 삼각 함수의 결과는 시야각도 왼쪽 경계 좌표의 x 값이 되고
    • cos 삼각 함수의 결과는 시야각도 왼쪽 경계 좌표의 z 값이 된다.


  • BoundaryAngle(float _angle)
    • 경계가 되는 델타 좌표 구하기
    • 삼각함수를 사용하여 현재의 z 축 중심으로 _angle 각도만큼 회전했을 때의 그 델타 x, z 값 구하기.
      • 현재의 z축이 바라보는 방향 (= y축 중심으로 한 회전 값)
        • transform.eulerAngles.y
      • 현재의 z축을 중심으로 _angle 만큼 회전한 방향 (= y축 중심으로 한 회전 값)
        • _angle += transform.eulerAngles.y
      • 플레이어의 중심의 앞 방향으로부터 _angle 각도만큼 회전하여 위치하게 할 때의 그 델타 벡터.
        • x 로는 Mathf.Sin(_angle * Mathf.Deg2Rad) 만큼 더 움직인 곳.
        • y 로는 움직임 X
        • z 로는 Mathf.Cos(_angle * Mathf.Deg2Rad 만큼 더 움직인 곳.
          • 삼각 함수는 라디안을 인수로 받기 때문에 각도를 라디안으로 변환하여 넘기기 위해 180 / 파이 값인 Mathf.Deg2Rad 을 곱해서 넘김!
  • View()
    • 개발자 편의를 위해 돼지 시야각의 왼쪽, 오른쪽 경계선 그리기
      • 돼지 위치의 조금 위부터 transform.position + transform.up
      • _leftBoundary 만큼 더 한 곳 까지 그림.
        • Debug.DrawRayRaycast와 다르게 두번째 인수가 방향이 아닌 델타 위치가 된다. 즉, 첫번재 인수를 시작점으로 (첫번째 인수 + 두번쨰 인수) 위치까지 선을 그림.
    • 돼지의 주변에 플레이어가 있다면 _target 배열에 담음
      • 돼지를 중심으로 viewDistance 반경 내에 targetMask “Player” 레이어를 가진 오브젝트가 있다면 _target 배열에 담는다.
    • _target 배열 원소(플레이어)들에 Raycast 를 쏜다.
      • 이 과정을 또 해주는 이유 👉 1️⃣ 돼지의 시야각 내에 있는지를 한번 더 체크해 주어야 함. 2️⃣ 플레이어가 돼지의 viewDistance 반경 내에 있을 수도 있지만 돼지와 플레이어 사이의 장애물이 있을 수도 있기 때문이다. 반경 내에 있어도 돼지 👉 플레이어 Raycast 사이에 장애물이 충돌된다면 돼지는 플레이러를 못보는 것이므로 도망갈 이유가 없음
      • Raycast 쏠 준비
        • _direction : 돼지 👉 플레이어 방향
        • _angle : 돼지의 앞 방향으로부터 플레이어가 있는 방향까지의 사이각
      • 돼지와 플레이어의 사이각이 시야 각도의 절반 안에 있다면 플레이어는 돼지의 시야각 내에 있다는 것
        • Raycast를 쏜다.
        • 충돌한 오브젝트가 뭐 다른 장애물 같은게 아닌, 플레이어라면
          • 플레이어가 있는 방향으로 파란 디버그 선을 그리고
          • 플레이어의 반대 방향으로 도망간다.
            • thePig.Run(_hit.transform.position);
              • 이때문에 📜Pig.cs 의 Run 함수를 public 으로 바꿈

결론 : 돼지의 시야각 내에 플레이어가 있으며, 장애물이 가로막는 상태도 아니라면, 돼지는 플레이어의 반대 방향으로 도망간다.

image

플레이어의 레이어를 Player로 해주었다. 자식들에게 적용 X 오직 딱 플레이어 오브젝트 하나에만 레이어를 적용.

image

  • 돼지의 시야각 130도
  • 돼지의 시야 거리 10
  • 돼지의 시야에 Player 레이어를 가진 오브젝트가 걸릴 때만 반응하도록

image

  • Debug.DrawRay 로 그린 돼지의 시야각이다. 실제 게임에서는 보이지 않고 Scene 에서만 보인다.
    • 빨간선 왼쪽 : 돼지 위치로부터 _leftBoundary 까지를 그린 선
    • 빨간선 오른쪽 : 돼지 위치로부터 _righftBoundary 까지를 그린 선
    • 파란선 : 시야각 내에서 플레이러를 발견했을 때 플레이어를 향하는 선


자기 자신과 충돌하지 않게

플레이어 오브젝트의 레이어를 Ignore Layer로 부터 Player로 해주었으므로, 곡괭이, 도끼 같은 무기를 휘두는 과정에서 플레이어에 닿으면 충돌 처리가 일어나게 된다. 따라서 무기들의 Raycast 충돌 처리를 특정 레이어에 대해서만 하게 하고, 그 특정 레이어에서 Player는 제외해주도록 하면 된다.

📜CloseWeaponController.cs

    [SerializeField]
    protected LayerMask layerMask;

    protected bool CheckObject()
    {
        if (Physics.Raycast(transform.position, transform.forward, out hitInfo, currentCloseWeapon.range, layerMask))
        {
            return true;
        }
        return false;
    }

image

Player 레이어는 Raycast 충돌 처리에서 제외

image

📜GunController.cs

    [SerializeField]
    private LayerMask layerMask;

    private void Hit()
    {
        // 카메라 월드 좌표!! (localPosition이 아님)
        if (Physics.Raycast(theCam.transform.position, 
            theCam.transform.forward + 
                new Vector3(Random.Range(-theCrosshair.GetAccuracy() - currentGun.accuracy, -theCrosshair.GetAccuracy() + currentGun.accuracy),
                            Random.Range(-theCrosshair.GetAccuracy() - currentGun.accuracy, -theCrosshair.GetAccuracy() + currentGun.accuracy),
                            0), 
            out hitInfo, 
            currentGun.range,
            layerMask))
        {
            GameObject clone = Instantiate(hitEffectPrefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));
            Destroy(clone, 2f);
        }
    }

image

Player 레이어는 Raycast 충돌 처리에서 제외

Main Camera 의 Culling Mask 에서도 Player 레이어를 렌더링 하도록 추가하였다. 어차피 1인칭 게임이고 플레이어의 팔과 플레이어는 별개의 오브젝트기 때문에 플레이어를 렌더링 하지 않아도 되긴 하나, 플레이어가 렌더링이 아예 안되면 그림자도 보이지 않길래.. 플레이어를 렌더링 하도록 추가해주었다.



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Unity Lesson 3 카테고리 내 다른 글 보러가기

댓글 남기기