테트리스 강좌

초등학생도 쉽게 따라할 수 있는 Unity 테트리스 게임 개발 강좌 자료

8단계: 게임 완성

🎯 이 단계에서 배울 것


🔷 구분하기: 내가 만드는 것 vs 엔진(API)이 주는 것

구분 이번 단계 예시 설명
내가 만드는 것 Spawner 클래스, SpawnNext(), “언제 다음 블록을 만들지”, “언제 게임 오버인지” 우리가 정의하는 게임 흐름: 블록 생성 타이밍, 게임 오버 조건. 규칙은 전부 우리 로직.
엔진(API)이 주는 것 Instantiate, Random.Range, Time.timeScale, FindObjectOfType, GetComponent Unity가 제공. 오브젝트 복제(Instantiate), 랜덤 수, 시간 멈춤(Time.timeScale) 은 엔진 기능. 우리는 “무엇을, 언제 생성할지”만 정함.

왜 엔진 것을 쓰나?
씬에 오브젝트를 실제로 만드는 것은 Instantiate 라는 엔진 API가 합니다. 우리는 “어떤 프리팹을, 어디에 만들지”만 인자로 넘깁니다. Random.Range는 엔진이 주는 “랜덤 수”, Time.timeScale = 0은 “게임 시간 멈춤”을 엔진에 요청하는 것입니다. 우리 확장 영역은 “언제 SpawnNext를 부를지, 언제 게임 오버로 볼지”이고, 나머지는 엔진을 호출해서 쓰는 것입니다.


📝 실습 내용

💡 8단계가 끝나면 플레이 가능한 테트리스가 됩니다.
Spawner 확장(랜덤·게임 오버) → LockPiece 연동 확인 → 하드 드롭 → 게임 오버 처리 순서로 되어 있습니다. 한 번에 다 안 해도, “다음 블록이 나온다”만 되면 일단 성공이라고 보면 됩니다.


📌 작업 씬 제차 확인 (매 단계 필수)
이번 단계 실습을 시작하기 전에 다시 한 번 확인하세요. 상단 씬 탭에서 지금 열린 씬이 Tetris (게임용)인지 봅니다. Tetris가 아니면 (SimpleScene, SampleScene, Untitled 등) 반드시 Tetris로 이름을 변경한 뒤 진행하세요. → 1단계 씬 이름 변경 방법

📌 8단계 시작 전 점검 (7·6·입력)
8단계는 Spawner 확장 + 하드 드롭 + 게임 오버만 붙이는 단계라서, 아래가 되어 있으면 편합니다.

확인 항목 설명
5단계 입력 키보드로 블록 이동·회전 되나요? → 5단계 - 입력 처리
6단계 행 제거 줄이 꽉 차면 줄이 사라지고 다음 블록이 나오나요? → 6단계 - 행 제거 시스템
7단계 GameController 점수·줄·레벨이 화면에 나오나요? → 7단계 - 점수·레벨 시스템

📌 붙여넣기 순서 (컴파일 에러 없이)
아래 순서대로 넣으면 서로 참조해서 에러가 나지 않습니다.

순서 파일 작업
1 GameBoard.cs IsGameOver() 추가 (Spawner가 호출하므로 먼저)
2 GameController.cs gameOverText 멤버 + GameOver() 메소드 추가
3 Spawner.cs 전체 아래 완성본으로 교체
4 TetrisBlock.cs HardDrop() 메소드 추가
5 InputHandler.cs Update() 안에 스페이스(하드 드롭) 처리 추가
UI Canvas GameOverText 만들기 + GameController에 연결 (코드 다음)

📐 최종 예시 구조 (가이드)
8단계가 끝나면 씬·UI·코드 연결이 아래처럼 갖춰집니다. 이 예시로 Hierarchy(위치)를 체크해 보면서 빠진 항목이 없는지 확인하면 됩니다.

씬 (Tetris)
├─ Main Camera
├─ GameBoard
│  └─ BoardBackground   (선택)
├─ Spawner              ← Tetris Block Prefabs(7개), Spawn Point 연결
│  └─ (Spawn Point 등)
├─ InputHandler         ← 루트 레벨
├─ Canvas
│  ├─ ScoreText
│  ├─ LinesText
│  ├─ LevelText
│  └─ GameOverText      ← 8단계 추가, GameController에 연결
├─ EventSystem
└─ GameController       ← Score/Lines/Level Text 연결

전체 시스템·클래스 역할·초기화 순서 → 시스템 설계 개요 · UI 계층 → UI 배치 구조 설계


1. GameBoard — IsGameOver 추가 (순서 1)

Spawner가 “다음 블록 만들기 전”에 게임 오버인지 확인하려면 GameBoard에 IsGameOver() 가 있어야 하므로, 가장 먼저 넣습니다.

변경점 — GameBoard.cs에 메소드 하나 추가:

public bool IsGameOver()   // [우리 메소드] Spawner에서 호출
{
    // 스폰 영역(위에서 2줄: row 18·19)에 블록이 하나라도 있으면 게임 오버
    for (int y = height - 2; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            if (grid[x, y] != null)
                return true;
        }
    }
    return false;
}

왜 맨 윗줄 1줄이 아니라 2줄인가? — 스폰 위치가 y = 18 (height - 2) 근처이므로, row 19만 검사하면 row 18까지 쌓인 블록을 놓칩니다. 위에서 2줄(row 18·19)을 보면 “스폰 영역에 블록이 있다 = 더 이상 못 만든다”를 확실히 감지합니다.

넣는 위치: ClearFullRows() 근처(앞이나 뒤) 아무데나. 클래스 안이면 됩니다.


2. Spawner — 전체 교체 (순서 2, 완성본)

4단계에서 Spawner를 썼다면 아래 완성본으로 전체 교체하세요. (없다면 4단계 - Spawner 점증적 도입 따라 만든 뒤 교체.)

Assets/Scripts/Spawner.cs 전체를 아래로 교체:

using UnityEngine;

public class Spawner : MonoBehaviour   // [MonoBehaviour 상속]
{
    public GameObject[] tetrisBlockPrefabs;   // [우리 멤버] Inspector에서 7개 연결
    public Transform spawnPoint;               // [우리 멤버] Inspector에서 연결 — Transform은 Unity 타입

    private GameBoard gameBoard;       // [우리 멤버]
    private GameController gameController;   // [우리 멤버]

    void Start()   // [Unity가 호출] 상속받은 메소드 — 엔진이 한 번 호출
    {
        gameBoard = FindObjectOfType<GameBoard>();   // [Unity API]
        gameController = FindObjectOfType<GameController>();
        SpawnNext();   // [우리 메소드]
    }

    public void SpawnNext()   // [우리 메소드] 6단계 LockPiece에서 호출
    {
        if (gameBoard.IsGameOver())   // IsGameOver: [우리 메소드] GameBoard 쪽
        {
            Debug.Log("Game Over!");   // [Unity API]
            gameController.GameOver();   // [우리 메소드] GameController 쪽 — 화면에 "Game Over" 표시
            Time.timeScale = 0;   // [Unity 프로퍼티] 0이면 게임 시간 일시정지
            return;
        }
        int randomIndex = Random.Range(0, tetrisBlockPrefabs.Length);   // Random.Range: [Unity API]
        Instantiate(   // Instantiate: [Unity API]
            tetrisBlockPrefabs[randomIndex],
            spawnPoint.position,   // spawnPoint: [우리 멤버], position: [Unity 프로퍼티]
            Quaternion.identity
        );
    }
}

왜 null 검사가 없나? — 튜토리얼을 순서대로 따라오면 GameBoard·GameController·SpawnPoint 모두 씬에 있습니다. 만약 빠뜨리면 Unity가 NullReferenceException과 함께 어떤 줄에서 오류인지 알려주므로, 뭘 빠뜨렸는지 바로 알 수 있습니다. null 검사로 오류를 숨기면 오히려 문제를 찾기 어렵습니다.

왜 스폰 후 ValidMove 검사를 안 하나? — Instantiate 직후에는 새 블록의 Start()가 아직 실행되지 않아 gameBoard를 못 찾습니다. 그래서 스폰 전에 IsGameOver() 로 미리 검사합니다. IsGameOver가 스폰 영역(위에서 2줄)을 보기 때문에, 블록이 그 영역까지 쌓이면 스폰 자체를 하지 않고 게임을 멈춥니다.

📌 복기: tetrisBlockPrefabs 는 배열(같은 타입 GameObject를 순서대로). 한 번에 이해 못했어도 배열에서 다시 실습해 보세요.


Spawner 변경 후 — 테트리스 블록 추가·연결 (필수)

코드만 교체하면 동작하지 않습니다. Inspector에서 프리팹·Spawn Point를 연결해야 합니다.

1. Spawner 오브젝트에 스크립트 붙이기

2. Tetris Block Prefabs 배열에 블록 7종 넣기

3. Spawn Point 연결

4. 스폰된 블록이 GameBoard를 쓰는 방법

정리: Spawner (Script)에 Tetris Block Prefabs 7개 + Spawn Point 연결, 각 프리팹 루트에 TetrisBlock 있으면 됩니다.


3. GameController — gameOverText + GameOver() 추가 (순서 2)

7단계에서 만든 GameController.cs에 멤버 1개 + 메소드 2개 를 추가합니다.

추가할 멤버 — 기존 levelText 아래에 한 줄 추가:

    public TMP_Text gameOverText;   // [우리 멤버] 게임 오버 시 화면에 표시

Start() 수정 — 기존 UpdateUI(); 위에 한 줄 추가:

    void Start()
    {
        gameOverText.gameObject.SetActive(false);   // 시작할 때 숨김
        UpdateUI();
    }

추가할 메소드 — AddScore() 아래에 넣기:

    public void GameOver()   // [우리 메소드] Spawner가 호출
    {
        gameOverText.gameObject.SetActive(true);   // SetActive: [Unity API] 오브젝트 켜기
        gameOverText.text = "Game Over";
    }

3-1. 게임 오버 UI 만들기 (Canvas에 GameOverText)

Spawner가 gameController.GameOver()를 호출하면 화면에 “Game Over”가 나와야 합니다.

  1. [Hierarchy > Canvas] 선택 → 우클릭 → UI → Text - TextMeshPro
  2. 이름을 GameOverText 로 변경
  3. [Inspector > GameOverText > TextMeshPro - Text (UI)]:
    • Text: Game Over
    • Font Size: 48
    • Font Asset: 7단계에서 만든 한글 폰트 SDF (또는 기본 폰트도 OK — “Game Over”는 영어)
    • Color: 빨간색 또는 원하는 색
    • Alignment: 가운데 정렬 (가로·세로 모두)
  4. [Inspector > Rect Transform]:
    • Anchor: 화면 중앙 (Anchor Presets에서 중앙 선택)
    • Pos X: 0, Pos Y: 0
    • Width: 400, Height: 100
  5. GameController에 연결:
    • [Hierarchy > GameController] 클릭
    • [Inspector > Game Controller (Script) > Game Over Text] 칸에 [Hierarchy > Canvas > GameOverText]를 드래그

Start()에서 숨기므로 Play 하면 처음에는 안 보이고, 블록이 꽉 쌓이면 화면 가운데에 “Game Over”가 나타납니다.


4. TetrisBlock — HardDrop 추가 (순서 4)

변경점 — TetrisBlock.cs에 메소드 하나 추가 (LockPiece 근처에 넣기):

public void HardDrop()
{
    while (ValidMove(Vector3.down))
    {
        transform.position += Vector3.down;
    }
    LockPiece();
}

넣는 위치: LockPiece() 메소드 바로 위나 아래. 클래스 안이면 됩니다.

📌 ValidMove 보호 수준(protection level)
HardDrop() 안에서 ValidMove(Vector3.down)를 호출합니다. ValidMove는 같은 클래스(TetrisBlock) 안에서만 부르므로 이론상 private이어도 되지만, 실습 일관성과 나중에 다른 스크립트에서 재사용할 수 있도록 public으로 두는 것을 권장합니다.
4·6단계에서 추가·수정한 TetrisBlock.cs의 ValidMove가 public bool ValidMove(Vector3 offset) 인지 확인하세요. private bool ValidMove로 되어 있으면 그대로 두어도 동작에는 문제 없고, 다른 스크립트에서 ValidMove를 호출할 일이 있다면 public으로 바꾸면 됩니다.

// 확인할 줄 (TetrisBlock.cs)
public bool ValidMove(Vector3 offset)   // ← public 인지 확인

5. InputHandler — Update에 스페이스(하드 드롭) 추가 (순서 5)

변경점 — InputHandler.cs의 Update() 안에, 기존 입력 처리 및 스페이스 키 처리만 추가하면 됩니다.

추가할 코드 (Update() 안, 기존 if문들 사이 또는 맨 위에 넣기):

if (Input.GetKeyDown(KeyCode.Space))
{
    TetrisBlock current = FindObjectOfType<TetrisBlock>();
    if (current != null)
        current.HardDrop();
}

Update() 완성본 예시 — 5단계에서 Update를 이미 쓰고 있다면, 그 안에 위 블록만 한 군데 추가하면 됩니다. 전체를 다시 넣고 싶다면 아래를 참고하세요 (기존 화살표/WASD + 스페이스):

void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        TetrisBlock current = FindObjectOfType<TetrisBlock>();
        if (current != null)
            current.HardDrop();
    }

    moveTimer += Time.deltaTime;
    if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
    {
        if (moveTimer >= moveCooldown) { MoveLeft(); moveTimer = 0f; }
    }
    if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D))
    {
        if (moveTimer >= moveCooldown) { MoveRight(); moveTimer = 0f; }
    }
    if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S))
    {
        TetrisBlock current = FindObjectOfType<TetrisBlock>();
        if (current != null && moveTimer >= moveCooldown) { current.MoveDown(); moveTimer = 0f; }
    }
    if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.W))
    {
        Rotate();
    }
}

(5단계에서 moveTimer·moveCooldown·MoveLeft 등이 있다면 그대로 두고, 스페이스 if 블록만 추가해도 됩니다.)

🔗 이전 단계와의 연결

이 섹션은 실습이 아닙니다. 따로 할 일이나 체크할 항목은 없습니다. “이 단계가 이전 단계와 어떻게 이어지는지”만 참고로 정리한 것이므로, 궁금할 때만 읽으면 됩니다.

이 단계에서는 모든 이전 단계의 기능을 연결합니다:

완전한 게임 루프:

  1. Spawner가 새 블록 생성
  2. InputHandler가 입력 처리 (이동, 회전, 하드 드롭)
  3. 블록이 자동으로 낙하
  4. 블록이 바닥에 닿으면 고정
  5. 행 제거 및 점수 계산
  6. 다음 블록 생성
  7. 게임 오버 확인

💡 지금까지 익힌 개념 정리

📌 복기: 변수(멤버) + 메소드. 한 번에 이해 못했어도 변수 · 멤버와 메소드에서 다시 실습해 보세요.

📖 기초 개념 학습 (복기 → 점진적 실습)

📌 복기로 위 개념을 떠올린 뒤, 아래 기초 개념 문서에서 점진적으로 확장된 실습을 진행하세요. 게임이 완성된 뒤에도 같은 개념이 단계별로 확장되므로, 차근차근 따라 하면 복습이 됩니다.

개념 문서 연결
MonoBehaviour Awake / Start / Update 용도·실행 순서 실습
멤버와 메소드 멤버·메소드 한눈에 + AddScore 실습
Instantiate / Destroy Spawner가 블록 생성·삭제. 프리팹 복제·Destroy 실습
게임 루프 게임이 도는 흐름. Update 반복 실습
개념 설명 (목차) 모든 개념 문서 한눈에

📐 이 단계 확장 실습 (전용 씬에서 점진적 연습): 8단계 확장 실습 01 - Instantiate와 Random
→ 단계별 확장 실습 전체 목차: 단계별 확장 실습 (목차)

📌 에셋·코드 연결 체크리스트 (누락 방지)

아래가 모두 되어 있어야 게임이 끝까지 동작합니다. 하나라도 빠지면 “블록이 안 나온다”, “점수가 안 오른다”, “키가 안 먹는다” 등이 발생할 수 있습니다.

에셋 연결 (Inspector에서 드래그·할당)

연결 대상 연결 내용 확인 방법
Spawner Tetris Block Prefabs 배열에 I_Block, O_Block 등 7개 프리팹 할당 [Hierarchy > Spawner] → [Inspector > Spawner (Script) > Tetris Block Prefabs] 각 슬롯에 프리팹 있음
Spawner Spawn Point에 SpawnPoint 오브젝트 할당 [Inspector > Spawner (Script) > Spawn Point]에 [Hierarchy > SpawnPoint] 연결됨
GameController Score Text에 Canvas > ScoreText 할당 [Hierarchy > GameController] → [Inspector > Game Controller (Script) > Score Text]에 ScoreText 표시
GameController Lines Text에 Canvas > LinesText 할당 [Inspector > Lines Text]에 LinesText 표시
GameController Level Text에 Canvas > LevelText 할당 [Inspector > Level Text]에 LevelText 표시
GameController Game Over Text에 Canvas > GameOverText 할당 [Inspector > Game Over Text]에 GameOverText 표시
블록 프리팹 (I_Block 등) 루트 오브젝트에 TetrisBlock 스크립트 붙음 [Project > Prefabs > I_Block] 더블클릭 → 루트 선택 → [Inspector > Tetris Block (Script)] 있음
보드 시각화(선택) BoardBackground의 Sprite Renderer > Sprite에 Square 연결 [Hierarchy > GameBoard > BoardBackground] → [Inspector > Sprite Renderer > Sprite]에 Square 있음

코드 연결 (스크립트 간 호출·씬 배치)

연결 설명 필요한 것
TetrisBlock → GameBoard FindObjectOfType<GameBoard>() Hierarchy에 GameBoard 오브젝트 + Game Board (Script)
TetrisBlock.LockPiece → GameBoard.AddToGrid, ClearFullRows 블록 고정·행 제거 6단계 LockPiece 코드에 포함
TetrisBlock.LockPiece → GameController.AddScore(linesCleared) 행 제거 개수 전달 Hierarchy에 GameController + 6단계 LockPiece에 코드 있음
TetrisBlock.LockPiece → Spawner.SpawnNext() 다음 블록 생성 Hierarchy에 Spawner + 6단계 LockPiece에 코드 있음
InputHandler → TetrisBlock (MoveLeft 등) FindObjectOfType<TetrisBlock>() Hierarchy에 InputHandler + Spawner가 먼저 블록 스폰
Spawner → GameBoard, GameController Start에서 FindObjectOfType Hierarchy에 GameBoard, GameController
TetrisBlock.Update → GameController.level (7단계) 레벨에 따른 낙하 속도 Hierarchy에 GameController + 7단계 TetrisBlock.Update 수정 반영
TetrisBlock.ValidMove 보호 수준 HardDrop 등에서 사용, public 권장 TetrisBlock.cs에서 public bool ValidMove(Vector3 offset) 인지 확인 (위 4. ValidMove 보호 수준 참고)

✅ 확인 사항

🎉 완성!

축하합니다! 테트리스 게임을 완성했습니다!
(게임 오버가 나와도 “로직이 동작한다”는 뜻이니, 그때는 반성공이라고 생각해도 됩니다. 😄)

🚀 개선 아이디어

✅ 게임 완성 후: 작업 씬이 Tetris인지 재차 확인해 두면, 나중에 프로젝트를 다시 열었을 때 혼란이 없습니다. Tetris가 아니면 씬 이름 변경 방법을 따라 Tetris로 바꾸세요.


📌 다음으로 가기

← 이전 단계: 7단계 - 점수 및 레벨 시스템 다음: 9단계 - 확장 코루틴 이펙트 →