본문 바로가기
프로그래밍/Algorithm

C#으로 미로 만들기

by bantomak 2023. 8. 24.

알고 가야 할 것들

기본적으로 게임은 메인 루프가 하나 있고 메인 루프를 돌며 게임을 올바르게 동작하기 위한 여러 로직을 처리한다.

메인 루프는 크게 세 가지로 분류된다.

 

  1.  유저의 입력 감지
  2. 유저의 입력과 기타 로직을 처리
  3. 렌더링 (화면에 뿌려줌)
    본 포스팅은 미로를 만들고 출력하는 것이 목적이기에 렌더링과 연관된 코드를 작성하려 한다.

렌더링 코드를 작성하기 전에 프레임(Frame)을 알아야 한다. 게임을 즐겨하는 사람이라면 "FPS"에 대해서 한 번쯤은 들어봤을 것이다. 우리가 플레이하는 게임 또는 영화와 같은 영상물은 연속된 사진들의 모음인데, 이 각각의 사진을 프레임이라고 부른다. FPS(Frames Per Second)란 초당 몇 개의 프레임(사진)을 화면에 뿌리는지 나타내는 수치이다.

 

일반적으로 사람의 눈은 60 프레임까진 시각적으로 어색함을 느끼지 않지만 60 프레임 미만으로 떨어지는 순간 영상이 뚝뚝 끊기는 이질감을 느끼게 된다.

 

결국 화면이 부드럽게 보인다 혹은 캐릭터가 부드럽게 움직인다는 의미는 짧은 시간 동안 렌더링을 여러 번 수행한다는 의미이다. 그렇다면 프로그래밍에서 프레임 관리는 어떻게 해야 할까? 간단하게 System.Environment.TickCount를 사용하면 된다. System.Environment.TickCount가 반환하는 값은 절대적인 시간의 개념이 아니라 시스템(컴퓨터)이 시작된 이후부터 경과된 시간을 밀리 세컨드로 반환한다. 우린 절대적인 시간의 개념이 필요한 게 아니라 마지막 측정 시간(lastTick)과 현재 시간(CurrentTick)의 차이만 알면 된다.

 

현재 시간과 이전 시간의 차이가 1/30 초보다 크다면 화면을 다시 렌더링 한다. 이를 우리는 30 FPS라고 부른다.

 

메인 루프 생성

(1초 / 30)의 시간마다 화면을 갱신하거나 유저의 입력을 받는다.

 

//이전 측정 시간
int lastTick = 0; 
while (true)
{
    //현재 측정 시간
    int currentTick = System.Environment.TickCount;
 
    //경과 시간이 1/30초보다 작다면?
    if (currentTick - lastTick < WAIT_TICK) 
        continue;
 
    lastTick = currentTick;
 }
static void Main(string[] args)
{
    const int WAIT_TICK = 1000 / 30;
    Console.CursorVisible = false;
 
    int lastTick = 0;
    while (true)
    {
        int currentTick = System.Environment.TickCount;
 
        //경과 시간이 1/30초보다 작다면? (30 FPS)
        if (currentTick - lastTick < WAIT_TICK)
            continue;
 
        lastTick = currentTick;
 
        // 1)사용자 입력 대기
 
        // 2)입력과 기타 로직 처리
 
        // 3)렌더링
        Console.SetCursorPosition(0, 0);
    }
}

 

2차원 배열로 사각형 출력하기

Board 클래스를 작성할 차례이다. Board를 초기화해 줄 InitializeBoard 메서드를 작성해서 보드의 크기를 받아주고 Render 메서드에서 Board의 크기만큼 이중 for문을 돌면서 특수문자 ('\u25cf')를 출력한다.

색이 밋밋하니 Console.ForegroundClor = ConsoleColor.Green;를 추가해 녹색 미로를 출력하도록 코드를 작성하자

 

/* Board */
class Board
{
    const char CIRCLE = '\u25cf';
    public int _size;
 
    public void InitializeBoard(int size)
    {
        _size = size;
    }
 
    public void Render()
    {
        for (int y = 0; y < _size; y++)
        {
            Console.ForegroundColor = ConsoleColor.Green;
 
            for (int x = 0; x < _size; x++)
                Console.Write(CIRCLE);
 
            Console.WriteLine();
        }
    }
}

 

다시 Program 클래스로 돌아와서 Board 객체를 만들어주고 Render 메서드를 호출하는 코드를 메인 루프 내에 추가해 준다.

 

/* Program.cs */
class Program
{
static void Main(string[] args)
{
    const int WAIT_TICK = 1000 / 30;
 
    Board board = new Board();
    board.InitializeBoard(25);
 
    Console.CursorVisible = false;
 
    int lastTick = 0;
    while (true)
    {
        #region 프레임 관리
        int currentTick = System.Environment.TickCount;
 
        //경과 시간이 1/30초보다 작다면?
        if (currentTick - lastTick < WAIT_TICK)
            continue;
 
        lastTick = currentTick;
        #endregion
 
        // 1)사용자 입력 대기
 
        // 2)입력과 기타 로직 처리
 
        // 3)렌더링
        Console.SetCursorPosition(0, 0);
        board.Render();
    }
}

 

결과 화면

 

Console.CursorVisible

네임스페이스: System

어셈블리: System.Console.dll

 

커서가 표시되는지를 나타내는 값을 가져오거나 설정합니다.

 

Console.CursorVisible = false로 설정하지 않으면 빠르게 점멸하는 커서가 출력된다.

 

Console.SetCursorPosition

네임스페이스: System

어셈블리: System.Console.dll

 

커서의 위치를 설정합니다.

 

Console.SetCursorPosition(0, 0)로 커서 위치를 고정하지 않으면 원을 그리면서 줄바꿈이 일어난다.

 

외곽에 벽 생성하기

/*board */
public void InitializeBoard(int size)
{
    m_size = size;
    m_tile = new eTileType[size, size];

    for (int y = 0; y < m_size; y++)
    {
        for (int x = 0; x < m_size; x++)
        {
            if (y == 0 || y == m_size - 1 || x == 0 || x == m_size -1)
            {
                m_tile[y, x] = eTileType.Wall;
            }
            else
            {
                m_tile[y, x] = eTileType.Empty;
            }
        }
    }
}

 

외곽선을 이동 불가 벽으로 표시

 

/*board */
public void InitializeBoard(int size)
{
    m_size = size;
    m_tile = new eTileType[size, size];

    for (int y = 0; y < m_size; y++)
    {
        for (int x = 0; x < m_size; x++)
        {
            if (y % 2 == 0 || x % 2 == 0)
            {
                m_tile[y, x] = eTileType.Wall;
            }
            else
            {
                m_tile[y, x] = eTileType.Empty;
            }
        }
    }
}

 

사방이 막힌 미로를 우선 생성

 

SideWinder 알고리즘으로 미로 만들기

sidewinder 알고리즘을 사용하면 현재 empty인 칸에서 오른쪽으로 갈지 아래로 갈지 랜덤하게 결정합니다.

다음 empty인 칸에서도 오른쪽으로 갈지 아래로 갈지 결정합니다. 아래로 간다는 선택지가 나오면 그동안 지나온 empty칸에서 랜덤하게 선택해서 아래 칸으로 이동합니다. 이 과정을 계속 반복합니다.

 

1. 오른쪽으로 간다.

2. 그동안 지나온 빈칸 중에서 무작위로 골라서 그 칸 아래로 간다.

 

/*board */
public void InitializeBoard(int size)
{
    m_size = size;
    m_tile = new eTileType[size, size];

    for (int y = 0; y < m_size; y++)
    {
        for (int x = 0; x < m_size; x++)
        {
            if (y % 2 == 0 || x % 2 == 0)
            {
                m_tile[y, x] = eTileType.Wall;
            }
            else
            {
                m_tile[y, x] = eTileType.Empty;
            }
        }
    }

    for (int y = 0; y < m_size; y++)
    {
        int count = 1;

        for (int x = 0; x < m_size; x++)
        {
            if (x % 2 == 0 || y % 2 == 0) continue;

            Random rnd = new Random();

            if (rnd.Next(0, 2) == 0)
            {
                m_tile[y, x + 1] = eTileType.Empty;
                count++;
            }
            else
            {
                var index = rnd.Next(0, count);

                m_tile[y + 1, x - (index * 2)] = eTileType.Empty;
                count = 1;
            }
        }
    }
}

 

SideWinder를 적용해서 만들어진 미로의 모습

 

/*board */
public void InitializeBoard(int size)
{
    m_size = size;
    m_tile = new eTileType[size, size];

    for (int y = 0; y < m_size; y++)
    {
        for (int x = 0; x < m_size; x++)
        {
            if (y % 2 == 0 || x % 2 == 0)
            {
                m_tile[y, x] = eTileType.Wall;
            }
            else
            {
                m_tile[y, x] = eTileType.Empty;
            }
        }
    }

    for (int y = 0; y < size; y++)
    {
        int count = 1;

        for (int x = 0; x < size; x++)
        {
            if (y % 2 == 0 || x % 2 == 0)
            {
                continue;
            }

            if (x == size - 2 && y == size - 2)
            {
                continue;
            }

            Random rnd = new Random();
            if (rnd.Next(0, 2) == 0)
            {
                if (x == size - 2)
                {
                    m_tile[y + 1, x] = eTileType.Empty;
                    continue;
                }

                m_tile[y, x + 1] = eTileType.Empty;
                count++;
            }
            else
            {
                if (y == size - 2)
                {
                    m_tile[y, x + 1] = eTileType.Empty;
                    continue;
                }

                int idx = rnd.Next(0, count);
                m_tile[y + 1, x - (idx * 2)] = eTileType.Empty;
                count = 1;
            }
        }
    }
}

 

외곽을 막아주도록 코드 변경! 그럴듯한 미로가 완성 되었다.

 

출처

 

[C#] 미로 만들기와 길찾기 알고리즘 Part 1 : 미로 만들기(1)

인트로 C# 콘솔 프로그래밍으로 미로를 만들고 BFS, A* 알고리즘으로 미로의 출구를 찾는 프로그램을 작성하려 한다. Part1에선 2차원 미로를 만들어보려 한다. (+ Visual Studio 기준으로 포스팅을 이어

kangworld.tistory.com

댓글