[Unity] 2D 카메라 범위 제한하기

세동네·2022년 1월 27일
17
post-thumbnail

2D 게임을 만들 때 카메라가 비추는 영역보다 맵 크기가 크면 맵을 돌아다니면서 서서히 맵이 모습을 드러낸다. 이때 맵의 가장자리로 플레이어가 이동하면 카메라가 구현되어 있는 맵을 넘어서 비추게 될 수도 있는데, 문제가 없는 경우도 있지만 이것을 제한해야 하는 경우도 있다.

왼쪽이 맵 외부까지 비추는 경우이고, 오른쪽은 맵 외부는 비추지 않도록 카메라의 영역을 제한한 경우이다.

이번 포스팅에선 카메라가 캐릭터를 추적하는 기능부터 맵 외부 영역을 비추지 않는 기능까지 2D 카메라 무빙에 대한 전반적인 내용을 다뤄보겠다.

· 카메라가 플레이어를 추적

우선 카메라가 플레이어를 추적하는 기능을 먼저 구현해보자. 유니티에서 사용하는 카메라는 유니티 내의 오브젝트이므로 카메라 오브젝트 자체의 위치는 World coordinate에 따라 결정된다.

즉, 카메라 오브젝트와 캐릭터 오브젝트는 같은 좌표계 상에 있으므로 아주 쉽게 플레이어를 추적하도록 할 수 있다.

// CameraController.cs

public class CameraController : MonoBehaviour
{
    Vector3 cameraPosition = new Vector3(0, 0, -10);
    
    ...
    
    void FixedUpdate()
    {
    	transform.position = playerTransform.position + cameraPosition;
    }
}

CameraController.cs는 카메라 오브젝트에 컴포넌트로 달려 있다. 따라서 카메라의 위치(transform.position)을 캐릭터의 위치(playerTransform.position)으로 변경해주기만 하면 된다. 어려울 것 없다. playerTransform은 Start 함수에서 미리 플레이어 오브젝트를 찾아 할당을 해준 상태이다.

이때 궁금증을 가질 것은 cameraPosition이라는 변수일 것이다. 이는 카메라의 깊이 설정을 위한 변수이다. 간단하게 2D 깊이 표현에 대해 알아보자.


  • 2D에서 오브젝트 깊이 설정

    2D 즉, 2차원는 입체가 아닌 평면이므로 두 개의 축으로 구성된다. 하지만 세 번째 축을 가상으로 사용할 필요가 있다. 게임을 시작하면 캐릭터가 화면에 놓여져 있을 것이고, 캐릭터가 활동하는 월드가 있다. 하지만 이러한 월드의 배경도 결국 캐릭터와 같은 하나의 이미지(스프라이트)이고, 캐릭터 이미지는 배경 이미지보다 앞에 있어야 한다는 조건이 있다.

    이때 사용되는 것이 세 번째 축이며, 2D 게임에서 Z축 좌표가 사용되는 이유이다.

    이러한 Z축 값을 깊이로 볼 수 있으며, 값이 작을수록 뒤에 있는 물체이다.

이렇게 스프라이트들의 깊이 설정이 되어 있다면 실제 화면에선 다음과 같이 보이는 것이다.

카메라가 비추는 방향에서 가장 가까운 초록색 사각형이 가장 위에, 그 다음이 파란색 원, 마지막으로 노란색 사각형이 표현되므로 이와 같이 이미지가 표현된다. 파란색 원 전체와 노란색 사각형 일부는 초록색 사각형에게 가려지는 위치에 있으므로 래스터화 되는 과정에서 가려진다.


앞서 말한 것처럼 2D에도 깊이 표현이 존재하고, 이를 Z 좌표를 변경해주는 것으로 해결할 수 있다.

값이 작을수록 뒤에 있는 물체이고, 카메라가 가장 뒤에 있어야 모든 오브젝트를 표현할 수 있을 것이다. 따라서 카메라 오브젝트는 가장 작은 Z값을 가져야 한다. 하지만,

transform.position = playerTransform.position;

라는 연산만으로 카메라의 위치를 이동시키는 것은 캐릭터의 Z값과 카메라의 Z값이 같아지는 연산이므로 카메라가 캐릭터와 같은 선상에 위치하게 되어 캐릭터의 스프라이트가 화면에 표현되지 않는다. 따라서

transform.position = playerTransform.position + cameraPosition;

이와 같이 카메라의 Z 좌표를 이동시켜줄 수 있는 변수를 만들어 추가로 Z값을 변경해주는 것이다. 이때 필자는 (0, 0, -10)으로 cameraPosition 변수값을 설정해주었다.

· 부드러운 카메라의 움직임

두 영상의 차이가 한 눈에 보이는가? 왼쪽 영상은 캐릭터의 위치를 카메라가 즉각적으로 따라가지만 오른쪽 영상은 카메라가 조금 늦게 캐릭터를 따라간다.

이러한 효과는 선형 보간(Linear interporlation) 기법을 이용한 것이다. 선형 보간을 이용하면 특정 오브젝트의 위치를 A에서 B로 옮길 때 A에서 B로 즉시 옮기는 것이 아니라 서서히 해당 위치로 이동하는 효과를 줄 수 있다. 이는 필수적인 효과는 아니지만, 화면 움직임이 좀 더 부드럽게 보이는 시각적 효과를 줄 수 있어 많은 게임에서 활용되는 효과이다.

이러한 부드러운 카메라의 움직임을 구현하기 위한 함수는 Vector3.Lerp()함수이다. 해당 함수는 다음과 같이 사용된다.


Vector3 Vector3.Lerp(Vector3 a, Vector3 b, float t);


Lerp 함수는

a+(ab)ta + (a - b) * t

라는 연산값을 반환하며, 함수 자체는 a 위치에서 b 위치로 t 비율만큼의 선형 보간을 의미한다. 이때 t는 0부터 1 사이의 값을 가진다.

즉, t0이라면 a, 1이라면 b를 반환하고 그 사이 값이라면 a부터 b까지 t값의 비율만큼을 반환한다. 즉, t0.5라면 a에서 출발하여 b까지 0.5만큼의 지점, 0.7이라면 a에서 출발하여 b까지 0.7만큼의 지점을 반환하는 것이다. 그림으로 보면 훨씬 이해가 쉽다.

따라서 t가 작을수록 천천히, 클수록 빠르게 b에 도달하게 된다. Lerp 함수도 다른 연산들처럼 Time.deltaTime를 자주 활용한다. tTime.deltaTime * cameraMoveSpeed와 같이 카메라 이동속도를 결정하는 변수를 정의하고 적당한 값을 넣어 보간 속도를 조절한다.

이러한 Lerp 함수를 활용한 카메라 이동 코드는 최종적으로 아래와 같다.

transform.position = Vector3.Lerp(transform.position, playerTransform.position + cameraPosition, 
                                  Time.deltaTime * cameraMoveSpeed);

· Unity Camera orthographic Size

다음으로 해야 할 것은 유니티 카메라에 대한 이해이다. 유니티에서 2D 카메라가 사용자에게 화면을 제공하는 원리를 알아야 한다.

유니티의 Camera size라는 개념에 대해 알고 넘어갈 필요가 있다. 2D에선 카메라가 직교 투영(Projection 필드가 Orthographic)이고, 이때 Size라는 필드의 존재를 확인할 수 있다. 유니티 에디터의 Scene view에서 카메라에 의해 형성된 회색 사각형을 관찰할 수 있는데, 이것이 실제 카메라가 플레이어에게 보여주는 영역을 표시한 것이며 Size 필드값에 따라 회색 사각형의 크기가 결정된다.

이러한 Size 필드는 카메라의 중앙에서 y축 끝지점까지의 거리를 의미한다. 즉, 카메라가 (0, 0)에 위치해있고, Camera의 Size 필드 값이 5라면, Transformposition.y5 ~ -5 사이에 존재하는 오브젝트들만 실제 게임 화면에서 관측할 수 있다.

이러한 오브젝트는 실제 게임에서 다음과 같이 관측할 수 있다.

이 내용을 바탕으로 나머지 구현 방식을 알아보자.

유니티의 Camera size라는 개념에 대해 알고 넘어갈 필요가 있다. 2D에선 카메라가 직교 투영 즉, Projection 필드가 Orthographic이고, 이때 Size라는 필드의 존재를 확인할 수 있다. 유니티 에디터의 Scene view에서 카메라에 의해 형성된 회색 사각형을 관찰할 수 있는데, 이것이 실제 카메라가 플레이어에게 보여주는 영역을 표시한 것이다.

이때 Size 필드는 카메라의 중앙에서 y축 끝지점까지의 거리이다. 즉, 카메라가 (0, 0)에 위치해있고, Camera의 Size 필드 값이 5라면, Transformposition.y5 ~ -5 사이에 존재하는 오브젝트들만 실제 게임 화면에서 관측할 수 있다.

이러한 오브젝트는 실제 게임에서 다음과 같이 관측할 수 있다.

이 내용을 바탕으로 카메라가 외부 배경을 비추지 않도록 설정해보자.

우선 카메라가 실제로 비추는 영역을 검정색 상자, 카메라가 비출 수 있는 영역을 빨간 상자라고 하자. 카메라는 플레이어의 위치에 따라 이동하면서 다양한 영역을 비추게 된다.

앞서 말한 것처럼 중앙에서부터 가로 변까지의 길이 즉, 높이/2의 값은 Camera 컴포넌트의 Size 필드로 구할 수 있다. 이를 이용하여 표현할 화면의 가로 및 세로 비율을 알면 너비/2의 값도 구할 수 있다. 이를 연산으로 표현한 것이 위 그림이고, 유니티 C# 스크립트로 표현하면 다음과 같다.

height = Camera.main.orthographicSize;
width = height * Screen.width / Screen.height;

이런 식으로 카메라가 비추는 영역의 가로, 세로 크기의 절반을 각각 widthheight 변수로 선언하고 저장해두었다. Screen.width, Screen.height 변수는 플레이 중인 게임의 실제 가로, 세로 크기의 픽셀 값을 반환해준다.

에디터 상에선 해당 탭을 클릭하여 설정한 해상도 값이 반환된다.

이렇게 카메라가 비추는 영역의 가로, 세로 길이를 알아냈으니 카메라가 얼만큼의 영역을 비출 것인지 그 영역 크기를 지정해주어야 한다. 이는 플레이어가 유동적으로 설정할 수 있게 하며, 간단하게 Vector2 타입의 mapSize라는 변수를 만들어 그 값을 저장해준다.

카메라 영역의 가로, 세로 크기도 전체 크기의 절반이므로 mapSize 변수에도 편의상 전체 맵 크기의 절반 값을 저장해준다.

mapSize.x는 카메라가 비출 수 있는 맵의 가로 길이, width는 카메라가 실제 비추고 있는 영역의 가로 길이라고 말하였다. 카메라가 실제로 이동할 수 있는 영역은 맵의 끝지점에서 width만큼 안쪽으로 떨어진 거리일 것이다. 그것보다 멀리 이동하게 되면 카메라가 빨간 박스를 넘어서 맵 바깥을 비추게 된다. 따라서 카메라가 가로로 이동할 수 있는 영역은 중앙에서부터 mapSize.x - width만큼 떨어진 영역으로 결정할 수 있다.

세로 영역도 같은 방식으로 우측은 mapSize.y - height만큼 떨어진 영역 안에서 움직일 수 있도록 결정할 수 있을 것이다. 이러한 내용을 바탕으로 카메라가 이동할 수 있는 구간을 다음과 같이 정의할 수 있다.

[(mapSize.xwidth),(mapSize.xwidth)][-(mapSize.x - width), (mapSize.x - width)]
[(mapSize.yheight),(mapSize.yheight)][-(mapSize.y - height), (mapSize.y - height)]

그리고 유니티에선 변수가 일정한 값을 벗어나지 못하도록 범위를 제한하는 Mathf.Clamp() 함수를 제공한다. 형태는 다음과 같다.


float Mathf.Lerp(float value, float min, float max);


value라는 변수를 [min, max]를 벗어나지 못하는 범위로 만들어주는 것이다. 결과적으로 카메라 위치를 위에서 말한 영역으로 제한한다면 다음과 같이 코드를 작성할 수 있다.

float lx = mapSize.x - width;
float clampX = Mathf.Clamp(transform.position.x, center.x - lx, center.x + lx);

float ly = mapSize.y - height;
float clampY = Mathf.Clamp(transform.position.y, center.y - ly, center.y + ly);

Clamp 함수를 이용하면 플레이어가 카메라의 최대 이동 범위를 벗어나 이동하더라도 플레이어를 억지로 추적하지 않고 정해진 범위 내만 비출 수 있게 된다.

이후 제한된 범위 안으로 카메라의 위치를 옮겨주면 기능 구현이 마무리된다.

transform.position = new Vector3(clampX, clampY, -10f);

· 정리

  1. 카메라의 플레이어 위치 추적을 Lerp 함수를 이용해 보간하여 부드럽게 만든다.
  2. 실제 카메라가 비추는 영역, 카메라가 비출 수 있는 영역을 설정하고 영역 내에서만 움직이도록 제한한다.
  3. 제한한 영역으로 카메라의 위치를 옮긴다.

로 전체 과정을 요약할 수 있으며, 스크립트 전체는 다음과 같이 작성할 수 있다.

using UnityEngine;

public class CameraController : MonoBehaviour
{
    [SerializeField]
    Transform playerTransform;
    [SerializeField]
    Vector3 cameraPosition;

    [SerializeField]
    Vector2 center;
    [SerializeField]
    Vector2 mapSize;

    [SerializeField]
    float cameraMoveSpeed;
    float height;
    float width;

    void Start()
    {
        playerTransform = GameObject.Find("Player").GetComponent<Transform>();

        height = Camera.main.orthographicSize;
        width = height * Screen.width / Screen.height;
    }

    void FixedUpdate()
    {
        LimitCameraArea();
    }

    void LimitCameraArea()
    {
        transform.position = Vector3.Lerp(transform.position, 
                                          playerTransform.position + cameraPosition, 
                                          Time.deltaTime * cameraMoveSpeed);
        float lx = mapSize.x - width;
        float clampX = Mathf.Clamp(transform.position.x, -lx + center.x, lx + center.x);

        float ly = mapSize.y - height;
        float clampY = Mathf.Clamp(transform.position.y, -ly + center.y, ly + center.y);

        transform.position = new Vector3(clampX, clampY, -10f);
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireCube(center, mapSize * 2);
    }
}

본문에서 설명하지 않은 OnDrawGizmos() 함수는 맵 영역을 Scene 창에 시각적으로 표현하기 위한 것으로,

위 화면에서 Scene 창의 탭 중 Gizmos를 활성화하면 스크립트의 OnDrawGizmos()가 호출되고, 중앙 지점으로부터 mapSize 크기의 빨간색 사각형 기즈모를 그리는 코드이다. 이때 mapSize는 가로, 세로 길이의 절반 만큼으로 선언해두었고, 기즈모를 그릴 땐 가로, 세로 변의 전체 길이를 요구하기 때문에 2를 곱해주었다.

변수들은 게임 상황에 맞게 위와 같이 값을 지정해주었다. 이때 Camera Position 필드 Y 값에 2가 저장돼있는 것은 필자가 사용한 스프라이트의 피벗이 중앙에 있지 않고 캐릭터의 발 아래에 위치해있어 카메라의 위치를 캐릭터의 눈 위치로 이동해주기 위한 Offset이며, 여러분이 사용하는 스프라이트와 사정에 따라 다른 필드들의 값을 적절하게 조절하여 사용할 수 있다.

3개의 댓글

comment-user-thumbnail
2022년 4월 20일

글 잘봤습니다 감사합니다. 제가 orthograpic 말고 perspective 카메라를 사용해서 2D게임을 제작중인데, 이럴 때는 width와 height값을 어떻게 구해야 하나요?

답글 달기
comment-user-thumbnail
2023년 8월 26일

혹시 center의 의미가 뭔가요??

답글 달기
comment-user-thumbnail
2024년 8월 1일

너무 자세해요. 감사합니다. ^^

답글 달기