본문 바로가기
C# 콘솔 & 윈도우폼

[C#] 콘솔창으로 만드는 텍스트RPG

by 17번 일개미 2022. 7. 14.
728x90

개요


콘솔창으로 텍스트 RPG를 구현한다.

화면 좌측 상단에는 플레이어의 정보가 나오며,

맵은 좌측상단에서 시작하여 우측하단으로 탈출하는 구조이다.

각 타일은 위험도에 따라 안전 - 초록색 / 보통 - 흰색 / 위험 - 노란색 으로 나누어진다.

위험도가 높을수록 더 강한 몬스터가 등장하며, 몬스터와는 홀,짝 대결을 하여 전투한다.

상점은 맵을 4분할 한 위치에 랜덤하게 각각 1개씩 생성되며, 물약을 구매할 수 있다.

게임화면은 30 프레임의 주기로 업데이트 된다.

 

제작 소요시간 : 5시간 내외


로직


매 프레임마다 화면을 새로 그려줄 것이다. 따라서 프레임 주기를 측정할 계산이 필요하다.

const float waitTick = 1000 / 30; // Frame Rate            
long lastTick = 0; // 마지막 틱
long currentTick; // 현재 틱
while (true)// 반복
            {
                currentTick = System.Environment.TickCount; // 현재 시간
                if (currentTick - lastTick < waitTick)
                {
                    continue; // 경과 시간이 1 / 30 초 보다 작다면 실행 건너뜀
                }
                else
                {
                    lastTick = currentTick;
                    // 게임 실행 부분
                }
            }

waitTick 이라는 상수를 설정해준다. 1000ms 는 1초 이므로 1000을 30으로 나눈 시간을 지정해준다.

currentTick에 현재 TickCount 를 lastTick에는 0을 대입하고, 둘의 차가 1000 / 30 을 넘어가면 게임연산을 실행한다.

이후 lastTick에는 마지막 실행 시 시간을 다시 대입한다.

 

화면을 Console.Clear()로 지우게 되면 빠른 반복을 통해 깜빡거림이 발생한다.

이를 해결하기 위해 모든 화면은 공백을 포함한 배열로 구성하였다.

변경된 정보를 배열에 담고, 콘솔의 커서포지션을 (Console.SetCursorPosition(0, 0);) 0, 0 으로 설정하여

화면을 그리게 되면 깜빡거림이 사라진다.


구현


기본 루프

GameManager.Instance.display.SetCursorZero(); // 커서 위치 초기화
GameManager.Instance.input.GetKey(); // 키 입력 감지
GameManager.Instance.display.DisplayInfo(); // 정보 표시
GameManager.Instance.display.DisplayMap(); // 타일 표시
GameManager.Instance.combat.EnterCombat(); // 전투 감지
GameManager.Instance.shop.EnterShop(); // 상점 감지
if(GameManager.Instance.display.GameOver()) // 게임 오버 감지
{
   return;
}
if(GameManager.Instance.display.EndGame()) // 게임 종료 감지
{
   return;
}

게임은 싱글톤 패턴을 사용하였으며 위와 같은 루프를 반복적으로 실행한다.


타일의 생성

타일은 Map 클래스에서 CreateTile()함수를 통해 게임실행 시 최초 1회 생성된다.

기본적으로, 20 x 20 의 크기로 생성되며, 게임 진행 중 설정을 통해 10~30의 범위로 재생성이 가능하다.

 

public void CreateTile()
        {
            X = GameManager.Instance.settings.GetHorizontal();
            Y = GameManager.Instance.settings.GetVertical();

            // 타일 생성 시 플레이어 위치 초기화
            GameManager.Instance.playerClass.ResetPos(0, 0);
            // 이하 생략

X 에서는 게임 설정의 가로 길이 정보를 가져와 저장하고, Y 에서는 세로 길이를 저장한다.

이후 플레이어의 위치를 0, 0 의 좌표로 초기화한다.

for (int i = 1; i <= 4; i++) // 상점 위치 생성
            {
                if (i == 1) // 1사분면
                {
                    shopX[i] = rand.Next(X / 2, X - 1);
                    shopY[i] = rand.Next(1, Y / 2);
                }
                else if (i == 2)// 2사분면
                {
                    shopX[i] = rand.Next(1, X / 2);
                    shopY[i] = rand.Next(1, Y / 2);
                }
                else if (i == 3)// 3사분면
                {
                    shopX[i] = rand.Next(1, X / 2);
                    shopY[i] = rand.Next(Y / 2, Y - 1);
                }
                else if (i == 4)// 4사분면
                {
                    shopX[i] = rand.Next(X / 2, X - 1);
                    shopY[i] = rand.Next(Y / 2, Y - 1);
                }
            }

상점의 위치는 맵의 전체 크기와 상관없이 항상 4개가 생성되므로, 타일의 중앙을 원점으로 생각하여

각 사분면 마다 상점의 좌표를 랜덤생성한다.

            for(int i = 0; i < Y; i++) // 타일 정보를 기반으로 맵 생성
            {
                for (int j = 0; j < X; j++)
                {
                    if (i == 0 && j == 0) // 시작 지점
                    {
                        tileInfo[i, j] = (int)Type.start;
                        tileDanger[i, j] = (int)Danger.special;
                    }
                    else if (i == Y - 1 && j == X - 1) // 도착 지점
                    {
                        tileInfo[i, j] = (int)Type.end;
                        tileDanger[i, j] = (int)Danger.special;
                    }
                    else if ((i == shopX[1] && j == shopY[1]) || // 상점
                        (i == shopX[2] && j == shopY[2]) ||
                        (i == shopX[3] && j == shopY[3]) ||
                        (i == shopX[4] && j == shopY[4]))
                    {
                        tile[i, j] = '◈';
                        tileInfo[i, j] = (int)Type.shop; // 상점 타입
                        tileDanger[i, j] = (int)Danger.safe; // 안전도 안전
                    }
                    else // 일반 타일
                    {
                        tile[i, j] = '■';
                        tileInfo[i, j] = rand.Next((int)Type.forest, (int)Type.flat + 1); // 
                        tileDanger[i, j] = rand.Next((int)Danger.safe, (int)Danger.VeryDanger + 1);
                    }
                }
            }

타일은 타일의 모양정보, 타입, 그리고 위험도 세가지 정보를 가진다. 각 타일의 정보에 대한 배열을 만들고

플레이어 위치, 시작지점, 도착지점, 상점을 제외한 위치에 일반타일을 랜덤하게 생성한다.

 

+ 이 프로젝트 당시에 배열 3종류를 만들었지만, 후에 생각해보니 타일 객체를 만들어 객체 안에 3가지 속성으로

만들었으면 더 좋았을 것 같다는 생각이 들었다.


전투

 

 

public void EnterCombat() // 전투 인카운터
        {
            X = GameManager.Instance.playerClass.GetNowX(); // 위치 가져오기
            Y = GameManager.Instance.playerClass.GetNowY();
            Danger = GameManager.Instance.mapClass.GetTileDanger(X, Y); // 위험도 가져오기

            switch(Danger)
            {
                case 1: // 위험도 1
                    {
                        isCombat = true;
                        GameManager.Instance.display.DisplayCombat(isCombat);
                        break;
                    }
                case 2: // 위험도 2
                    {
                        isCombat = true;
                        GameManager.Instance.display.DisplayCombat(isCombat);
                        break;
                    }
                case 0: // 일반 타일
                    {
                        isCombat = false;
                        GameManager.Instance.display.DisplayCombat(isCombat);
                        break;
                    }
            }
        }

플레이어가 이동한 후, 밟고 있는 타일의 좌표와 위험도를 가져온다.

해당 타일의 위험도에 따라 위험도에 알맞는 전투 환경을 불러온다.

switch (Danger)
            {
                case 1: // 위험도 1
                    {
                        monster = new NormalMonster(); // 노말
                        break;
                    }
                case 2: // 위험도 2
                    {
                        monster = new HardMonster(); // 하드
                        break;
                    }
                default: // 기본값
                    {
                        monster = new NormalMonster();
                        break;
                    }
            }

타일의 위험도에 따라 Monster 클래스 변수인 monster

Monster를 상속받은 Normal, HardMonster 를 알맞게 생성한다.

이후 몬스터 랜덤난수와 홀수, 짝수 대결을 하여 맞추면 데미지를 주고, 틀리면 데미지를 받는다.


분석

구조적으로 짜는 연습을 본격적으로 했던 첫 번째 시도였다.

싱글톤 제네릭을 기본으로 GameManager를 만들고, GameManager에서 각 객체들을 호출해서

사용하였다. 하지만 기능 구현에 집중하다보니 점점 구조가 망가지는 것을 알 수 있었고,

이후에는 구조적으로 아쉬운 부분들이 보이기 시작하였다.

 

예를 들면 타일 배열 3개를 선언하지말고, 타일 객체를 만들었어야 했다.

 

다음 프로젝트에서는 좀 더 구조적인 발전을 하도록 노력해야겠다.

728x90