소소한 나의 하루들

2d 종스크롤 슈팅(7) - 원근감있는 무한 배경 만들기 본문

개발/유니티

2d 종스크롤 슈팅(7) - 원근감있는 무한 배경 만들기

소소한 나의 하루 2024. 2. 21. 19:52

출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared

 

📚 유니티 기초 강좌

유니티 게임 개발을 배우고 싶은 분들을 위한 기초 강좌

www.youtube.com

이번부터는 좀 더 심화적으로 접근해본다. 끝없이 움직이는 무한 배경을 만들어본다.


#1. 준비하기

주어진 에셋의 배경 sprite 3개를 준비한다. (왜 3개인지는 나중에 알아본다) Pixels per Unit은 24, Filter Mode는 Point (no filter), Compression은 None으로 설정한다. 

Scene에 집어넣는데, 빈 오브젝트를 생성해서 그룹을 만들어서 넣는다. (Back A) 배경 A sprite는 A 그룹에, 배경 B sprite는 B 그룹에, 배경 C sprite는 C 그룹에 넣어준다. 그리고 스프라이트 Order in Layer로 순서를 조절해준다.

*지금까지 사용했던 배경은 지운다.

*설정한 배경이 모두 보이지 않는다면 카메라의 Size를 조절해본다.

 

그리고 각자 2개씩 더 복사해서 총 3개씩 sprite 오브젝트를 생성한다. (지금 Back A의 하위 Top1, Top2, Top3는 다 같은 sprite인 상황) 이제 1번대 하위 오브젝트의 y축은 모두 -12으로 값을 설정한다. : 한 블럭만큼 아래로 내려온다.

2번대 하위 오브젝트의 y축은 0으로, 3번대 하위 오브젝트의 y축은 12로 값을 설정한다. : 이제 배경 화면이 확실히 길어져보인다.

→카메라는 그대로이지만, 배경만 아래쪽으로 움직여 마치 플레이어가 움직이는 것처럼 눈속임을 하는 것이다.

public class Background : MonoBehaviour
{
    public float speed;
    
    void Update()
    {
        Vector3 curPos = transform.position;
        Vector3 nextPos = Vector3.down * speed * Time.deltaTime;
        transform.position = curPos + nextPos;
    }
}

배경 3개를 계속 아래로 움직이도록 하기위해 스크립트를 생성한다. (Background) 그리고 부모 오브젝트 Back A, B, C에 컴포넌트로 적용시킨다. 

speed 값을 주고 실행하면 배경이 아래로 내려가는 것을 확인할 수 있지만, 배경이 끝나면 배경이 끊어지고 Camera 색상이 나타나게 된다. 그래서 '무한 배경'으로 만들어야한다.

 

Camera View 높이 = Size x 2 [Size가 5라면, View 높이는 10칸이라는 의미]

그렇다면, 카메라 뷰가 y 12까지 간다면 더 이상 배경이 없어서 그 위로는 배경이 안보이게 되겠다. 그러면 이미 지나간 아래쪽의 배경도 필요가 없어지겠다. 필요가 없어진 배경은 다시 재활용을 해야하기 때문에 다시 위쪽으로 올려서 재활용한다.

: 카메라에서 벗어난 배경은 다시 위쪽으로 붙여서 재활용

→카메라 밖에서 일어나는 작업이기 때문에 플레이어 입장에서는 정렬하는 작업이 보이지 않는다. 결국 플레이어는 무한으로 배경을 볼 수 있다.

이것이 스크롤링(Scrolling)이라는 기법이다.


#2. 스크롤링

public int startIndex;
public int endIndex;
public Transform[] sprites;

각자 그룹은 sprite를 3개씩 가지고 있고, 가장 아래쪽 지점을 End, 가장 위쪽 지점을 Start로 정할 것이다. 

Start

   ↑

 End

if (sprites[endIndex].position.y < -12)
{
    Vector3 backSpritePos = sprites[endIndex].localPosition;
    Vector3 frontSpritePos = sprites[startIndex].localPosition;

    sprites[endIndex].transform.localPosition = backSpritePos + Vector3.up * 12;
}

가장 아래쪽 sprite(End 배경)의 위치가 y축이 -12 미만이라면, 가장 아래쪽 배경(End 배경)을 맨 위쪽 sprite 바로 위로 올려야한다.

: 일정한 위치까지 내려온 배경만 위쪽으로 올려주기 위해서

*position은 월드(global) 좌표계 기준, localposition은 부모의 position을 기준으로 잡은 position

(만약 부모 오브젝트의 좌표가 (10, 10, 10) 자식 오브젝트의 좌표가 (15, 15, 10)이라면 월드 좌표 기준으로 부모 오브젝트는 (10, 10, 10), 자식 오브젝트는 (15, 15, 10)이지만 로컬 좌표는 부모 오브젝트는 (10,10, 10) 자식 오브젝트는 (5, 5, 0)이다.

그런데 여기서 12이라는 것은 현재 프로젝트의 카메라 Size가 6이라서 그런 것이지, 카메라 Size가 8이었으면 값을 16으로 주고, 배경 sprite도 y좌표를 16, 0, -16으로 주었어야했다.

float viewHeight;

private void Awake()
{
    viewHeight = Camera.main.orthographicSize * 2;
}

void Update()
{
    Vector3 curPos = transform.position;
    Vector3 nextPos = Vector3.down * speed * Time.deltaTime;
    transform.position = curPos + nextPos;
    
    if (sprites[endIndex].position.y < viewHeight * (-1))
    {
        Vector3 backSpritePos = sprites[endIndex].localPosition;
        Vector3 frontSpritePos = sprites[startIndex].localPosition;

        sprites[endIndex].transform.localPosition = frontSpritePos + Vector3.up * viewHeight;
    }
}

그래서 에디터에서 카메라 Size를 수정한다면 이와 연관된 스크립트 로직을 모두 수정하지 않도록 카메라 높이를 가져와서 사용하도록 한다.

Camera.main : Scene에서 메인으로 사용중인 카메라를 가져올 수 있다.

Camera.main.orthographicSize : orthographic 카메라 Size

* main 뒤에 Projection 타입을 붙히면 된다.

Camera.main.orthographicSize에 2를 곱하면 실제 view의 높이가 나온다. 그래서 앞에서 12로 입력했던 부분을 모두 viewHeight으로 바꾸면 된다.

: 카메라 설정에 따라서 유기적으로 로직에 반영이 될 것이다.

int startIndexSave = startIndex;
startIndex = endIndex;
//endIndex = --startIndexSave == -1 ? sprites.Length - 1 : --startIndexSave;
endIndex = startIndexSave - 1 == -1 ? sprites.Length - 1 : startIndexSave -1;

endIndex라는 의미는 가장 아래쪽의 배경 sprite를 말하는 것이다. 만약 이 spirte가 맨 위쪽으로 이동이 완료되면 endIndex와 startIndex를 다시 갱신해야한다. 

우선 이동하기 이전 상황에서 가장 위쪽의 sprite(startIndex)가 저장되고, 업데이트 후 가장 위쪽의 sprite는 이전에 가장 아래쪽 sprite(endIndex)가 될 것이다. 그리고 가장 아래쪽의 sprite는 가장 위쪽이었던 sprite보다 한칸 아래쪽(가운데였던) sprite가 된다.

증감 연산자를 사용할 때 후위연산자가 아닌, 전위연산자를 사용해야한다. 후위 연산자는 해당 라인이 끝나야 변수에 연산이 적용된다.

* 반드시 배열을 넘어가지 않도록 예외 처리를 해준다. : -1이면 끝점으로 오고, 아니라면 그대로 해주세요

(인덱스는 0부터 시작하므로 위치 업데이트 전 end 지점이 1 start 지점이 0일때 위치 업데이트 후 end 지점이 -1이 아니라 2가 되어야한다)

2    0    1

1 →2→0

0    1    2

실행하기 전에 endIndex, startIndex 값(번호)를 입력해주고, 위치에 맞게 배경 sprite를 적용시켜준다. 배열 이름에 들어갈 오브젝트를 다중선택해서 모두 드래그하면 자동할당된다.


#2. 패럴랙스

일단 무한 배경이기는 한데, 그렇다면 왜 Back A, Back B, Back C는 나눠놨을까?

이것은 원근감을 주기 위함이다.

이런 2d 그래픽도 스크롤링의 속도만 조절하면 실제로 멀리있는 것은 느리고 짧게 움직이고 가까이 있는 것은 빠르게 많이 움직이는 원근감을 살릴 수 있다. 이러한 기법을 패럴랙스(Parallax)라고 한다.

Parallax : 거리에 따른 상대적 속도를 활용한 기술

 

그런데 이미 에디터 설정은 끝났고, 플레이어가 볼 때 멀리있는 것은 느리게 가까이 있는 것은 빠르게 해주면 된다.

화면에서 볼 때 Back C→Back B →Back A 순으로 가까워진다. (Back A가 맨 위에 올라와있고, Back C가 가장 아래에 있음) 따라서 Back C는 가장 느리게, Back A는 가장 빠르게 움직이도록 해준다.

 

보통 패럴랙스 기법은 종스크롤보다는 횡스크롤 게임, 특히 플랫포머에 많이 사용된다.

Comments