[C#] Project. Console_3

Lingtea_luv·2025년 3월 22일

Project

목록 보기
3/38
post-thumbnail

Snake Game


1. pseudo 코드

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) 출력
}

2. 구조체, 열거형 선언

struct Position
{
    public int x;
    public int y;
}

struct Direction
{
    public Direct direct;
}

enum Direct
{ 
	UP, LEFT, DOWN, RIGHT
}

좌표와 방향 설정을 위해 구조체 및 열거형 선언을 했다.

3. Start( )

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] = '□';
            }
        }
    }
}

여기서 고민한 것은 맵을 제작할 때 뱀의 움직임과 가장 자연스럽게 보일 수 있을까? 였다. 이를 위해 맵을 이루는 타일과 뱀의 몸통을 동일한 사각형으로 제작을 했다.

4. Render( )

static void Render()
{
    Console.SetCursorPosition(0, 0);   // 기준 위치

    PrintMap();   // 1. 맵 구현
    PrintFood();  // 2. 음식 구현 - 순서 중요!
    PrintSnake(); // 3. 뱀 구현     
}

위 순서는 반드시 지켜져야한다. 그려지는 순서는 단순히 레이어드라고 생각하면 편하다. 맵 구현이 이전 정보를 리셋하고 빈 도화지를 맨 아래에 생성하는 역할을 하기 때문에 1순위이며, 음식이 뱀보다 먼저 구현이 되어야 뱀이 음식 위에 있기 때문에, 음식을 먹을 때 뱀이 음식을 가려 조금 더 자연스러운 표현이 가능하다.

  • Map

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();
    }
}
  • Food

static void PrintFood()
{
    Console.SetCursorPosition(foodPos.x, foodPos.y);
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine('●');
    Console.ResetColor();
}
  • Snake

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에 추가하였다. 이렇게 하면 방향 전환이 이루어져도 머리가 갔던 길로 몸통이 따라갈 수 있게 된다.

5. Input( )

if (Console.KeyAvailable)   // 입력 받으면 아래의 기능 수행
{
    ConsoleKeyInfo key = Console.ReadKey();
}

Input의 경우 KeyAvailable을 사용하기 위해 Update와 통합했다.

6. Update( )

static void Update()
{
    Move();
    SnakeAdd();
    Food();

    gameOver = IsDead();
}

Update의 경우 게임의 근본이 되는 로직이기에 신경써야할 요소들이 많았다.

  • Move

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을 사용함에 따라 키 입력이 없는 경우 기존의 방향을 계속해서 유지한다. 해당 기능은 단순히 키입력이 있으면~ 이라고 해석하면 편하다.

  • SnakeAdd

static void SnakeAdd()
{
    if (snakePos.x == foodPos.x && snakePos.y == foodPos.y)
    {
        snakeList.Add(foodPos);
    }
}

음식과 뱀의 머리 좌표가 일치하는 경우, 음식 좌표를 List에 추가시켜 새로운 뱀의 몸통이 되도록 구현했다.

  • FoodSpawn

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가 여기서 증가하도록 만들었다.

  • IsDead

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] 은 단순히 뱀의 머리를 의미한다. 즉 뱀의 머리 좌표가 뱀의 몸통에 닿는 경우 게임오버가 되도록 구현했고, 머리가 벽에 닿는 경우에도 게임오버가 되도록 했다.

7. End( )

static void End()
{
    // 최대 길이 출력, 수고 메세지
    Console.Clear();
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine("Game Over");
    Console.ResetColor();
    Console.WriteLine($"Score : {snakeLength}");
}

8. 앞으로 보완, 추가할 기능

  • 뱀이 뒤로가면 게임이 종료되는 조건
  • 맵 디자인을 좀 더 보기 쉽게 설정
  • 음식을 먹을수록 뱀의 속도가 빨라지는 기능
  • R키를 누르면 다시 시작하는 기능
  • 뱀의 머리와 몸통을 다른 모양으로 출력하는 기능
  • 입력이 끝나고 반영되기까지 딜레이 해결(가능 여부 확실치 않음

프로젝트 GitHub 링크

profile
뚠뚠뚠뚠

0개의 댓글