전체 코드

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Algorithm
{
    class Board
    {
        public enum TileType
        {
            Empty, // 빈 공간
            Wall,  // 벽
        }
        const char CIRCLE = '\u25cf'; // 유니코드 문자로 ● (검은색 원)을 출력하기 위한 상수. 맵 타일을 표시하는 데 사용됨.
        public TileType[,] Tile { get; private set; } // 2차원 배열로 맵의 각 타일의 상태를 저장.
        public int Size { get; private set; } // 맵의 크기. NxN 크기를 나타냄.
                                              // 맵 초기화 메서드. 맵의 크기를 받아 타일을 생성하고 초기화.
        public int DestY { get; private set; }
        public int DestX { get; private set; }


        Player _player;
        public void Initialize(int size, Player player)
        {


            if (size % 2 == 0)
            {
                return;
            }


            _player = player;

            Tile = new TileType[size, size]; // 맵의 크기(size x size)만큼의 2차원 배열 생성.
            Size = size; // 맵 크기 설정.

            DestY = Size - 2;
            DestX = Size - 2;

            //GenrateByBinaryTree();
            GenerateBySideWinder();

        }
        public void GenrateByBinaryTree()
        {
            // mazes for Programmers
            // Binary Tree Algorithm
            // 길을 다 막는 작업
            for (int y = 0; y < Size; y++)
            {
                for (int x = 0; x < Size; x++)
                {
                    if (x % 2 == 0 || y % 2 == 0)
                    {
                        Tile[y, x] = TileType.Wall;
                    }
                    else
                    {
                        Tile[y, x] = TileType.Empty;
                    }
                }
            }

            // 랜덤으로 우측 혹은 아래로 길을 뚫는 작업
            Random rand = new Random();
            for (int y = 0; y < Size; y++)
            {
                for (int x = 0; x < Size; x++)
                {
                    if (x % 2 == 0 || y % 2 == 0)
                    {
                        continue;
                    }

                    if (y == Size - 2 && x == Size - 2)
                    {
                        continue;
                    }
                    if (y == Size - 2)
                    {
                        Tile[y, x + 1] = TileType.Empty;
                        continue;
                    }
                    if (x == Size - 2)
                    {
                        Tile[y + 1, x] = TileType.Empty;
                        continue;
                    }
                    if (rand.Next(0, 2) == 0)
                    {
                        Tile[y, x + 1] = TileType.Empty;
                    }
                    else
                    {
                        Tile[y + 1, x] = TileType.Empty;
                    }
                }
            }
        }

        // Sidewinder 알고리즘으로 미로를 생성하는 메서드
        public void GenerateBySideWinder()
        {
            // 1. 우선 모든 타일을 벽으로 막고, 홀수 좌표에 빈 공간을 만듦
            for (int y = 0; y < Size; y++)
            {
                for (int x = 0; x < Size; x++)
                {
                    // x 또는 y가 짝수일 경우에는 벽을 설치하고, 나머지에는 빈 공간을 생성
                    if (x % 2 == 0 || y % 2 == 0)
                    {
                        Tile[y, x] = TileType.Wall;  // 벽 타일
                    }
                    else
                    {
                        Tile[y, x] = TileType.Empty; // 빈 공간
                    }
                }
            }

            // 2. 각 좌표에서 무작위로 오른쪽 또는 아래쪽으로 길을 뚫음
            Random rand = new Random();  // 무작위 생성기

            for (int y = 0; y < Size; y++)
            {
                for (int x = 0; x < Size; x++)
                {
                    // 연속된 세로 구역의 길 개수를 세기 위한 변수 (미완성 코드로 보임)
                    int count = 0;

                    // 짝수 좌표는 벽이므로 건너뜀
                    if (x % 2 == 0 || y % 2 == 0)
                    {
                        continue;
                    }

                    // 미로의 끝 부분에서는 길을 뚫지 않음
                    if (y == Size - 2 && x == Size - 2)
                    {
                        continue;
                    }

                    // 마지막 행에서는 오른쪽으로만 길을 뚫음
                    if (y == Size - 2)
                    {
                        Tile[y, x + 1] = TileType.Empty; // 오른쪽으로 길 뚫기
                        continue;
                    }

                    // 마지막 열에서는 아래쪽으로만 길을 뚫음
                    if (x == Size - 2)
                    {
                        Tile[y + 1, x] = TileType.Empty; // 아래로 길 뚫기
                        continue;
                    }

                    // 50% 확률로 오른쪽 또는 아래쪽으로 길을 뚫음
                    if (rand.Next(0, 2) == 0)
                    {
                        Tile[y, x + 1] = TileType.Empty; // 오른쪽으로 길을 뚫음
                        count++; // 길의 개수 카운트 증가
                    }
                    else
                    {
                        // 우측으로 길을 뚫지 않은 경우, 아래쪽으로 길을 뚫음
                        // 특정 구간에서 무작위로 아래쪽으로 길을 뚫음 (아직 완성되지 않은 부분)
                        int randomIndex = rand.Next(0, count);
                        Tile[y + 1, x - randomIndex * 2] = TileType.Empty; // 아래로 길을 뚫음
                        count = 1; // 카운트 초기화
                    }
                }
            }
        }

        // 맵을 화면에 출력하는 메서드.
        public void Render()
        {
            ConsoleColor prevColor = Console.ForegroundColor;

            for (int y = 0; y < Size; y++)
            {
                for (int x = 0; x < Size; x++)
                {
                    if (y == _player.PosY && x == _player.PosX)
                        Console.ForegroundColor = ConsoleColor.Blue;
                    else if (y == DestY && x == DestX)
                        Console.ForegroundColor = ConsoleColor.Yellow;
                    else
                        Console.ForegroundColor = GetTileColor(Tile[y, x]);

                    Console.Write(CIRCLE);
                }
                Console.WriteLine();
            }
        }

        // 타일의 종류에 따라 콘솔 글자색을 반환하는 메서드.
        ConsoleColor GetTileColor(TileType type)
        {
            switch (type)
            {
                case TileType.Empty:
                    return ConsoleColor.Green; // 빈 공간은 초록색.
                case TileType.Wall:
                    return ConsoleColor.Red; // 벽은 빨간색.
                default:
                    return ConsoleColor.Green; // 기본값으로 초록색 반환.
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Algorithm
{
    class Pos
    {
        public Pos(int y, int x) { Y = y; X = x; }
        public int Y;
        public int X;
    }

    class Player
    {
        public int PosY { get; private set; }
        public int PosX { get; private set; }

        Random _random = new Random();

        Board _board;

        enum Dir  // 반시계방향
        {
            Up = 0,
            Left = 1,
            Down = 2,
            Right = 3
        }

        int _dir = (int)Dir.Up;

        List<Pos> _points = new List<Pos>();

        public void Initialize(int posY, int posX, Board board)
        {
            PosX = posX;
            PosY = posY;

            _board = board;

            // 현재 바라보고 있는 방향을 기존으로 좌표 변화를 나타냄
            int[] frontY = new int[] { -1, 0, 1, 0 };
            int[] frontX = new int[] { 0, -1, 0, 1 };
            int[] rightY = new int[] { 0, -1, 0, 1 };
            int[] rightX = new int[] { 1, 0, -1, 0 };

            _points.Add(new Pos(PosY, PosX));

            // 목표 지점에 도달할 때까지 뺑뺑이를 돌겠다
            // 목적지 도착하기 전에는 계속 실행
            // 실시간으로 로직 돌리기 전에, 렌더링도 하기 전에, 미리 길을 찾아 보는 것이다.
            // 현재 바라보는 방향이 어디냐에 따라 오른손 위치가 다름.(왼쪽을 보고 있는 상태라면 오른손은 절대 기준으로 위쪽일것)
            while (PosY != board.DestY || PosX != board.DestX)
            {
                // 1. 현재 바라보는 방향을 기준으로 오른쪽으로 갈 수 있는지 확인
                if (_board.Tile[PosY + rightY[_dir], PosX + rightX[_dir]] == Board.TileType.Empty)
                {
                    // 오른쪽으로 가기
                    // 1. 오른쪽 방향으로 90 도 회전
                    _dir = (_dir - 1 + 4) % 4;
                    // 2. 앞으로 한 보 전진
                    PosY = PosY + frontY[_dir];
                    PosX = PosX + frontX[_dir];

                    _points.Add(new Pos(PosY, PosX));
                }
                // 2. 현재 바라보는 방향을 기준으로 앞 쪽으로 갈 수 있는지
                else if (_board.Tile[PosY + frontY[_dir], PosX + frontX[_dir]] == Board.TileType.Empty)
                {
                    // 앞으로 한 보 전진
                    PosY = PosY + frontY[_dir];
                    PosX = PosX + frontX[_dir];

                    _points.Add(new Pos(PosY, PosX));
                }
                else
                {
                    // 왼쪽 방향으로 90 도 회전 해주고 다음 반복 하러 (반시계방향으로 여러 방향 따져봄) 
                    _dir = (_dir + 1 + 4) % 4;
                }
            }
        }

        const int MOVE_TICK = 10;   // 10밀리세컨즈 = 0.01 초 마다 움직이게
        int _sumTick = 0;
        int _lastIndex = 0;
        public void Update(int deltaTick)
        {
            if (_lastIndex >= _points.Count)
                return;

            _sumTick += deltaTick;
            if (_sumTick >= MOVE_TICK)  // 이부분은 0.1초마다 실행
            {
                _sumTick = 0;

                PosY = _points[_lastIndex].Y;
                PosX = _points[_lastIndex].X;
                _lastIndex++;
            }
        }
    }
}
namespace Algorithm
{
    class Program
    {
        static void Main(string[] args)
        {

            // Board 클래스의 인스턴스를 생성하고 맵을 초기화.
            Board board = new Board();
            Player player = new Player();   
            board.Initialize(25,player); // 맵의 크기를 25x25로 설정.
            Console.CursorVisible = false; // 콘솔의 커서를 숨김. 깔끔한 화면 출력.
            player.Initialize(1, 1, board);
            const int MAX_TICK = 1000 / 30; // 초당 30프레임을 유지하기 위한 최대 틱. (1초 / 30프레임)

            int lastTick = 0; // 마지막 프레임이 실행된 시간을 저장.

            // 메인 게임 루프
            while (true)
            {
                #region 프레임 관리
                // 프레임 시간 관리 (FPS 관리)
                // 현재 시간을 가져와서 지난 프레임 시간과 비교하여, 일정 시간이 지나지 않았다면 다음 루프를 건너뜀.
                int currentTick = System.Environment.TickCount; // 현재 시간을 밀리초 단위로 가져옴.
                int elapsedTick = currentTick - lastTick; // 지난 프레임 이후 경과 시간 계산.

                // 만약 경과 시간이 설정한 프레임 간격(MAX_TICK)보다 작다면 루프를 건너뜀.
                if (elapsedTick < MAX_TICK)
                {
                    continue; // 일정 시간이 지나지 않았으므로 이번 루프는 건너뜀.
                }
                int deltaTick = currentTick - lastTick; // 1프레임 지날 때마다 업데이트

                lastTick = currentTick; // 마지막 프레임 시간을 현재 시간으로 갱신.

                #endregion

                // 입력 처리 부분 (현재는 구현되어 있지 않음).
                // 사용자 입력: 키보드, 마우스 등.

                // 게임 로직 처리 부분 (현재는 구현되어 있지 않음).
                // AI 및 게임 내 로직 처리.
                player.Update(deltaTick);


                // 렌더링: 화면에 그려주는 단계.
                Console.SetCursorPosition(0, 0); // 콘솔 출력 위치를 맨 위로 이동하여 화면을 새로 그리도록 함.
                board.Render(); // 맵을 그리는 메서드 호출
            }
        }
    }
}

1. Program 클래스 (메인 게임 루프)

class Program
{
    static void Main(string[] args)
    {
        Board board = new Board();
        Player player = new Player();
        
        board.Initialize(25, player);
        player.Initialize(1, 1, board);
        
        Console.CursorVisible = false;
        const int MAX_TICK = 1000 / 30;
        int lastTick = 0;

        while (true)
        {
            int currentTick = Environment.TickCount;
            int deltaTick = currentTick - lastTick;
            if (deltaTick < MAX_TICK) continue;
            lastTick = currentTick;

            player.Update(deltaTick);
            Console.SetCursorPosition(0, 0);
            board.Render();
        }
    }
}

설명

  • BoardPlayer 객체를 생성하고 초기화합니다.
  • while (true): 무한 루프를 통해 일정한 프레임 속도로 게임을 실행합니다.
  • player.Update(deltaTick): 플레이어 이동을 갱신합니다.
  • board.Render(): 미로와 플레이어를 화면에 출력합니다.

2. Player 클래스 (플레이어 이동 구현)

class Player
{
    public int PosY { get; private set; }
    public int PosX { get; private set; }
    Board _board;

    enum Dir { Up = 0, Left = 1, Down = 2, Right = 3 }
    int _dir = (int)Dir.Up;
    List<Pos> _points = new List<Pos>();

    public void Initialize(int posY, int posX, Board board)
    {
        PosX = posX;
        PosY = posY;
        _board = board;
        
        int[] frontY = { -1, 0, 1, 0 };
        int[] frontX = { 0, -1, 0, 1 };
        int[] rightY = { 0, -1, 0, 1 };
        int[] rightX = { 1, 0, -1, 0 };

        _points.Add(new Pos(PosY, PosX));
        
        while (PosY != board.DestY || PosX != board.DestX)
        {
            if (_board.Tile[PosY + rightY[_dir], PosX + rightX[_dir]] == Board.TileType.Empty)
            {
                _dir = (_dir - 1 + 4) % 4;
                PosY += frontY[_dir];
                PosX += frontX[_dir];
                _points.Add(new Pos(PosY, PosX));
            }
            else if (_board.Tile[PosY + frontY[_dir], PosX + frontX[_dir]] == Board.TileType.Empty)
            {
                PosY += frontY[_dir];
                PosX += frontX[_dir];
                _points.Add(new Pos(PosY, PosX));
            }
            else
            {
                _dir = (_dir + 1 + 4) % 4;
            }
        }
    }

    const int MOVE_TICK = 10;
    int _sumTick = 0;
    int _lastIndex = 0;

    public void Update(int deltaTick)
    {
        if (_lastIndex >= _points.Count) return;

        _sumTick += deltaTick;
        if (_sumTick >= MOVE_TICK)
        {
            _sumTick = 0;
            PosY = _points[_lastIndex].Y;
            PosX = _points[_lastIndex].X;
            _lastIndex++;
        }
    }
}

설명

  • 오른손 법칙을 적용하여 미리 경로를 계산합니다.
  • _points 리스트에 경로를 저장하고, Update()에서 해당 경로를 따라 이동합니다.
  • Initialize()에서 미로 탐색 경로를 미리 계산하여 성능을 최적화합니다.
  • 프레임 단위 이동을 위해 MOVE_TICK 값을 사용합니다.

3. Board 클래스 (미로 생성 및 출력)

class Board
{
    const char CIRCLE = '\u25cf';
    public TileType[,] Tile { get; private set; }
    public int Size { get; private set; }
    public int DestY { get; private set; }
    public int DestX { get; private set; }
    Player _player;

    public enum TileType { Empty, Wall }

    public void Initialize(int size, Player player)
    {
        if (size % 2 == 0) return;

        _player = player;
        Tile = new TileType[size, size];
        Size = size;
        DestY = Size - 2;
        DestX = Size - 2;

        GenerateBySideWinder();
    }

    public void Render()
    {
        ConsoleColor prevColor = Console.ForegroundColor;
        for (int y = 0; y < Size; y++)
        {
            for (int x = 0; x < Size; x++)
            {
                if (y == _player.PosY && x == _player.PosX)
                    Console.ForegroundColor = ConsoleColor.Blue;
                else if (y == DestY && x == DestX)
                    Console.ForegroundColor = ConsoleColor.Yellow;
                else
                    Console.ForegroundColor = GetTileColor(Tile[y, x]);
                
                Console.Write(CIRCLE);
            }
            Console.WriteLine();
        }
        Console.ForegroundColor = prevColor;
    }
}

설명

  • GenerateBySideWinder(): Sidewinder 알고리즘을 사용해 미로를 생성합니다.
  • Render(): 미로와 플레이어의 현재 위치를 출력합니다.
  • DestY, DestX: 목적지 좌표를 저장하여 플레이어가 목표 지점을 인식할 수 있도록 합니다.

프레임 관리 및 최적화

const int MOVE_TICK = 10;   // 10밀리초마다 이동
int _sumTick = 0;
int _lastIndex = 0;
public void Update(int deltaTick)
{
    if (_lastIndex >= _points.Count) return;

    _sumTick += deltaTick;
    if (_sumTick >= MOVE_TICK)
    {
        _sumTick = 0;
        PosY = _points[_lastIndex].Y;
        PosX = _points[_lastIndex].X;
        _lastIndex++;
    }
}

설명

  • 프레임마다 이동이 일어나지 않도록 일정한 속도로 이동합니다.
  • 미리 계산된 경로를 따라가므로 실시간 탐색보다 성능이 우수합니다.
profile
李家네_공부방

0개의 댓글