강께르의 개발일지
[C++] 슬라이드 퍼즐 게임 본문
1. 이것은 무엇인가?
- 이 게임은 4x4 2차원 배열 게임판에 16개의 숫자를 무작위로 섞고 그 섞은 배열을 순서대로 나열할 수 있게 조작하여 완성하는 게임이다. 일반적으로 그림 맞추기 게임으로 익숙할 것이다.
2. 요구 사항
- 게임 시작과 게임 종료를 제공하는 메뉴 화면 구현
- 3가지 선택지에 따라서 섞는 횟수가 다른 난이도 시스템 구현
- 메뉴 화면에서 게임 종료를 선택하지 않는 한, 계속 해서 게임을 할 수 있는 시스템 구현
- 게임의 시작점을 항상 가장 왼쪽 상단, 하단, 가장 오른쪽 상단, 하단 중 하나로 시작할 수 있게 구현
- 플레이어가 조종하는 커서가 게임판을 벗어나는 불상사가 일어나지 않도록 구현
3. 새로 알게 된 점
- 클래스의 생성자를 통해서 다양한 것을 할 수 있다는 점을 알게 됐다. 예를 들어, 개발 중에 배열을 무작위로 섞는 기능을 생성자에 넣었는데 무리 없이 생성자만으로 가능했다는 것이다. 멤버 변수의 초기화 느낌으로 뭐든 가능하다는 생각이 들었다.
- enum class는 정수와의 비교를 거부한다. 그런데 나는 정수 비교가 필요했다... enum class가 데이터를 보호하기에 적합하다는 것을 알았지만, 데이터 입력을 위해서 int형 변수에 저장하고 있지만 enum class와의 비교를 위해서 정수 승급이 불가하기에 형 변환으로 enum class와 같은 형으로 변환시켜줘야했다. 이게 맞는 사용 방법일까..?
- 어떤 분이 해석해서 작성해주신 C++ 코딩 스탠다드를 읽은 적이 있다. 읽을 때 생각에 남은 방법을 적용했다. 멤버 함수의 오버로딩을 사용하지 않고 비슷한 기능이라도 명확한 구분을 위해 함수명으로 차별화를 두었다. 아마 직관적인 이해를 돕기 위해 이렇게 한 것 같다. 함수 오버로딩을 기껏 배웠는데 써먹지 않은 것은 아쉽지만 좋은 것을 알려주신 그 어떤 분이라고 부를 수 밖에 없는 분께 감사를...
- _getch의 사용법에 대해 익혔다. 화면에 출력하지 않고 개행 문자를 입력하지 않고 바로 단 하나의 문자를 입력 버퍼에 저장하는 기능임을 알았다. 그리고 두번의 _getch를 사용함으로 방향키를 입력할 수 있고 그 방향키의 값은 아스키 코드의 정수값임을 새로 알았다.
4. 실행 과정
- 프로그램은 크게 2개의 while문을 통해 돌아간다. 프로그램이 일단 시작하면 서로 다른 객체 두 개를 생성한다. 하나는 GameManager로 전반적인 게임 진행에 대한 기능과 변수를 담고 있다. 하나는 SlideTable로 게임판과 관련된 변수와 기능을 담고 있다. 객체를 생성한다는 것은 각각의 객체가 실행해야할 초기화를 한다는 것이다.
- 처음에 게임을 시작할지, 종료할지 선택하는 메뉴부터 시작하여 게임을 시작하는 선택지를 고르면 첫번째 while문으로 들어간다.
- 난이도에 대한 입력을 받고 그 입력에 따른 4x4 2차원 배열을 무작위로 섞는다. 그 섞는 횟수는 난이도 입력에 따라 다르다.
- 섞는 횟수가 모두 충족하여도 한가지 조건을 만족해야한다. 그것은 게임의 시작점이 좌측 상단, 하단 / 우측 상단, 하단 중 하나에 위치해야한다는 것이다. 그것이 만족하지 않는다면 계속 섞는다.
- 난이도에 대한 처리를 끝내면 두번째 while문으로 들어간다. 2차원 배열 게임판을 출력하고 그 게임판을 본 플레이어는 자신이 기대하는 방향키 입력을 하게 될 것이다. 그 입력에 따라 게임판의 배열을 바꾸는 기능을 수행한다.
- 그 다음 게임의 모든 배열이 순서대로 나열된 그 배열에 자리하고 있는지 체크한다. 하나라도 불만족할 시 계속 진행하고, 만족할 경우 두번째 while문을 나갈 수 있는 조건을 수정한다.
- 게임을 클리어했다는 출력문을 제공하고 다시 메뉴를 제공한다.
- 게임을 종료하는 메뉴를 선택하면 종료 출력문을 출력하고 끝낸다.
5. 코드
5-1. header.h
#ifndef __HEADER_H__
#define __HEADER_H__
#include<iostream>
#include<ctime>
#include<Windows.h>
#include<conio.h>
#include<cassert>
using namespace std;
enum class MOVE
{
UP = 72,
DOWN = 80,
LEFT = 75,
RIGHT = 77
};
enum class DIFF
{
EASY = 1,
NORMAL,
HARD
};
enum class MENU
{
PLAY = 1,
QUIT
};
#endif // !__HEADER_H__
5-2. SlideTable.h
#ifndef __SLIDETABLE_H__
#define __SLIDETABLE_H__
#include"header.h"
class SlideTable
{
private:
int m_table[4][4];
int m_horizontal;
int m_vertical;
int m_zeroIdx_X;
int m_zeroIdx_Y;
public:
SlideTable();
int GetTableNum(int, int);
int GetHorizontal();
int GetVertical();
void SuffleByDiff(int);
void SwapUp();
void SwapDown();
void SwapLeft();
void SwapRight();
void Swap(int&, int&);
bool IsCorrectPuzzle();
bool IsCorrectStartPlace();
};
#endif
5-3.GameManager.h
#ifndef __GAMEMANAGER_H__
#define __GAMEMANAGER_H__
#include"header.h"
#include"SlideTable.h"
class GameManager
{
private:
bool m_isPlay;
int m_key;
int m_menu;
int m_diff;
public:
GameManager();
int GetIsPlay();
int GetKey();
int GetMenu();
void SetMenu(SlideTable);
void SetDiff(SlideTable);
void GoToXY(int, int);
void SetConsoleView(void);
void ShowMenu(SlideTable);
void ShowDifficulty(SlideTable);
void ShowEmptyTable(SlideTable);
void ShowFullTable(SlideTable);
inline void ShowTableNum(SlideTable, int, int);
void SetTableByDiff(SlideTable&);
void GetInputKey();
void ChangeTable(SlideTable&);
void IsGameover(SlideTable);
void ShowGameover(SlideTable);
void ShowQuit(SlideTable);
};
#endif
5-4. SlideTable.cpp
#include "SlideTable.h"
SlideTable::SlideTable() : m_horizontal(4), m_vertical(4)
{
m_zeroIdx_X = -1;
m_zeroIdx_Y = -1;
for (int i = 0; i < m_horizontal; i++) // 모든 배열에 숫자를 나열
{
for (int j = 0; j < m_vertical; j++)
{
m_table[i][j] = i * 4 + j + 1;
}
}
for (int i = 0; i < m_horizontal; i++) // 그 중 커서를 갖는 배열의 위치를 저장
{
for (int j = 0; j < m_vertical; j++)
{
if (m_table[i][j] == 16)
{
m_zeroIdx_X = i;
m_zeroIdx_Y = j;
}
}
}
}
int SlideTable::GetTableNum(int horiz, int verti)
{
return m_table[horiz][verti];
}
int SlideTable::GetHorizontal()
{
return m_horizontal;
}
int SlideTable::GetVertical()
{
return m_vertical;
}
void SlideTable::SuffleByDiff(int SuffleTime)
{
bool correctStartPlace = false;
int moveNum = -1;
while (!correctStartPlace)
{
for (int i = 0; i < SuffleTime; i++)
{
moveNum = rand() % 4;
switch (moveNum)
{
case 0:
SwapUp();
break;
case 1:
SwapDown();
break;
case 2:
SwapLeft();
break;
case 3:
SwapRight();
break;
}
}
if (IsCorrectStartPlace())
correctStartPlace = true;
}
}
void SlideTable::SwapUp()
{
if (m_zeroIdx_X > 0)
{
Swap(m_table[m_zeroIdx_X][m_zeroIdx_Y],
m_table[m_zeroIdx_X - 1][m_zeroIdx_Y]);
m_zeroIdx_X -= 1;
}
}
void SlideTable::SwapDown()
{
if (m_zeroIdx_X < m_horizontal - 1)
{
Swap(m_table[m_zeroIdx_X][m_zeroIdx_Y],
m_table[m_zeroIdx_X + 1][m_zeroIdx_Y]);
m_zeroIdx_X += 1;
}
}
void SlideTable::SwapLeft()
{
if (m_zeroIdx_Y > 0)
{
Swap(m_table[m_zeroIdx_X][m_zeroIdx_Y],
m_table[m_zeroIdx_X][m_zeroIdx_Y - 1]);
m_zeroIdx_Y -= 1;
}
}
void SlideTable::SwapRight()
{
if (m_zeroIdx_Y < m_vertical - 1)
{
Swap(m_table[m_zeroIdx_X][m_zeroIdx_Y],
m_table[m_zeroIdx_X][m_zeroIdx_Y + 1]);
m_zeroIdx_Y += 1;
}
}
void SlideTable::Swap(int& zeroInArr, int& Another)
{
int temp = zeroInArr;
zeroInArr = Another;
Another = temp;
}
bool SlideTable::IsCorrectPuzzle()
{
bool result = true;
int expr;
for (int i = 0; i < m_horizontal; i++)
{
for (int j = 0; j < m_vertical; j++)
{
expr = i * 4 + j + 1;
if (m_table[i][j] != expr)
{
i = m_horizontal + 1;
result = false;
break;
}
}
}
return result;
}
bool SlideTable::IsCorrectStartPlace()
{
bool result = false;
int leftUpPlace = m_table[0][0];
int rightUpPlace = m_table[0][m_vertical - 1];
int leftDownPlace = m_table[m_horizontal - 1][0];
int rightDownPlace = m_table[m_horizontal - 1][m_vertical - 1];
if ((leftUpPlace == 16) || (rightUpPlace == 16) ||
(leftDownPlace == 16) || (rightDownPlace == 16))
result = true;
return result;
}
5-5. GameManager.cpp
#include "GameManager.h"
GameManager::GameManager() : m_isPlay(true), m_key(0), m_menu(0), m_diff(0)
{ }
int GameManager::GetIsPlay() { return m_isPlay; }
int GameManager::GetKey() { return m_key; }
int GameManager::GetMenu() { return m_menu; }
void GameManager::SetMenu(SlideTable slideTable)
{
cin >> m_menu;
while ((m_menu < 0) || (m_menu > 2))
{
system("cls");
ShowEmptyTable(slideTable);
cout << "--------+-------------------------------|" << endl;
cout << " 메세지\t| 올바른 숫자를 선택하세요. |" << endl;
cout << "==============================================" << endl;
cout << " 입력\t|" << endl;
cout << "==============================================" << endl;
cin.get();
GoToXY(5, 15);
cin.get();
system("cls");
ShowEmptyTable(slideTable);
cout << "--------+-------------------------------|" << endl;
cout << " 메세지\t| 1.게임시작\t 2.게임종료 |" << endl;
cout << "==============================================" << endl;
cout << " 입력\t|" << endl;
cout << "==============================================" << endl;
GoToXY(5, 15);
cin >> m_menu;
}
}
void GameManager::SetDiff(SlideTable slideTable)
{
cin >> m_diff;
while ((m_diff < 0) || (m_diff > 3))
{
system("cls");
ShowEmptyTable(slideTable);
cout << "--------+-------------------------------|" << endl;
cout << " 메세지\t| 올바른 숫자를 선택하세요. |" << endl;
cout << "==============================================" << endl;
cout << " 입력\t|" << endl;
cout << "==============================================" << endl;
cin.get();
GoToXY(5, 15);
cin.get();
system("cls");
ShowEmptyTable(slideTable);
cout << "--------+-------------------------------|" << endl;
cout << " 메세지\t| 1.쉬움 2.보통 3.어려움 |" << endl;
cout << "==============================================" << endl;
cout << " 입력\t|" << endl;
cout << "==============================================" << endl;
GoToXY(5, 15);
cin >> m_diff;
}
}
void GameManager::GoToXY(int x, int y)
{
COORD position;
position.X = 2 * x;
position.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), position);
}
void GameManager::SetConsoleView()
{
system("mode con:cols=47 lines=25");
system("title Slide Puzzle Game");
}
void GameManager::ShowMenu(SlideTable slideTable)
{
ShowEmptyTable(slideTable);
cout << "--------+-------------------------------|" << endl;
cout << " 메세지\t| 1.게임시작\t 2.게임종료 |" << endl;
cout << "==============================================" << endl;
cout << " 입력\t|" << endl;
cout << "==============================================" << endl;
GoToXY(5, 15);
SetMenu(slideTable);
system("cls");
}
void GameManager::ShowDifficulty(SlideTable slideTable)
{
ShowEmptyTable(slideTable);
cout << "--------+-------------------------------|" << endl;
cout << " 메세지\t| 1.쉬움 2.보통 3.어려움 |" << endl;
cout << "==============================================" << endl;
cout << " 입력\t|" << endl;
cout << "==============================================" << endl;
GoToXY(5, 15);
SetDiff(slideTable);
system("cls");
}
void GameManager::ShowEmptyTable(SlideTable slideTable)
{
cout << "==============================================" << endl;
cout << "==S==L==I==D==E=============P==U==Z==Z==L==E==" << endl;
cout << "==============================================" << endl;
cout << "\t| [1]\t| [2]\t| [3]\t| [4]\t|" << endl;
cout << "--------+-------+-------+-------+-------|" << endl;
for (int i = 0; i < slideTable.GetHorizontal(); i++)
{
cout << " [" << i + 1 << "]\t| ";
for (int j = 0; j < slideTable.GetVertical(); j++)
{
cout << "\t| ";
}
cout << endl;
if (i != slideTable.GetHorizontal() - 1)
cout << "--------+-------+-------+-------+-------|" << endl;
}
}
void GameManager::ShowFullTable(SlideTable slideTable)
{
cout << "==============================================" << endl;
cout << "==S==L==I==D==E=============P==U==Z==Z==L==E==" << endl;
cout << "==============================================" << endl;
cout << "\t| [1]\t| [2]\t| [3]\t| [4]\t|" << endl;
cout << "--------+-------+-------+-------+-------|" << endl;
for (int i = 0; i < slideTable.GetHorizontal(); i++)
{
cout << " [" << i + 1 << "]\t| ";
for (int j = 0; j < slideTable.GetVertical(); j++)
{
ShowTableNum(slideTable, i, j);
cout << "\t| ";
}
cout << endl;
if(i != slideTable.GetHorizontal() - 1)
cout << "--------+-------+-------+-------+-------|" << endl;
}
cout << "--------+-------------------------------|" << endl;
cout << " 메세지\t|\t\t\t\t|" << endl;
cout << "==============================================" << endl;
cout << " 입력\t|" << endl;
cout << "==============================================" << endl;
GoToXY(5, 15);
}
inline void GameManager::ShowTableNum(SlideTable slideTable, int horiz, int verti)
{
int showNum = slideTable.GetTableNum(horiz, verti);
if (showNum == 16) cout << "@";
else cout << showNum;
}
void GameManager::SetTableByDiff(SlideTable& slideTable)
{
int SuffleTime = 0;
switch ((DIFF)m_diff)
{
case DIFF::EASY:
SuffleTime = 5;
break;
case DIFF::NORMAL:
SuffleTime = 20;
break;
case DIFF::HARD:
SuffleTime = 50;
break;
}
slideTable.SuffleByDiff(SuffleTime);
}
void GameManager::GetInputKey()
{
while (true)
{
m_key = _getch();
if (m_key == 224)
{
m_key = _getch();
if ((m_key == (int)MOVE::UP) || (m_key == (int)MOVE::DOWN)
|| (m_key == (int)MOVE::LEFT) || (m_key == (int)MOVE::RIGHT))
break;
}
}
}
void GameManager::ChangeTable(SlideTable& slideTable)
{
MOVE move = (MOVE)m_key;
switch (move)
{
case MOVE::UP: // UP key
slideTable.SwapUp();
break;
case MOVE::DOWN: // DOWN key
slideTable.SwapDown();
break;
case MOVE::LEFT: // LEFT key
slideTable.SwapLeft();
break;
case MOVE::RIGHT: // RIGHT key
slideTable.SwapRight();
break;
default:
break;
}
}
void GameManager::IsGameover(SlideTable slideTable)
{
if(slideTable.IsCorrectPuzzle())
m_isPlay = false;
system("cls");
}
void GameManager::ShowGameover(SlideTable slideTable)
{
system("cls");
ShowEmptyTable(slideTable);
cout << "--------+-------------------------------|" << endl;
cout << " 메세지\t| 게임을 클리어하셨습니다. |" << endl;
cout << "==============================================" << endl;
cout << " 입력\t|" << endl;
cout << "==============================================" << endl;
GoToXY(5, 15);
cin.get();
cin.get();
system("cls");
m_isPlay = true;
}
void GameManager::ShowQuit(SlideTable slideTable)
{
system("cls");
ShowEmptyTable(slideTable);
cout << "--------+-------------------------------|" << endl;
cout << " 메세지\t|\t게임을 종료합니다.\t|" << endl;
cout << "==============================================" << endl;
cout << " 입력\t|" << endl;
cout << "==============================================" << endl;
}
5-7. Main.cpp
#include"GameManager.h"
int main(void)
{
srand((unsigned int)time(NULL));
GameManager gameManager;
SlideTable slideTable;
gameManager.SetConsoleView();
gameManager.ShowMenu(slideTable);
while (gameManager.GetMenu() == (int)MENU::PLAY)
{
gameManager.ShowDifficulty(slideTable);
gameManager.SetTableByDiff(slideTable);
while (gameManager.GetIsPlay())
{
gameManager.ShowFullTable(slideTable);
gameManager.GetInputKey();
gameManager.ChangeTable(slideTable);
gameManager.IsGameover(slideTable);
}
gameManager.ShowGameover(slideTable);
gameManager.ShowMenu(slideTable);
}
gameManager.ShowQuit(slideTable);
return 0;
}
6. 결과
6. 아쉬운 점
- 객체 지향 프로그래밍에 맞는 프로그래밍을 했는지 아쉬움이 있다. 과연 클래스를 두개만으로 끝낼 프로그램이었을까? 한 클래스를 더 세분화하여 확장성 있는 느낌의 프로그래밍 할 수 있지 않았을까? 코드도 깔끔하게 보이고 결과물도 흠 없어 보이지만 그저 클래스의 갯수와 과다하게 몰린 함수의 모습 때문에 아쉬움이 있다. 하다보면 문제점을 스스로 알 수 있을 것 같다.
'연습' 카테고리의 다른 글
[C++] 월남뽕 게임 (0) | 2021.06.16 |
---|---|
[C++] 몬스터와 1대3 턴제 전투 게임 (0) | 2021.06.14 |
[C++] 빙고 게임_미완성 (0) | 2021.06.13 |
[C++] 몬스터와 일대일 턴제 전투 게임_구조체로 수정 (0) | 2021.06.12 |
[C++] 숫자야구 게임 (0) | 2021.06.08 |