미니게임 005
제목 : 스네이크
지금까지는 아직 나에게 2차원 배열은 생소한 영역이어서 회피했었다.
하지만 언제까지 2차원 배열을 안쓸수도 없고, 쓰지 않고 작성하려니 한계가 왔다.
요번에는 2차원 배열을 공부하며 열심히 다른 사람 코드도 찾아보면서 구현 또는 구현 시도를 해보자
규칙
- 사방이 막혀있는 네모난 공간에 플레이어가 조종하는 뱀 한마리가 놓여진다
- 뱀은 현재 머리가 향하고 있는 방향으로 멈추지 않고 이동한다
- 플레이어의 조작으로 머리가 진행하는 방향만 바꿀 수 있다.
- 머리 방향은 상하좌우로 방향을 바꿀수 있으며 몸통으로 바로 회전할수 없다.
- 뱀이 벽이나 자신의 몸 일부에 머리를 부딪히면 죽는다
- 목적은 화면에 놓여있는 먹이를 먹는 것이다.
- 먹이를 여러개 스폰해도 되지만, 여기서는 하나를 먹을 때마다 하나가 랜덤으로 생성되는 방식을 택한다.
- 먹이를 먹을 때마다 몸통이 한칸씩 늘어난다
- 몸통이 늘어날 때마다 이동하는 속도를 조금씩 늘리자
스네이크는 변종도 많고 게임에 따라서 세부적인 룰이 다르지만 공통적 규칙이 있다
- 뱀은 무언가를 먹으면 길어진다
- 자신의 몸에 부딪히면 죽는다
어떤 자료구조가 좋을까?

스택 ( Stack )
- 한쪽 끝 ( Top ) 에서만 push / pop 하는 LIFO 구조이다.
- 뱀의 머리만 추가 / 제거하는 상황에서는 편할것 같다.
- 하지만 스네이크는 보통 머리로 한 칸 전진하면서 꼬리를 한 칸 지우는 ( 먹이를 먹지 않았다면 ) 양쪽 끝 조작이 필요하다.
- 스택만으로 구현하면 꼬리 제거가 비효율적이거나, 결국 보조 구조가 필요해진다.
연결 리스트 ( LinkedList<T> )
- 머리 ( Head ) 쪽에 새 노드 추가 ( 전진 )를 AddFirst 또는 AddLast
- 꼬리 제거 ( 한 칸 당기기 ) 를 RemoveFirst 또는 RemoveLast 로 처리 가능
- 따라서 전진 / 성장 로직이 간단해지고, 가독성이 좋아질 듯 하다
충돌 처리는 어떻게 해야 할까?
뱀의 머리가 몸통에 충돌 / 벽에 충돌 판정은 어떻게 해야 할까?
그리고 예외처리가 하나 더 필요하다
이번 턴에 성장하지 않는 경우, 지금 꼬리가 있던 자리로 머리가 들어가는 것은 허용해야 한다.
이동과 동시에 꼬리가 빠져나가니까 실질적으로는 빈칸이 된다.
- 벽 ( 경계 ) 충돌 : 다음 머리 좌표가 맵 밖이거나 벽 셀인지
- 뱀 몸통 충돌 : 다음 머리 좌표가 현재 뱀 몸에 이미 들어있는지
LinkedList<pos> body 로 뱀 신체를 구현해야 한다
HashSet<T>
수업에서 배우지 않은 키워드이다.
System.Collections.Generic 네임스페이스에 포함된 집합 ( Collection ) 자료형으로,
같은 값을 중복해서 저장할 수 없고, 요소의 순서가 보장되지 않는 구조이다.
이미 방문한 위치 저장 , 중복 없는 단어 목록 만들기 , 아이템 중복 방지 등에 쓰인다고 한다.
같은 값은 한 번만 들어가므로, 뱀의 머리와 몸통이 만났을때 충돌을 구현할 수 있지 않을까?
일단은 머리 속에서 기억만 하고 있고, 없는 채로 구현해보고 기능에 개선이 필요하면 써보는 걸로 하자.
2차원 배열
요번에는 생소한 2차원 배열을 이용해야 한다.
C# 에서 2차원 배열은 타입 [ , ] 이름 = new 타입 [ 행 , 열 ];
인덱스는 [ 행 , 열 ] 순서 ( 즉 [ x , y ] 라고 생각하면 편하다 )
이번 코드는 보드를 2차원 배열로 들고 있으면서 , 뱀의 좌표들은 LinkedList 로 관리해야 한다.
보드는 벽 / 빈칸 / 먹이 / 뱀 을 화면에 찍기 좋게 해준다
실제 이동과 성장 , 충돌 처리는 LinkedList 로 하면 될듯 하다.
스네이크 게임의 동작 원리
- 보드 생성
- 뱀 생성
- 매 프레임마다 키 입력 → 이동 → 충돌 판단 → 출력
- 게임 오버시 종료
동작되든 안되든 일단 작성해보자!
▼좌표를 담을 구조체 선언
public struct Pos // 좌표 값을 담는 구조체
{
public int X;
public int Y;
public Pos(int x, int y)
{
X = x;
Y = y;
}
public enum Direction
{
Up, Down, Left, Right
}
}
▼플레이 영역 설정을 위한 변수 선언
// 보드 설정
private const int BoardWidth = 30; // 가로 X
private const int BoardHeight = 30; // 세로 Y
private const int TickMs = 120; // 게임 속도
// 2차원 배열 보드
private static char[,] board;
// 2차원 배열에 그릴 문자들
private const char Wall = '#';
private const char Empty = ' ';
private const char Food = '*';
private const char Head = 'O';
private const char Body = 'o';
// 뱀 몸체 : 머리는 Last , 꼬리는 First
private static LinkedList<Pos> snake;
// 이동 방향
private static Pos.Direction dir;
// 먹이 좌표
private static Pos food;
▼점수표시와 난수 생성
// 점수 ( 먹이 하나당 1점 )
private static int score = 0;
private static bool isGameOver = false;
private static Random rnd = new Random();
▼플레이 영역 설정
private static void GameSetting()
{
// 2차원 보드 생성
board = new char[BoardHeight, BoardWidth];
// 전체를 빈 칸으로 채우기
int x = 0;
while (x < BoardHeight)
{
int y = 0;
while (y < BoardWidth)
{
board[x, y] = Empty; // 현재 위치에 채우기
y = y + 1; // y 좌표 하나씩 이동하면서 채우기
}
x = x + 1; // x 좌표 하나씩 이동하면서 채우기
}
// 외곽에 벽 세우기
int wx = 0;
while (wx < BoardWidth) // 위와 아래 벽 세우기
{
board[0, wx] = Wall;
board[BoardHeight - 1, wx] = Wall;
wx = wx + 1; // 한 칸씩 이동하면서 채우기
}
int wy = 0;
while (wy < BoardHeight) // 좌측과 우측 벽 세우기
{
board[wy, 0] = Wall;
board[wy, BoardWidth - 1] = Wall;
wy = wy + 1;
}
▼뱀 생성 위치 ( 중앙 )
// 뱀 초기 위치 ( 중앙 )
int startX = BoardWidth / 2;
int startY = BoardHeight / 2;
snake = new LinkedList<Pos>();
Pos p1 = new Pos(startX - 2, startY);
Pos p2 = new Pos(startX - 1, startY);
Pos p3 = new Pos(startX, startY);
snake.AddLast(p1);
snake.AddLast(p2);
snake.AddLast(p3);
dir = Pos.Direction.Right; // 초기 방향
▼LinkedList 로 순회하며 특정 좌표가 뱀 몸체인지 확인
private static bool IsOnSnake(Pos p) //링크드리스트 순회하며 특정 좌표가 뱀 몸 위치인지 확인
{
LinkedListNode<Pos> node = snake.First;
while (node != null)
{
Pos s = node.Value;
if (s.X == p.X && s.Y == p.Y)
{
return true;
}
node = node.Next;
}
return false;
}
▼먹이 생성
private static void SpawnFood()
{
// Board 내부 ( 벽을 제외 ) 난수 좌표 뽑아
// 뱀 몸과 겹치지 않게 생성해야함!!
int trySpawn = 0;
int tryMaxSpawn = 1000;
while ( trySpawn < tryMaxSpawn )
{
int foodX = rnd.Next(1, BoardWidth - 1);
int foodY = rnd.Next(1, BoardHeight - 1);
Pos spawnPos = new Pos(foodX, foodY);
// 뱀 위인지 확인 : 링크드리스트로 순회하며 체크
if ( !IsOnSnake(spawnPos))
{
food = spawnPos;
return;
}
trySpawn++;
}
}
▼키입력 처리
private static void HandleInput() // 키입력 처리 ( 뱀 머리 반대방향 즉시 전환 금지 )
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo key = Console.ReadKey(true);
if(key.Key == ConsoleKey.UpArrow)
{
if(dir != Pos.Direction.Down)
{
dir = Pos.Direction.Up;
}
}
else if (key.Key == ConsoleKey.DownArrow)
{
if (dir != Pos.Direction.Up)
{
dir = Pos.Direction.Down;
}
}
else if (key.Key == ConsoleKey.LeftArrow)
{
if (dir != Pos.Direction.Right)
{
dir = Pos.Direction.Left;
}
}
else if (key.Key == ConsoleKey.RightArrow)
{
if (dir != Pos.Direction.Left)
{
dir = Pos.Direction.Right;
}
}
}
}
▼이동 / 충돌 / 성장
private static void Step() // 이동 / 충돌 / 성장
{
// 현재 머리 좌표 ( Last 로 머리 사용 )
Pos head = snake.Last.Value;
int nextX = head.X;
int nextY = head.Y;
if (dir == Pos.Direction.Up)
{
nextY = nextY - 1;
}
else if (dir == Pos.Direction.Down)
{
nextY = nextY + 1;
}
else if (dir == Pos.Direction.Left)
{
nextX = nextX - 1;
}
else if(dir == Pos.Direction.Right)
{
nextX = nextX + 1;
}
Pos nextHead = new Pos(nextX, nextY);
if (nextX < 0 || nextX >= BoardWidth || nextY < 0 || nextY >= BoardHeight)
{
isGameOver = true;
return;
}
// 충돌 판단을 위해 "다음 칸"의 현재 보드 문자 확인
// 다음 칸이 꼬리이고 이번 턴에 성장 ( 먹이를 먹지 않으면 )하지 않으면,
// 꼬리는 곧 빠질 예정이므로 충돌이 아니다!!
char nextCell = board[nextY, nextX];
// 먹이를 먹는지 판단
bool willEat = (nextHead.X == food.X) && (nextHead.Y == food.Y);
// 꼬리 좌표
Pos tail = snake.First.Value;
// 다음 칸이 현재 꼬리 칸인지 판정
bool isNextTail = (nextHead.X == tail.X) && (nextHead.Y == tail.Y);
if (nextX < 0 || nextX >= BoardWidth || nextY < 0 || nextY >= BoardHeight)
{
isGameOver = true;
return;
}
// 벽 충돌 확인
if(nextCell == Wall)
{
isGameOver = true;
return;
}
// 몸 충돌
if (nextCell == Body)
{
bool conflictBody = true;
if (isNextTail && !willEat) // 꼬리 위치이고 성장하지 않는다면
{
conflictBody = false; // 충돌이 아님
}
if (conflictBody)
{
isGameOver = true;
return;
}
}
▼현재 상태를 보드에 반영
// 현재 상태 ( 벽 / 빈칸 / 먹이 / 뱀 )를 보드에 반영해야함
private static void RefreshBoard()
{
// 전체를 빈 칸으로 채우기
int y = 0;
while (y < BoardHeight)
{
int x = 0;
while (x < BoardWidth)
{
board[y, x] = Empty; // 현재 위치에 채우기
x = x + 1; // x 좌표 하나씩 이동하면서 채우기
}
y = y + 1; // y 좌표 하나씩 이동하면서 채우기
}
// 외곽에 벽 세우기
int wallX = 0;
while (wallX < BoardWidth) // 위와 아래 벽 세우기
{
board[0, wallX] = Wall;
board[BoardHeight - 1, wallX] = Wall;
wallX = wallX + 1; // 한 칸씩 이동하면서 채우기
}
int wallY = 0;
while (wallY < BoardHeight) // 좌측과 우측 벽 세우기
{
board[wallY, 0] = Wall;
board[wallY, BoardWidth - 1] = Wall;
wallY = wallY + 1;
}
// 먹이 생성
board[food.Y, food.X] = Food;
// 뱀 생성
LinkedListNode<Pos> node = snake.First;
while (node != null)
{
Pos p = node.Value;
// 마지막 노드 확인 ( 머리 / 몸통 구분 )
bool isHead = (node == snake.Last);
if (isHead)
{
board[p.X, p.Y] = Head;
}
else
{
board[p.X, p.Y] = Body;
}
node = node.Next;
}
}
▼매 프레임마다 전체 갱신
// 매 프레임 전체 갱신
private static void UpdateBoard()
{
// 커서를 0,0 이동시킨 후 화면 덮어씌울 목적
Console.SetCursorPosition(0, 0);
// 전체를 빈 칸으로 채우기
int y = 0;
while (y < BoardHeight)
{
int x = 0;
while (x < BoardWidth)
{
board[y, x] = Empty; // 현재 위치에 채우기
x = x + 1; // x 좌표 하나씩 이동하면서 채우기
}
y = y + 1; // y 좌표 하나씩 이동하면서 채우기
}
}
▼점수판 / 조작법 표시
private static void UpdateHUD()
{
int hudRow = BoardHeight; // 보드 아래줄에 표시
Console.SetCursorPosition(0, hudRow);
int clearLine = Math.Max(BoardWidth, 60); // 60줄 까지 비울것
int i = 0;
while ( i < clearLine )
{
Console.Write(' ');
i++;
}
Console.SetCursorPosition(0, BoardHeight);
Console.Write("Score : ");
Console.WriteLine(score);
Console.Write(" ↑ \n");
Console.Write("← ↓ → Move : Arrow Keys");
}
▼게임 루프 관리
private static void GameLoop()
{
while (!isGameOver)
{
HandleInput();
Step();
RefreshBoard();
UpdateBoard();
UpdateHUD();
Thread.Sleep(gameSpeed);
}
}
// 게임오버 안내
private static void GameOver()
{
Console.SetCursorPosition(0, BoardHeight + 2);
Console.WriteLine("======GAME OVER======");
Console.Write("Result : ");
Console.WriteLine(score);
Console.WriteLine("아무 키나 눌러주세요.");
Console.ReadKey(true);
}
'📖TIL > 🔥Projects' 카테고리의 다른 글
| 프로젝트 진행과정 009 (0) | 2025.10.06 |
|---|---|
| 프로젝트 진행과정 008 (0) | 2025.10.06 |
| 프로젝트 진행과정 006 (0) | 2025.10.05 |
| 프로젝트 진행과정 005 (0) | 2025.10.04 |
| 프로젝트 진행과정 004 (0) | 2025.10.03 |