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

[PUSH PUSH] 게임 로직 구현 - AssortRock 11~12일차 오프라인 수업_220921-22

by 헛둘이 2022. 9. 23.
본 글은 어소트락에서 배운 내용을 나름 정리하여 기존 코드를 안보고 제 방식대로 다시 코딩한 것이므로,
디테일한 부분은 학원에서 진행한 부분이 아닌 제 개인적인 코드이 들어갔음을 먼저 밝힙니다.

 

1. 메인 로직
// main.cpp

#include "pch.h"

int main()
{
	std::locale::global(std::locale(".UTF-8"));
	Application::GetInstance().Initialize();

	while (Application::GetInstance().GetStatus())
	{
		Application::GetInstance().Update();
		Application::GetInstance().Rendering();
	}

	Application::GetInstance().Destroy();
}
  • 이전 글과 크게 달라진 점은 없음
  • 메모장에서 Stage를 불러올 때 UTF-8로 불러오므로, wifstream에서 읽을 수 있게 유니코드 지역 설정을 UTF-8로 변경하였음

 

 

 

 

 

2. 전역적으로 사용될 클래스들 선언
// ConsoleHelper.h

#pragma once

#define LOGO_CENTER_X 50
#define LOGO_CENTER_Y 4

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 : 하양 */
};

class ConsoleHelper
{
public:

	static void GoToXY(int x, int y);
	static void SetColor(int color);
};
// ConsoleHelper.cpp

#include "pch.h"
#include "ConsoleHelper.h"

void ConsoleHelper::GoToXY(int x, int y)
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD coord = { static_cast<SHORT>(x), static_cast<SHORT>(y) };
	SetConsoleCursorPosition(handle, coord);
}

void ConsoleHelper::SetColor(int color)
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleTextAttribute(handle, color);
}
  • 맵을 그려주거나 로그를 띄울 때 사용할 좌표 이동 함수들과, 색상 관련 함수들을
  • ConsoleHelper 클래스의 static 함수로 만들어서 가져다 씀

 

 

 

// Application.h

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

	void SetStatus(bool bFlag);
	bool GetStatus() const;

	void SetSceneIndex(int index);

public:
	static Application& GetInstance();

private:
	Application() = default;
	~Application();

	Application(const Application&) = delete;
	Application& operator=(const Application&) = delete;

private:
	Scene* mScenes[SCENETYPE::END];
	int mSceneIdx = SCENETYPE::TITLE;
	bool mStatus = true;
};
//Application.cpp

#include "pch.h"
#include "Application.h"

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

void 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();
	}
}

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

void Application::Rendering()
{

	mScenes[mSceneIdx]->Render();
	Sleep(200);
}

void Application::Destroy()
{
	mScenes[mSceneIdx]->Destroy();
}

void Application::SetStatus(bool bFlag)
{
	mStatus = bFlag;
}

bool Application::GetStatus() const
{
	return mStatus;
}

void Application::SetSceneIndex(int index)
{
	mSceneIdx = index;
}

Application& Application::GetInstance()
{
	static Application instance;
	return instance;
}

Application::~Application()
{
	for (int i = 0; i < SCENETYPE::END; i++)
	{
		delete mScenes[i];
	}
}
  • Application을 싱글턴 객체로 만들어서 사용, 굉장히 편하다
  • 싱글턴 패턴은 Meyer's Singleton 패턴을 사용하였다.
  • Rendering 하는 부분에서 창을 지워주는 system("cls")는 횟수가 많다 보니 너무 깜빡거려서
  • Update 시 엔터를 눌러 창을 넘길 때 지워질 수 있도록 했다.

 

 

 

// Mathlib.h

#pragma once

struct Vector2
{
	int x;
	int y;

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

	}

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

	Vector2(const Vector2& other)
		: x(other.x), y(other.y)
	{}

	Vector2& operator=(const Vector2& other)
	{
		x = other.x;
		y = other.y;

		return *this;
	}

	bool operator==(const Vector2& other) const
	{
		return (x == other.x && y == other.y);
	}

	bool operator!=(const Vector2& other) const
	{
		return !(*this == other);
	}
};


typedef Vector2 Pos;
typedef Vector2 Size;
  • 수학 관련 함수를 정리할 MathLib 헤더를 만들었다.
  • Vector2를 typedef하여 좌표와 크기로 변환해서 사용하다 보니 활용도가 높았다.
  • 실질적으로 Vector2라는 이름의 거의 쓰지 않다보니 현재로썬 더 범용성이 좋은 Pos로 짓는 것이 좋을 것 같기도 하다.
  • 아마 앞으로 Vector2가 더 많이 쓰이겠지.. 생각 중

 

// pch.h

#pragma once
#include <iostream>
#include <fstream>
#include <vector>
#include <memory>
#include <string>
#include <queue>

#include <Windows.h>
#include <tchar.h>
#include <conio.h>

#include "Application.h"
#include "ConsoleHelper.h"
#include "MathLib.h"
  • 미리 컴파일된 헤더를 사용했다.
  • 컴파일 속도를 줄여주는 장점이 있고, 내가 사용할 라이브러리들을 모아놓고
  • 클래스를 만들 때마다 cpp에 헤더가 자동으로 추가되니 작업량이 줄어서 좋다.

 

 

 

 

3. 씬 관련 작업
// Scene.h

#pragma once

enum SCENETYPE
{
	TITLE = 0,
	PLAY = 1,
	DEATH = 2,
	END = 3
};

class Scene
{
public:
	Scene() = default;
	virtual ~Scene() {};

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

};
  • 추상 클래스 씬 자체도 이전 작업물과 크게 달라진 것이 없다.
  • 이번 작업은 PlayScene에 100% 전념했다고 봐도 과언이 아님

 

 

 

// PlayScene.h

#pragma once
#include "Scene.h"
#include "Map.h"


class Stage;

class PlayScene : public Scene
{
public:
	PlayScene();
	virtual ~PlayScene();

	virtual void Initialize() override;
	virtual void Update() override;
	virtual void Render() override;
	virtual void Destroy() override;

private:

	Stage* mStages[5];
	Map* mMap;

};

 

// PlayScene.cpp

#include "pch.h"
#include "PlayScene.h"
#include "Stage01.h"

PlayScene::PlayScene()
	: mStages{}
	, mMap(nullptr)
{
}

PlayScene::~PlayScene()
{
}

void PlayScene::Initialize()
{
	mMap = new Map(8, 8);
	mStages[STAGES::STAGE01] = new Stage01;
	mStages[STAGES::STAGE01]->Load();
	mMap->Load(mStages[STAGES::STAGE01]);

	mMap->Initialize();

}

void PlayScene::Update()
{
	mMap->Update();
}

void PlayScene::Render()
{
	int x = LOGO_CENTER_X;
	int y = LOGO_CENTER_Y - 3;

	ConsoleHelper::SetColor(Color::DARK_BLUE);
	y++; 
	y++;
	ConsoleHelper::SetColor(Color::RED);
	ConsoleHelper::GoToXY(x, y++); wprintf(L"┏━━━━━━━━━━━━━┓");
	ConsoleHelper::GoToXY(x, y++); wprintf(L"┃ P L A Y     ┃");
	ConsoleHelper::GoToXY(x, y++); wprintf(L"┃   S C E N E ┃");
	ConsoleHelper::GoToXY(x, y++); wprintf(L"┃    44TH KHM ┃");
	ConsoleHelper::GoToXY(x, y++); wprintf(L"┗━━━━━━━━━━━━━┛");
	y++;
	y++;
	y++;
	ConsoleHelper::SetColor(Color::DARK_VIOLET);
	mMap->Render();

}

void PlayScene::Destroy()
{
}
  • PlayScene의 멤버 변수로 mStages가 추가되었다.
  • 기존 방식의 경우 Map에 문자열 하드코딩해서 맵으로 사용했다면,
  • 이제는 Stage 객체를 만들고 Stage별로 외부에 있는 메모장에서 긁어 온다.
  • 아마 맵의 크기를 결정짓는 초기값 사이즈는 다음번엔 가장 큰 사이즈를 기준으로 선언해줘야 할 것 같다.
  • 다시 생각해보니 로직을 변경해서 맵을 파일에서 읽어오는 로직을 먼저 실행하고 그 로직 도중에 사이즈를 따와서 맵을 할당하는 것이 좋을듯?
PlayScene이 Map과 Stages를 가지고 있게끔 하고 아래와 같이 로직을 짰다.

1. Stages의 멤버에 메모장에서 불러온 스테이지 파일를 할당
2. Map에 Load 함수를 통해 Stage를 넣어주고, 그 후에 Map은 렌더링을 진행함
3. 확장성을 위해 Stage를 가상함수로 만들어서 Stage01, Stage02, Stage03...이 다 따로 작동하도록 만들었다.
(Stage02, 03...은 아직 미구현)

 

 

 

 

 

맵 관련 작업
  • 여기서부터는 Map과 GameObject, Stage가 버무려지듯 서로간 영향을 주는 곳들이 많아서 이것 저것 신경쓸 게 많았다.

 

// Map.h

#pragma once
#include "Stage.h"

class GameObject;
class Map
{
public:
	Map();
	Map(Size size);
	Map(int y, int x);
	~Map();

	void Initialize();
	void Update();
	void Render();
	void Destroy();

	GameObject* GetPositionBall(Pos pos);

	bool Load(Stage* stage);
	void GameObjectMapping();
	void AddGameObject(GameObject* object);
	void EraseMark(Pos pos);
	wchar_t** GetMapData();

private:
	wchar_t** mMapData;
	Size mMapSize;
	
	GameObject* mGameObjects[128];

};
// Map.cpp

#include "pch.h"
#include "Map.h"
#include "GameObject.h"
#include "Player.h"
#include "Ball.h"

Map::Map()
	: mMapData(nullptr)
	, mMapSize(Size{0, 0})
	, mGameObjects{}
{

}

Map::Map(Size size)
	:mGameObjects{}
{
	mMapSize = size;
	mMapData = new wchar_t* [mMapSize.y] {};

	for (int y = 0; y < mMapSize.y; y++)
	{
		mMapData[y] = new wchar_t[mMapSize.x] {};
	}
}

Map::Map(int y, int x)
	:mGameObjects{}
{
	mMapSize = { x, y };
	mMapData = new wchar_t* [mMapSize.y] {};

	for (int y = 0; y < mMapSize.y; y++)
	{
		mMapData[y] = new wchar_t[mMapSize.x] {};
	}
}

Map::~Map()
{
	for (int y = 0; y < mMapSize.y; y++)
	{
		delete[] mMapData[y];
	}

	delete[] mMapData;

	for (size_t i = 0; i < 128; i++)
	{
		if (mGameObjects[i] != nullptr)
			delete mGameObjects[i];
	}
}

void Map::Initialize()
{
	GameObjectMapping();

	for (int i = 0; i < 128; i++)
	{
		if (mGameObjects[i] != nullptr)
		{
			mGameObjects[i]->Initialize(this);
		}
	}
	
}

void Map::Update()
{
	for (int i = 0; i < 128; i++)
	{
		if (mGameObjects[i] != nullptr)
		{
			mGameObjects[i]->Update();
		}
	}
}

void Map::Render()
{
	for (int i = 0; i < 128; i++)
	{
		if (mGameObjects[i] != nullptr)
		{
			mGameObjects[i]->Render();
		}
	}

	for (int y = 0; y < mMapSize.y; y++)
	{
		for (int x = 0; x < mMapSize.x; x++)
		{
			ConsoleHelper::GoToXY(
				LOGO_CENTER_X + x * 2,
				LOGO_CENTER_Y + y + 10);

			switch (mMapData[y][x])
			{
			case L'★':
				ConsoleHelper::SetColor(Color::DARK_GREEN);
				break;

			case L'●':
				ConsoleHelper::SetColor(Color::BLUE);
				break;

			case L'▒':
				ConsoleHelper::SetColor(Color::DARK_YELLOW);
				break;

			}

			std::wcout << mMapData[y][x];
			ConsoleHelper::SetColor(Color::DARK_VIOLET);
		}

		std::wcout << std::endl;
	}
}

void Map::Destroy()
{
}

GameObject* Map::GetPositionBall(Pos pos)
{
	for (size_t i = 0; i < 128; i++)
	{
		if (mGameObjects[i] != nullptr)
		{
			if (mGameObjects[i]->GetMark() == L'●' &&
				mGameObjects[i]->GetPos() == pos)
				return mGameObjects[i];
		}

		else 
			return nullptr;
			
	}

	return nullptr;
}

bool Map::Load(Stage* stage)
{

	std::wstring mapData = stage->GetMapData();

	if (mapData.empty())
		return false;
	
	int index = 0;
	for (int y = 0; y < mMapSize.y; y++)
	{
		for (int x = 0; x < mMapSize.x; x++)
		{
			mMapData[y][x] = mapData[index];
			index++;
		}
	}

	return true;
}

void Map::GameObjectMapping()
{
	for (int y = 0; y < mMapSize.y; y++)
	{
		for (int x = 0; x < mMapSize.x; x++)
		{
			switch (mMapData[y][x])
			{
			case L'★': AddGameObject(new Player(y, x));
				break;
			
			case L'●': AddGameObject(new Ball(y, x));
				break;

			}
		}
	}
}

void Map::AddGameObject(GameObject* object)
{
	for (int i = 0; i < 128; i++)
	{
		if (mGameObjects[i] == nullptr)
		{
			mGameObjects[i] = object;
			break;
		}
	}
}

void Map::EraseMark(Pos pos)
{
	mMapData[pos.y][pos.x] = L'ㅤ';
}



wchar_t** Map::GetMapData()
{
	return mMapData;
}

 

 

 

 

 

 

 

 

Stage 관련 작업
#pragma once

enum STAGES
{
	STAGE01 = 0,
	STAGE02 = 1,
	STAGE03 = 2,
	STAGE04 = 3,
	STAGE05 = 4
};

class Stage
{

public:
	Stage();
	virtual ~Stage();

	virtual bool Load() = 0;

	virtual std::wstring GetMapData() const = 0;

protected:
	std::wstring mMapData;
};
#include "pch.h"
#include "Stage.h"

Stage::Stage()
	: mMapData()
{
}

Stage::~Stage()
{
}
  • 스테이지는 추상 클래스로 만들었다.
  • 맵에 따라 다른 파일을 불러올 수 있도록 하기 위함임

 

 

 

// Stage01.h

#pragma once
#include "Stage.h"

class Stage01 : public Stage
{
public:
	Stage01();
	virtual ~Stage01();
	virtual bool Load() override;

	virtual std::wstring GetMapData() const override;

private:


};
// Stage01.cpp

#include "pch.h"
#include "Stage01.h"

Stage01::Stage01()
{
}

Stage01::~Stage01()
{
}

bool Stage01::Load()
{

	std::wifstream wifs(L"..\\Stages\\Stage01.txt");


	std::wstring temp;
	while (std::getline(wifs, temp))
	{
		mMapData += temp;
	}

	wifs.close();

	if (mMapData.empty())
		return false;

	return true;
}

std::wstring Stage01::GetMapData() const
{
	return mMapData;
}
  • wifstream으로 파일을 읽어오는 로직을 짜봤다.
  • 학원에서 알려주신 파일 포인터를 이용한 방법도 물론 숙지했지만, 최신 문법에서 오는 간결함은 보는 것만으로도 마음이 상쾌해진다.
  • 하지만 내부에서 그 작업을 대신 해주고 있는 거니까 내가 관여할 수 없는 부분에 오버헤드도 있겠지라는 막연한 생각..

 

 

 

 

플레이어 관련 처리
#pragma once
#include "GameObject.h"
#include "Map.h"

class Player : public GameObject
{
public:
	Player(Pos pos);
	Player(int y, int x);

	virtual ~Player();

	virtual void Initialize(Map* map);
	virtual void Update() override;
	virtual void Render() override;
	virtual wchar_t GetMark() override;

	void MovePlayer();
	bool IsCorrectWay(Pos pos);
	bool PushIsPossible(Pos pos);
	bool NextBlockIsBall(Pos pos);

private:
	Map* mMap;
};
#include "pch.h"
#include "Player.h"

Player::Player(Pos pos) 
	: GameObject(pos)
	, mMap(nullptr)
{
	mMark = L'★';
}

Player::Player(int y, int x)
	: GameObject(Pos{x, y})
	, mMap(nullptr)
{
	mMark = L'★';
}

Player::~Player()
{
}

void Player::Initialize(Map* map)
{
	mMap = map;
}

void Player::Update()
{
	MovePlayer();
}

void Player::Render()
{
	wchar_t** mapData = mMap->GetMapData();
	mapData[mPos.y][mPos.x] = L'★';
}

wchar_t Player::GetMark()
{
	return L'★';
}

void Player::MovePlayer()
{
	Pos nextPos = mPos;
	Pos nextNextPos = mPos;
	if (_kbhit())
	{
		char ch = _getch();
		if (islower(ch))
			ch -= 32;

		switch (ch)
		{
		case 'W': 
			nextPos.y -= 1; 
			nextNextPos.y -= 2;
			break;

		case 'S': 
			nextPos.y += 1;
			nextNextPos.y += 2;
			break;

		case 'A': 
			nextPos.x -= 1;
			nextNextPos.x -= 2;
			break;

		case 'D': 
			nextPos.x += 1; 
			nextNextPos.x += 2;
			break;
		}

		if (IsCorrectWay(nextPos))
		{
			mMap->EraseMark(mPos);
			std::swap(mPos, nextPos);
		}

		// 다음 칸에 볼이 있는지 확인
		if (NextBlockIsBall(nextPos))
		{
			//볼이 있다면 밀 수 있는지 확인
			if (PushIsPossible(nextNextPos))
			{
				//해당 칸에 있는 Ball 객체를 가져옴
				GameObject* ball = mMap->GetPositionBall(nextPos);

				//널 체크
				if (ball != nullptr)
				{

					//볼의 위치를 변경
					ball->SetPos(nextNextPos);

					//기존 위치에 그려진 볼을 지움
					//볼은 Ball의 렌더링에서 그려짐
					mMap->EraseMark(nextPos);

				}

				mMap->EraseMark(mPos);
				std::swap(mPos, nextPos);
			}
		}
	}
}

bool Player::IsCorrectWay(Pos pos)
{
	wchar_t** mMapData = mMap->GetMapData();

	if (mMapData[pos.y][pos.x] == L'ㅤ')
	{
		return true;
	}

	return false;
}

bool Player::PushIsPossible(Pos pos)
{
	wchar_t** mMapData = mMap->GetMapData();
	if (mMapData[pos.y][pos.x] == L'ㅤ' ||
		mMapData[pos.y][pos.x] == L'▒')
	{
		return true;
	}

	return false;
}

bool Player::NextBlockIsBall(Pos pos)
{
	wchar_t** mMapData = mMap->GetMapData();

	if (mMapData[pos.y][pos.x] == L'●')
	{
		return true;
	}

	return false;
}
  • 이동 관련 로직들이 막 추가되고 앞의 Ball을 어떻게 치울 것인가에 대한 대책으로
  • 여러 함수들이 많이 만들어졌다..
  • 이 부분도 처음부터 몇 번 더 만들어보고 개선해나갈 예정

 

 

 

 

 

#pragma once
#include "GameObject.h"
#include "Map.h"

class Ball : public GameObject
{
public:
	Ball(Pos pos);
	Ball(int y, int x);

	virtual ~Ball();

	virtual void Initialize(Map* map) override;
	virtual void Update() override;
	virtual void Render() override;

	virtual wchar_t GetMark() override;

private:
	Map* mMap;

	
};
#include "pch.h"
#include "Ball.h"

Ball::Ball(Pos pos)
	: GameObject(pos)
{
	mMark = L'●';
}

Ball::Ball(int y, int x)
	: GameObject(Pos{ x, y })
{
	mMark = L'●';
}

Ball::~Ball()
{
}

void Ball::Initialize(Map* map)
{
	mMap = map;
}

void Ball::Update()
{
}

void Ball::Render()
{
	wchar_t** mapData = mMap->GetMapData();
	mapData[mPos.y][mPos.x] = L'●';
}

wchar_t Ball::GetMark()
{
	return L'●';
}
  • Ball은 거의 타의에 의해 움직이는 객체라는 설정이라서 Player에 비해 난이도가 낮았다.

댓글