본문 바로가기
게임 프로그래밍/게임개발 중급

게임개발 중급(17) - 2D 플랫포머 게임 만들기(총정리)

by jyppro 2023. 5. 6.

2D 플랫포머 게임 만들기(총정리)

 

여태까지 저희는 2D 플랫포머 게임 만들기를 진행하였고, 그 과정에서 다양한 스크립트와 오브젝트 등 많은 것들이 사용되었습니다. 그리고 중간에 이미 만든 것을 변형하기도 하였습니다. 저희가 만약 게임을 만드는 개발자가 아니라 플레이하는 유저라면, 저희가 만든 게임을 보고 만들기 어렵다고 생각하지 않을 것입니다. 하지만 직접 해보면 생각보다 많은 것들이 필요하고 어렵다는 것을 알 수 있습니다. 오늘은 최종적으로 만들어진 결과물을 정리하는 내용으로 마무리 하겠습니다.

 

하이어라키 창과 프로젝트 창

하이어라키-창과-프로젝트-창
제작에 사용된 오브젝트와 폴더들

 

오브젝트 폴더는 비어있습니다. 저는 유니티 내부에서 제공하는 기본 오브젝트를 하이어라키에 바로 생성하여 사용했기 때문입니다. 마찬가지로 Scenes 폴더에도 현재 사용하고 있는 SampleScene 하나만 들어가 있습니다.

 

Prefabs

프리팹들
프리팹 폴더

게임제작에서 사용된 프리팹 입니다. 코인은 아이템의 역할을 하고, 나머지 프리팹은 각자 다른 역할의 장애물입니다.

 

Scripts

사용된-스크립트들
스크립트 폴더

게임을 만드는 데 사용된 스크립트는 총 7개 입니다. 스크립트의 갯수는 개발자의 성향에 따라 다릅니다. 하나의 스크립트에 많은 기능을 압축해서 사용하는 경우도 있고, 여러 개의 스크립트로 분할하여 사용하는 경우도 있습니다. 하지만, 일반적으로 압축하여 사용하는 것보다는 여러 개로 나눠서 사용하는 것이 더 좋습니다. 그래야 관리가 쉽고, 문제가 발생했을 때, 큰 문제로 번지지 않고 처리할 수 있기 때문입니다.

 

이제부터는 최종적으로 완성된 스크립트의 내용에 대해 정리해 드리겠습니다.

 

PlayerController

using UnityEngine;
using UnityEngine.UI;

public class PlayerController : MonoBehaviour
{
    public float moveSpeed = 5f;
    public float jumpForce = 15f;
    public int scoreValue = 10;  // 코인 하나의 점수
    public int score = 0;  // 현재 점수
    public float boundaryX = 10f; // x축 경계
    public float boundaryY = 5f; // y축 경계
    public int life = 3;
    public Text PauseText;
    private Rigidbody2D rb;
    private bool isGrounded = false;
    private bool isPaused = false;
    GameObject ST;
    GameObject LT;

    void Start()
    {
        this.ST = GameObject.Find("ScoreText");
        this.LT = GameObject.Find("LifeText");
        rb = GetComponent<Rigidbody2D>();
    }

    void FixedUpdate()
    {
        float moveDirection = Input.GetAxis("Horizontal");
        rb.velocity = new Vector2(moveDirection * moveSpeed, rb.velocity.y);

        if (moveDirection > 0)
        {
            transform.localScale = new Vector3(1f, 1f, 1f);
        }
        else if (moveDirection < 0)
        {
            transform.localScale = new Vector3(-1f, 1f, 1f);
        }
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
        {
            rb.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse);
            isGrounded = false;
        }

        // 현재 위치
        Vector3 currentPosition = transform.position;

        // 경계를 벗어난 경우
        if (Mathf.Abs(currentPosition.x) > boundaryX || Mathf.Abs(currentPosition.y) > boundaryY)
        {
            // 원점으로 돌아가기
            transform.position = Vector3.zero;
        }

        if (Input.GetKeyDown(KeyCode.Escape))
        {
            if (isPaused)
            {
                Time.timeScale = 1f;
                isPaused = false;
                PauseText.gameObject.SetActive(false);
            }
            else
            {
                Time.timeScale = 0f;
                isPaused = true;
                
                // ESC 키 입력 시 Text UI를 활성화
                PauseText.gameObject.SetActive(true);
            }
        }
    }

    void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.CompareTag("Ground"))
        {
            isGrounded = true;
        }  
    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.CompareTag("Item"))
        {
            Destroy(other.gameObject); // 코인 제거
            score += scoreValue;  // 점수 증가
        }
        else if (other.gameObject.CompareTag("Obstacle"))
        {
            gameObject.SetActive(false); // 플레이어 죽음
            Invoke("RespawnPlayerAfterDelay", 3f);

            if(life <= 3)
            {
                life -= 1;
                this.LT.GetComponent<Text>().text = "Life: " + life.ToString();
            }
            
            if(life == 0)
            {
                this.LT.GetComponent<Text>().text = "Game Over";
                Destroy(gameObject);
                Time.timeScale = 0f;

            }
        }

        if(score >= 0)
        {
            this.ST.GetComponent<Text>().text = "Score: " + score.ToString();
        }  // UI Text에 점수 반영
    }

    private void RespawnPlayerAfterDelay()
    {
        gameObject.SetActive(true);
    }

}

플레이어의 움직임과 충돌처리 등 많은 일을 하는 PlayerController 입니다. 해당 스크립트에서 구현한 기능은

키 입력을 통한 플레이어 움직임(좌, 우, 점프), 바닥처리, 맵 이탈 방지를 위한 경계설정, 아이템과 장애물의 충돌처리, UI 컨트롤을 통한 점수 및 라이프 관리, ESC 키를 통한 일시정지, 플레이어 사망 및 리스폰 처리, 게임오버 처리가 있습니다.

 

CoinSpawner

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

public class CoinSpawner : MonoBehaviour
{
    public GameObject coinPrefab;
    
    void Start()
    {
        InvokeRepeating("SpawnCoin", 0f, 1f); // 1초마다 SpawnCoin 함수 실행
    }
    
    void SpawnCoin()
    {
        Vector2 spawnPosition = new Vector2(10f, 0f);
        
        Instantiate(coinPrefab, spawnPosition, Quaternion.identity);
    }
}

코인을 생성시키는 CoinSpawner 입니다. 정해진 위치에 프리팹으로 생성한 코인 오브젝트를 1초마다 생성시키는 역할을 합니다. 해당 코드의 작동은 하이어라키에 빈 오브젝트로 생성한 Generator에서 실행됩니다.

 

CoinMovement

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

public class CoinMovement : MonoBehaviour
{
    public float speed = 3f;
    private Rigidbody2D rb;

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        rb.velocity = new Vector2(-speed, 0);
    }

    void Update()
    {
        if(transform.position.x <= -10)
        {
            Destroy(gameObject);
        }
    }
}

생성된 코인이 왼쪽 방향으로 계속 움직이게 해주는 역할을 하는 CoinMovement 입니다. 주어진 속도만큼 X축의 음수만큼 이동시킵니다. 그리고 맵을 벗어나면 없어지도록 처리해 주었습니다.

 

ObstacleSpawner

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

public class ObstacleSpawner : MonoBehaviour
{
    public GameObject ObstaclePrefab;
    public GameObject FixedMovingObstaclePrefab;
    public GameObject RandomMovingObstaclePrefab;
    
    void Start()
    {
        InvokeRepeating("SpawnObstacle", 0f, 5f); // 5초마다 SpawnObstacle 함수 실행
        InvokeRepeating("SpawnFixedMovingObstacle", 0f, 8f); // 8초마다 실행
        InvokeRepeating("SpawnRandomMovingObstacle", 0f, 10f); // 10초마다 실행
    }
    
    void SpawnObstacle()
    {
        Vector2 spawnPosition = new Vector2(10f, -3f);
        
        Instantiate(ObstaclePrefab, spawnPosition, Quaternion.identity);
    }

    void SpawnFixedMovingObstacle()
    {
        Vector2 spawnPosition = new Vector2(10f, -3f);
        
        Instantiate(FixedMovingObstaclePrefab, spawnPosition, Quaternion.identity);
    }

    void SpawnRandomMovingObstacle()
    {
        Vector2 spawnPosition = new Vector2(10f, -3f);
        
        Instantiate(RandomMovingObstaclePrefab, spawnPosition, Quaternion.identity);
    }
}

장애물을 생성시켜주는 ObstacleSpawner 입니다. 현재 설정된 3개의 장애물을 전부 InvokeRepeating()을 통해 주어진 시간마다 생성하는 동작을 반복시켜 줍니다. 마찬가지로 Generator에서 실행됩니다.

 

ObstacleMovement

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

public class ObstacleMovement : MonoBehaviour
{
    public float speed = 5f;
    private Rigidbody2D rb;

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        rb.velocity = new Vector2(-speed, 0);
    }

    void Update()
    {
        if(transform.position.x <= -10)
        {
            Destroy(gameObject);
        }
    }
}

CoinMovement와 마찬가지로 장애물을 움직여 주는 스크립트 입니다. 속도만 다르게 정해주고 나머지는 같습니다. 이런 경우에는 굳이 두개로 나눠서 작성할 필요가 없지만, 예시로 보기 편하게 만들기 위해 분리해서 작성했습니다.

 

FixedMovingObstacle

using UnityEngine;

public class FixedMovingObstacle : MonoBehaviour
{
    public float speed = 5f;  // 장애물이 움직일 속도
    public float moveRange = 4f;

    private Vector2 moveDirection;  // 장애물이 움직일 방향

    void Start()
    {
        moveDirection = new Vector2(0f, 1f);
    }

    void Update()
    {
        // 장애물을 이동시킴
        transform.Translate(moveDirection * speed * Time.deltaTime);

        // 장애물이 움직일 범위를 벗어나면, 반대 방향으로 움직이도록 방향을 바꿈
        if (transform.position.y <= -moveRange || transform.position.y >= moveRange)
        {
            moveDirection *= -1f;
        }
    }
}

장애물이 고정된 움직임을 반복하도록 만들어주는 FixedMovingObstacle 입니다. 속도와 범위를 지정해주고, 방향을 설정합니다. 처음에 정한 방향을 향해 일정속도로 움직여주고, 범위의 끝에 다다르면, 반대 방향으로 움직이도록 바꿔줍니다.

 

RandomMovingObstacle

using UnityEngine;

public class RandomMovingObstacle : MonoBehaviour
{
    public float moveRange = 4f;  // 장애물이 움직일 범위
    public float moveTime = 2f;  // 장애물이 한 방향으로 움직일 시간
    private float speed;
    private Vector2 moveDirection;  // 장애물이 움직일 방향
    private float timer;  // 타이머

    void Start()
    {
        // 장애물이 처음에 움직일 방향을 랜덤하게 설정
        moveDirection = new Vector2(Random.Range(-1f, 1f), Random.Range(-1f, 1f)).normalized;
        speed = Random.Range(2f, 5f);
    }

    void Update()
    {
        // 타이머가 moveTime을 초과하면 새로운 방향으로 움직임을 설정
        if (timer > moveTime)
        {
            moveDirection = new Vector2(Random.Range(-1f, 1f), Random.Range(-1f, 1f)).normalized;
            timer = 0f;
        }

        // 장애물을 이동시킴
        transform.Translate(moveDirection * speed * Time.deltaTime);

        // 장애물이 움직일 범위를 벗어나면, 반대 방향으로 움직이도록 방향을 바꿈
        if (transform.position.x < -moveRange || transform.position.x > moveRange ||
            transform.position.y < -moveRange || transform.position.y > moveRange)
        {
            moveDirection *= -1f;
        }

        timer += Time.deltaTime;
    }
}

마지막으로, 장애물이 무작위로 움직이도록 만들어주는 RandomMovingObstacle 입니다. 범위, 움직이는 시간, 속도, 방향, 타이머까지 선언해주고, 방향과 속도를 범위 내에서 랜덤하게 정하여 움직이는 시간만큼 행동합니다. 그리고 시간이 지나면, 다시 랜덤하게 방향과 속도를 정하여 움직이는 것을 반복합니다. 마찬가지로 범위 끝에 다다르면 반대로 움직이도록 해줍니다.

 

게임 플레이

게임을 실행하게 되면, 플레이어는 지면 위에서 방향키와 스페이스바를 통해 좌우방향 움직임과 점프를 할 수 있고, 맵 바깥을 벗어날 수 없습니다. 라이프는 총 3개가 있고, 오른쪽에서 먹으면 점수를 올려주는 코인과 맞으면 라이프가 깎이고 죽는 장애물들이 다가옵니다. 장애물은 가만히 있는 기본 장애물, 위 아래로 움직이는 고정이동 장애물, 어디로 움직일 지 모르는 랜덤이동 장애물로 총 3가지의 종류가 있습니다. ESC 키를 눌러 게임 도중 일시정지를 시킬 수 있고, 다시 눌러서 게임을 재개할 수 있습니다. 라이프가 전부 깎이면 게임오버가 되어 더 이상 게임을 진행할 수 없도록 게임이 멈춥니다.

 

게임화면

게임-플레이-화면
게임 플레이 화면

게임을 플레이하는 화면입니다. (사각형 : 플레이어, 원 : 코인아이템, 다이아 : 장애물)

<NEXT>

이것으로 2D 플랫포머 게임 만들기 챕터는 끝을 내렸습니다. 하지만, 이걸 보고 계시는 여러분들은 이것을 본 뒤에 완벽하다고 생각하시면 안됩니다. 제가 만든 것은 어디까지나 예시에 불과하기 때문에, 부족한 점이 아주 많습니다. 그렇기에 어떤 점을 어떻게 고치면 좋을지 한번 생각해보시고 더 나은 게임을 만들 수 있도록 발전하는 것이 중요합니다. 이미 늦었지만, 총정리를 하는 와중에 한가지 더 응용할 점이 떠올라서 얘기를 해드리면, 게임오버가 됐을 때, 다시 게임을 재시작 할 수 있는 ReStart 버튼을 UI로 만들어서 부자연스럽게 멈추지 않도록 만들어줄 수 있습니다. 이것은 여러분이 직접 한 번 만들어 보시기 바랍니다.

 

다음에는 유니티 엔진에서 자주 사용되고, 게임을 만드는 데 꼭 필요한 툴 사용에 대한 지식들에 대해 이야기 하는 시간을 갖도록 하겠습니다. 감사합니다.