게임 엔진/Unity

Unity 9일차 - 점프점프 게임-2(배경, 시점 이동, 인터랙션, 씬 전환, 버그픽스)

FakeZero 2022. 2. 3. 16:47

  이번 강좌가 2D 게임 마지막 강좌가 됩니다. 마지막까지 후딱 달리고 3D로 넘어가봅시다.


8. 배경

  오브젝트도 애니메이션도 전부 만들어졌으니 이제 남은 것은 배경과 목표지점, 클리어 화면만 만들면 게임이 완성됩니다.

  일단 기존에 있던 구름을 프리팹으로 만들어 줍시다. Hierarchy 창의 cloud 오브젝트를 에셋 창으로 끌어내리면 프리팹이 된다는 것을 기억하시리라 믿습니다. 만들어진 프리팹은 cloudPrefab이란 이름으로 해두고 원래 있던 cloud 오브젝트는 지웠습니다.

 

  자 이제 만들어진 프리팹을 이용해 구름을 배치하도록 합시다. 구름의 자리와 크기들은(크기는 Transform 컴포넌트의 Scale을 조정하면 조절할 수 있습니다.) 전부 적어두도록 하겠습니다. 참고로 오브젝트나 에셋을 클릭한 후 Ctrl+D를 누르면 복제가 됩니다. 이 기능을 이용해 Hierarchy창에 총 17개의 구름을 만들껍니다. 추가로 목표 지점도 설정합시다. flag 에셋을 끌고와서 0.9, 17.4, 0의 위치에 배치하도록 합시다.

 

  구름1:

    위치:(-1.55, -5.1, 0)

    스케일:(1.1, 1, 1)

  구름2:

    위치:(0, -5.1, 0)

    스케일:(1.1, 1, 1)

  구름3:

    위치:(1.55, -5.1, 0)

    스케일:(1.1, 1, 1)

  구름4:

    위치:(-1.6, -2.8, 0)

    스케일:(1, 1, 1)

  구름5:

    위치:(1.6, -2.1, 0)

    스케일:(1, 1, 1)

  구름6:

    위치:(-0.2, -0.7, 0)

    스케일:(1, 1, 1)

  구름7:

    위치:(-1.6, 1.8, 0)

    스케일:(1, 1, 1)

  구름8:

    위치:(1.4, 2.5, 0)

    스케일:(1, 1, 1)

  구름9:

    위치:(-0.1, 4.2, 0)

    스케일:(1, 1, 1)

  구름10:

    위치:(1.6, 6.4, 0)

    스케일:(1, 1, 1)

  구름11:

    위치:(-1.6, 7.7, 0)

    스케일:(1, 1, 1)

  구름12:

    위치:(1.1, 9, 0)

    스케일:(0.9, 1, 1)

  구름13:

    위치:(-1, 11, 0)

    스케일:(0.8, 1, 1)

  구름14:

    위치:(-1.6, 13, 0)

    스케일:(0.7, 1, 1)

  구름15:

    위치:(1.4, 13.2, 0)

    스케일:(1, 1, 1)

  구름16:

    위치:(-1.1, 15.5, 0)

    스케일:(0.6, 1, 1)

  구름17:

    위치:(1, 16.6, 0)

    스케일:(1, 1, 1)

  깃발:

    위치:(0.9, 17,4, 0)

    스케일:(1, 1, 1)

  완성하면 이렇게 됩니다. 구름은 전부 할 필요는 없습니다. 간단하게 하려면 조금만 만들어도 됩니다.

 

  오브젝트도 다 배치 했으니 다음은 배경입니다. background 에셋을 위치는 0, 11, 0으로 스케일은 2, 12, 1에 배치하도록 합시다. 그리고 Sprite Renderer 컴포넌트의 Order in Layer의 값을 -1로 바꿔줍시다.

  다만들면 이런 형태가 됩니다. 이제 게임을 실행해서 제대로 올라가 지는지 확인합시다.

 

  잘 작동하긴 합니다만 시점이 이동하지 않아서 기껏 만든 오브젝트들이 전혀 보이질 않습니다. 아무래도 플레이어에 맞춰서 카메라를 움직여야 겠습니다.


9. 시점 이동

  카메라도 엄연히 오브젝트입니다. 그러니 스크립트로 움직일 수 있습니다. CameraController이라는 이름으로 새로운 C# 스크립트를 만들어주고 아래의 코드를 넣어봅시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    GameObject player;

    void Start()
    {
        this.player = GameObject.Find("cat");    
    }

    void Update()
    {
        Vector3 playerPos = this.player.transform.position;
        transform.position = new Vector3(transform.position.x, playerPos.y, transform.position.z);
    }
}

  player이라는 변수에 cat 오브젝트를 할당하고 cat 오브젝트의 y좌표에 따라 카메라가 이동하도록 만들었습니다. 이제 만든 CameraController 스크립트를 Main Camera 오브젝트에 할당하도록 합시다.

 

  제대로 목표지점까지 이동할 수 있었습니다. 이제 목표지점에서 클리어 화면이 나오면 됩니다.


10. 인터랙션

  이제 플레이어 캐릭터와 목표지점간의 상호작용, 즉, 인터랙션을 만들어야 합니다.

 

  이전에 말했듯이 이제 충돌 판정을 콜라이더가 대신 해주기 때문에 스크립트로 충돌 판정 알고리즘을 짤 필요가 없습니다. 심지어 충돌할 때에 그대로 충돌해서 서로의 움직임에 영향을 줄지(콜리전(Collision)모드), 혹은 그대로 통과 할 지도(트리거(Trigger)모드) 정할 수 있습니다.

 

  콜라이더가 정상적으로 충돌 판정을 하려면 2가지 조건이 있습니다.

  1. 판정할 오브젝트 모두에 Collider 컴포넌트가 적용되어 있어야 한다.

  2. 충돌할 오브젝트 중 적어도 한쪽에는 Rigidbody 컴포넌트가 적용되어 있어야 한다.

 

  이미 cat 오브젝트에 Rigidbody가 적용되어 있으므로 우리는 깃발에 Collider 컴포넌트만 추가하면 됩니다.

  flag 오브젝트에 Box Collider 2D 컴포넌트를 추가하고 cat 오브젝트가 충돌한뒤 그대로 통과해야하므로 Is Trigger 항목을 활성화 해 줍시다. 이제 인터랙션을 프로그램 하기 위해 PlayerController 스크립트를 열고 다음과 같이 수정해봅시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rigid2D;
    Animator animator;
    float jumpForce = 680.0f;
    float walkForce = 30.0f;
    float maxWalkSpeed = 2.0f;

    void Start()
    {
        this.rigid2D = GetComponent<Rigidbody2D>();
        this.animator = GetComponent<Animator>();
    }

    void Update()
    {
        //점프한다.
        if(Input.GetKeyDown(KeyCode.Space))
        {
            this.animator.SetTrigger("JumpTrigger");
            this.rigid2D.AddForce(transform.up * this.jumpForce);
        }

        //좌우 이동
        int key = 0;
        if (Input.GetKey(KeyCode.RightArrow)) key = 1;
        if (Input.GetKey(KeyCode.LeftArrow)) key = -1;

        //플레이어 속도
        float speedx = Mathf.Abs(this.rigid2D.velocity.x);

        //스피드 제한, 외부힘 추가
        if(speedx < this.maxWalkSpeed)
        {
            this.rigid2D.AddForce(transform.right* key * this.walkForce);
        }

        //움직이는 방향에 따라 플레이어 이미지 반전
        if(key != 0)
        {
            transform.localScale = new Vector3(key, 1, 1);
        }

        //플레이어 속도에 맞춰 애니메이션 속도를 바꾼다.
        if(this.rigid2D.velocity.y == 0)
        {
            this.animator.speed = speedx / 2.0f;
        }
        else
        {
            this.animator.speed = 1.0f;
        }
    }
    //골 도착
    void OnTriggerEnter2D(Collider2D other)
    {
        Debug.Log("골");
    }
}

(59~63번 줄이 추가되었습니다.)

        //골 도착
        void OnTriggerEnter2D(Collider2D other)
        {
            Debug.Log("골");
        }

  콜라이더의 충돌 판정을 정할 때는 메서드를 추가해야합니다. 그 메서드 종류는 아래와 같습니다.

상태 Collision 모드 Trigger 모드
충돌한 순간 OnCollisionEnter2D OnTriggerEnter2D
충돌 중 OnCollisionStay2D OnTriggerStay2D
충돌이 끝난 순간 OnCollisionExit2D OnTriggerExit2D

(3D의 경우 2D를 뺀 메서드를 이용하게 됩니다.)

  본 게임에선 콜라이더끼리 닿자마자 클리어 화면이 나오면 되므로 OntriggerEnter2D를 썼습니다. Collider2D other은 다른 Collider 2D 컴포넌트가 들어가 있는 모든 오브젝트를 얘기하지만 이 메서드는 Trigger 모드인 Collider 2D 컴포넌트 만을 감지 하므로 이 코드가 반응하는 것은 flag 오브젝트 뿐입니다.

 

  그리고 메서드를 추가하는 것이기 때문에 Update 메서드 밖에 만들어야 합니다. 그렇지 않으면 작동 자체를 하지 않으니 주의하시길 바랍니다.

 

  제대로 작동하는 것을 확인했습니다. 콜라이더가 제대로 기동하는 것을 확인했으니 이제 씬을 바꾸도록 프로그램해서 게임을 클리어 했다는 것을 플레이어 알려주도록 하겠습니다.


11. 씬 전환

  유니티에서는 게임 화면을 묶어 씬(Scene) 형태로 관리합니다. 예를 들면 타이틀 화면, 메뉴 화면, 게임 화면, 게임 오버 화면등 다양한 화면을 씬으로 묶고 서로 전환하면서 게임을 구성합니다.

 

  이번 게임에서는 클리어 화면을 만들껍니다. 일단 현재의 씬을 저장합시다.

  이렇게 새로운 씬을 만들고 다시 한 번 도구 바의 File -> Save As를 이용해 에셋창에 ClearScene이라는 이름으로 새로 만든 씬을 저장하도록 합시다.

  참고로 씬 파일을 더블 클릭하면 해당 씬이 불러와집니다.

 

  이제 에셋 창에서 background_clear를 Scene 탭에 불러오도록 합시다. 위치는 0, 0, 0에 배치하도록 합시다.

  이미 에셋이 완성되어 있기 떄문에 이것만으로도 클리어 화면은 완성입니다. 이제 클리어 씬과 게임 씬을 왔다갔다 할 수 있는 감독 스크립트만 짜면 됩니다.

 

  하지만 스크립트를 짜기 전에 해야할 것이 있습니다. 유니티에 씬을 등록하는 것입니다. 지금까지는 씬을 전환하지 않아서 하지 않았지만 씬을 전환하기 위해서는 어떤 씬을 먼저 불러올 것인지 등록해야 합니다. 그렇지 않으면 유니티는 씬이 있다는 것을 받아들이지 못합니다.

 

  도구 바에서 File -> Build Settings를 클릭합시다.

  Build Settings에 씬을 추가한 뒤에는 Scenes/SampleScene(기본 생성 씬)을 비활성화하고 GameScene을 가장 위로 올려서 가장 먼저 나오는 씬이 되도록 합니다.(옆의 숫자가 순번입니다. 0번이 가장 처음 실행되는 씬이 됩니다.)

 

  이제 새로운 C# 스크립트를 만들어 ClearDirector란 이름으로 만들어 줍시다. 그리고 다음과 같이 코드를 짭시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; //LoadScene 메서드를 사용하는데 필요

public class ClearDirector : MonoBehaviour
{
    void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            SceneManager.LoadScene("GameScene");
        }
    }
}

  씬을 전환하는 메서드인 LoadScene 메서드를 사용하기 위해서는 SceneManagement 패키지가 필요합니다.

 

  클리어 화면에 클릭하면 다시 시작할 수 있다고 표기 되어있으니 마우스 왼클릭으로 게임 씬으로 돌아가게 해 두었습니다.

SceneManager.LoadScene("|등록한 씬 이름|");

  이 메서드를 통해 씬을 마음대로 전환할 수 있습니다.

 

  이제 Hierarchy 창에서 빈 오브젝트를 생성해서 ClearDirector이란 이름으로 만들고 ClearDirector 스크립트를 적용합시다.

  이제 게임을 실행해서 제대로 씬이 전환되는지 확인해봅시다.

 

  제대로 씬이 전환되는군요. 하지만 이러면 클리어 화면에서 게임 화면으로만 넘어가집니다. 클리어 화면으로 가는 장치는 만들지도 않았는데 말이죠. PlayerController 스크립트를 열어서 씬을 전환하는 코드를 넣어 줍시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; //LoadScene을 사용하는 데 필요하다.

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rigid2D;
    Animator animator;
    float jumpForce = 680.0f;
    float walkForce = 30.0f;
    float maxWalkSpeed = 2.0f;

    void Start()
    {
        Application.targetFrameRate = 60;
        this.rigid2D = GetComponent<Rigidbody2D>();
        this.animator = GetComponent<Animator>();
    }

    void Update()
    {
        //점프한다.
        if (Input.GetKeyDown(KeyCode.Space))
        {
            this.animator.SetTrigger("JumpTrigger");
            this.rigid2D.AddForce(transform.up * this.jumpForce);
        }

        //좌우 이동
        int key = 0;
        if (Input.GetKey(KeyCode.RightArrow)) key = 1;
        if (Input.GetKey(KeyCode.LeftArrow)) key = -1;

        //플레이어 속도
        float speedx = Mathf.Abs(this.rigid2D.velocity.x);

        //스피드 제한, 외부힘 추가
        if(speedx < this.maxWalkSpeed)
        {
            this.rigid2D.AddForce(transform.right* key * this.walkForce);
        }

        //움직이는 방향에 따라 플레이어 이미지 반전
        if(key != 0)
        {
            transform.localScale = new Vector3(key, 1, 1);
        }

        //플레이어 속도에 맞춰 애니메이션 속도를 바꾼다.
        if(this.rigid2D.velocity.y == 0)
        {
            this.animator.speed = speedx / 2.0f;
        }
        else
        {
            this.animator.speed = 1.0f;
        }   
    }
    //골 도착
    void OnTriggerEnter2D(Collider2D collider)
    {
        Debug.Log("골");
        SceneManager.LoadScene("ClearScene");
    }
}

(4, 16, 64번 줄이 추가 되었습니다.)

  수정하는 김에 프레임을 고정하는 코드도 같이 넣었습니다. 이제 제대로 전환이 되는지 확인해봅시다.

 

  분명 씬 전환은 잘 됬습니다. 그런데 2회차 플레이때 뭔가 잘못된 점을 2가지 발견했습니다.

  1. 점프가 무한대로 가능하다. 심지어 점프 도중에도 점프가 가능하다.

  2. 플레이어가 화면밖으로 나가도 계속 떨어진다.

 

  큰일이 났습니다. 프로그래머 최대의 적, 버그(Bug)입니다.


12. 버그 픽스(Bug Fix)

  이번 게임 같은 경우 규모가 작아서 버그가 생기면 원인을 빠르게 찾을 수 있지만 대규모 게임이 될 경우 얘기가 달라집니다. 사용하는 스크립트, 메서드, 클래스, 컴포넌트의 양의 수준이 다릅니다.

 

  버그를 찾기 힘든 이유 중 가장 큰 이유는 '버그는 프로그램 오류가 아니기' 때문입니다. 버그로 일어나는 현상 또한 프로그램이 제대로 연산한 결과입니다. 프로그램의 문법적 오류가 아닌 알고리즘의 오류이기 때문에 IDE에서는 오류 메세지가 대부분 나오지 않으며, 보통 찾기 위해서는 문제가 될 것으로 예상되는 모든 코드와 알고리즘을 검사해야합니다.

 

  때문에 버그를 잡기 위해서는 지금까지 잘 작동되던 프로그램을 다시 뜯어서 고쳐야할 경우도 있고 아예 새로운 코드를 짜야할 때도 있습니다. 프로그래머인 이상 버그 픽스는 당연하게 겪게 되는 현상들입니다. 그러니 프로그래머들은 예상치 못한 현상, 버그를 고치기 위해 다양한 상황에 따른 다양한 방법을 연구해야합니다.

 

  그럼 이제 저희도 버그를 한 개씩 보며 해결방법을 생각해봅시다.

 

  1. 점프가 무한대로 가능하다. 심지어 점프 도중에도 점프가 가능하다.

  이 문제는 PlayerController 스크립트에서 점프의 조건을 스페이스 바를 누르는 것 만으로만 설정했기에 일어나는 버그입니다. 점프 도중이나 떨어지는 도중에 점프를 할 수 없도록 저번 시간에 점프 여부에 따라 애니메이션 속도를 바꿨던 것 처럼 y좌표의 속도가 0일때만 점프를 하도록 바꾸면 될 것 같습니다. PlayerController 스크립트를 다음과 같이 바꿉니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; //LoadScene을 사용하는 데 필요하다.

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rigid2D;
    Animator animator;
    float jumpForce = 680.0f;
    float walkForce = 30.0f;
    float maxWalkSpeed = 2.0f;

    void Start()
    {
        Application.targetFrameRate = 60;
        this.rigid2D = GetComponent<Rigidbody2D>();
        this.animator = GetComponent<Animator>();
    }

    void Update()
    {
        //점프한다.
        if (Input.GetKeyDown(KeyCode.Space) && this.rigid2D.velocity.y == 0)
        {
            this.animator.SetTrigger("JumpTrigger");
            this.rigid2D.AddForce(transform.up * this.jumpForce);
        }

        //좌우 이동
        int key = 0;
        if (Input.GetKey(KeyCode.RightArrow)) key = 1;
        if (Input.GetKey(KeyCode.LeftArrow)) key = -1;

        //플레이어 속도
        float speedx = Mathf.Abs(this.rigid2D.velocity.x);

        //스피드 제한, 외부힘 추가
        if(speedx < this.maxWalkSpeed)
        {
            this.rigid2D.AddForce(transform.right* key * this.walkForce);
        }

        //움직이는 방향에 따라 플레이어 이미지 반전
        if(key != 0)
        {
            transform.localScale = new Vector3(key, 1, 1);
        }

        //플레이어 속도에 맞춰 애니메이션 속도를 바꾼다.
        if(this.rigid2D.velocity.y == 0)
        {
            this.animator.speed = speedx / 2.0f;
        }
        else
        {
            this.animator.speed = 1.0f;
        }   
    }
    //골 도착
    void OnTriggerEnter2D(Collider2D collider)
    {
        Debug.Log("골");
        SceneManager.LoadScene("ClearScene");
    }
}

(24번 줄이 추가되었습니다.)

  점프 알고리즘에 조건을 추가하는 것으로 1번 버그는 해결되었습니다.

 

  2. 플레이어가 화면밖으로 나가도 계속 떨어진다.

  이와 관련된 코드는 애초에 관련된 코드 자체가 없으니 새로 작성해 주도록 합시다. 플레이어의 위치가 y좌표 -10 밖으로 나가면 게임 씬을 다시 불러와서 처음부터 다시 시작하도록 하겠습니다. PlayerController 스크립트를 다음과 같이 수정하겠습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; //LoadScene을 사용하는 데 필요하다.

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rigid2D;
    Animator animator;
    float jumpForce = 680.0f;
    float walkForce = 30.0f;
    float maxWalkSpeed = 2.0f;

    void Start()
    {
        Application.targetFrameRate = 60;
        this.rigid2D = GetComponent<Rigidbody2D>();
        this.animator = GetComponent<Animator>();
    }

    void Update()
    {
        //점프한다.
        if (Input.GetKeyDown(KeyCode.Space) && this.rigid2D.velocity.y == 0)
        {
            this.animator.SetTrigger("JumpTrigger");
            this.rigid2D.AddForce(transform.up * this.jumpForce);
        }

        //좌우 이동
        int key = 0;
        if (Input.GetKey(KeyCode.RightArrow)) key = 1;
        if (Input.GetKey(KeyCode.LeftArrow)) key = -1;

        //플레이어 속도
        float speedx = Mathf.Abs(this.rigid2D.velocity.x);

        //스피드 제한, 외부힘 추가
        if(speedx < this.maxWalkSpeed)
        {
            this.rigid2D.AddForce(transform.right* key * this.walkForce);
        }

        //움직이는 방향에 따라 플레이어 이미지 반전
        if(key != 0)
        {
            transform.localScale = new Vector3(key, 1, 1);
        }

        //플레이어 속도에 맞춰 애니메이션 속도를 바꾼다.
        if(this.rigid2D.velocity.y == 0)
        {
            this.animator.speed = speedx / 2.0f;
        }
        else
        {
            this.animator.speed = 1.0f;
        }

        //플레이어가 화면 밖으로 나갔다면 처음부터
        if(transform.position.y < -10)
        {
            SceneManager.LoadScene("GameScene");
        }
    }
    //골 도착
    void OnTriggerEnter2D(Collider2D collider)
    {
        Debug.Log("골");
        SceneManager.LoadScene("ClearScene");
    }
}

(60~64번 줄이 추가되었습니다.)

 

  자, 이제 모든 과정이 끝났습니다. 마지막으로 한 번 게임이 버그없이 제대로 작동하는지 확인해봅시다.

 

  이제야 모든 것이 제대로 작동하는군요!


13. 빌드하기

  이제 게임을 빌드하고 정상적으로 작동되는지 확인해봅시다.

 

  이렇게 해서 또 하나의 게임이 만들어 졌습니다!


  이번 강좌를 마지막으로 2D 게임 강좌는 끝났습니다...만 이전에도 말했듯이 아직 많은 기능들이 Unity에는 남아있습니다. 남은 요소들은 여러분들의 필요에 따라 직접 찾아 보시고 적용해보시기 바랍니다.

 

  긴 강좌 읽고 따라하시느라 정말 수고하셨습니다. 다음엔 3D로 찾아뵙겠습니다!