본문 바로가기

TIL

[TIL]2024-1-3 / 8일차 - '또' 어려웠던 스네이크 게임 만들기

 

"스네이크 게임"

 

역시나라면 역시나였다. 어제 진행했던 2번 과제에 비해 훨씬 어려웠다. 

 

어려움에 원인 중 하나는 이전에 수동적인 input을 기다리는 방식들에 비해 이미 능동적으로 움직이고 있는 물체를 콘솔로 만들어야 한다는 것이다.

이런 점에서 많은 부분 강의에서 알지 못했던 내용을 찾아봤어야 했다. 너무 많은 시간을 소모하게 될 것 같아 많은 부분을 주변에 도움을 받으면서도 답안의 해석을 보면서 해석하여 해결하기도 해야했다.

공부용으로 써야 했던 많은 cs 창들...

 

 

3-1 과제 스네이크 게임 만들기

 

본 과제는 클래스와 객채, 생성자, 상속 등 이전에 단순히 위에서 아래로 진행되는 코드의 느낌에서 훨씬 복잡한 구조로 넘어가게 되었다. 여기서 각 클래스를 작성하고 생성자를 쓰는 법등에서 크게 헤매었다.

 

과제를 해결하는데 중요했던 추가점 중 하나로 Console.KeyAvailable Console.ReadKey() 기능 ConsoleKey. 등의 기능을 알게 된 것이었다. 처음 머리 아프고 생각하기 힘들었던 것은 방향키를 입력하면 뱀이 방향을 바꾸고 움직이게 만드는 것이었고 그것을 구현하는데 가장 큰 영향을 끼쳤던 기능들 이었기 때문이다. 

 

우선 기본 제공되는 기능들을 기반으로 구현하길 요구되는 기능들 중에 알만한 기능부터 구현을 해야 했다.

 

단순하게 뱀이 돌아다닐 필드를 Console.WriteLine등으로 구현하는 것.

그런데 이번에는 이전처럼 단순하게 이차원배열로 필드를 저장하거나 WriteLine으로 하나하나 적기에는 제시된 필드의 크기가 x=80 y=20으로 틱택토의 9칸과 비교하여 매우 크기 때문에 좀 더 깔끔하게 구현할 방법을 강구해야 했다.

 

그렇기에 하나하나 적기보다 특정 범위까지 Write로 채우는 방법을 생각해야했다.

for문을 통해 CursorPosition을 체크하여 채우는 방법으로 구현 하였다.

더보기

static void Field()
{
    for (int i = 1; i < 20; i++)
    {
        Console.SetCursorPosition(0, i);
        Console.Write("W");
        Console.SetCursorPosition(80, i);
        Console.Write("W");
    }
    for (int i = 0; i < 80; i++)
    {
        Console.SetCursorPosition(i, 0);
        Console.Write("W");
        Console.SetCursorPosition(i, 20);
        Console.Write("W");
    }
}

키 입력이 있을 때마다 방향전환을 실행하는 코드는 답안에서 보고 새로 배운 코드를 이용해야 했다. 이는 미래를 위해 메모해놓고자 한다.

// 키 입력이 있는 경우에만 방향을 변경합니다.
if (Console.KeyAvailable)
{
    var key = Console.ReadKey(true).Key;

    switch (key)
    {
        case ConsoleKey.W:
            snake.direction = Direction.UP;
            break;
        case ConsoleKey.S:
            snake.direction = Direction.DOWN;
            break;
        case ConsoleKey.A:
            snake.direction = Direction.LEFT;
            break;
        case ConsoleKey.D:
            snake.direction = Direction.RIGHT;
            break;
    }
}

 

이후 움직이는 방법을 구현하는데 가장 오랜시간을 고민해야 했다.

결국 답안의 내용을 공부해야 했다.

public void Move()
{
    Point tail = length.First();
    length.Remove(tail);
    Point head = isMoving();
    length.Add(head);

    tail.Clear();
    head.Draw();
}

public Point isMoving()
{
    Point head = length.Last();
    Point nextPoint = new Point(head.x, head.y, head.sym);
    switch (direction)
    {
        case Direction.LEFT:
            nextPoint.x -= 2;
            break;
        case Direction.RIGHT:
            nextPoint.x += 2;
            break;
        case Direction.UP:
            nextPoint.y -= 1;
            break;
        case Direction.DOWN:
            nextPoint.y += 1;
            break;
    }
    return nextPoint;
}

얼마나 오래 시도했는지는 적을 시간이 부족하여 넘기기로 하겠다.

약간 사소한 디테일 하나를 보자면 좌/우 이동때는 2칸씩 움직이도록 되어 있는데 이는 세로의 길이가 가로보다 길어 속도를 맞추기 위해서이다. 

 

그렇지만 이를 위해서는 필드에 소환될 뱀의 먹이가 x축이 짝수값에 있어야 뱀이 먹이에 닿을 수 있다.

뱀의 머리를 기준으로 접촉을 체크하고 2칸씩 움직이는 뱀의 머리가 홀수 좌표에 있는 먹이를 넘어가기 때문이다.

이를 가장 직관적으로 해결하는 방법은 애초에 먹이가 짝수값 x축에만 소환되게 하는 것 이었다.

이를 위해 추가된 코드는

public Point CreateFood()
{
    int y = random.Next(1, mapHeight);
    int x = random.Next(1, mapWidth);
    if (x % 2 == 0) // x좌표 속도 맞추기 위해 2단위로 만들기
    {
        return new Point(x+1, y, Icon);
    }
    else
    {
        return new Point(x, y, Icon);
    }
}

 

기존 CreateFood()메소드에 if조건문을 통해 랜덤 값이 홀수이면 +1을 x좌표에 더해주고 아니면 그대로 배치하는 코드를 만들었다.

그렇지만 답안 해설을 보니 이를 ?를 이용한 3차조건연산자로 해결한 것 같지만 아직 배워보지 못했고 알고있지 못했던 방법이다. 이후 공부해봐야 할 것.

 

이후 패배 조건은 각각 뱀이 본인과 닿았을 때, 벽에 닿았을 때 인데 이미 좌표값이 겹치는 것에 대한 코드는 준비되어 있어 이를 이용하여 뱀의 경우 리스트(length) 갯수를 카운트하여서 / 벽의 경우 WriteLine으로 만들어놓은 벽의 좌표까지 뱀의 머리가 이동하게 되면 이라는 bool 체크 조건을 만들어 해결하였다.

public bool isColideB()
{
    var head = length.Last();
    for (int i = 0; i < length.Count - 1; i++)
    {
        if (head.IsHit(length[i])) 
        { 
            return true;    
        }
    }
    return false;
}
public bool isColideW()
{ 
    var head = length.Last();
    if(head.x <= 0 || head.x >= 80 || head.y <= 0 || head.y >= 20)
        return true;
    return false;
}

 

 

이후 뱀이 먹이를 먹으면 길이가 길어지는 코드는 코드에 대한 좀 더 이해가 필요했다.

왜냐면 지금 움직이는 뱀의 코드가 간단하게 '뱀'이라는 리스트의 처음과 끝의 좌표를 체크해 머리와 꼬리로 저장되어 움직이는 상황인데 처음에 생각했던 방법은 그저 머리 혹은 꼬리에 +1을 해주면 되지 않을까? 했지만 해당 머리와 꼬리는 좌표를 쫓는 것이기에 단순하게 +1을 한다고 리스트가 늘어나는게 아니었다.

 

그래서 답안에서 했던 방식을 보고 기막힌 방법이라 생각해 배껴보게 되었다.

"먹이를 뱀으로 바꿔 머리에 이어붙이고 먹이를 다시 소환하는 것"

아마 끝까지 이런 방법은 생각 못했을 것이라고 생각한다.

public bool Eating(Point food)
{
    Point head = isMoving();
    if (head.IsHit(food))
    {
        food.sym = head.sym; 
        length.Add(food);
        food.Draw();
        return true;
    }
    return false;
}

 

비록 보고 하더라도 새로운 것을 배운다면 부끄러움 없이 배우는 것도 좋은 자세일지라.

이런 다양한 방법으로 문제 해결은 지금 내가 골머리를 썩힌다고 해낼 수 있는 것은 아니었다.

이렇게 한번 베끼더라도 내 것으로 만들어 나중에 응용할 수 있으면 될 것이다.

 

이런 식으로 많은 과정을 겪어 문제를 발견하고 해결하는 과정을 거쳐 결국 기본 요구되는 모든 기능을 구현된 코드가 완성되었다. 비록 많은 부분 답안을 보고 하며 해석하여 이해해야 했던, 이전 고난이었던 틱택토에 비해 직접 만든 파트의 비중이 적어 아쉬울 수 있는 코드지만, 힘들기로는 훨씬 힘들었던 것 같아 해결된 것으로 만족한다.

 

전체 코드는 접은 글로서 남겨둔다.

더보기

스네이크 게임

static void Field()
{
    for (int i = 1; i < 20; i++)
    {
        Console.SetCursorPosition(0, i);
        Console.Write("W");
        Console.SetCursorPosition(80, i);
        Console.Write("W");
    }
    for (int i = 0; i < 80; i++)
    {
        Console.SetCursorPosition(i, 0);
        Console.Write("W");
        Console.SetCursorPosition(i, 20);
        Console.Write("W");
    }
}

static void Main(string[] args)
{
    int foodCount = 0;
    Field();
    // 뱀의 초기 위치와 방향을 설정하고, 그립니다.
    Point p = new Point(4, 5, '+');
    Snake snake = new Snake(p, 4, Direction.RIGHT);
    snake.Draw();

    // 음식의 위치를 무작위로 생성하고, 그립니다.
    FoodCreator foodCreator = new FoodCreator(80, 20, '@');
    Point food = foodCreator.CreateFood();
    food.Draw();


    // 게임 루프: 이 루프는 게임이 끝날 때까지 계속 실행됩니다.
    while (true)
    {
        // 키 입력이 있는 경우에만 방향을 변경합니다.
        if (Console.KeyAvailable)
        {
            var key = Console.ReadKey(true).Key;

            switch (key)
            {
                case ConsoleKey.W:
                    snake.direction = Direction.UP;
                    break;
                case ConsoleKey.S:
                    snake.direction = Direction.DOWN;
                    break;
                case ConsoleKey.A:
                    snake.direction = Direction.LEFT;
                    break;
                case ConsoleKey.D:
                    snake.direction = Direction.RIGHT;
                    break;
            }
        }
        
        if (snake.Eating(food))
        {
            food = foodCreator.CreateFood();
            foodCount++;
        }
        else
        {
            snake.Move();
        }
        food.Draw();

        if (snake.isColideB() || snake.isColideW())
        {
            break;
        }

        // 뱀이 이동하고, 음식을 먹었는지, 벽이나 자신의 몸에 부딪혔는지 등을 확인하고 처리하는 로직을 작성하세요.
        // 이동, 음식 먹기, 충돌 처리 등의 로직을 완성하세요.

        Thread.Sleep(100); // 게임 속도 조절 (이 값을 변경하면 게임의 속도가 바뀝니다)

        // 뱀의 상태를 출력합니다 (예: 현재 길이, 먹은 음식의 수 등)
        Console.SetCursorPosition(15, 22);
        Console.WriteLine("현재 먹은 음식 숫자: " + foodCount);
        Console.SetCursorPosition(15, 23);
        Console.WriteLine("현재 뱀의 길이: " +  (foodCount+4));
    }
    Console.SetCursorPosition(15, 24);
    Console.WriteLine("***###게임에서 패배하셨습니다###***");
}

public class Snake
{
    public List<Point> length;
    public int sLength;
    public Direction direction;

    public Snake(Point point, int _sLength, Direction _direction)
    {
        sLength = _sLength;
        direction = _direction;
        length = new List<Point>();
        for (int i = 0; i < _sLength; i++)
        {
            Point p = new Point(point.x, point.y, '+');
            length.Add(p);
            point.x += 1;
        }
    }
    public void Draw()
    {
        foreach (Point p in length)
        {
            p.Draw();
        }
    }
    public void Move()
    {
        Point tail = length.First();
        length.Remove(tail);
        Point head = isMoving();
        length.Add(head);

        tail.Clear();
        head.Draw();
    }
    
    public Point isMoving()
    {
        Point head = length.Last();
        Point nextPoint = new Point(head.x, head.y, head.sym);
        switch (direction)
        {
            case Direction.LEFT:
                nextPoint.x -= 2;
                break;
            case Direction.RIGHT:
                nextPoint.x += 2;
                break;
            case Direction.UP:
                nextPoint.y -= 1;
                break;
            case Direction.DOWN:
                nextPoint.y += 1;
                break;
        }
        return nextPoint;
    }
    public bool Eating(Point food)
    {
        Point head = isMoving();
        if (head.IsHit(food))
        {
            food.sym = head.sym; 
            length.Add(food);
            food.Draw();
            return true;
        }
        return false;
    }
    public bool isColideB()
    {
        var head = length.Last();
        for (int i = 0; i < length.Count - 1; i++)
        {
            if (head.IsHit(length[i])) 
            { 
                return true;    
            }
        }
        return false;
    }
    public bool isColideW()
    { 
        var head = length.Last();
        if(head.x <= 0 || head.x >= 80 || head.y <= 0 || head.y >= 20)
            return true;
        return false;
    }
}
public class FoodCreator
{
    int mapWidth;
    int mapHeight;
    char Icon;
    public FoodCreator(int newWidth, int newHeight, char newIcon)
    {
        mapWidth = newWidth;
        mapHeight = newHeight;
        Icon = newIcon;
    }
    Random random = new Random();
    public Point CreateFood()
    {
        int y = random.Next(1, mapHeight);
        int x = random.Next(1, mapWidth);
        if (x % 2 == 0) // x좌표 속도 맞추기 위해 2단위로 만들기
        {
            return new Point(x+1, y, Icon);
        }
        else
        {
            return new Point(x, y, Icon);
        }
    }
}
public class Point
{
    public int x { get; set; }
    public int y { get; set; }
    public char sym { get; set; }

    // Point 클래스 생성자
    public Point(int _x, int _y, char _sym)
    {
        x = _x;
        y = _y;
        sym = _sym;
    }

    // 점을 그리는 메서드
    public void Draw()
    {
        Console.SetCursorPosition(x, y);
        Console.Write(sym);
    }

    // 점을 지우는 메서드
    public void Clear()
    {
        sym = ' ';
        Draw();
    }

    // 두 점이 같은지 비교하는 메서드
    public bool IsHit(Point p)
    {
        return p.x == x && p.y == y;
    }
}
// 방향을 표현하는 열거형입니다.
public enum Direction
{
    LEFT,
    RIGHT,
    UP,
    DOWN
}

 

실제 구동하면. 먹이를 54개 먹었다. 테스트를 계속 하다보니 잘하게 되어가는게 이래서 개발자들이 초보자 튜토리얼을 만들기 어려워 한다는 것일까?

 

 

비록 과제 하나를 끝냈지만 블랙잭 게임을 만드는 3-2번 과제가 남아있으며 이 또한 크게 어려운 것으로 예상되는 상황에

금요일까지 5번 강의까지 마무리 하고 개인 과제 제출까지 해야되는 시점에 너무 진행된 것이 없어 시간에 크게 쫓기고 있다.

 

시간에 쫓기면 패닉하는 나에게는 남은 목 금이 힘든 날들이 될 거 같다. 아악