본문 바로가기
게임 개발/[AssortRock] 콘솔 게임(CUI) 설계 및 분석

[PUSH PUSH] 게임 구조 파악 및 설계 - AssortRock 10일차 오프라인 수업_220920

by 헛둘이 2022. 9. 21.
1, 게임 전반적 흐름을 관리할 관리자 객체 만들기
  • 새 필터 -> HighLevel_Interface를 만들고 클래스 Application 추가
//Application.h
#pragma once
class Application
{

public:
	Application();
	~Application();

	bool Initialize();
	void Update();
	void Rendering();
	void Destroy();

private:

};
//Application.cpp
#include "Application.h"

Application::Application()
{
}

Application::~Application()
{
}

bool Application::Initialize()
{

	return true;
}

void Application::Update()
{
}

void Application::Rendering()
{
}

void Application::Destroy()
{
}

 


 

2. 게임에서 사용할 객체들의 부모 클래스 GameObject 만들기
  • 새 필터 -> GameObject를 만들고 클래스 GameObject 추가
  • 모든 게임 오브젝트들이 가져야 할 것이 무엇인지 생각해보기 -> 좌표가 있어야 함
  • 좌표와 그 외 다른 공통적으로 쓰일 것들을 정의할 뭔가가 필요함 (재사용성 고려)
  • 수학 관련 라이브러리 yaMath.h와 그것을 포함한 범용적인 헤더 Common.h를 만듦 
// yaMath.h
#pragma once

struct Vector2
{
	int x;
	int y;

	Vector2()
		: x(0), y(0)
	{

	}

	Vector2(int x, int y)
		: x(x), y(y)
	{

	}
};

typedef Vector2 Pos;
// Common.h
#pragma once

#include <iostream>
#include <Windows.h>
#include <vector>
#include "yaMath.h"

 


 

3. Application 클래스에서 개략적인 게임 로직 틀 만들기

 

// Application.h

#pragma once
class Application
{

public:
	Application();
	~Application();

	bool Initialize();
	void Update();
	void Rendering();
	void Destroy();

	bool GetIsRunning() { return isRunning; }

private:
	bool isRunning;
};
// Application.cpp

#include "Application.h"

Application::Application()
	: isRunning(true)
{
}

Application::~Application()
{
}

bool Application::Initialize()
{

	return true;
}

void Application::Update()
{
}

void Application::Rendering()
{
}

void Application::Destroy()
{
}
// main.cpp

#include <iostream>
#include "Application.h"

Application application;

int main()
{
	application.Initialize();

	while (application.GetIsRunning())
	{
		application.Update();
		application.Rendering();
	}

	application.Destroy();
}
  • 전역 변수로 만들어진 Application 객체를 사용해서
  • Initialize 함수로 초기화 (이후에 게임 오브젝트들의 초기값을 세팅해주는 역할)
  • while 루프를 통해 Update(게임의 내부 상태값 변화) 하고, Rendering(변화된 값을 그려줌)을 무한히 반복
  • 아마 Update 함수를 통해 Application의 내부 변수인 isRunning을 false로 만들어서 게임을 종료시킬 것 같다. 
  • while 루프는 GetIsRunning이라는 상태값이 true인지 체크하고 false일 경우 루프를 빠져나간다.
  • Destroy는 게임이 종료된 후 리소스를 해제해주는 역할

 

 

 

 

 


 4. 맵을 그려줄 Map 클래스 설계하기

 

// Map.h

#pragma once

#define SIZE_MAP_Y 30
#define SIZE_MAP_X 24

class Map 
{
public:
	Map();
	~Map();

	void Initialize();
	void Render();


private:

	wchar_t mMap[SIZE_MAP_Y][SIZE_MAP_X];
	// 상수를 넣으면 맵을 탐색하려면 상수를 집어넣어야 하는 불편함이 있음
	// 그리고 사이즈 변경 시 매우 불편함

};
// Map.cpp

#include "Common.h"
#include "Map.h"

Map::Map()
{
}

Map::~Map()
{
}

void Map::Initialize()
{
	for (size_t y = 0; y < SIZE_MAP_Y; y++)
	{
		for (size_t x = 0; x < SIZE_MAP_X; x++)
		{
			mMap[y][x] = L'□';
		}

	}
}

void Map::Render()
{
	for (size_t y = 0; y < SIZE_MAP_Y; y++)
	{
		for (size_t x = 0; x < SIZE_MAP_X; x++)
		{
			std::wcout << mMap[y][x];
		}

		std::cout << std::endl;

	}
}
  • Map 클래스는 게임이 진행될 맵의 정보를 가진 클래스
  • GameObject가 가진 좌표랑은 별 상관이 없으므로 상속받지 않는 별개의 객체
  • Initialize 함수에서 Map의 좌표에 초기값 세팅해주기 (Application의 Initialize가 호출해서 초기화)
  • Render 함수에서 Map 이중 for문을 돌며 출력 (이 Render 함수는 Application의 Render에서 작동함)

 

 

 

// Application.cpp

#include "Common.h"
#include "Application.h"


Application::Application()
	: isRunning(true)
{
}

Application::~Application()
{
}

bool Application::Initialize()
{
	mMap.Initialize();



	return true;
}

void Application::Update()
{
}

void Application::Rendering()
{
	system("cls");
	mMap.Render();
	Sleep(500);
}

void Application::Destroy()
{
}
  • Rendering 함수에 system("cls");는 그리기 전 콘솔 창을 한 번 지워달라는 의미
  • Sleep(500)은 출력 속도가 너무 빨라서 깜빡거리는 문제가 있기 때문에, 
  • 0.5초에 한번씩 출력해달라는 의미, (ms 단위라서 500)

 

 

 

 

 


5. 콘솔의 커서 위치와 색상 조정하는 함수 추가
  • Common.h에서 언제 어디서나 쓸 수 있는 함수 2개 추가
  • 출력 시 커서가 항상 맨 아래 위치하므로 변경이 필요함
  • 게임을 만들 때 색상도 시각적으로 중요한 역할을 하므로 세팅 필요

 

// Common.h

#pragma once

#include <iostream>
#include <Windows.h>
#include <vector>
#include "yaMath.h"

enum Color
{
	BLACK, /* 0 : 까망 */
	DARK_BLUE, /* 1 : 어두운 파랑 */
	DARK_GREEN, /* 2 : 어두운 초록 */
	DARK_SKY_BLUE, /* 3 : 어두운 하늘 */
	DARK_RED, /* 4 : 어두운 빨강 */
	DARK_VIOLET, /* 5 : 어두운 보라 */
	DARK_YELLOW, /* 6 : 어두운 노랑 */
	GRAY, /* 7 : 회색 */
	DARK_GRAY, /* 8 : 어두운 회색 */
	BLUE, /* 9 : 파랑 */
	GREEN, /* 10 : 초록 */
	SKY_BLUE, /* 11 : 하늘 */
	RED, /* 12 : 빨강 */
	VIOLET, /* 13 : 보라 */
	YELLOW, /* 14 : 노랑 */
	WHITE, /* 15 : 하양 */
};

inline void GoToXY(int x, int y) {
	COORD xy = { static_cast<SHORT>(x), static_cast<SHORT>(y) };
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), xy);
}


inline void SetColor(int color)
{
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE) , color);
}
// Map.cpp

#include "Common.h"
#include "Map.h"

Map::Map()
{
}

Map::~Map()
{
}

void Map::Initialize()
{
	for (size_t y = 0; y < SIZE_MAP_Y; y++)
	{
		for (size_t x = 0; x < SIZE_MAP_X; x++)
		{
			mMap[y][x] = L'□';
		}

	}
}

void Map::Render()
{

	SetColor(Color::DARK_VIOLET);
	for (size_t y = 0; y < SIZE_MAP_Y; y++)
	{
		for (size_t x = 0; x < SIZE_MAP_X; x++)
		{
			std::wcout << mMap[y][x];
		}

		std::cout << std::endl;

	}
}
  • 컬러 세팅

 

 

 

 

 


6. 씬 (Scene) 도입하기
  • Scene은 각 장면을 보여주는 클래스
  • 이후 TitleScene과 PlayScene, DeathScene을 만들기 위한 일종의 추상 클래스
  • Application은 Scene의 포인터를 가지고 가상함수를 통해 각 장면마다 다르게 표현할 수 있다.
  • -> Map 객체는 Application에서 Scene으로 넘어가고, Map의 자리를 Scene이 대신하는 것

 

// Scene.h

#pragma once
#include "Map.h"
class Scene
{
public:
	void Initialize();
	void Update();
	void Render();
	void Destroy();

private:
	Map mMap;
};
// Scene.cpp

#include "Common.h"
#include "Scene.h"

void Scene::Initialize()
{
	mMap.Initialize();
}

void Scene::Update()
{
}

void Scene::Render()
{
	mMap.Render();
}

void Scene::Destroy()
{
}
// Application.h

#pragma once
#include "Scene.h"

enum SCENETYPE
{
	TITLE,
	PLAY,
	DEATH,
	END
};


class Application
{

public:
	Application();
	~Application();

	bool Initialize();
	void Update();
	void Rendering();
	void Destroy();

	bool GetIsRunning() { return mbRunning; }

private:
	bool mbRunning;
	Scene* mScenes[SCENETYPE::END] = {nullptr};
	int mPlaySceneNumber;

};
// Application.cpp

#include "Common.h"
#include "Application.h"


Application::Application()
	: mbRunning(true)
	, mPlaySceneNumber((int)SCENETYPE::TITLE)
{
}

Application::~Application()
{
}

bool Application::Initialize()
{
	for (int i = 0; i < SCENETYPE::END; i++)
	{
		mScenes[i]->Initialize();
	}



	return true;
}

void Application::Update()
{
	mScenes[mPlaySceneNumber]->Update();
}

void Application::Rendering()
{
	system("cls");
	mScenes[mPlaySceneNumber]->Render();
	Sleep(500);
}

void Application::Destroy()
{
	for (int i = 0; i < SCENETYPE::END; i++)
	{
		mScenes[i]->Destroy();
	}
}

 

 

 

 

 


7. Ttitle Scene 작성하기
  • 우선 Scene은 기타 씬들의 설계도 역할을 하기 때문에, 객체가 만들어질 필요가 없으므로 추상클래스 처리

 

// Scene.h

#pragma once
#include "Map.h"
class Scene
{
public:
	Scene();
	virtual ~Scene();

	virtual void Initialize() = 0;
	virtual void Update() = 0;
	virtual void Render() = 0;
	virtual void Destroy() = 0;

	

protected:
	Map mMap;
};
  • 순수 가상 함수가 1개라도 있으면 추상 클래스가 된다.

 

// Scene.cpp

#pragma once
#include "Scene.h"
class TitleScene : public Scene
{
public:
	TitleScene();
	~TitleScene();

	virtual void Initialize() override;
	virtual void Update() override;
	virtual void Render() override;
	virtual void Destroy() override;
};
// TitleScene.cpp

#include "Common.h"
#include "TitleScene.h"

TitleScene::TitleScene()
{
}

TitleScene::~TitleScene()
{
}

void TitleScene::Initialize()
{
}

void TitleScene::Update()
{
}

void TitleScene::Render()
{
	short x = SIZE_MAP_X * 2;
	short y = 0;
	SET_COLOR(Color::DARK_YELLOW);
	GOTO_XY(x, y + 2);	printf("┏━━━━━━━━━━━━━━━━━━━━┓");
	GOTO_XY(x, y + 3);	printf("┃ P U S H    P U S H ┃");
	GOTO_XY(x, y + 4);	printf("┃  made by     KHM   ┃");
	GOTO_XY(x, y + 5);	printf("┗━━━━━━━━━━━━━━━━━━━━┛");
	SET_COLOR(Color::DARK_YELLOW);
	GOTO_XY(x + 2, y + 1);
}

void TitleScene::Destroy()
{
}
  • inline 함수로 만들어도 되지만, 매크로 함수의 사용법을 알기 위함과, 수업의 내용을 100% 이해하기 위해서
  • GOTO_XY, SET_COLOR 함수를 매크로 함수로 변경
  • Render 함수는 Application에서 가지고 있는 mScenes의 멤버로써 Rendering에서 불리게 됨

 

// Common.h

#pragma once

#include <iostream>
#include <Windows.h>
#include <vector>
#include <conio.h>
#include "yaMath.h"

enum Color
{
	BLACK, /* 0 : 까망 */
	DARK_BLUE, /* 1 : 어두운 파랑 */
	DARK_GREEN, /* 2 : 어두운 초록 */
	DARK_SKY_BLUE, /* 3 : 어두운 하늘 */
	DARK_RED, /* 4 : 어두운 빨강 */
	DARK_VIOLET, /* 5 : 어두운 보라 */
	DARK_YELLOW, /* 6 : 어두운 노랑 */
	GRAY, /* 7 : 회색 */
	DARK_GRAY, /* 8 : 어두운 회색 */
	BLUE, /* 9 : 파랑 */
	GREEN, /* 10 : 초록 */
	SKY_BLUE, /* 11 : 하늘 */
	RED, /* 12 : 빨강 */
	VIOLET, /* 13 : 보라 */
	YELLOW, /* 14 : 노랑 */
	WHITE, /* 15 : 하양 */
};

#define GOTO_XY(x, y) SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), {static_cast<short>(x), static_cast<short>(y)})
#define SET_COLOR(color) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE) , color)

//inline void GoToXY(int x, int y) {
//	COORD xy = { static_cast<SHORT>(x), static_cast<SHORT>(y) };
//	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), xy);
//}
//
//inline void SetColor(int color)
//{
//	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE) , color);
//}

 

#include "Common.h"
#include "Application.h"
#include "TitleScene.h"
#include "PlayScene.h"
#include "DeathScene.h"


Application::Application()
	: mbRunning(true)
	, mPlaySceneNumber((int)SCENETYPE::TITLE)
{
}

Application::~Application()
{
}

bool Application::Initialize()
{
	mScenes[SCENETYPE::TITLE] = new TitleScene;
	mScenes[SCENETYPE::PLAY] = new PlayScene;
	mScenes[SCENETYPE::DEATH] = new DeathScene;

	for (int i = 0; i < SCENETYPE::END; i++)
	{
		mScenes[i]->Initialize();
	}



	return true;
}

void Application::Update()
{
	mScenes[mPlaySceneNumber]->Update();
}

void Application::Rendering()
{
	system("cls");
	mScenes[mPlaySceneNumber]->Render();
	Sleep(500);
}

void Application::Destroy()
{
	for (int i = 0; i < SCENETYPE::END; i++)
	{
		mScenes[i]->Destroy();
	}
}
  • mScenes[mPlaySceneNumber]->Render()에서 가상함수 테이블에 덮어써진 TitleScene의 Render를 호출

 

 

 

 

 


8.  키 입력 받는 로직 만들기 + 나머지 씬 작성하기
  • 추상 클래스의 Initialize 로직 변경 (Application을 인자로 받음)
// Scene.h

#pragma once
#include "Map.h"
class Application;
class Scene
{
public:
	Scene();
	virtual ~Scene();

	virtual void Initialize(Application* app) = 0;
	virtual void Update() = 0;
	virtual void Render() = 0;
	virtual void Destroy() = 0;

	

protected:
	Map mMap;
	Application* application;
};
  • 이후 씬을 넘길 때 Application이 가진 mPlaySceneNumber 멤버의 값을 수정해야 하므로
  • Initialize 함수에서 자신의 포인터인 this를 넘김
  • Update에 씬 넘기는 로직 추가

 

// TitleScene.cpp

#include "Common.h"
#include "TitleScene.h"
#include "Application.h"

TitleScene::TitleScene()
{
}

TitleScene::~TitleScene()
{
}

void TitleScene::Initialize(Application* app)
{
	application = app;
}

void TitleScene::Update()
{
	//char key = 0;
	//key = _getch(); // 입력이 있을 때까지 멈춰있음

	if (_kbhit()) // 키 눌림여부를 확인하는 함수
	{
		char input = _getch();
		if (input == '\r')
		{
			application->SetPlaySceneNumber(SCENETYPE::PLAY);
		}
	}
}

void TitleScene::Render()
{
	short x = SIZE_MAP_X * 2;
	short y = 10;
	SET_COLOR(Color::DARK_YELLOW);
	GOTO_XY(x, y + 2);	printf("┏━━━━━━━━━━━━━━━━━━━━┓");
	GOTO_XY(x, y + 3);	printf("┃ P U S H    P U S H ┃");
	GOTO_XY(x, y + 4);	printf("┃  made by     KHM   ┃");
	GOTO_XY(x, y + 5);	printf("┗━━━━━━━━━━━━━━━━━━━━┛");

	SET_COLOR(Color::DARK_GREEN);
	GOTO_XY(x, y + 8);	printf("     PRESS ENTER    ");
	
	GOTO_XY(x + 2, y + 1);
}

void TitleScene::Destroy()
{
}
// PlayScene.cpp

#include "Common.h"
#include "PlayScene.h"
#include "Application.h"

PlayScene::PlayScene()
{
}

PlayScene::~PlayScene()
{
}

void PlayScene::Initialize(Application* app)
{
	application = app;
}

void PlayScene::Update()
{
	if (_kbhit()) // 키 눌림여부를 확인하는 함수
	{
		char input = _getch();
		if (input == '\r')
		{
			application->SetPlaySceneNumber(SCENETYPE::DEATH);
		}
	}
}

void PlayScene::Render()
{
	short x = SIZE_MAP_X * 2;
	short y = 10;
	SET_COLOR(Color::DARK_RED);
	GOTO_XY(x, y + 2);	printf("┏━━━━━━━━━━━━━━━━━━━━━┓");
	GOTO_XY(x, y + 3);	printf("┃ P L A Y   S C E N E ┃");
	GOTO_XY(x, y + 4);	printf("┗━━━━━━━━━━━━━━━━━━━━━┛");
	GOTO_XY(x + 2, y + 1);
}

void PlayScene::Destroy()
{
}
  • _kbhit() 함수는 키 입력이 있는지 유무를 검사해주는 함수이며, 키를 받기 위해 멈추거나 하지 않음
  • 키가 눌리면 _getch() 함수를 호출해서 어떤 키인지 파악하고, 그게 엔터('\r')면 씬을 넘겨준다.

 

 

// DeathScene.cpp

#include "Common.h"
#include "DeathScene.h"
#include "Application.h"

DeathScene::DeathScene()
	: changeColor((int)Color::BLACK)
{
}

DeathScene::~DeathScene()
{
}

void DeathScene::Initialize(Application* app)
{
	application = app;
}

void DeathScene::Update()
{
	if (_kbhit()) // 키 눌림여부를 확인하는 함수
	{
		char input = _getch();
		if (input == '\r')
		{
			application->SetIsRunning(false);
		}
	}
}

void DeathScene::Render()
{
	changeColor = (changeColor + 1) % 10;
	short x = SIZE_MAP_X * 2;
	short y = 10;
	SET_COLOR(changeColor);
	GOTO_XY(x, y + 2);	printf("┏━━━━━━━━━━━━━━━━━━━━━━━━━┓");
	GOTO_XY(x, y + 3);	printf("┃ Your character is dead..┃");
	GOTO_XY(x, y + 4);	printf("┗━━━━━━━━━━━━━━━━━━━━━━━━━┛");
	GOTO_XY(x + 2, y + 1);
}

void DeathScene::Destroy()
{
}
  • 숙제로 나온 DeathScene.
  • enum 값으로 되어 있는 Color를 순환시켜서 매 Render마다 색이 바뀌게 함
  • Update문에서 한 번 더 엔터를 누르면 종료되게 함

댓글