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를 최소 기능으로 둡니다.
- C# 스크립트 생성
- 이름:
Spawner
- 이름:
- 다음 코드 작성 (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]
} } ```
- 빈 GameObject 생성 (이름: “Spawner”(읽는 법: 스포너)) → 위 스크립트 추가
- [Hierarchy] (읽는 법: 하이어러키) 빈 공간 우클릭 → Create Empty → 이름 Spawner 로 변경 → [Hierarchy > Spawner]
- [Project > Assets > Scripts > Spawner.cs] 를 [Hierarchy > Spawner] 위로 드래그하여 스크립트 추가
- 블록 위치는 Spawn Point Position입니다. Spawner는 빈 오브젝트라 위치는 상관없습니다.
- Spawn Point
- Hierarchy 우클릭 → Create Empty → 이름 SpawnPoint
- (선택) SpawnPoint를 Spawner 자식으로 드래그
- [SpawnPoint] 선택 → Position (5, 18, 0) (보드 위쪽 중앙)
- [Spawner] 선택 → Spawn Point 칸에 SpawnPoint 드래그 연결
- Block Prefab
- [Spawner] → Block Prefab 칸에 [Prefabs > I_Block] 드래그
- 프리팹 루트에 TetrisBlock 스크립트가 있어야 합니다. → 3단계
- 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 따로 부르지 않아도 됩니다.
📌 연산자 설명: += , && , || (이번 단계에서 자주 쓰는 것)
| 이번 단계 코드에서 += , && , ** | ** 가 나옵니다. 의미를 알아두면 읽기 편합니다. |
+= 연산자 (복합 대입) — 쉽게 이해하기
쉽게 말하면: 저장소(변수)에 있는 값에, 조금을 더해서, 그 결과를 다시 그 저장소에 넣는 것입니다.
두 가지 쓰는 법 (똑같은 뜻)
fallTimer = fallTimer + Time.deltaTime;← “지금 fallTimer에 Time.deltaTime을 더한 값을, fallTimer에 넣는다”fallTimer += Time.deltaTime;← 위와 완전히 같은 뜻인데, 짧게 쓴 것입니다.
즉, += 는 “더한 다음에, 그 결과를 다시 그 변수에 넣는다”를 한 번에 쓰는 기호라고 보면 됩니다.
숫자로 보기 (자동 낙하)
- 처음:
fallTimer= 0 - 1프레임 후:
fallTimer += 0.016→ fallTimer = 0 + 0.016 = 0.016 - 2프레임 후:
fallTimer += 0.016→ fallTimer = 0.016 + 0.016 = 0.032 - … 이렇게 조금씩 더해져서 1초(예: 1.0)가 되면 MoveDown()을 부르고, fallTimer를 0으로 돌립니다.
이동할 때
transform.position += Vector3.left;
→ “지금 위치” + “왼쪽 한 칸” = 새 위치 → 그 새 위치를 다시 position에 넣기 → 블록이 왼쪽으로 한 칸 이동한 것처럼 보입니다.
헷갈리면: “더하기를 한 다음, 그 결과를 다시 그 변수(저장소)에 넣는 것“이라고만 생각해도 됩니다. a += b = “a에 b를 더해서, 다시 a에 넣기.”
&& (그리고) 와 || (또는) — 쉽게 이해하기
|| (또는) — 쉽게 말하면
- “둘 중 하나라도 맞으면 된다” 라고 생각하면 됩니다.
예: “오늘 비 오거나 눈 오면 우산 챙긴다” → 비 오기 또는 눈 오기 하나라도 해당되면 우산 챙김. - 코드에서는:
A || B→ A가 참이거나 B가 참이면 결과가 true. 둘 다 거짓일 때만 false.
&& (그리고) — 쉽게 말하면
- “둘 다 맞아야 한다” 라고 생각하면 됩니다.
예: “x가 0 이상 이고 10 미만이다” → 둘 다 만족해야 true.
ValidMove에서는 “범위 밖인가?”를 묻습니다.
- 범위 밖 = “x가 너무 작다 또는 x가 너무 크다 또는 y가 너무 작다 또는 y가 너무 크다”
→ 이 중 하나라도 해당되면 “범위 밖”이에요. 그래서 || (또는) 를 씁니다. - 읽는 법:
cell.x < 0 || cell.x >= width || ...
= “x가 0 미만 이거나, x가 width 이상 이거나, … 하나라도 참이면” → 범위 밖 → false(이동 불가).
반대로 “범위 안”이면
- “x가 0 이상 이고 width 미만 이고 y가 0 이상 이고 height 미만” → 모두 만족해야 하므로 && 를 씁니다.
정리
| 묻는 말 | 쉽게 말하면 | 연산자 |
| —————- | ——————– | ————— |
| 범위 밖인가? | 하나라도 해당되면 밖 | || (또는) |
| 범위 안인가? | 모두 만족해야 안 | && (그리고) |
| 헷갈리면: ** | ** = “또는” = 하나라도 맞으면 된다. && = “그리고” = 다 맞아야 된다. |
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() 호출
}
작동 방식:
ValidMove()로 이동 가능한지 확인- 가능하면
transform.position을 직접 변경 - 부모가 이동하면 자식 블록들도 함께 이동 (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 연결 방식 (선택)
찾기 대신 연결을 쓰려면 아래처럼 바꿀 수 있습니다.
- TetrisBlock에서
gameBoard를 Inspector에 노출하고, Start에서 찾지 않기:[SerializeField] private GameBoard gameBoard; // [Unity 프로퍼티] Inspector에서 연결 // Start() 안의 FindObjectOfType<GameBoard>() 줄은 삭제 - 수동으로 씬에 둔 블록: [Hierarchy] 에서 TetrisBlock이 붙은 오브젝트 선택 → [Inspector > Tetris Block (Script)] 의 Game Board 칸에 [Hierarchy > GameBoard] 를 드래그.
- 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단계에서 블록이 씬에 오는 방법 (둘 중 하나):
- 수동: Hierarchy에 3단계 프리팹을 Project에서 끌어다 놓기
- Spawner(점증적): Spawner 클래스를 두고
SpawnNext()로 블록 한 개만 Instantiate (위 0. Spawner 점증적 도입 — 방법 B에 코드 있음)
이후 단계:
- 6단계:
gameBoard.IsCellEmpty(),AddToGrid(),LockPiece()로 블록 고정 - 8단계: Spawner 확장 — 4단계에서 만든 Spawner에 프리팹 배열·랜덤·게임 오버·LockPiece에서 SpawnNext() 호출 추가. 8단계 - 게임 완성 참고.
작동 방식:
- 프리팹을 Instantiate하면 자식 블록들이 함께 생성됨
- 부모(TetrisBlock)의
transform.position을 변경하면 자식도 함께 이동 - 자식 블록의
localPosition을 변경하면 회전 효과
💡 이 방식의 장점
이 방식의 장점 (프리팹 기반):
- ✅ 간단함:
transform.position직접 변경 - ✅ 직관적: Unity 좌표를 그대로 사용
- ✅ Vector3Int.RoundToInt(Vector3): 좌표를 한 번에 정수 칸으로 변환하여 그리드에 맞춤
- ✅ 자식 블록 활용:
transform.GetChild()로 직접 접근 - ❌ Dictionary/List 불필요: 2차원 배열만 알면 됨
💡 이번 단계에서 익히는 개념
- += (복합 대입) = “기존 값에 더해서 다시 넣기”.
fallTimer += Time.deltaTime,transform.position += Vector3.left처럼 이번 단계에서 자주 씁니다. 위 연산자 설명 참고. -
&& (그리고) = 조건이 모두 참일 때만 true. ** (또는)** = 조건 하나라도 참이면 true. 범위 밖인가? → 하나라도 해당되면 밖 → ** ** 사용. 범위 안인가? → 모두 만족해야 안 → && 사용. ValidMove는 “범위 밖인가?”를 묻므로 ** ** 를 씁니다. 위 연산자 설명 참고. - 저장소를 다루는 함수 = 어떤 변수(저장소)의 값을 읽거나 바꾸는 함수
예:MoveLeft()는position이라는 저장소를 바꿉니다.
이런 식으로 “저장소 + 그걸 다루는 함수”가 짝이 됩니다. - Start() = 준비가 끝난 뒤 한 번 (예: GameBoard 찾기, 초기 위치)
Update() = 매 프레임 반복 (예: 자동 낙하 타이머)
📌 복기: 변수(저장소)를 다루는 함수 = 메소드(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) 등은 아래에서 확장해서 볼 수 있습니다.
- Transform 개념 확장 (스케일, 로테이션, localPosition, 부모·자식 정리)
→ Transform (개념 확장)
4단계 확장 실습은 변수 → 멤버 → 배열 순으로 점증적으로 실습할 수 있는 개별 파일입니다. 이전 단계(1~3단계)를 다 이해하지 못했더라도 각 실습 파일만 따라 하면 진행할 수 있도록 되어 있고, 필요할 때만 이전 단계·개념 문서 링크를 참고하면 됩니다.
| 확장 실습 | 내용 | 이전 단계 링크 (필요 시 참고) |
|---|---|---|
| 01 - 변수와 Transform | 변수에 위치·크기 저장 후 Transform에 적용 | 1단계, 4단계, 변수 |
| 02 - 멤버와 Transform | 멤버 변수 + 메소드로 이동 (MoveRight 등) | 4단계, 멤버와 메소드 |
| 03 - 배열과 Transform | 배열에 자식 localPosition 저장 후 90도 회전 적용 | 4단계, 배열, 회전 수학 |
📐 단계별 확장 실습 전체 목차 (1~8단계 분산): 단계별 확장 실습 (목차)
📺 UI 기본 구조 만들기 (선택사항, 7단계에서 자세히 설명)
게임을 테스트하면서 점수를 확인하려면 UI가 필요합니다. 7단계에서 자세히 설명하지만, 미리 만들어두고 싶다면:
- Canvas 생성
- Hierarchy에서 우클릭 → “UI > Canvas” 선택
- 기본 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단계
- 4단계: 자동 낙하만. 키보드 이동은 5단계 InputHandler 추가 후.
- 낙하가 안 되면 → 아래 2~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
- Hierarchy에 GameBoard + Game Board (Script) 있어야 함.
- 없으면 Console에 NullReferenceException. ValidMove/Rotate에서 gameBoard 접근 시 발생.
3. Width / Height
- GameBoard Width 10, Height 20 확인. 0이면 판단 오류.
4. 블록 한 개 (Spawner 사용 시)
- Spawner에 Block Prefab, Spawn Point 연결. 프리팹 루트에 TetrisBlock → 3단계.
5. 씬 이름
- 씬 탭이 Tetris인지. 아니면 → 씬 이름 변경.
6. 블록 안 보일 때
- 카메라 (5, 10, -10), Size 10~12 → 5단계 테스트 전 설정 · 카메라 가이드.
7. FindObjectOfType 경고가 뜰 때
- Unity 6에서 해당 API에 경고가 뜨면 → 그 줄만 FindFirstObjectByType으로 바꾸면 됩니다. 동작은 같습니다.
8. 키로 안 움직일 때
→ 5단계 - 키 눌러도 안 될 때 · 회전 2번 후 안 됨
✅ 확인 사항
- 블록이 자동으로 낙하하는가? (4단계에서 확인)
- 5단계까지 했다면: 블록이 좌우로 이동하는가?
- 5단계까지 했다면: 회전 기능이 작동하는가?
📖 기초 개념 학습으로 이어가기
위 표의 기초 개념 학습을 진행한 뒤, 좌표·게임 흐름이 궁금하면 좌표 처리 가이드, 프로그램 처리 흐름에서 점진적으로 이어가세요.
✅ 다음 단계 전 점검: 작업 씬이 Tetris인지 재차 확인하세요. Tetris가 아니면 씬 이름 변경 방법을 따라 Tetris로 바꾼 뒤 다음 단계로 넘어가세요.
| ← 이전 단계: 3단계 - 테트리스 블록 만들기 | 다음 단계: 5단계 - 입력 처리 → |