Home [Unity] 월드(World) 좌표 vs 스크린(Screen) 좌표 vs 뷰포트(viewPort) 좌표
Post
Cancel

[Unity] 월드(World) 좌표 vs 스크린(Screen) 좌표 vs 뷰포트(viewPort) 좌표

캐릭터가 데미지를 입거나 무언가를 먹었거나 상태 이상이 걸렸을 때 캐릭터 위에 관련 상태 문구를 띄워준다.

TIL-18

이때 캐릭터는 3D 객체로써 월드 좌표로 표현이 되어 있을 것이고 UI는 월드 좌표로 표현이 되지 않을 것이다.
캐릭터 위에 UI를 어떻게 표현해야 할까??

이때 필요한 것이 스크린 좌표 또는 뷰포트 좌표이다.
스크린 좌표, 뷰포트 좌표가 무엇이고 이걸 어떻게 활용할 수 있는지 예제 코드로도 확인해보자

1. 월드 좌표와 로컬 좌표

월드 좌표

월드 좌표란,
좌표로 표현할 수 있는 세계(월드)에서 어느 한 점을 원점(0,0,0)이라 보고 그 원점을 기준으로 x, y, z로 표현할 수 있는 위치(좌표)이다.
그러므로, 월드 내에 모든 객체는 월드 좌표를 가지고 있다.

TIL-19

그럼 유니티를 열어서 확인해보자.
유니티를 열어서 큐브를 생성해주고 큐브의 Position을 (2,0,0)으로 세팅해주자.
그럼 큐브는 원점 기준으로 x 축 +2 만큼 이동한 것을 볼 수 있다.

TIL-20

로컬 좌표

큐브 자식으로 Sphere를 넣어보자.
Sphere의 Position을 보면 (0,0,0) 임에도 불구하고 큐브와 동일한 위치에 있음을 볼 수 있다.

TIL-21

TIL-22

왜 이렇게 보이는 걸까?? 이유는 유니티 인스펙터에서 보이는 Position이 로컬 좌표계이기 때문이다.

TIL-23

월드 좌표에선 세계의 원점이 기준이었다면 로컬 좌표는 특정 객체를 기준으로 보는 좌표계이다.
Sphere에서는 Sphere의 부모인 큐브의 피벗이 좌표계의 원점이 되는 것이고 그렇기 때문에 Sphere의 Position이 (0,0,0) 일 때,
큐브와 동일한 위치에 존재하게 되는 것이다.

아까 위에서 큐브의 Position이 월드 좌표계처럼 움직였는데 이건 뭘까??
답은 간단하다.

큐브 또한 월드라는 세계의 자식이기 때문에 월드의 로컬 좌표계에 따라 움직인 것이다.
월드의 피벗(기준)은 (0,0,0)이고 그렇기 때문에 월드 좌표처럼 기능하는 것이라 볼 수 있다.

큐브를 클릭했을 때 나오는 기즈모 좌표계는 큐브를 회전, 이동할 때 큐브의 피벗을 기준으로 쫓아오게 되는데
이것으로 기즈모 좌표계 또한 로컬 좌표계인 것을 알 수 있다.

TIL-24

2. 스크린 좌표

유니티에는 스크린 공간(screen space)이라는 게 있고
그 공간의 좌표계가 스크린 좌표계이다.

스크린 좌표계는 화면 왼쪽 아래 구석을 원점으로 하고 화면 해상도에 따른 픽셀 위치를 단위로 하는 2D 직교 좌표계이다.
즉, 화면 해상도가 1920x1080이라 할 때
왼쪽 하단 구석은 (0,0) 우측 상단 구석은 (1920,1080) 이 된다.

TIL-25

이걸로 우리는 3D 세계에 있는 특정 객체의 위치를 스크린 좌표계로 가져와 그 위치에 UI 표시할 수 있는 것이다.

Camera 클래스에는 WorldToScreenPoint 메서드가 있다
이 메서드는 월드 좌표를 스크린 좌표로 바꿔주는 메서드이다

WorldToScreenPoint 메서드가 Camera 클래스에 있는 이유는 월드에 카메라가 여러 대 있을 때 특정 위치에 있는 객체를 여러 대의 카메라로 볼 때 카메라마다 객체가 화면(스크린)에 그려지는 위치가 다르기 때문이다

그러므로,
카메라마다 객체의 스크린 좌표 값은 다를 것이고 그렇기 때문에 Camera 클래스에 WorldToScreenPoint 메서드가 있는 것이다.

3. 뷰포트 좌표

뷰포트 좌표는 스크린 좌표를 정규화한 좌표계이다

정규화는 특정 값의 범위를 최솟값이 0 최댓값이 1 인 범위로 변환해주는 것이다.

그래서 왼쪽 하단 구석은 (0,0) 오른쪽 상단 구석은 (1,1)로 되어 있다.

TIL-26

뷰포트 좌표계는 정규화 때문에 화면 해상도에 상관없이 동일한 좌표를 얻을 수 있다.
그러므로, 뷰포트 좌표계는 화면 안에 특정 객체가 있는지 없는지를 구분할 때 사용하면 좋다.

4. 예제

위에서 우리는 월드 좌표, 스크린 좌표, 뷰포트 좌표에 대해 알아보았다.

이제 스크린 좌표를 어떻게 활용할 수 있는지 예제를 통해 알아보자.

예제를 간단하게 설명하면 아래와 같다.
마우스 좌클릭을 할 때 적에게 데미지를 가한다.

적이 데미지를 입으면 적 위치에 데미지 텍스트 UI가 표시되는 걸 만들어볼 것이다.

메이플스토리, 로스트아크 등 많은 게임에서 볼 수 있는 데미지 표기 방법이다.

기본 세팅

적을 담당할 큐브를 (0,0,0)에 만들어주고 Canvas를 만들어준다.

Canvas의 Render Mode는 Screen Space - Overlay로 세팅해주고

데미지 텍스트 객체를 담아주는 부모 객체 damges와 텍스트 객체인 TXT_damage 객체를 만들어준다.

TXT_damage는 Instantiate 를 이용해서 데미지 텍스트 클론 객체를 생성할 때 쓸 객체이다.

TIL-27

시작하기 앞서 큐브를 움직이게 만들기 위해 아래와 같이 코드를 세팅해준다.

1
2
3
4
5
6
7
8
9
10
[SerializeField]
Transform _cube;
[SerializeField]
float _cubeSpeed = 10f;
    
 private void Update()
{
     _cube.transform.position += 
            Vector3.right * Input.GetAxis("Horizontal") * Time.deltaTime * _cubeSpeed;
}

스크린 좌표를 활용하여 텍스트 객체 생성

다시 한번 되짚어 보면 마우스 좌클릭을 할 때 적에게 데미지를 가할 것이다.

그리고 데미지 표시는 단순히 노출되는 것이 아니라 위로 올라가는 약간의 애니메이션이 포함될 것이다.

그러므로, 우리는 아래와 같이 코드를 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        StartCoroutine(ShowText());
    }

    _cube.transform.position += 
        Vector3.right * Input.GetAxis("Horizontal") * Time.deltaTime * _cubeSpeed;
}

// 위로 올라가는 애니메이션을 보여주기 위해 코루틴을 사용
IEnumerator ShowText()
{

}

스크린 좌표 구하기

스크린 좌표가 아닌 뷰포트 좌표로 구해도 된다.
현재 예제에서는 해상도 크기와 상관없이 구현할 수 있기 때문에
뷰포트 좌표를 쓰지 않고 스크린 좌표로 구현하였다.

이제 큐브 위치에 따른 스크린 좌표 값을  얻어보자

월드 포지션 값을 스크린 좌표 또는 뷰포트 좌표로 바꾸는 메서드는 Camera 클래스 안에 존재한다.

그러므로, 메인 카메라를 통해서 큐브의 월드 포지션 값을 스크린 좌표로 바꿀 것이다.

1
Camera.main.WorldToScreenPoint(_cube.position);

이를 통해 얻은 Vector3 포지션 값으로 텍스트 객체를 Instantiate 화하여  클론 객체를 생성해준다.

1
2
3
4
5
6
7
Text clone = 
    Instantiate(
    _textPrefab, // TXT_damage 객체
    Camera.main.WorldToScreenPoint(_cube.position), // 큐브 객체의 스크린 좌표 값
    Quaternion.identity, 
    _textParent // 클론 텍스트를 모아두는 damages 객체
    );

이로써 큐브의 월드 포지션 값을 통해 큐브의 스크린 좌표값을 얻어냈고 그 좌표값에 텍스트를 띄울 수 있게 되었다.

전체 코드

그 외 애니메이션과 데미지 표기 방식은 간단하게 구현할 수 있음으로 설명 없이 전체 코드를 통해 확인해보자.

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
public class ScreenSpaceTest : MonoBehaviour
{
    [SerializeField]
    Transform _cube;
    [SerializeField]
    float _cubeSpeed = 10f;

    [SerializeField]
    Transform _textParent;
    [SerializeField]
    Text _textPrefab;

    string[] _damages = new string[] { "100", "miss","125","200","50","miss"};

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            StartCoroutine(ShowText());
        }

        _cube.transform.position += 
            Vector3.right * Input.GetAxis("Horizontal") * Time.deltaTime * _cubeSpeed;
    }

    IEnumerator ShowText()
    {
        int randomIndex = Random.Range(0, _damages.Length);

        Text clone = 
            Instantiate(_textPrefab, 
            Camera.main.WorldToScreenPoint(_cube.position),
            Quaternion.identity, 
            _textParent);
            
        clone.gameObject.SetActive(true);

        clone.text = _damages[randomIndex];

        float duration = 0.0f;

        Vector3 startPos = clone.transform.position;
        Vector3 target = clone.transform.position + (Vector3.up * 500f);

        while(duration < 1f)
        {
            duration += Time.deltaTime;

            clone.transform.position = Vector3.Lerp(startPos, target, duration);

            yield return null;
        }

        Destroy(clone.gameObject);
    }
}
This post is licensed under CC BY 4.0 by the author.

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

[Unity Editor] GUIStyle를 이용하여 인스펙터 group box 만들기