Development/Unity Engine

[Retro유니티] 배경 구현, 배경스크롤링 구현 및 Deadzone 구현

사이바 미도리 2024. 11. 23. 18:29

목표

- 움직이는 배경 구현

- 움직이는 발판 구현

- 낙사존 구현

 

Player 캐릭터는 계속 뛰는 것처럼 보이지만, 실제 X축 위치는 고정시킬 것이다.

대신, 배경을 움직이게 만들어서 뛰는것처럼 보이게 만들 것이다.

 

배경추가

- 먼저 게임 배경하늘을 추가하자.

 

Main Camera의 Background 컬러색을 Sky 스프라이트에 어울리게 변경하자.

 

이 상태로 플레이를 하면, Player랑 Sky Sprite랑 앞뒤 서순이슈로 가려지는 문제가 있다.

Player가 항상 Sky보다 앞에 나오게 해야한다.

- Sprite Renderer Component가 그리는 Graphic의 앞뒤정렬은, Transform상의 Position과 독립적이다.

  - 앞뒤정렬 순서는 Sprite Renderer의 Sorting Layer가 결정한다.

 

Sorting Layer 추가

레이어를 추가하자.

최하단 레이어가 가장 앞에 노출된다.(클립스튜디오와 반대다)

이제, Background로 SortingLayer를 바꿔준다.

마찬가지로, Player는 Foreground로 바꿔준다. 발판은 Background로 바꿔준다.

 

Scrolling Object를 통한 움직이는 배경과 발판 구현

 

스크립트를 짜자.

using UnityEngine;

// 게임 오브젝트를 계속 왼쪽으로 움직이는 스크립트
public class ScrollingObject : MonoBehaviour {
    public float speed = 10f; // 이동 속도

    private void Update() {
        // 게임 오브젝트를 왼쪽으로 일정 속도로 평행 이동하는 처리
        transform.Translate(Vector3.left * speed * Time.deltaTime);
    }
}

 

이제 배경이 이동하는것을 확인할수있다.

 

는 낭떠러지로

 

낭떠러지로 다이브 치지 않기 위해서는,

움직이는 발판을 추가해야하고, 반복적으로 발판이 나타나게 해야한다.

 

일단 BackgroundLoop 스크립트도 Sky에 넣고, IsTrigger를 체크한다.

 

Awake함수는 Start처럼 초기 1회 자동실행되는 Unity Event 메서드이지만, 실행시점이 Start보다 1프레임 더 빠르다.

using UnityEngine;

// 왼쪽 끝으로 이동한 배경을 오른쪽 끝으로 재배치하는 스크립트
public class BackgroundLoop : MonoBehaviour {
    private float width; // 배경의 가로 길이

    private void Awake() {
        // 가로 길이를 측정하는 처리
        BoxCollider2D backgroundCollider = GetComponent<BoxCollider2D>();
        width = backgroundCollider.size.x;
    }

    private void Update() {
        // 현재 위치가 원점에서 왼쪽으로 width 이상 이동했을때 위치를 리셋
        if(transform.position.x <= -width)
        {
            Reposition();
        }
    }

    // 위치를 리셋하는 메서드
    private void Reposition() {
        Vector2 offset = new Vector2(width * 2f, 0);
        transform.position = (Vector2)transform.position + offset;
    }
}

 

이제 Sky(1) 을 추가로 만들고, 하나가 Camera에서 빠지면 다른 쪽을 이어붙이는 식으로 구현할 것이다.

(참고: Game Object 복제는 Hierarchy에서 Ctrl D이다.)

재생하면, sky의 반복재생을 확인할 수 있다. (저 앞에 찍었던 낭떠러지 dive 사진과 다르게, 구름이 계속 반복된다.)

 

Canvas가 다양한 해상도에 대응되게 만들기

- Canvas는 UI를 구성하는 틀이다.

  - 가로세로비율이 달라지면 캔버스에 배치된 UI의 모습도 달라진다.

1. Constant Pixel Size

-> 캔버스 크기가 변해도, UI의 절대위치가 변하지 않는다.

따라서, 화면 크기가 달라지면 UI 요소 크기 및 간격이 의도와 다르게 넓어지거나 좁아지는 문제가 생긴다.

4K 모니터에서 플레이할 시, 캐릭터가 너무 작게 보이는 문제가 발생할 수 있다.

 

2. Scale with Screen Size

- 화면크기와 비율에 따라 동적으로 UI의 크기와 배치를 조절한다.

- 기준 화면크기를 정하고, 실행화면이 기준화면보다 크냐 작냐에 따라 자동으로 Scale up / Scale down 한다.

- 만약 기준화면과 실행화면의 화면 비율이 다르다면, 그 비율에 맞춰서 해당 축에 가중치를 두어 자동으로 Scale up / Scale down 한다.

  - 이 때는, Canvas Scaler Component의 Match 필드값이 높은 방향의 길이를 유지하고, 다른 방향의 길이를 조절한다.

예를 들어

기준해상도: 640 * 360

실행해상도: 1280 * 900

UI크기(기준해상도 기준): 600 * 100

가로Match 1.0

세로Match 0.0

이라면,

이라면, 기준/실행해상도 비중을 가로비율에 맞추게 되어(640*360 -> 640*450 을 1280 * 900 짜리 화면에 넣어야 하므로)

600*100짜리 버튼은 1200 * 200 이 된다.

 

만약 반대로,

가로Match 0.0

세로Match 1.0

이라면, 기준/실행해상도 비중을 세로비율에 맞추게 되어(640*360 -> 512*360 을 1280 * 900 짜리 화면에 넣어야 하므로)

600*100짜리 버튼은 1500 * 250 이 된다.(100/6 * 15 === 250)

 

UI요소가 많이 나열된 방향의 일치값을 크게 주는게 좋다.

예를 들어, 세로방향으로 버튼이 많이 나열되어있다면,

화면비율이 변했을 때 세로비율 UI가 망가지기 쉽다는 것을 의미한다.

이 경우, 세로일치값을 높여서 세로변동률을 줄이게끔 해야 한다.

 

아무튼, 기준해상도를 잡자.

 

 

640*360으로 Reference Resolution 을 잡고 Scale with screen size 로 스케일모드를 바꿨다.

이제, 실제 화면크기를 신경쓰지 않고 UI 배치만 신경쓰면 된다.

 

점수 UI 만들기

 

 

Canvas Render모드 변경: ScreenSpace - Camera

Canvas 위치가 안맞아서, Camera 옵션을 사용하기로 했다.

이 방법 찾는데 삽질 좀 했다... 별것도 아니지만 아예 모르니 어쩔수없지.

 

일단 이렇게 되었다.

Restart 텍스트는 Gameover 텍스트 자식으로 넣고, Gameover 텍스트는 비활성화한다.

 

 

Game Manager 만들기

- 용도: UI와 Player의 상태를 참조하여, 게임의 전반적 상태를 관리

  - 점수저장

  - 게임오버 상태 표현

  - 플레이어의 사망을 감지하여 Game Over 텍스트 띄우기

  - 점수에 따라 점수UI 텍스트 갱신

  - 게임오버시 게임오버 UI 활성화

 

- 게임매니저/플레이어매니저/몬스터매니저/점수매니저 등은 일반적으로 한 프로그램에 하나만 존재해야 한다.

  - "단 하나만 존재해야하는 오브젝트가 어느 곳에서도 쉽게 접근가능해야 할 때", 싱글턴패턴을 사용한다.

    - 장점: 단일 오브젝트를 통한 손쉬운 접근과 관리.

  - 어떤 멤버변수를 static으로 선언하면, 여러 인스턴스가 해당 static 멤버변수를 공유한다.

    - 이 멤버변수를 cnt로 삼고, 특정 cnt 에 다다르면 자기자신을 리턴하게 해서, new 호출을 막을 수 있다.

 

 

public class GameManager : MonoBehaviour {
    public static GameManager instance;

 

자기자신을 static으로써 멤버변수로 저장할 수 있다.

이걸로 singleton 패턴을 구현한다.

 

using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement; // 게임 재시작은 현재 활성화된 씬을 다시 로드하는 방식으로 이루어지므로, SceneManagement가 필요하다.
using UnityEngine.UI;

// 게임 오버 상태를 표현하고, 게임 점수와 UI를 관리하는 게임 매니저
// 씬에는 단 하나의 게임 매니저만 존재할 수 있다.
public class GameManager : MonoBehaviour {
    public static GameManager instance; // 싱글톤을 할당할 전역 변수

    public bool isGameover = false; // 게임 오버 상태
    public TextMeshProUGUI scoreText; // 점수를 출력할 UI 텍스트
    public GameObject gameoverUI; // 게임 오버시 활성화 할 UI 게임 오브젝트

    private int score = 0; // 게임 점수

    // 게임 시작과 동시에 싱글톤을 구성
    void Awake() {
        // 싱글톤 변수 instance가 비어있는가?
        if (instance == null)
        {
            // instance가 비어있다면(null) 그곳에 자기 자신을 할당
            instance = this;
        }
        else
        {
            // instance에 이미 다른 GameManager 오브젝트가 할당되어 있는 경우

            // 씬에 두개 이상의 GameManager 오브젝트가 존재한다는 의미.
            // 싱글톤 오브젝트는 하나만 존재해야 하므로 자신의 게임 오브젝트를 파괴
            Debug.LogWarning("씬에 두개 이상의 게임 매니저가 존재합니다!");
            Destroy(gameObject);
        }
    }

    void Update() {
        // 게임 오버 상태에서 게임을 재시작할 수 있게 하는 처리
        if(isGameover && Input.GetMouseButtonDown(0))
        {
            // GameOver상태에서, 마우스 왼쪽 버튼을 클릭하면 현재 씬을 다시 로드
            SceneManager.LoadScene(SceneManager.GetActiveScene().name);
        }
    }

    // 점수를 증가시키는 메서드
    public void AddScore(int newScore) {
        if(!isGameover)
        {
            score += newScore;
            scoreText.text = "Score : " + score;
        }
    }

    // 플레이어 캐릭터가 사망시 게임 오버를 실행하는 메서드
    public void OnPlayerDead() {
        isGameover = true;
        gameoverUI.SetActive(true);
    }
}

 

 

그리고, PlayerController 스크립트를 수정해야한다.

Player 사망시, GameManager의 OnPlayerDead()를 실행해야 하기 때문이다.

private void Die() {
    // 사망 처리
    animator.SetTrigger("Die");
    playerAudio.clip = deathClip;
    playerAudio.Play(); // 사망효과음 재생
    playerRigidbody.linearVelocity = Vector2.zero; // 속도를 제로(0, 0)로 만듦
    isDead = true;
    GameManager.instance.OnPlayerDead(); // 게임 매니저의 플레이어 사망 처리 실행
}

막줄이 추가되었다.

 

또한, ScrollingObject 스크립트도 바꿔야한다.

GameManager의 isGameOver를 검사해서, 움직임 여부를 결정하는 로직을 넣어야 한다.

// 게임 오브젝트를 계속 왼쪽으로 움직이는 스크립트
public class ScrollingObject : MonoBehaviour {
    public float speed = 10f; // 이동 속도

    private void Update() {
        if(!GameManager.instance.isGameover)
        {
            // 게임 오브젝트를 왼쪽으로 일정 속도로 평행 이동하는 처리
            transform.Translate(Vector3.left * speed * Time.deltaTime);
        }
    }
}

 

 

Deadzone 설정

구현법에는 2가지가 있는데

1. Player의 Update에 아래 구문을 추가하여, 매 y축 값을 검사하여 특정값 이하면 Die 호출

2. Deadzone을 만들고, Deadzone과 충돌하면 Die함수 호출

 

누가봐도 2번이 깔끔하다.

1번으로 짜면, 자유자재로 Deadzone을 구성할 수 없을 뿐더러, 매번 y축값을 풀링해야 하여 성능상 불이익이 있을 것이다.

일단 1번으로 짜게 될 경우의 PlayerController.Update()함수를 첨부한다.

다만 너무 쓰레기같으므로 이렇게 구현하지는 말자.

  private void Update() {
      // 사용자 입력을 감지하고 점프하는 처리
      if(isDead) return;
      if(Input.GetMouseButtonDown(0) && jumpCount < 2)
       {
           jumpCount++;
           playerRigidbody.velocity = Vector2.zero; // 현재 속도를 0으로 만들어 점프 직전 상태로 만듦
           playerRigidbody.AddForce(new Vector2(0, jumpForce));
           playerAudio.Play(); // 점프 사운드 재생
       }
      else if(Input.GetMouseButtonUp(0) && playerRigidbody.velocity.y > 0)
      {
          playerRigidbody.velocity = playerRigidbody.velocity * 0.5f;
      }
      animator.SetBool("Grounded", isGrounded);

      // 낙사 처리
      if(transform.position.y < fallThreshold && !isDead)
      {
          Die();
      }
  }

 

 

애초에 2번을 상정하고,

낙사를 정의할 때, "Dead 태그를 가진 오브젝트와 충돌할 시 OnTriggerEnter2D 함수 호출과 함께 Die 호출" 로 정의했는데,

우린 여태 정작 Deadzone을 정의하지 않았다.

 

낙사 구현이 추가되었다.

 

 

오늘 배운 것은 아래와 같다.

- 2D Sprite Game Object를 그리는 순서는, Sorting Layer로 조정한다.

- sky같은 배경은 스크립트 2개를 넣어서 구현할 수 있다. 단일 Game Object에 복수개의 C# 스크립트를 넣을 수 있다.

  - 기본적으로 속도에 따라 배경의 x축을 계속 빼는 스크립트를 만든다.

  - sky 인스턴스의 위치가 -width가 되는 순간, +width 로 변경하는 Reroll 스크립트도 넣는다.

  - isTrigger로 설정하는 이유는, Sky 오브젝트가 다른 Game Object를 물리적으로 밀어내지 못하게 하기 위해서이다.

  -> GPT: isTrigger가 체크된 객체는 물리적 충돌에서 벗어나게 됩니다. isTrigger가 체크된 콜라이더는 물리적인 충돌을 발생시키지 않지만, 다른 콜라이더와의 접촉을 감지하여 트리거 이벤트를 발생시킵니다.

- TextPro 같은 UI는 Canvas 위에 그릴 수 있으며, Canvas의 Scalemode는 다양한 크기의 화면에 대해 Canvas가 어떻게 대응될지를 결정한다.

- Canvas의 Renderer모드는 다양하게 있으며, Camera 모드를 사용하면 Camera에 맞춘 Position 설정이 가능하다.

- Static을 통해 싱글톤패턴을 구현할 수 있으며, GameManager 같은 단일객체에 알맞다.

- Deadzone을 설정하고 OnTriggerEnter2D 메서드를 오버라이딩해서, 낙사를 구현할수있다.