2025년, 코딩은 선택이 아닌 필수!

2025년 모든 학교에서 코딩이 시작 됩니다. 먼저 준비하는 사람만이 기술을 선도해 갑니다~

응용프로그래밍/유니티기초

[유니티2D 활용] 테트리스 게임 만들기

파아란기쁨1 2022. 6. 3. 12:01
반응형
목표

배운것을 활용하여 테트리스 게임을 만들어 보자.

 

실습

1. 먼저 테트리스 게임을 위해서 다음과 같은 형태의 블럭을 만들어 놓자.

만드는 과정은 다음과 같다.

먼저 T 블럭을 만드는 과정을 살펴 보자.

먼저 Square 를 하나 추가 한다. 한칸을 1*1 짜리로 만드는데 중간에 차이를 만들어 주기 위해 한개 블록의 크기를 0.9 * 0.9 크기로 만들어 주자.

CTRL+D 를 이용해서 3개를 복사 한 후 각각의 위치를 (-1,0),(1,0),(0,-1) 로 만들어 주면 다음과 같이 만들어 진다.

이것을 하나의 객체로 만들어 주자

GameObject를 하나 만든 후 이름을 TBlock으로 만들어 준다.

그리고 4개의 Square 를 끌어서 TBlock 안으로 끌어다 넣어 준다.

T 블럭의 색상을 보라색으로 지정하기 위해서는 4개의 Squre를 모두 선택하고 Color 를 보라색으로 선택

T블럭의 Z축을 조정해서 회전시켜 보자.

나중에 Z축을 회전 시키면 이렇게 블록의 모양이 회전이 된다.

이러한 T블록을 6개 복사해서 각각의 위치를 위에서 보이는 것과 같이 IBlock,JBlock,LBlock,OBlock,SBlock,ZBlock 를 생성한 후에 프리팹으로 만들어 주자.

 

2. 게임을 관리하는 GameManager를 다음과 같이 만들어 놓는다.

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

public class GameManager : MonoBehaviour
{
    // 외부에서 싱글톤 오브젝트를 가져올때 사용할 프로퍼티
    public static GameManager instance
    {
        get
        {
            // 만약 싱글톤 변수에 아직 오브젝트가 할당되지 않았다면
            if (m_instance == null)
            {
                // 씬에서 GameManager 오브젝트를 찾아 할당
                m_instance = FindObjectOfType<GameManager>();
            }

            // 싱글톤 오브젝트를 반환
            return m_instance;
        }
    }

    private static GameManager m_instance; // 싱글톤이 할당될 static 변수

    private int score = 0; // 현재 게임 점수
    public bool isGameover { get; private set; } // 게임 오버 상태


    private void Awake()
    {
        // 씬에 싱글톤 오브젝트가 된 다른 GameManager 오브젝트가 있다면
        if (instance != this)
        {
            // 자신을 파괴
            Destroy(gameObject);
        }
    }

    private void Start()
    {

    }

    // 점수를 추가하고 UI 갱신
    public void AddScore(int newScore)
    {
        // 게임 오버가 아닌 상태에서만 점수 증가 가능
        if (!isGameover)
        {
            // 점수 추가
            score += newScore;
            // 점수 UI 텍스트 갱신
            UIManager.instance.UpdateScoreText(score);
        }
    }

    // 게임 오버 처리
    public void EndGame()
    {
        // 게임 오버 상태를 참으로 변경
        isGameover = true;
        // 게임 오버 UI를 활성화
        UIManager.instance.SetActiveGameoverUI(true);
    }

    private void Update()
    {

    }
}

4. UI를 관리하는 UIManger 를 만들자.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; // 씬 관리자 관련 코드
using UnityEngine.UI; // UI 관련 코드

public class UIManager : MonoBehaviour
{
    // 싱글톤 접근용 프로퍼티
    public static UIManager instance
    {
        get
        {
            if (m_instance == null)
            {
                m_instance = FindObjectOfType<UIManager>();
            }

            return m_instance;
        }
    }

    private static UIManager m_instance; // 싱글톤이 할당될 변수


    public Text scoreText; // 점수 표시용 텍스트
    public GameObject gameoverUI; // 게임 오버시 활성화할 UI 


    // 점수 텍스트 갱신
    public void UpdateScoreText(int newScore)
    {
        if(scoreText!=null) scoreText.text = "Score : " + newScore;
    }

    // 게임 오버 UI 활성화
    public void SetActiveGameoverUI(bool active)
    {
        if(gameoverUI!=null) gameoverUI.SetActive(active);
    }

    // 게임 재시작
    public void GameRestart()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}

 

 

 

3. 게임 시작을 하면 블록이 생성되기 위한 게임오브젝트를 추가하여 이름을 Spawner 로 변경한 다음 다음과 같이 스크립트를 추가한다.

7개의 블록 중 임의의 블록을  생성해 주자.

 

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

public class Play : MonoBehaviour
{
    public GameObject[] tetrisBlock=new GameObject[7]; //7���� ��Ʈ���� ������ ��� ���� �迭 
    // Start is called before the first frame update
    void Start()
    {
        tetrisBlockCreate(); //테트리스 블록을 생성하자.
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    void tetrisBlockCreate()
    {
        Instantiate(tetrisBlock[Random.Range(0, tetrisBlock.Length)], transform.position, Quaternion.identity); //��Ʈ���� ������ �������� ������ �ϱ�
    }
}

Spawner 에 다음과 같이 스크립트 추가 후 테트리스 블록에 각각의 블록을 매칭 후 실행 해 보면 임의의 블록이 하나 생기는 것을 알 수 있다.

4. 배경이 되는 Squer를 10,20 크기로 하나 추가해서 background로 이름 변경후 원하는 색으로 변경하자. 그리고 왼쪽 하단을 0,0 으로 맞추기 위해 위치를 5,10 으로 만들어 준다. order in Layer는 가장 뒷쪽으로 배치하기 위해 -1 값을 넣어 주었다.

카메라 위치를 배경에 맞춰 조정한다.

 

5. 생성이 된 블록은 키보드를 입력 받아 회전을 처리해 주어야 한다.

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

public class TetriceMove : MonoBehaviour
{
    private Vector3 rotationPoint;
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.LeftArrow)){
            transform.position += new Vector3(-1, 0, 0);
        }
        else if (Input.GetKeyDown(KeyCode.RightArrow)){
            transform.position += new Vector3(1, 0, 0);
        }
        else if (Input.GetKeyDown(KeyCode.UpArrow)){
            transform.RotateAround(transform.TransformPoint(rotationPoint), new Vector3(0, 0, 1), 90);    
        }
        else if (Input.GetKeyDown(KeyCode.DownArrow)){
            transform.position += new Vector3(0, -1, 0);
        }      
        

    }
}

실행을 해 보면 위의 화살표로 방향 회전,좌,우 아래 이동을 하게 된다.

여기서 아무것도 누르지 않았을때 떨어지는 시간만큼 떨어 지도록 처리 해 보자.

 

        timer+=Time.deltaTime; 
        if(timer<1) return; 
        timer=0; //1초 마다 해당 위치 만큼 이동하자.
        transform.position += new Vector3(0, -downPos, 0);

1초에 한번씩 이동위치 만큼 이동하자.

실행해서 정상으로 동작하는지 체크해 보자.

 

6.바닥 y 축이 0 위치 이거나 혹은 x축이 0~10 사이의 범위가 아니라면 밖으로 나가지 않도록 처리하자.

GameManager 에 다음과 같이 넓이 와 높이를 정해 놓은 후

    public int width = 10;
    public int height = 20;
    bool MoveCheck(){

        foreach (Transform child in transform) //내 객체의 모든 자식을 가져 오자 
        {
            int x = Mathf.RoundToInt(child.transform.position.x); //현재 위치를 반올림해서 자식 객체의 위치를 int 값으로 변경하자.
            int y = Mathf.RoundToInt(child.transform.position.y);

            if (x < 0 || x >= GameManager.instance.width || y < 0 || y >= GameManager.instance.height) //범위를 벗어나면 return false
            {
                return false;
            }
        }

        return true;
    }

위와 같은 함수를 만든 후 다음과 같이 처리하자.

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.LeftArrow)){
            transform.position += new Vector3(-1, 0, 0);
            if(!MoveCheck())transform.position -= new Vector3(-1, 0, 0); //움직일 수 없으면 다시 원래대로 복원
        }
        else if (Input.GetKeyDown(KeyCode.RightArrow)){
            transform.position += new Vector3(1, 0, 0);
            if(!MoveCheck())transform.position -= new Vector3(1, 0, 0); //움직일 수 없으면 다시 원래대로 복원
        }
        else if (Input.GetKeyDown(KeyCode.UpArrow)){
            transform.RotateAround(transform.TransformPoint(rotationPoint), new Vector3(0, 0, 1), 90); 
            if(!MoveCheck())transform.RotateAround(transform.TransformPoint(rotationPoint), new Vector3(0, 0, 1), -90); //움직일 수 없으면 다시 원래대로 복원   
        }
        else if (Input.GetKeyDown(KeyCode.DownArrow)){
            transform.position += new Vector3(0, -1, 0);
            if(!MoveCheck())transform.position -= new Vector3(0, -1, 0);
        }  

        timer+=Time.deltaTime; 
        if(timer<1) return; 
        timer=0; //1초 마다 해당 위치 만큼 이동하자.
        transform.position += new Vector3(0, -downPos, 0);
        if(!MoveCheck())transform.position -= new Vector3(0, -downPos, 0);

    }

이렇게 하고 실행을 해 보면 약간의 오차로 x축과 y축의 범위를 벗어나는 것을 확인 할 수 있다.

이것은 child 객체의 위치를 반올림하기 때문에 생기는 오차이다.

배경과 메인 카메라를 적절하게 이동 시켜 주자.

 

실행을 해 보면 가장 아래에 도착해서도 계속 이동하는 것을 확인 할 수 있다.

7. 마지막에 도착하면 더 이상 이동하지 못하도록 처리하자.

            this.enabled = false;

위의 코드를 입력하여 컴포넌트를 비활성화 시키자.

 

8. 마지막 도착하면 다음 블럭을 생성해야 한다.

먼저 GameManager 에 다음 스크립트를 추가 하여 이동 중인지 아닌지 판단하도록 한다.

public bool isBlockMoved { get; private set; }

그리고 Play 스크립트를 다음과 같이 수정하여 Moved가 false 라면 블럭을 생성하도록 변경

    void Update()
    {
        if(!GameManager.instance.isBlockMoved){
            GameManager.instance.isBlockMoved=true; //이동중으로 변경 후 블럭 생성
            tetrisBlockCreate();
        }     
    }

마지막으로 TetrisMove 스크립트에 마지막 도착을 하면 이동이 끝났음을 알려 주자.

GameManager.instance.isBlockMoved=false;

실행을 해 보면 이동이 끝난 후 블록이 생기는 것을 확인 할 수 있다.

하지만 위와 같이 가장 아래쪽에 겹쳐서 쌓이는 것을 확인 할 수 있다.

 

9. 테트리스를 다른 블럭 위에 올리는 작업을 하자.

테트리스에 블럭을 쌓기 위해서는 객체 배열을 만들어서 가장 마지막에 도착 한 경우 해당 객체들을 객체 배열 안에 넣어 놓은 후에 이동시에 내가 이동할 경로에 다른 객체가 있다면 이동하지 못한다.

GameManager 스크립트에 다음과 같이 수정한다.

    private Transform[,] blockList;
    
    private void Start()
    {
        blockList = new Transform[width, height];
    }
    
    //blockList 에 객체 추가
    public void AddBlock(int x,int y,Transform block){
        blockList[x,y] = block;
    }
    //blockList 에 객체가 존재하는지 확인
    public bool CheckBlock(int x,int y){
        if(blockList[x,y]!=null) return true;
        else return false;
    }

 

 

TetrisMove 스크립트를 다음과 같이 수정한다.

    void AddBlock()
    {
        // 마지막 도착 한 경우 자신의 자식 객체를 객체 배열에 모두 쌓아 놓는다.
        foreach (Transform child in transform)
        {
            int x = Mathf.RoundToInt(child.transform.position.x);
            int y = Mathf.RoundToInt(child.transform.position.y);

            GameManager.instance.AddBlock(x, y, child);
        }
    }

내려가는 것이 종료 되는 시점에 AddBlock 를 호출 한다.

그리고 MoveCheck() 함수도 다음과 같이 수정하자.

    bool MoveCheck(){

        foreach (Transform child in transform) //내 객체의 모든 자식을 가져 오자 
        {
            int x = Mathf.RoundToInt(child.transform.position.x); //현재 위치를 반올림해서 자식 객체의 위치를 int 값으로 변경하자.
            int y = Mathf.RoundToInt(child.transform.position.y);

            if (x < 0 || x >= width || y < 0 || y >= height) //범위를 벗어나면 return false
            {
                return false;
            }
            if (GameManager.instance.CheckBlock(x,y)) return false;
        }

        return true;
    }

실행해 보면 차곡차곡 쌓이는 것을 확인 할 수 있다.

다음으로 한줄이 쌓이면 한 라인을 삭제하자.

 

10. 한 라인 삭제 부분도 GameManager 에서 구현해 주어야 한다.

    public void CheckLines()
    {
        for (int i = height - 1; i >= 0; i--) // 위에서 부터 내려가면서 한 라인씩 삭제해야 한다. 그렇지 않으면 0번째 줄 삭제 후 다시 0번째 줄을 찾아야 한다.
        {
            if (FullLine(i))  //한줄이 꽉 찼다면
            {
                DeleteLine(i);  //한줄 삭제 후
                BlockDown(i);  // 끌어 내리자. 
            }
        }
    }
    bool FullLine(int row)  //해당 row 위치가 꽉 찼는지 확인하자.
    {
        for (int x = 0; x < width; x++)  
        {
            if (blockList[x, row] == null)  return false;        
        }
        return true;  
    }
    void DeleteLine(int row)  //해당 row를 삭제하자
    {
        for (int x = 0; x < width; x++) 
        {
            Destroy(blockList[x, row].gameObject);
            blockList[x, row] = null;
        }
    }
    
    void BlockDown(int row)  // row 위에 줄을 하나씩 아래로 끌어 내리자.
    {
        for (int y = row+1; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                if (blockList[x, y] != null) 
                {
                    blockList[x, y - 1] = blockList[x, y];
                    blockList[x, y] = null;
                    blockList[x, y - 1].transform.position -= new Vector3(0, 1, 0); //보여주는 위치를 한줄 아래로 내려 줘야 한다.
                }
            }
        }
    }

위와 같이 구현 후 TetrisMove 에서 바닥에 닿았을때 CheckLine() 을 호출해 준다.

 

 

연습문제

1.생성 되자 마자 놓일 곳이 없다면 GameOver 화면을 띄우자.

2.한 라인이 꽉 차서 한 줄씩 삭제를 한다면 점수를 증가하는 프로그램을 만들자.

3. 게임오버시 R 키를 눌러서 게임을 재시작 하도록 처리하자.

 

 

 

더보기

지금까지 작업한 소스코드

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

public class GameManager : MonoBehaviour
{
    // �ܺο��� �̱��� ������Ʈ�� �����ö� ����� ������Ƽ
    public static GameManager instance
    {
        get
        {
            // ���� �̱��� ������ ���� ������Ʈ�� �Ҵ���� �ʾҴٸ�
            if (m_instance == null)
            {
                // ������ GameManager ������Ʈ�� ã�� �Ҵ�
                m_instance = FindObjectOfType<GameManager>();
            }

            // �̱��� ������Ʈ�� ��ȯ
            return m_instance;
        }
    }

    private static GameManager m_instance; // �̱����� �Ҵ�� static ����

    private int score = 0; // ���� ���� ����
    public bool isGameover { get; private set; } // ���� ���� ����
    public bool isBlockMoved { get; set; } //블럭 이동 중인지 이동이 끝났는지
    public int width = 10;
    public int height = 20;
    private Transform[,] blockList;

    private void Awake()
    {
        if (instance != this)
        {
            Destroy(gameObject);
        }
    }

    private void Start()
    {
        blockList = new Transform[width, height];
    }

    //blockList 에 객체 추가
    public void AddBlock(int x,int y,Transform block){
        blockList[x,y] = block;
    }
    //blockList 에 객체가 존재하는지 확인
    public bool CheckBlock(int x,int y){
        if(blockList[x,y]!=null) return true;
        else return false;
    }

    // 점수 추가
    public void AddScore(int newScore)
    {
        // ���� ������ �ƴ� ���¿����� ���� ���� ����
        if (!isGameover)
        {

            score += newScore;

            UIManager.instance.UpdateScoreText(score);
        }
    }

    // ���� ���� ó��
    public void EndGame()
    {
        // ���� ���� ���¸� ������ ����
        isGameover = true;
        // ���� ���� UI�� Ȱ��ȭ
        UIManager.instance.SetActiveGameoverUI(true);
    }

    private void Update()
    {

    }


    public void CheckLines()
    {
        for (int i = height - 1; i >= 0; i--) // 위에서 부터 내려가면서 한 라인씩 삭제해야 한다. 그렇지 않으면 0번째 줄 삭제 후 다시 0번째 줄을 찾아야 한다.
        {
            if (FullLine(i))  //한줄이 꽉 찼다면
            {
                DeleteLine(i);  //한줄 삭제 후
                BlockDown(i);  // 끌어 내리자. 
            }
        }
    }
    bool FullLine(int row)  //해당 row 위치가 꽉 찼는지 확인하자.
    {
        for (int x = 0; x < width; x++)  
        {
            if (blockList[x, row] == null)  return false;        
        }
        return true;  
    }
    void DeleteLine(int row)  //해당 row를 삭제하자
    {
        for (int x = 0; x < width; x++) 
        {
            Destroy(blockList[x, row].gameObject);
            blockList[x, row] = null;
        }
    }
    
    void BlockDown(int row)  // row 위에 줄을 하나씩 아래로 끌어 내리자.
    {
        for (int y = row+1; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                if (blockList[x, y] != null) 
                {
                    blockList[x, y - 1] = blockList[x, y];
                    blockList[x, y] = null;
                    blockList[x, y - 1].transform.position -= new Vector3(0, 1, 0); //보여주는 위치를 한줄 아래로 내려 줘야 한다.
                }
            }
        }
    }
}


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

public class TetriceMove : MonoBehaviour
{
    private Vector3 rotationPoint;
    private float downPos=1.0f;
    private float timer = 0;

    // Start is called before the first frame update
    private void Awake() {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.LeftArrow)){
            transform.position += new Vector3(-1, 0, 0);
            if(!MoveCheck())transform.position -= new Vector3(-1, 0, 0); //움직일 수 없으면 다시 원래대로 복원
        }
        else if (Input.GetKeyDown(KeyCode.RightArrow)){
            transform.position += new Vector3(1, 0, 0);
            if(!MoveCheck())transform.position -= new Vector3(1, 0, 0); //움직일 수 없으면 다시 원래대로 복원
        }
        else if (Input.GetKeyDown(KeyCode.UpArrow)){
            transform.RotateAround(transform.TransformPoint(rotationPoint), new Vector3(0, 0, 1), 90); 
            if(!MoveCheck())transform.RotateAround(transform.TransformPoint(rotationPoint), new Vector3(0, 0, 1), -90); //움직일 수 없으면 다시 원래대로 복원   
        }
        else if (Input.GetKeyDown(KeyCode.DownArrow)){
            transform.position += new Vector3(0, -1, 0);
            if(!MoveCheck()){
                transform.position -= new Vector3(0, -1, 0);
                AddBlock();
                GameManager.instance.CheckLines(); 
                GameManager.instance.isBlockMoved=false;
                this.enabled = false;
            }
        }  

        timer+=Time.deltaTime; 
        if(timer<1) return; 
        timer=0; //1초 마다 해당 위치 만큼 이동하자.
        transform.position += new Vector3(0, -downPos, 0);
        if(!MoveCheck()){
            transform.position -= new Vector3(0, -downPos, 0);
            AddBlock();
            GameManager.instance.CheckLines();
            GameManager.instance.isBlockMoved=false;
            this.enabled = false;            
        }

    }

    bool MoveCheck(){

        foreach (Transform child in transform) //내 객체의 모든 자식을 가져 오자 
        {
            int x = Mathf.RoundToInt(child.transform.position.x); //현재 위치를 반올림해서 자식 객체의 위치를 int 값으로 변경하자.
            int y = Mathf.RoundToInt(child.transform.position.y);

            if (x < 0 || x >= GameManager.instance.width || y < 0 || y >= GameManager.instance.height) //범위를 벗어나면 return false
            {
                return false;
            }
            if (GameManager.instance.CheckBlock(x,y)) return false;
        }

        return true;
    }

    
    void AddBlock()
    {
        // 마지막 도착 한 경우 자신의 자식 객체를 객체 배열에 모두 쌓아 놓는다.
        foreach (Transform child in transform)
        {
            int x = Mathf.RoundToInt(child.transform.position.x);
            int y = Mathf.RoundToInt(child.transform.position.y);
            GameManager.instance.AddBlock(x, y, child);            
        }
    }
}


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

public class Play : MonoBehaviour
{
    public GameObject[] tetrisBlock=new GameObject[7]; //7���� ��Ʈ���� ������ ��� ���� �迭 
    // Start is called before the first frame update
    void Start()
    {
         
    }

    // Update is called once per frame
    void Update()
    {
      
        //Debug.Log("BlockMoved: " + GameManager.instance.isBlockMoved.ToString());
  
        if(!GameManager.instance.isBlockMoved){
            GameManager.instance.isBlockMoved=true; //이동중으로 변경 후 블럭 생성
            tetrisBlockCreate();
        }     
    }

    void tetrisBlockCreate()
    {
        Instantiate(tetrisBlock[Random.Range(0, tetrisBlock.Length)], transform.position, Quaternion.identity); //��Ʈ���� ������ �������� ������ �ϱ�
    }
}
반응형