Home [Unity] 시야각을 이용한 적 탐지 기능 만들기
Post
Cancel

[Unity] 시야각을 이용한 적 탐지 기능 만들기

1
2
3
4
5
게임에서 시야각은 자주 쓰이는 내용이고 그래서 관련 자료도 많이 있습니다.
하지만 자료마다 만드는 법이 다 다르다보니 직접 만들어보면서 정리를 해보았습니다.

개인 공부임으로 틀린 점이 있을 수 있습니다. 이 점 참고 부탁드리며
댓글은 큰 도움이 되니 언제나 환영입니다 😀

서론

A가 플레이어이고, A는 세타 각도를 가진 시야각이 있으며 이 때 적(빨간 점)이 시야에 들어오는지 아닌지를 확인하는 걸 만들 예정이다.

Unity_img_21

Gizmos를 써서 시야각을 만들까 했는데 그것보다 개발 중에도 보이는 게 편할 것 같아 Editor 기능인

private void OnSceneGUI() 를 사용했다.

실제 게임에 바로 적용해 볼 수 있다 보다는 기본적인 기능 구현에 초점을 맞추었다.

(정리하는 글인데 실제 게임에 적용할 수 있게 하면 너무 길고 지저분하니까..!)

본론

일단 구현을 위해 오브젝트를 세팅해두자

Player를 담당할 Cube 객체와 Enemy를 담당할 Sphere 객체를 생성해주고

Player는 Vector3.zero에 Enemy는 Player의 전방에 존재할 수 있도록 세팅한다.

Unity_img_22

이 때 큐브의 전방이 어딘지 확실히 알아야 구현을 정확하게 할 수 있다.

그러므로 전방이 어딘지 확인할 수 있는 코드를 먼저 구현해보자.

1. 객체 전방은 어디..?

먼저 DetectRangeTest.cs 스크립트를 생성하고 Player 객체에 넣어주자

그리고 Editor 폴더를 생성한 후 DetectRangeEditor.cs 를 생성 그 다음 아래와 같이 코드를 넣어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEditor;

[CustomEditor(typeof(DetectRangeTest))]
public class DetectRangeEditor : Editor
{
    private void OnSceneGUI()
    {
		// target를 통해 해당 에디터가 참조하는 class를 가져온다.
		// 여기서는 DetectRangeTest class에 해당한다.
        DetectRangeTest player = (DetectRangeTest)target;

        Handles.color = Color.cyan;
				
		// 유니티 내 x,y,z 축 기즈모와 겹치게 됨으로 라인 두께를 5f 정도 주었다.
        Handles.DrawLine(player.transform.position, player.transform.forward * 2f, 5f);
    }
}

적용 후 Player 객체를 클릭하면 아래와 같이 cyan 색의 선이 생기는 걸 볼 수 있다.

Unity_img_23

선을 통해 Z축이 전방임을 알 수 있다.

이제 전방을 알았으니 시야각을 만들어보자!

2. 시야 범위 구현

표현이 좀 이상한데 Player를 원점으로 하면서 시야 거리가 반지름인 원을 그리는 거다.

이걸 만드는 이유는 Player의 시야 거리를 직관적으로 체크하기 위해서 이다.

그림으로 만들어보면 아래와 같다.

Unity_img_24

반지름 r은 Player의 시야 거리가 된다.

이제 저 원 안에 들어오면 player의 시야에 들어오게 되는 것이다.

이제 코드로 만들어보자.

코드로는 Handles.DrawWireArc 를 통해 만들 것이다.

1
2
3
4
5
6
7
8
9
10
11
private void OnSceneGUI()
{
    DetectRangeTest player = (DetectRangeTest)target;

    Handles.color = Color.cyan;
    Handles.DrawLine(player.transform.position, player.transform.forward * 2f, 5f);

	// 원은 컬러를 흰색으로 해주었다.
    Handles.color = Color.white;
    Handles.DrawWireArc(player.transform.position, player.transform.up, Vector3.forward, 360f, player._radius);
}

여기서 from 파라미터가 이해가 잘 안되었는데

공식 문서 예제를 보면

1
2
3
4
5
6
var center = position;
var start = Vector3.left;
var normal = Vector3.forward;
var radius = 3 - i * 0.3f;
var angle = 40 + 30 * i;
Handles.DrawWireArc(center, normal, start, angle, radius, i);

from 을 start라고 표현하고 있다. 즉, Arc(호)의 시작 부분이라 보면 될 것 같다.

normal은 법선 벡터를 의미한다.

그러므로, 아래 코드는 이렇게 해석할 수 있다.

1
2
3
4
5
6
7
8
/** 
* 호의 중심은 Player 위치이고 
* player.transform.up을 법선 벡터로 가졌으며
* player의 로컬 벡터 (0,0,1) 기준으로 
* _radius(DetectRangeTest class의 변수) 반지름을 가진 원을 그리는데
* Arc(호)는 원의 360도 부분이다.
*/
Handles.DrawWireArc(player.transform.position, player.transform.up, Vector3.forward, 360f, player._radius);

2-1. 시야각 만들기

1
2
3
4
5
위에서 말했듯이, 기능 구현에 초점을 맞추었기 때문에
Player가 Vector3.zero에 고정되어 있다고 가정하고 구현했습니다.

그래서 아래 코드 예시들은
Player 위치에 상관 없이 시야각은 Vector3.zero를 기준으로 고정되어 있습니다.

시야각을 위의 초록색 선처럼 만들 것이다.

저렇게 초록색 선을 만들기 위해서는 시야각에 따른 벡터 값이 필요한데 이를 구현해 볼 것이다.

2-2. 각도에 따른 벡터 값 찾기

구현을 위해 우리는 삼각 함수를 사용할 것이다.

평소 우리가 아는 공부했던 것처럼 좌표계를 그려보면

아래와 같이 X, Z 축을 가진 좌표계가 있고 빗변 길이가 r인 직각삼각형이 있을 때

노란색 점의 좌표 값은 (rcos(a),rsin(a)) 일 것이다.

Unity_img_25

하지만 우리는 Z축이 전방이기 때문에 아래와 같이 봐야 한다.

이렇게 되면 노란색 점의 좌표 값은 (rsin(a),rcos(a))가 될 것이다.

Unity_img_26

해당 내용을 코드로 작성해보면 아래와 같다.

반지름 r을 정규화해서 r = 1 로 만들어 준 상태이다. -> 단위 벡터를 의미한다.

1
2
3
4
5
6
7
8
// z축이 전방이 상태에서 angle 만큼 회전한 Vector3 값 반환
Vector3 AngleToDir(float angle)
{
    // UnityEngine.Mathf 의 Sin, Cos 에 들어가는 파라미터 값은 라디안이다.
    // 그러므로, 라디안으로 바꿔줘야 한다. 
    float rad = angle * Mathf.Deg2Rad;
    return new Vector3(Mathf.Sin(rad), 0, Mathf.Cos(rad));
}

2-3. 시야각 만들기

이제 시야각을 만들어 보자! 우리는 아래와 같이 시야각을 만들 예정이다.

Unity_img_27

자세히 보면 주의할 점이 하나 있는데 우리가 원하는 시야각은 a + a 이다.

즉, 시야각이 90도라고 하면 a 는 45도가 되어야 하는 것이다.

이 점을 유의하면서 코드를 작성해보자

1
2
3
4
5
6
7
8
9
10
Handles.color = Color.green;

// 시야각이 90도일 때, 45도
Vector3 rightDir = AngleToDir(player._angle * 0.5f);

// 시야각이 90도일 때, -45도
Vector3 leftDir = AngleToDir(player._angle * -1 * 0.5f);

Handles.DrawLine(Vector3.zero, rightDir * player._radius);
Handles.DrawLine(Vector3.zero, leftDir * player._radius);

2-4. 적 탐지하기

플레이어의 시야각은 전부 만들었다. 이제 시야각에 적이 들어왔을 때 적을 탐지하는 기능을 만들어보자

플레이어 시야에 적이 들어왔다는 걸 어떻게 알 수 있을까??

적이 시야에 들어왔을 때는 아래 이미지와 같을 것이다.

Unity_img_28

사각형이 Player 고 빨간 원이 Enemy 이다.

이미지를 통해 우리는 시야에 적이 들어왔다는 걸 알기 위해서 2가지 기능이 필요하다는 것을 알 수 있다.

  1. 플레이어와 적 간의 거리
  2. 플레이어와 적 간의 각도

1번부터 확인해보자.

2-4-1. 플레이어와 적 간의 거리

이건 아주 간단하다.

왜냐하면 이미 유니티에서 두 객체 간의 거리가 얼마인지 구할 수 있는 함수를 지원해주고 있기 때문이다.

지원해주고 있는 것들은 3가지가 있는데 아래와 같다.

  • Vector3.Distance(Vector3 a, Vector3 b) → 두 벡터 간 거리 반환
  • Vector3.Magnitude(Vector3 vector) → 벡터의 길이 반환
  • Vector3.SqrMagnitude(Vector3 vector) → 벡터 길이의 제곱을 반환

여기서 연산의 효율성을 위해 Vector3.SqrMagnitude(Vector3 vector) 를 사용할 것이다.

(x,y,z) 벡터의 크기는 sqrt(x^2+y^2+z^2) 이다. 여기서 루트는 연산 속도가 좋지 않기 때문에 제곱한 값인 x^2+y^2+z^2 로 처리하는 것이다.

이제 코드로 구현해보자.

1
2
3
4
5
6
7
8
9
Handles.color = Color.red;

// 연산을 빠르게 처리하기 위해 제곱된 값을 구한다.
float sqrDistance = Vector3.SqrMagnitude(player._enemy.transform.position - player.transform.position);

if(sqrDistance < player._radius * player._radius)
{
     Handles.DrawLine(player.transform.position, player._enemy.position, 5f);
}

결과는 아래와 같이 확인할 수 있다.

Unity_img_29

탐지 거리에 Enemy가 들어왔을 때

Unity_img_30

탐지 거리에 Enemy가 없을 때

2-4-2. 플레이어와 적 간의 각도

이제 각도를 확인해야 한다.

왜냐하면 각도가 없으면 시야각 안에 없어도 탐지가 되기 때문이다

Unity_img_31

시야각 외부에 있는데 탐지가 되고 있다.

여기서 우리는 2가지 방법을 할 수 있다.

아래와 같은 직각삼각형이 있을 때

Unity_img_32

  • tan(θ)=a/b 를 이용한 θ=arctan⁡(a/b)
  • cos(θ)=b/c 를 이용한 θ=arccos⁡(b/c)

우린 여기서 θ=arctan⁡(a/b) 를 사용할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
float GetAngle(Vector3 origin, Vector3 target)
{
    Vector3 direction = target.normalized - origin.normalized;

	/**
     * z가 전방이기 때문에
     * y 파라미터 : x
     * x 파라미터 : z
     * 값이 들어간다.
     */
    return Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;
}

이제 거리와 각도 코드를 합치면 아래와 같다.

1
2
3
4
5
if(sqrDistance < player._radius * player._radius &&
  GetAngle(player.transform.position, player._enemy.position) < player._angle * 0.5f)
{
      Handles.DrawLine(player.transform.position, player._enemy.position, 5f);
}

그 결과 시야 범위 내에 들어왔을 때 적을 탐지하는 것을 볼 수 있다.

Unity_img_33

Unity_img_34

전체 코드

전체 코드는 아래와 같다.

1
2
3
4
5
6
7
8
using UnityEngine;

public class DetectRangeTest : MonoBehaviour
{
    public int _radius;
    public Transform _enemy;
    public int _angle;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(DetectRangeTest))]
public class DetectRangeEditor : Editor
{
    private void OnSceneGUI()
    {
        // target를 통해 해당 에디터가 참조하는 class를 가져온다.
        // 여기서는 DetectRangeTest class에 해당한다.
        DetectRangeTest player = (DetectRangeTest)target;

        Handles.color = Color.cyan;
        Handles.DrawLine(player.transform.position, player.transform.forward * 2f, 5f);

        // 원은 컬러를 흰색으로 해주었다.
        Handles.color = Color.white;
        Handles.DrawWireArc(player.transform.position, player.transform.up, Vector3.forward, 360f, player._radius);

        Handles.color = Color.green;

        // 시야각이 90도일 때, 45도
        Vector3 rightDir = AngleToDir(player._angle * 0.5f);

        // 시야각이 90도일 때, -45도
        Vector3 leftDir = AngleToDir(player._angle * -1 * 0.5f);

        Handles.DrawLine(Vector3.zero, rightDir * player._radius);
        Handles.DrawLine(Vector3.zero, leftDir * player._radius);

        Handles.color = Color.red;

        // 연산을 빠르게 처리하기 위해 제곱된 값을 구한다.
        float sqrDistance = Vector3.SqrMagnitude(player._enemy.transform.position - player.transform.position);

        if(sqrDistance < player._radius * player._radius &&
            GetAngle(player.transform.position, player._enemy.position) < player._angle * 0.5f)
        {
            Handles.DrawLine(player.transform.position, player._enemy.position, 5f);
        }
    }

    Vector3 AngleToDir(float angle)
    {
        // UnityEngine.Mathf 의 Sin, Cos 에 들어가는 파라미터 값은 라디안이다.
        // 그러므로, 라디안으로 바꿔줘야 한다. 
        float rad = angle * Mathf.Deg2Rad;
        return new Vector3(Mathf.Sin(rad), 0, Mathf.Cos(rad));
    }

    float GetAngle(Vector3 origin, Vector3 target)
    {
        Vector3 direction = target.normalized - origin.normalized;

        /**
         * z가 전방이기 때문에
         * y 파라미터 : x
         * x 파라미터 : z
         * 값이 들어간다.
         */
        return Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;
    }
}
This post is licensed under CC BY 4.0 by the author.