6단계: 행 제거 시스템
이 단계에서 배울 것
- 블록을 보드(그리드)에 고정
- 가득 찬 행 감지·제거
- 위 블록들을 아래로 이동
점수·UI는 7단계에서 다룹니다. 6단계에서는 블록 고정 + 행 제거에만 집중합니다.
시작 전 점검
| 확인 항목 | 확인 방법 |
|---|---|
| 입력 (5단계) | Play → 화살표/WASD로 블록이 좌우·회전·아래로 움직이는지 |
| GameBoard·Spawner | Hierarchy에 GameBoard, Spawner, 블록 프리팹(루트에 TetrisBlock)이 있고 연결돼 있는지 |
| Spawn Point Y < 20 | Spawn Point의 Position Y가 18 정도인지 (height=20이면 19 미만이어야 블록이 보드 안에서 시작) |
| 보드가 논리좌표와 어긋남 | 7단계에서 게임 보드 최종 완성할 때 씬에서 손으로 맞춥니다. 코드로 좌표 맞추지 않음. |
붙여넣기 순서 (한눈에)
컴파일 에러 없이 진행하려면 이 순서로 넣으세요.
| 순서 | 파일 | 추가할 것 |
|---|---|---|
| 1 | GameBoard.cs | IsCellEmpty |
| 2 | GameBoard.cs | AddToGrid |
| 3 | GameBoard.cs | IsRowFull, ClearRow, MoveRowsDown, ClearFullRows |
| 4 | TetrisBlock.cs | ValidMove 수정 (IsCellEmpty 체크 추가) |
| 5 | TetrisBlock.cs | LockPiece 추가 |
| 6 | TetrisBlock.cs | MoveDown 수정 (else에 LockPiece 호출) |
0. 게임 보드 대충 맞춰 두기 (6단계)
GameBoard가 없으면 2단계 - 게임 보드 만들기 먼저: Hierarchy 우클릭 → Create Empty → 이름 GameBoard → GameBoard.cs 드래그.
보드 스프라이트(배경)를 쓰려면
GameBoard 자식으로 빈 오브젝트 BoardBackground 만들고, Sprite Renderer 추가 → Square 연결. Position·Scale은 씬에서 대충 맞춰 두면 됩니다. (블록이 보드 안에 보이게만 조절.)
최종 위치·크기는 7단계에서 Play 보면서 손으로 맞춥니다. 6단계에서는 코드만 넣고, 보드는 대충만 두어도 됩니다.
💡 손으로 대충 맞추는 이유 — 해상도·카메라·화면 비율마다 보이는 구간이 달라서, 코드로 고정 좌표를 넣으면 기기마다 어긋날 수 있습니다. 씬에서 눈으로 맞추면 단순하고, 나중에 카메라나 UI 바꿔도 다시 조절하기 쉽습니다.
1. IsCellEmpty (GameBoard.cs)
GameBoard.cs에 추가:
public bool IsCellEmpty(int x, int y)
{
if (x < 0 || x >= width || y < 0 || y >= height)
return false;
return grid[x, y] == null;
}
4단계까지 ValidMove는 보드 범위만 체크했습니다. 블록을 그리드에 쌓기 시작하면 이미 쌓인 칸도 막아야 하므로 이 함수가 필요합니다.
💡 충돌 시스템(물리)을 안 쓰는 이유 — 테트리스는 칸 단위로 블록이 딱 맞게 쌓이고, 행 제거도 칸 단위입니다. Unity 물리(Rigidbody, OnCollisionEnter)를 쓰면 픽셀 단위로 움직여서 칸과 어긋나기 쉽고, “이동 가능 여부”를 우리가 직접 제어하기 어렵습니다. 그리드(
grid,IsCellEmpty)로 판단하면 칸과 일치하고, 규칙을 코드로 다루기 쉽습니다.
2. AddToGrid (GameBoard.cs)
GameBoard.cs에 추가:
public void AddToGrid(TetrisBlock block)
{
Vector3 parentPos = block.transform.position;
for (int i = block.transform.childCount - 1; i >= 0; i--) // 반드시 역순
{
Transform child = block.transform.GetChild(i);
Vector3 childWorld = parentPos + block.transform.TransformDirection(child.localPosition);
Vector3Int cell = Vector3Int.RoundToInt(childWorld);
if (cell.x >= 0 && cell.x < width && cell.y >= 0 && cell.y < height)
{
child.SetParent(transform);
grid[cell.x, cell.y] = child;
child.position = new Vector3(cell.x, cell.y, 0f);
}
}
}
왜 역순인가? SetParent를 하면 자식이 block에서 빠져 childCount가 줄어듭니다. 앞(i=0)에서부터 돌면 인덱스가 밀려 4개 중 2개만 처리됩니다. 뒤에서부터 돌면 이미 처리한 쪽만 빠지므로 전부 처리됩니다.
여기까지 넣었으면 Play → 블록을 바닥까지 내려보세요. 블록이 고정되면 AddToGrid가 동작한 겁니다.
3. 행 제거 함수 (GameBoard.cs)
GameBoard.cs에 추가 — LockPiece에서 ClearFullRows()를 부르므로 LockPiece보다 먼저 넣어야 합니다:
bool IsRowFull(int row)
{
for (int x = 0; x < width; x++)
{
if (grid[x, row] == null)
return false;
}
return true;
}
void ClearRow(int row)
{
for (int x = 0; x < width; x++)
{
if (grid[x, row] != null)
{
Destroy(grid[x, row].gameObject);
grid[x, row] = null;
}
}
}
void MoveRowsDown(int clearedRow)
{
for (int y = clearedRow + 1; y < height; y++)
{
for (int x = 0; x < width; x++)
{
if (grid[x, y] != null)
{
grid[x, y - 1] = grid[x, y];
grid[x, y] = null;
grid[x, y - 1].position = new Vector3(x, y - 1, 0f);
}
}
}
}
public int ClearFullRows()
{
int linesCleared = 0;
for (int y = 0; y < height; y++)
{
if (IsRowFull(y))
{
ClearRow(y);
MoveRowsDown(y);
y--;
linesCleared++;
}
}
return linesCleared;
}
행 제거 흐름: 아래(y=0)부터 위로 검사 → 가득 찬 행 지우기 → 윗줄 한 칸씩 내리기 → y--로 같은 줄 재검사.
4. ValidMove 수정 (TetrisBlock.cs)
TetrisBlock.cs의 ValidMove를 아래처럼 수정합니다. 추가된 부분은 // ← 6단계 추가 표시:
public bool ValidMove(Vector3 offset) // 보호 수준(public) 확인은 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;
if (!gameBoard.IsCellEmpty(cell.x, cell.y)) // ← 6단계 추가
return false;
}
return true;
}
이걸 안 넣으면 블록이 쌓인 블록을 뚫고 지나갑니다.
5. LockPiece (TetrisBlock.cs)
TetrisBlock.cs에 추가:
public void LockPiece()
{
gameBoard.AddToGrid(this);
gameBoard.ClearFullRows();
FindObjectOfType<Spawner>().SpawnNext();
Destroy(gameObject);
}
7단계에서 GameController를 만든 뒤,
int linesCleared = gameBoard.ClearFullRows();로 받아AddScore(linesCleared)를 호출하도록 수정합니다. 지금은 이대로 두면 됩니다.
6. MoveDown 수정 (TetrisBlock.cs)
4단계 MoveDown의 else에 LockPiece() 호출을 추가합니다:
public void MoveDown()
{
if (ValidMove(Vector3.down))
{
transform.position += Vector3.down;
}
else
{
LockPiece();
}
}
확인 사항
- 블록이 바닥에 닿으면 고정되는가?
- 고정된 블록 위에 다음 블록이 걸쳐서 쌓이는가?
- 한 줄을 채우면 제거되는가?
- 제거 후 윗줄이 아래로 내려오는가?
트러블슈팅
| 증상 | 원인·해결 |
|---|---|
| 블록이 쌓이지도 않았는데 사라짐 | Spawn Point Y가 height(20) 이상이면 ValidMove가 처음부터 false → 바로 LockPiece → 그리드에 안 들어가고 Destroy. Spawn Point Y를 18로 두세요. |
| 블록이 쌓인 블록을 뚫고 내려감 | ValidMove에 IsCellEmpty 체크를 안 넣은 경우. 위 4. ValidMove 수정 참고. |
| 4개 중 2개만 쌓임 | AddToGrid에서 for (int i = 0; ...) 정순으로 돌고 있음. 역순 for (int i = childCount - 1; ...) 으로 고치세요. |
| 행 제거 후 위치 어긋남 | MoveRowsDown에서 position += Vector3.down 대신 position = new Vector3(x, y-1, 0f)로 스냅해야 합니다. |
| gameBoard가 null | 씬에 GameBoard 오브젝트가 있는지, TetrisBlock Start()에서 FindObjectOfType<GameBoard>()로 찾고 있는지 확인. |
개념 정리
- 멤버 = 클래스 안에 선언한 변수 (예:
grid,width,height) - 메소드 = 멤버를 읽거나 바꾸는 함수 (예:
AddToGrid,ClearFullRows) - 프로퍼티 = 변수처럼 쓰지만 Unity가 내부 처리 (예:
transform,position,childCount)
확장 실습 → 6단계 확장 실습 01 - 그리드와 행 검사
| ← 5단계 - 입력 처리 | 7단계 - 점수 및 레벨 시스템 → |