
struct Position // 데카르트 좌표계
struct Direction // 뱀 방향 벡터
enum Direct // Direction 자료형 열거
static void Main(string[] args)
{
bool gameOver = false; // 게임 기본 조건
Start(); // 뱀, 음식 초기 위치 및 방향 벡터 설정. 맵 설정
while(gameOver == false)
{
Render(); // 맵, 오브젝트 구현 및 출력 함수
Input(); // 입력부 ConsoleKey 할당 -> Update랑 통합?
Update(); // 뱀 이동 로직, 성장, 음식 생성 구현, 종료 조건 설정
}
End(); // 종료 메세지, score 출력
}
static void Start()
{
// 뱀, 음식의 초기 위치 Position 표현
// 뱀 방향 벡터 Direction 표현
// 뱀 몸통 List<Position> 표현
// 맵 좌표 설정 - char[y,x] 표현. 데카르트 좌표와 반대
}
static void Render()
{
// 1. 맵 이중 for 문으로 구현.
// 2. 음식 위치, 생성 기준 맵 위에 구현
// 3. 뱀 위치, 성장, 이동 방향 기준 움직임 맵 위의 구현
}
static void Input()
{
// 입력이 없는 경우 계속 움직이도록 Console.KeyAvailable 사용
}
static void Update()
{
// 사용자 입력 ConsoleKey.WASD 및 Arrow로 한정.
// 입력값에 따른 방향 벡터 설정
// 뱀 성장 로직 구현 - 음식 좌표 = 뱀 머리 좌표일 때 List에 음식 좌표값 추가
// 음식 맵 내에 랜덤 생성 - Random, Next() 기능 사용.
// 게임 종료 조건 설정 - 뱀의 머리가 몸통 혹은 벽에 닿은 경우 종료
}
static void End()
{
// 종료 안내 메세지 및 score(List.Length) 출력
}
struct Position
{
public int x;
public int y;
}
struct Direction
{
public Direct direct;
}
enum Direct
{
UP, LEFT, DOWN, RIGHT
}
좌표와 방향 설정을 위해 구조체 및 열거형 선언을 했다.
static void Start()
{
snakePos.x = 5; // 뱀 초기 위치
snakePos.y = 5;
foodPos.x = 5; // 먹이 초기 위치
foodPos.y = 6;
snakeDir.direct = Direct.RIGHT; // 뱀 초기 방향
snakeList.Add(snakePos);
map = new char[20, 20]; // 맵 설정
for (int i = 0; i < 20; i++)
{
for (int j = 0; j < 20; j++)
{
if (i == 0 || j == 0 || i == 19 || j == 19)
{
map[i, j] = ' ';
}
else
{
map[i, j] = '□';
}
}
}
}
여기서 고민한 것은 맵을 제작할 때 뱀의 움직임과 가장 자연스럽게 보일 수 있을까? 였다. 이를 위해 맵을 이루는 타일과 뱀의 몸통을 동일한 사각형으로 제작을 했다.
static void Render()
{
Console.SetCursorPosition(0, 0); // 기준 위치
PrintMap(); // 1. 맵 구현
PrintFood(); // 2. 음식 구현 - 순서 중요!
PrintSnake(); // 3. 뱀 구현
}
위 순서는 반드시 지켜져야한다. 그려지는 순서는 단순히 레이어드라고 생각하면 편하다. 맵 구현이 이전 정보를 리셋하고 빈 도화지를 맨 아래에 생성하는 역할을 하기 때문에 1순위이며, 음식이 뱀보다 먼저 구현이 되어야 뱀이 음식 위에 있기 때문에, 음식을 먹을 때 뱀이 음식을 가려 조금 더 자연스러운 표현이 가능하다.
static void PrintMap()
{
for (int i = 0; i < 20; i++)
{
for (int j = 0; j < 20; j++)
{
if (map[i, j] != '■')
{
Console.Write(map[i, j]);
}
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write(map[i, j]);
Console.ResetColor();
}
}
Console.WriteLine();
}
}
static void PrintFood()
{
Console.SetCursorPosition(foodPos.x, foodPos.y);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine('●');
Console.ResetColor();
}
static void PrintSnake()
{
if (snakeDir.direct == Direct.UP)
{
snakePos.y--;
}
else if (snakeDir.direct == Direct.LEFT)
{
snakePos.x--;
}
else if (snakeDir.direct == Direct.DOWN)
{
snakePos.y++;
}
else if (snakeDir.direct == Direct.RIGHT)
{
snakePos.x++;
}
snakeList.RemoveAt(0);
snakeList.Add(snakePos);
for (int i = 0; i < snakeLength; i++)
{
Console.SetCursorPosition(snakeList[i].x, snakeList[i].y);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine('■');
Console.ResetColor();
}
}
맵과 음식을 출력하는데에는 큰 어려움이 없었으나, 뱀을 출력하기 위해서는 고려할 요소가 조금 많았다. 우선 방향 벡터를 활용하여 사용자 입력이 없는 경우 해당 방향으로 계속 가도록 if문으로 구현했으며, 앞으로 가면서 흔적이 지워질 수 있도록, 뱀의 꼬리 좌표[0]를 List에서 제거하고 나아갈 좌표를 새롭게 List에 추가하였다. 이렇게 하면 방향 전환이 이루어져도 머리가 갔던 길로 몸통이 따라갈 수 있게 된다.
if (Console.KeyAvailable) // 입력 받으면 아래의 기능 수행
{
ConsoleKeyInfo key = Console.ReadKey();
}
Input의 경우 KeyAvailable을 사용하기 위해 Update와 통합했다.
static void Update()
{
Move();
SnakeAdd();
Food();
gameOver = IsDead();
}
Update의 경우 게임의 근본이 되는 로직이기에 신경써야할 요소들이 많았다.
static void Move()
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo key = Console.ReadKey();
switch (key.Key)
{
case ConsoleKey.W:
case ConsoleKey.UpArrow:
snakeDir.direct = Direct.UP;
break;
case ConsoleKey.A:
case ConsoleKey.LeftArrow:
snakeDir.direct = Direct.LEFT;
break;
case ConsoleKey.S:
case ConsoleKey.DownArrow:
snakeDir.direct = Direct.DOWN;
break;
case ConsoleKey.D:
case ConsoleKey.RightArrow:
snakeDir.direct = Direct.RIGHT;
break;
}
}
}
뱀의 움직임의 경우 Render에 주 로직을 설정했기에, 키 입력이 있는 경우 입력값에 따라 방향 전환이 이루어질 수 있도록 구현했다. KeyAvailable을 사용함에 따라 키 입력이 없는 경우 기존의 방향을 계속해서 유지한다. 해당 기능은 단순히 키입력이 있으면~ 이라고 해석하면 편하다.
static void SnakeAdd()
{
if (snakePos.x == foodPos.x && snakePos.y == foodPos.y)
{
snakeList.Add(foodPos);
}
}
음식과 뱀의 머리 좌표가 일치하는 경우, 음식 좌표를 List에 추가시켜 새로운 뱀의 몸통이 되도록 구현했다.
static void FoodSpawn()
{ // 뱀이 음식을 먹을 경우, 맵 안에 랜덤 스폰되도록 구현.
Random foodNewPos = new Random();
if (playerPos.x == foodPos.x && playerPos.y == foodPos.y)
{
foodPos.x = foodNewPos.Next(1, map.GetLength(1) - 1);
foodPos.y = foodNewPos.Next(1, map.GetLength(0) - 1);
snakeLength++;
}
//맵에 랜덤으로 생성되도록 맵 크기 한정한 난수 생성
}
마찬가지로 음식과 뱀의 머리 좌표가 일치하는 경우, 맵 내부 공간 한정하여 난수 추출로 새로운 음식 좌표 값을 설정했다. 또한 score의 역할을 하는 snakeLength가 여기서 증가하도록 만들었다.
static bool IsDead()
{
{
for (int i = 0; i < snakeLength - 2; i++)
{
if (snakeList[snakeLength - 1].x == snakeList[i].x &&
snakeList[snakeLength - 1].y == snakeList[i].y)
{
gameOver = true;
}
}
if (map[snakeList[snakeLength - 1].y, snakeList[snakeLength - 1].x] == ' ')
{
gameOver = true;
}
}
return gameOver;
}
snakeList[snakeLength - 1] 은 단순히 뱀의 머리를 의미한다. 즉 뱀의 머리 좌표가 뱀의 몸통에 닿는 경우 게임오버가 되도록 구현했고, 머리가 벽에 닿는 경우에도 게임오버가 되도록 했다.
static void End()
{
// 최대 길이 출력, 수고 메세지
Console.Clear();
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Game Over");
Console.ResetColor();
Console.WriteLine($"Score : {snakeLength}");
}