테트리스 강좌

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

4단계: 블록 이동 구현

🎯 이 단계에서 배울 것


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

구분 이번 단계 예시 설명
내가 만드는 것 TetrisBlock의 MoveLeft/MoveRight/MoveDown/Rotate, ValidMove 우리가 정의하는 “이동·회전 규칙”과 “이동 가능한지” 판단. 테트리스 규칙은 전부 우리 로직.
엔진(API)이 주는 것 MonoBehaviour, Start()/Update(), FindObjectOfType, Transform, Time.deltaTime, Vector3Int.RoundToInt Unity가 제공. Update()는 엔진이 “매 프레임” 불러 주는 자리.

왜 엔진 것을 쓰나?
Update()에 우리 코드를 넣으면 매 프레임 실행됩니다. Time.deltaTime으로 “1초에 한 칸” 같은 규칙만 짜면 됩니다.


📝 실습 내용

📐 이번 단계 후 예시 구조
Spawner 사용 시 씬은 아래와 같고, Play하면 블록 하나가 생성됩니다.

씬 (Tetris)
├─ Main Camera
├─ GameBoard
├─ Spawner            ← Spawner (Script), Block Prefab·Spawn Point 연결
│  └─ (Spawn Point 등)  ← 빈 오브젝트, Position (5, 18, 0) 권장. Inspector에서 Spawner에 드래그 연결
└─ (Play 시) 스폰된 블록 1개   ← TetrisBlock 스크립트 붙은 프리팹 인스턴스

📌 기본 위치 (이렇게 두면 됨)
| 오브젝트 | Transform Position | 비고 | | ————— | ——————————— | ————————————————————- | | GameBoard | (0, 0, 0) | 보드 논리 좌표 = 월드 좌표. 바닥이 y=0 | | Spawn Point | (5, 18, 0) | 블록이 나오는 위치. 보드 위쪽 중앙 | | Spawner | (0,0,0)이든 (5,18,0)이든 상관없음 | 스크립트만 담는 빈 오브젝트. 실제 스폰 위치는 Spawn Point |

Spawn Point만 (5, 18, 0) 으로 두면 블록이 그 위치에 나옵니다.

전체 시스템·클래스 구조 → 시스템 설계 개요


📌 작업 씬
상단 씬 탭이 Tetris인지 확인하세요. 아니면 → 씬 이름 변경.
발음 → 영어 용어 발음·표기

0. Spawner 점증적 도입 — 블록을 씬에 두기

씬에 블록이 하나 있어야 이동·회전을 테스트할 수 있습니다.

방법 A — 수동: 3단계 프리팹(I_Block 등)을 Hierarchy로 끌어다 놓기.
방법 B — Spawner: Spawner 스크립트로 Start에서 블록 한 개만 생성. (8단계에서 반복·랜덤 추가)

방법 B 사용 시 — Spawner를 최소 기능으로 둡니다.

  1. C# 스크립트 생성
    • 이름: Spawner
  2. 다음 코드 작성 (4단계용 최소 버전) ```csharp using UnityEngine;

public class Spawner : MonoBehaviour // [MonoBehaviour 상속] { public GameObject blockPrefab; // [우리 멤버] Inspector에서 3단계 프리팹 연결 public Transform spawnPoint; // [우리 멤버] 생성 위치 (Inspector에서 연결)

void Start()   // [Unity가 호출] 상속받은 메소드 — 엔진이 한 번 호출
{
    SpawnNext();   // [우리 메소드]
}

public void SpawnNext()   // [우리 메소드] 4단계: 블록 한 개만. 8단계에서 랜덤·게임오버 추가
{
    if (blockPrefab == null)
    {
        return;   // blockPrefab: [우리 멤버]
    }
    Vector3 pos = spawnPoint != null ? spawnPoint.position : new Vector3(5f, 18f, 0f);   // spawnPoint: [우리 멤버], position: [Unity 프로퍼티]
    Instantiate(blockPrefab, pos, Quaternion.identity);   // Instantiate: [Unity API]
    Debug.Log($"블록 생성됨 (Spawner) — 위치: {pos}");   // Debug.Log: [Unity API]
} } ```
  1. 빈 GameObject 생성 (이름: “Spawner”(읽는 법: 스포너)) → 위 스크립트 추가
    • [Hierarchy] (읽는 법: 하이어러키) 빈 공간 우클릭 → Create Empty → 이름 Spawner 로 변경 → [Hierarchy > Spawner]
    • [Project > Assets > Scripts > Spawner.cs] 를 [Hierarchy > Spawner] 위로 드래그하여 스크립트 추가
    • 블록 위치는 Spawn Point Position입니다. Spawner는 빈 오브젝트라 위치는 상관없습니다.
  2. Spawn Point
    • Hierarchy 우클릭 → Create Empty → 이름 SpawnPoint
    • (선택) SpawnPoint를 Spawner 자식으로 드래그
    • [SpawnPoint] 선택 → Position (5, 18, 0) (보드 위쪽 중앙)
    • [Spawner] 선택 → Spawn Point 칸에 SpawnPoint 드래그 연결
  3. Block Prefab
    • [Spawner] → Block Prefab 칸에 [Prefabs > I_Block] 드래그
    • 프리팹 루트에 TetrisBlock 스크립트가 있어야 합니다. → 3단계
  4. Play → 블록 하나 생성되면 이동·회전 테스트

스폰은 되는데 안 떨어지면 → 프리팹 루트에 TetrisBlock 있는지 확인.
블록이 안 보이면 → Spawn Point (5,18,0), GameBoard Width/Height 10·20, 카메라 설정 확인.

8단계에서 Spawner에 반복·랜덤 추가 → 8단계. 개념 → Instantiate/Destroy


1. GameBoard 연결 및 기본 구조

연결 vs 찾기: Inspector에서 GameBoard를 연결해도 됩니다. 이번 단계는 찾기(FindObjectOfType)로 하고, 연결은 본문 끝 “연결 방식 (선택)” 참고.

📌 코드 주석
[우리 멤버] = 우리가 만든 변수, [Unity API] = Unity가 주는 것. 주석만 보고 구분하면 됩니다.

표기 의미
[우리 멤버] 우리 변수. gameBoard, fallSpeed 등
[우리 메소드] 우리 함수. ValidMove(), MoveLeft() 등
[Unity가 호출] Start(), Update() — 이름만 맞추면 엔진이 호출
[Unity API] Unity 함수. FindObjectOfType(), GetChild() 등

📌 순서
1번 → 2번(ValidMove) → 3번(이동) → 4번(Update) → 5번(Rotate) 순서로 같은 TetrisBlock.cs에 넣습니다. 끝에 전체 코드 있음.

Assets/Scripts/TetrisBlock.cs — 1번: 클래스와 멤버, Start()

(Update()는 MoveDown()을 부르므로, MoveDown을 만든 3번 다음에 4번에서 넣습니다.)

using UnityEngine;

public class TetrisBlock : MonoBehaviour   // [MonoBehaviour 상속]
{
    private GameBoard gameBoard;   // [우리 멤버]
    public float fallSpeed = 1f;   // [우리 멤버] 낙하 속도 (초)
    private float fallTimer = 0f;  // [우리 멤버]

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

2. 이동 가능한지 확인하는 함수 (ValidMove)

📍 넣는 위치
| 항목 | 위치 | | ———- | ————————————————————— | | 파일 | Assets/Scripts/TetrisBlock.cs | | 클래스 | TetrisBlock 안 | | 삽입 | Start() 를 닫는 } 바로 다음 · 클래스 맨 끝 } 앞 |

1번에서 만든 Start()의 } 다음 줄에 아래 ValidMove 를 넣습니다. (이동 함수들이 이걸 먼저 써서, 순서상 ValidMove를 먼저 정의합니다.)

왜 자식 위치를 비교하나요?
블록은 칸이 여러 개(자식 4개)라서, 한 칸이라도 보드 밖이면 이동하면 안 됩니다. 그래서 자식 위치를 하나씩 검사합니다. 부모만 보면 나머지 칸이 밖인지 알 수 없습니다.

하나로 묶어서 콜라이더로 검사하면 안 되나요?
가능합니다. 블록을 하나의 콜라이더(또는 복합 콜라이더)로 두고, 이동 전에 Physics.Overlap·충돌 검사로 “보드 경계·다른 블록과 겹치는지” 보는 방식입니다. 이번 단계에서는 그리드(칸) 단위만 쓰고, 물리 엔진 없이 칸 좌표 범위만 검사하는 방식으로 합니다. 나중에 물리/콜라이더 기반으로 바꾸고 싶으면 그때 설계를 바꾸면 됩니다.

💡 충돌 시스템(물리)을 안 쓰는 이유 — 테트리스는 블록이 칸 단위로 딱 맞게 쌓이고, 행 제거도 칸 단위입니다. 물리 엔진은 픽셀 단위라 칸과 어긋나기 쉽고, “이동 가능 여부”를 우리가 직접 제어하기 어렵습니다. 그리드로 판단하면 칸과 일치하고, 규칙을 코드로 다루기 쉽습니다.

아이디어: 부모가 옮겨질 위치를 두고, 각 자식은 “부모 예정 위치 + 자식 로컬 오프셋(월드로)”으로 칸을 계산. TransformDirection(localPosition) = 로컬 오프셋을 월드 방향으로 변환.

    public bool ValidMove(Vector3 offset)   // [우리 메소드] 이동·회전 전 검사용 (보호 수준은 8단계에서 정리)
    {
        Vector3 parentNew = transform.position + offset;   // 부모가 옮겨질 예정 위치
        for (int i = 0; i < transform.childCount; i++)
        {
            Transform child = transform.GetChild(i);
            Vector3 childWorld = parentNew + transform.TransformDirection(child.localPosition);   // 부모 예정 위치 + 자식의 로컬 오프셋(월드로)
            Vector3Int cell = Vector3Int.RoundToInt(childWorld);
            if (cell.x < 0 || cell.x >= gameBoard.width || cell.y < 0 || cell.y >= gameBoard.height)
            {
                return false;
            }
        }
        return true;
    }

Vector3Int.RoundToInt(Vector3)
Unity API. Vector3(소수 좌표)를 한 번에 정수 칸 좌표로 바꿉니다. Mathf.RoundToInt를 x, y 따로 부르지 않아도 됩니다.

📌 연산자 설명: += , && , || (이번 단계에서 자주 쓰는 것)

이번 단계 코드에서 += , && , **   ** 가 나옵니다. 의미를 알아두면 읽기 편합니다.

+= 연산자 (복합 대입) — 쉽게 이해하기

쉽게 말하면: 저장소(변수)에 있는 값에, 조금을 더해서, 그 결과를 다시 그 저장소에 넣는 것입니다.

두 가지 쓰는 법 (똑같은 뜻)

즉, += 는 “더한 다음에, 그 결과를 다시 그 변수에 넣는다”를 한 번에 쓰는 기호라고 보면 됩니다.

숫자로 보기 (자동 낙하)

이동할 때

헷갈리면: “더하기를 한 다음, 그 결과를 다시 그 변수(저장소)에 넣는 것“이라고만 생각해도 됩니다. a += b = “a에 b를 더해서, 다시 a에 넣기.”

&& (그리고) 와 || (또는) — 쉽게 이해하기

|| (또는) — 쉽게 말하면

&& (그리고) — 쉽게 말하면

ValidMove에서는 “범위 밖인가?”를 묻습니다.

반대로 “범위 안”이면

정리
| 묻는 말 | 쉽게 말하면 | 연산자 | | —————- | ——————– | ————— | | 범위 밖인가? | 하나라도 해당되면 밖 | || (또는) | | 범위 안인가? | 모두 만족해야 안 | && (그리고) |

헷갈리면: **   ** = “또는” = 하나라도 맞으면 된다. && = “그리고” = 다 맞아야 된다.

3. 이동 함수 구현 (MoveLeft, MoveRight, MoveDown)

📍 넣는 위치
| 항목 | 위치 | | ———- | ——————————————————————- | | 파일 | Assets/Scripts/TetrisBlock.cs | | 클래스 | TetrisBlock 안 | | 삽입 | ValidMove() 를 닫는 } 바로 다음 · 클래스 맨 끝 } 앞 |

ValidMove 다음에 아래 MoveLeft, MoveRight, MoveDown 을 넣습니다. (Update()에서 MoveDown()을 부르므로, MoveDown을 먼저 정의한 뒤 4번에서 Update를 넣습니다.)

    public void MoveLeft()   // [우리 메소드]
    {
        if (ValidMove(Vector3.left))   // ValidMove: [우리 메소드], Vector3.left: [Unity 프로퍼티]
        {
            transform.position += Vector3.left;   // transform, position: [Unity 프로퍼티]
        }
    }
    public void MoveRight()   // [우리 메소드]
    {
        if (ValidMove(Vector3.right))
        {
            transform.position += Vector3.right;
        }
    }
    public void MoveDown()   // [우리 메소드]
    {
        if (ValidMove(Vector3.down))
        {
            transform.position += Vector3.down;
        }
        // else: 6단계에서 LockPiece() 호출
    }

작동 방식:

  1. ValidMove()로 이동 가능한지 확인
  2. 가능하면 transform.position을 직접 변경
  3. 부모가 이동하면 자식 블록들도 함께 이동 (Unity의 Transform 시스템)

4. 자동 낙하 (Update)

📍 넣는 위치
| 항목 | 위치 | | ———- | —————————————————————— | | 파일 | Assets/Scripts/TetrisBlock.cs | | 클래스 | TetrisBlock 안 | | 삽입 | MoveDown() 를 닫는 } 바로 다음 · 클래스 맨 끝 } 앞 |

이제 MoveDown() 이 정의돼 있으므로, 그 다음에 Update() 를 넣습니다. Update() 안에서 MoveDown()을 부릅니다.

    void Update()   // [Unity가 호출] 매 프레임 엔진이 호출
    {
        fallTimer += Time.deltaTime;   // Time.deltaTime: [Unity 프로퍼티]
        if (fallTimer >= fallSpeed)
        {
            MoveDown();   // [우리 메소드] — 위 3번에서 정의함
            fallTimer = 0f;
        }
    }

5. 회전 기능 구현 (Rotate)

📍 넣는 위치
| 항목 | 위치 | | ———- | —————————————————————- | | 파일 | Assets/Scripts/TetrisBlock.cs | | 클래스 | TetrisBlock 안 | | 삽입 | Update() 를 닫는 } 바로 다음 · 클래스 맨 끝 } 앞 |

Update() 다음 줄에 아래 Rotate만 넣습니다.

아이디어: 부모만 돌리면 자식은 알아서 따라 감. 자식 위치를 바꿀 필요 없음. 돌린 뒤 ValidMove로 확인하고, 나가면 다시 돌려서 되돌리기만 하면 됨.

    public void Rotate()   // 90도 시계 방향
    {
        transform.Rotate(0f, 0f, -90f);
        if (!ValidMove(Vector3.zero))
        {
            transform.Rotate(0f, 0f, 90f);
        }
    }

요약: 부모 Rotate 한 번 → ValidMove로 확인 → 나가면 Rotate 반대. 자식 포지션 변경 없음.

GameBoard 연결 방식 (선택)

찾기 대신 연결을 쓰려면 아래처럼 바꿀 수 있습니다.

  1. TetrisBlock에서 gameBoard를 Inspector에 노출하고, Start에서 찾지 않기:
    [SerializeField] private GameBoard gameBoard;   // [Unity 프로퍼티] Inspector에서 연결
    // Start() 안의 FindObjectOfType<GameBoard>() 줄은 삭제
    
  2. 수동으로 씬에 둔 블록: [Hierarchy] 에서 TetrisBlock이 붙은 오브젝트 선택 → [Inspector > Tetris Block (Script)] 의 Game Board 칸에 [Hierarchy > GameBoard] 를 드래그.
  3. Spawner로 스폰되는 블록: 프리팹은 씬에 없을 때 만들기 때문에 Inspector에서 GameBoard를 미리 넣을 수 없습니다. Spawner가 Instantiate한 직후 GetComponent<TetrisBlock>().SetGameBoard(FindObjectOfType<GameBoard>()) 처럼 한 번만 찾아서 넣어 주는 방식을 8단계에서 쓸 수 있습니다. (Spawner 쪽에서 GameBoard를 [SerializeField]로 연결해 두고, 그걸 블록에 넘겨 주면 됩니다.)

이렇게 하면 의존성이 Inspector에 보이고, GameBoard가 비어 있으면 바로 확인할 수 있습니다.

4단계 끝난 뒤 TetrisBlock.cs 전체 (연결 확인용)

삽입 순서는 1번 → 2번(ValidMove) → 3번(이동) → 4번(Update) → 5번(Rotate)입니다. 아래 전체 코드에서는 이동·회전 메소드를 묶어서 Rotate를 MoveDown 다음에 두었습니다. (동작은 같고, 읽기만 편하게.)

using UnityEngine;

public class TetrisBlock : MonoBehaviour   // [MonoBehaviour 상속]
{
    // ----- [우리 멤버] -----
    private GameBoard gameBoard;   // 게임 보드 참조 (Start에서 찾음)
    public float fallSpeed = 1f;  // 낙하 속도: 몇 초에 한 칸
    private float fallTimer = 0f; // 낙하 타이머 (Update에서 누적)

    void Start()   // [Unity가 호출] 씬에 올라온 뒤 한 번
    {
        gameBoard = FindObjectOfType<GameBoard>();   // [Unity API] 씬에서 GameBoard 찾기
    }

    // [우리 메소드] offset 방향으로 한 칸 갔을 때 보드 범위 안인지 (부모 예정 위치 기준)
    // ※ 6단계에서 IsCellEmpty 체크를 추가합니다 → 6단계-행-제거-시스템.md 의 "1-1. ValidMove 업데이트" 참고
    public bool ValidMove(Vector3 offset)
    {
        Vector3 parentNew = transform.position + offset;
        for (int i = 0; i < transform.childCount; i++)
        {
            Transform child = transform.GetChild(i);
            Vector3 childWorld = parentNew + transform.TransformDirection(child.localPosition);
            Vector3Int cell = Vector3Int.RoundToInt(childWorld);
            if (cell.x < 0 || cell.x >= gameBoard.width || cell.y < 0 || cell.y >= gameBoard.height)
            {
                return false;
            }
        }
        return true;
    }

    public void MoveLeft()   // [우리 메소드] 5단계 InputHandler가 호출
    {
        if (ValidMove(Vector3.left))
        {
            transform.position += Vector3.left;   // [Unity 프로퍼티]
        }
    }
    public void MoveRight()
    {
        if (ValidMove(Vector3.right))
        {
            transform.position += Vector3.right;
        }
    }
    public void MoveDown()
    {
        if (ValidMove(Vector3.down))
        {
            transform.position += Vector3.down;
        }
        // else: 6단계에서 바닥이면 LockPiece() 호출
    }

    public void Rotate()   // 90도 시계 방향 (부모만 돌림, 자식은 따라 감)
    {
        transform.Rotate(0f, 0f, -90f);
        if (!ValidMove(Vector3.zero))
        {
            transform.Rotate(0f, 0f, 90f);
        }
    }

    void Update()   // [Unity가 호출] 매 프레임
    {
        fallTimer += Time.deltaTime;   // [Unity 프로퍼티] 프레임 간 경과 시간
        if (fallTimer >= fallSpeed)
        {
            MoveDown();
            fallTimer = 0f;
        }
    }
}

🔗 이전 단계와의 연결

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

단계 이 단계에서 하는 일 4단계와의 연결
2단계 GameBoard, width/height, grid, IsValidPosition 4단계 TetrisBlock이 gameBoard.width, gameBoard.height로 범위 검사 (ValidMove)
3단계 블록 프리팹(I_Block, O_Block 등) 만들기 4단계에서 Spawner가 그 프리팹을 Instantiate로 씬에 하나 생성 → 그 블록을 이동·회전 (위 0. Spawner 점증적 도입 참고)
4단계 TetrisBlock 이동·회전 + Spawner 점증적 도입 Spawner가 Start에서 블록 한 개만 Instantiate. 블록은 수동으로 둘 수도 있음

4단계에서 블록이 씬에 오는 방법 (둘 중 하나):

이후 단계:

작동 방식:

💡 이 방식의 장점

이 방식의 장점 (프리팹 기반):

💡 이번 단계에서 익히는 개념

📌 복기: 변수(저장소)를 다루는 함수 = 메소드(MoveLeft, MoveRight, Rotate). 한 번에 이해 못했어도 변수 · 멤버와 메소드에서 다시 실습해 보세요.

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

📌 복기로 위 개념을 떠올린 뒤, 아래 기초 개념 문서에서 점진적으로 확장된 실습을 진행하세요. 단계가 올라갈수록 같은 개념이 조금씩 확장되므로, 차근차근 따라 하면 됩니다.

개념 문서 이번 단계와의 연결
멤버와 메소드 저장소(멤버)와 그걸 다루는 함수(메소드). AddScore 실습으로 짝 느끼기
Vector Vector3.left, right, down. 위치·방향 실습
Time.deltaTime 자동 낙하 타이머. 1초마다 실행하는 실습
충돌 감지 ValidMove처럼 “이동 가능한지” 확인. CanMoveTo 실습

📐 Transform 확장 설명 & 4단계 확장 실습

4단계에서 position, localPosition, GetChild, childCount 등을 사용합니다. 스케일(scale), 로테이션(rotation), 월드 vs 부모 기준(local) 등은 아래에서 확장해서 볼 수 있습니다.

4단계 확장 실습은 변수 → 멤버 → 배열 순으로 점증적으로 실습할 수 있는 개별 파일입니다. 이전 단계(1~3단계)를 다 이해하지 못했더라도 각 실습 파일만 따라 하면 진행할 수 있도록 되어 있고, 필요할 때만 이전 단계·개념 문서 링크를 참고하면 됩니다.

확장 실습 내용 이전 단계 링크 (필요 시 참고)
01 - 변수와 Transform 변수에 위치·크기 저장 후 Transform에 적용 1단계, 4단계, 변수
02 - 멤버와 Transform 멤버 변수 + 메소드로 이동 (MoveRight 등) 4단계, 멤버와 메소드
03 - 배열과 Transform 배열에 자식 localPosition 저장 후 90도 회전 적용 4단계, 배열, 회전 수학

📐 단계별 확장 실습 전체 목차 (1~8단계 분산): 단계별 확장 실습 (목차)

📺 UI 기본 구조 만들기 (선택사항, 7단계에서 자세히 설명)

게임을 테스트하면서 점수를 확인하려면 UI가 필요합니다. 7단계에서 자세히 설명하지만, 미리 만들어두고 싶다면:

  1. Canvas 생성
    • Hierarchy에서 우클릭 → “UI > Canvas” 선택
  2. 기본 Text 생성 (나중에 사용)
    • Canvas 선택 후 우클릭 → “UI > Text - TextMeshPro” 또는 “UI > Legacy > Text”
    • 이름: “ScoreText”
    • Text: “Score: 0”
    • 위치: Pos X: 400, Pos Y: 200

참고: UI는 7단계에서 자세히 설명합니다. 지금은 게임 로직에 집중하세요!

⚠️ 4단계 강의대로 했는데 동작이 안 될 때

아래를 순서대로 확인하세요.

1. 4단계 vs 5단계

1-1. 스폰 위치·블록 위치가 이상할 때 (스폰됐는데 안 떨어지거나 위치가 어색할 때)

위치는 Spawn Point Position입니다 (Spawner 아님).

확인 할 일
스폰 위치 SpawnPoint Position (5, 18, 0)
스폰되는데 안 떨어짐 프리팹 루트에 TetrisBlock 있는지 → 3단계
위치가 어색함 카메라 (5, 10, -10), Size 10~12 → 카메라 가이드

1-2. 바닥이 안 맞아 보일 때

확인 할 일
GameBoard Position (0, 0, 0)
화면 바닥 카메라 (5, 10, -10), Size 10 → 화면 아래 = y=0
맨 아래 줄 반쯤 잘림 카메라 Size 10.5로 키우기

→ 좌표 처리 가이드

2. GameBoard

3. Width / Height

4. 블록 한 개 (Spawner 사용 시)

5. 씬 이름

6. 블록 안 보일 때

7. FindObjectOfType 경고가 뜰 때

8. 키로 안 움직일 때

→ 5단계 - 키 눌러도 안 될 때 · 회전 2번 후 안 됨


✅ 확인 사항

📖 기초 개념 학습으로 이어가기

위 표의 기초 개념 학습을 진행한 뒤, 좌표·게임 흐름이 궁금하면 좌표 처리 가이드, 프로그램 처리 흐름에서 점진적으로 이어가세요.

✅ 다음 단계 전 점검: 작업 씬이 Tetris인지 재차 확인하세요. Tetris가 아니면 씬 이름 변경 방법을 따라 Tetris로 바꾼 뒤 다음 단계로 넘어가세요.


← 이전 단계: 3단계 - 테트리스 블록 만들기 다음 단계: 5단계 - 입력 처리 →