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

게임개발 중급(14) - 2D 플랫포머 게임 만들기(9)

by jyppro 2023. 5. 4.

2D 플랫포머 게임 만들기(9)

 

이전에는 오브젝트의 갯수를 제어하여 메모리사용과 시스템 과부하를 예방하는 방법에 대해 알아보았습니다.
이번에는 장애물을 설치해 플레이어가 닿으면 죽고, 정해진 목숨을 전부 사용하면 게임오버가 되도록 설계해 보겠습니다.

 

장애물 설치

 

직관적으로 장애물이라는 것을 보여주기 위해 삼각형 모양의 오브젝트를 사용하겠습니다. 하지만, 유니티 기본 2D 오브젝트에서 삼각형을 제공하지 않아 Isometric Diamond를 사용해서 삼각형 모양으로 보이게끔 대체 하겠습니다.

 

장애물
장애물 생성

장애물의 기능을 하기 위해서는 기본적으로 코인과 같은 방식으로 동작해야 합니다. 하지만 다른점은 코인은 플레이어와 부딪혔을 때, 코인이 없어지면서 점수가 올라가지만, 장애물은 플레이어가 없어지면서 라이프가 깎여야 합니다.

 

우선 해당 오브젝트에 필요한 리지드바디, 콜라이더를 추가해주고 프리팹으로 생성시켜줍니다. 저는 콜라이더를 모양에 딱 맞게 사용하고 싶어서, Edge Collider2D를 사용해서 모양대로 만들어주었습니다.

장애물-디테일
장애물 오브젝트 프리팹요소

위에서 미리 이야기했듯이, 코인과 기본적인 동작은 같습니다. 그러니, 코인에서 사용된 코인스포너와 코인무브먼트 스크립트를 그대로 복사하여 ObstacleSpawner, ObstacleMovement를 만들어 줍니다.

 

ObstacleSpawner

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

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

 

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);
        }
    }
}

 

코드의 구조는 코인과 동일합니다. 하지만 코인과 같은 위치, 속도로 생성된다면, 게임진행이 되지 않기 때문에 위치와 속도를 임의로 변경해줍니다. 저는 다이아몬드 모양의 오브젝트를 사용했기 때문에 삼각형으로 보이도록 바닥에 닿는 위치에 생성되도록 했습니다.

 

이렇게 해주면 생성도 잘 되고 마찬가지로 충돌판정도 잘 됩니다. 하지만, 코인과 다르게 부딪혔을 때 플레이어가 사망하여야 합니다. 우리는 다시 PlayerController 스크립트로 돌아가야 합니다.

 

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;
    private Rigidbody2D rb;
    private bool isGrounded = 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;
        }
    }

    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);
            }
        }

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

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

}

 

코드가 처음과 비교하면 상당히 길어졌습니다. 간략하게 바뀐 것만 리뷰하자면, life가 추가되고, life를 표시해주는 UI도 사용합니다. 이전에는 아무거나 부딪히면 부딪힌 상대방 오브젝트를 삭제하도록 했었는데, 코인 이외에 충돌판정을 해주는 오브젝트가 생겼기 때문에 태그로 분리해서 처리해줍니다. life는 3으로 설정되어 있고, 장애물과 부딪혔을 때, 라이프가 줄어들고, 플레이어가 사망했다가 3초 뒤 다시 부활하도록 하였습니다. 라이프가 0이 되면 플레이어가 더이상 부활하지 않고, 게임오버 상태가 됩니다.

 

태그를 따로 작성하여 아이템과 장애물을 구분하도록 해주고, RespawnPlaterAfterDelay 함수를 작성하고, Invoke함수를 사용해 3초마다 부활하도록 만들어 주었습니다. 부활과 사망판정은 SetActive를 통해서 오브젝트를 삭제하는 것이 아닌 껏다가 다시 켜는 방향으로 만들었고, life 3개를 다 사용하면 Destroy를 통해 오브젝트 자체를 삭제시켜줍니다.

 

장애물에-부딪혀-사망한-플레이어
장애물에 부딪혀 사망한 플레이어

게임을 진행하다 장애물에 부딪혀 사망해 life가 1깎인 모습입니다.

 

 

3초가-지나-부활한-모습
3초 후 부활한 모습

사망한 지 3초가 지나 부활한 모습입니다.

 

 

라이프를-전부-사용하면-더-이상-부활하지-못한다.
Life를 전부 사용하여 부활하지 못함

life를 전부 사용하면 Game Over 문구로 바뀌고, 플레이어는 더 이상 부활하지 못해 게임을 진행할 수 없습니다.

 

 

<NEXT>

이번에는 장애물을 설치하고 플레이어가 닿으면 죽으며, 정해진 목숨을 전부 사용하면 게임오버가 되도록 게임을 설계해 보았습니다. 물론 현재 상태에선 게임 자체를 멈추진 못하지만, 플레이를 할 수 없는 상태로는 만들었습니다. 다음에는 게임 자체를 멈출 수 있는 방법에 대해 한번 알아보고, 고정된 위치에 있는 장애물이 아닌, 움직이는 장애물을 한번 만들어 보겠습니다. 감사합니다.