ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C#콘솔 플레이어와 몬스터 맵 만들기 (2) - 몬스터 경로 만들기(BFS)
    C#콘솔프로젝트(BFS,DFS,A*알고리즘을 이용한 몬스터 경로) 2024. 4. 1. 23:56
    반응형

    일단 유닛이라는 클래스를 만들어주고 유닛을 플레이어와 몬스터가 상속받게끔 만들어주려고합니다

        internal class Pos
        {
            public Pos(int y, int x) { Y = y; X = x; }
            public int Y;
            public int X;
        }
    
        internal class Unit
        {
            protected Map _board;
    
            public int PosY { get; set; }
            public int PosX { get; set; }
    
            public void Initialize(int posY, int posX, Map board)
            {
               PosY = posY;
               PosX = posX;
    
                _board = board;
            }
        }

     

    Unit을 상속받는 uMonster 클래스

    internal class uMonster : Unit
    {
        Random _random = new Random();
        MonsterMap _mboard;
    
        public void Initialize(int posY, int posX, MonsterMap mmap)
        {
            PosY = posY;
            PosX = posX;
            _mboard = mmap;
            BFS();
        }
    
        List<Pos> _points = new List<Pos>();
    
        void BFS()
        {
        							 // 상 하 좌 우 로 이동할때의 좌표 변화
            int[] deltaY = new int[] { -1, 0, 1, 0 };
            int[] deltaX = new int[] { 0, -1, 0, 1 };
    		
            // 이미 방문한 위치 표시
            bool[,] found = new bool[_mboard.Size, _mboard.Size];
            // 어디서부터 왔는지 추적하기 경로
            Pos[,] parent = new Pos[_mboard.Size, _mboard.Size];
    
    		//  현재 위치를 큐에 넣고 방문했으니 true로 (Initialize에서 지정된 시작지점을 넣어줌)
            Queue<Pos> q = new Queue<Pos>();
            q.Enqueue(new Pos(PosY, PosX));
            found[PosY, PosX] = true;      
            parent[PosY, PosX] = new Pos(PosY, PosX);
    
    		// 대기열에 있으면 
            while (q.Count > 0)
            {
                // 대기열에서 제일 오래기다린 좌표 뽑기
                Pos pos = q.Dequeue();
                int nowY = pos.Y;
                int nowX = pos.X;
    
                for (int i = 0; i < 4; i++)
                {
                    // 다음 좌표는 현재 좌표에서 상하좌우로 이동한 좌표를 더한 값
                    int nextY = nowY + deltaY[i];
                    int nextX = nowX + deltaX[i];
                    
                    // 다음 좌표가 보드의 위치를 벗어났을때
                    if (nextX < 0 || nextX >= _mboard.Size || nextY < 0 || nextY >= _mboard.Size)
                        continue;
                    // 다음 좌표가 보드 타입의 벽일때
                    if (_mboard.board[nextY, nextX] == Map.BoardType.WALL)
                        continue;
                    // 이미 방문한 좌표일때
                    if (found[nextY, nextX])
                        continue;
    
    				// 위와 같은 상황들을 다 제외한 다음 좌표를 q에 넣고(예약)
                    // found를 true처리 그리고 최단 경로를 저장할 parent에 저장
                    q.Enqueue(new Pos(nextY, nextX));
                    found[nextY, nextX] = true;
                    // 새로 예약한 지점의 부모님 now
                    parent[nextY, nextX] = new Pos(nowY, nowX);
                }
            }
    		
            // 내가 방문한 모든 지점마다 내가 어디서 왔는지 추적
            CalcPathFromParent(parent);
        }
    
        void CalcPathFromParent(Pos[,] parent)
        {
            int EndY = _mboard._DestY;
            int EndX = _mboard._DestX;
    
            // 목적지에서 시작점까지의 경로 추적
            List<Pos> GoToStart = new List<Pos>();
            while (parent[EndY, EndX].Y != EndY || parent[EndY, EndX].X != EndX)
            {
                GoToStart.Add(new Pos(EndY, EndX));
                Pos pos = parent[EndY, EndX];
                EndY = pos.Y;
                EndX = pos.X;
            }
            // Add를 한번더 한 이유 : 시작점을 넣어줘야함
            GoToStart.Add(new Pos(EndY, EndX));
    
            // 시작점에서 목적지까지의 경로 저장
            _points.AddRange(GoToStart);
    
            // 목적지에서 시작점까지의 경로를 역순으로 저장 -> 시작점부터 목적지까지 경로
            List<Pos> Reverse = new List<Pos>();
            for (int i = GoToStart.Count - 1; i >= 0; i--)
            {
                Reverse.Add(GoToStart[i]);
            }
    
            // 경로를 5배로 늘려줍니다.
            for (int i = 0; i < 5; i++)
            {
                _points.AddRange(Reverse);
                _points.AddRange(GoToStart);
            }
            // 경로방향을 랜덤으로 뒤집기
            Random random = new Random();
            if (random.Next(0, 2) == 0) _points.Reverse();
    
        }
    
        const int MOVE_TICK = 50;
        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++;
            }
        }
    }

     

     

    MonsterMap에 몬스터가 도착해야할 지점을 넣어야해서 코드를 수정했습니다 _DestY와 _DestX를 추가하였습니다

     internal class MonsterMap : Map
     {
         public int _DestY { get; private set; }
         public int _DestX { get; private set; }
    
         public void Initialize(int size, int y, int x)
         {
             if (size % 2 == 0)
                 return;
    
             board = new BoardType[size, size];
             Size = size;
    
             _DestY = y;
             _DestX = x;
    
             GenerateBySideWinder();
         }
    
         void GenerateBySideWinder()
         {
             // 길을 다 막아버리는 작업
             for (int y = 0; y < Size; y++)
             {
                 for (int x = 0; x < Size; x++)
                 {
                     if (x % 2 == 0 || y % 2 == 0)
                         board[y, x] = BoardType.WALL;
                     else
                         board[y, x] = BoardType.NONE;
                 }
             }
    
             //랜덤으로 우측 혹은 아래로 길을 뚫는 작업
             Random rand = new Random();
             for (int y = 0; y < Size; y++)
             {
                 int count = 1;
                 for (int x = 0; x < Size; x++)
                 {
                     // 막힌 부분
                     if (x % 2 == 0 || y % 2 == 0)
                         continue;
                     // 가장자리 부분
                     if (y == Size - 2 && x == Size - 2) continue;
    
                     // y축 맨끝 부분은 다 뚫어둠 (외각에서 보드를 뚫을수도 있음)
                     if (y == Size - 2)
                     {
                         board[y, x + 1] = BoardType.NONE;
                         continue;
                     }
                     // x축 맨끝부분은 다 뚫어둠
                     if (x == Size - 2)
                     {
                         board[y + 1, x] = BoardType.NONE;
                         continue;
                     }
    
                     // 1/2확률로 우측길을 뚫음
                     if (rand.Next(0, 2) == 0)
                     {
                         board[y, x + 1] = BoardType.NONE;
                         count++;
                     }
                     // 아래로 길을 뚫음 
                     else
                     {
                         int randomIndex = rand.Next(0, count);
                         // 벽 빈공간 막힌공간 이런식으로 이어지기 때문에 인덱스를 *2 
                         // 빈 공간에서 뚫어야 하기 때문
                         board[y + 1, x - randomIndex * 2] = BoardType.NONE;
                         count = 1;
                     }
                 }
             }
         }
    
         public void Render(uMonster monster)
         {
             // 원래의 콘솔 색깔을 저장해둡니다
             ConsoleColor prevColor = Console.ForegroundColor;
    
             for (int y = 0; y < Size; y++)
             {
                 // 커서 위치 지정
                 Console.SetCursorPosition(15, y + 5);
                 for (int x = 0; x < Size; x++)
                 {
                     Console.ForegroundColor = MAPcolor(board[y, x]);
                     if (y == monster.PosY && x == monster.PosX)
                     {
                         Console.ForegroundColor = ConsoleColor.Gray;
                         Console.Write(MONSTER);
                     }
                     else
                     Console.Write(RECTANGLE);
                 }
                 Console.WriteLine();
             }
             // 원래 콘솔 색깔로 다시 설정
             Console.ForegroundColor = prevColor;
         }
     }

     

     

    Map클래스에도 에도 몬스터를 출력할 변수를 만들었습니다

    internal class Map
    {
        public enum BoardType // 보드 타입
        {
            EAST,
            WEST,
            NORTH,
            SOUTH,
            NONE,
            WALL,
        }
    
        public BoardType[,] board { get; protected set; } // 보드 타입을 넣을 배열
        protected const char RECTANGLE = '■';			  // 보드 출력시 사각형으로 출력
        protected const char MONSTER = '★';				 // 몬스터 출력시 별로 출력
    
        public int Size { get; protected set; }		      // 보드 사이즈
    
        protected ConsoleColor MAPcolor(BoardType type)   // 보드 타입에 따른 출력색깔
        {
            switch (type)
            {
                case BoardType.WALL:
                    return ConsoleColor.Red;
                case BoardType.EAST:
                case BoardType.WEST:
                case BoardType.NORTH:
                case BoardType.SOUTH:
                    return ConsoleColor.Yellow;
                default:
                    return ConsoleColor.Green;
            }
        }
    }

     

    Main 함수입니다

    static void Main(string[] args)
    {
        MonsterMap map = new MonsterMap();
        map.Initialize(15, 13, 13);
        uMonster monster = new uMonster();
        monster.Initialize(1, 1, map);
        Console.CursorVisible = false;
    
        const int WAIT_TICK = 1000 / 30;
    
        int lastTick = 0;
        while (true)
        {
            #region 프레임 관리
            // FPS 프레임(60프레임 OK, 30프레임 이하 NO)
    
            int currenTick = System.Environment.TickCount; // 현재 시간, 시스템이 시작된 이후의 경과된 m/s 
            // int ela
            // psedTick = currenTick - lastTick; // 경과한 시간 = 현재 시간 - 지난 시간
    
            // 만약에 경과한 시간이 1 * 1000/30초보다 작다면 (밀리 새컨드 단위라서 * 1000)
            if (currenTick - lastTick < WAIT_TICK) continue;
    
            // 현재시간 - 이전시간
            int deltaTick = currenTick - lastTick;
            lastTick = currenTick;
            #endregion
    
            // 입력
    
            // 로직
            monster.Update(deltaTick);
    
            // 렌더링
            
            map.Render(monster);
        }
    }

     

     

    아래는 실행 동영상 예시입니다 

     

     

    저도 공부하면서 올리는거라 코드 가독성이나 효율이 떨어지거나 잘못된 오류가 있을수도 있습니다 감사합니다

    반응형
Designed by Tistory.