개요
콘솔창으로 텍스트 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개를 선언하지말고, 타일 객체를 만들었어야 했다.
다음 프로젝트에서는 좀 더 구조적인 발전을 하도록 노력해야겠다.
'C# 콘솔 & 윈도우폼' 카테고리의 다른 글
[C#] 숫자야구 게임 구현하기 (0) | 2022.07.05 |
---|---|
[C# Window Form] 윈도우 폼으로 슈팅게임 구현하기 4 : 갤러그 (2) | 2022.07.02 |
[C#] 콘솔로 슈팅게임 구현하기 3 : 갤러그 (0) | 2022.07.02 |
[C#] 콘솔로 슈팅게임 구현하기 2 : 갤러그 (0) | 2022.07.01 |
[C#] 콘솔 창으로 슈팅게임 구현하기 1 : 갤러그 (2) | 2022.06.30 |