~~stoggle_buttons~~
이 프로젝트는 [[raylib:flappybird:flappy_bird_만들기|플래피 버드 만들기]]에서 분기한 프로젝트다.
{{ :raylib:flappybird_1.zip |Flappy Bird Chapter1}} 파일에서 부터 코딩을 할 것이다.
===== 필요성 =====
우리는 지금까지 게임을 객체지향으로 만들고 있다. 각각의 객체를 나눔으로써 코드가 보기에 깔끔해졌다.
이에따라 main.cpp와 game.cpp 그리고 bird.cpp 등 파일이 많아졌다.
이렇게 각각의 cpp파일에서는 각각 서로 다른 메모리를 호출할 것이다.
그런데 게임을 실행하는 내내 같은 여러 파일을 오고가며 일관성을 유지하는 개체도 필요하다.
이를테면 스테이지는 바뀌어도 플레이어의 점수는 누적되어야 하는 등의 필요성 말이다.
각 씬을 바꾸는 매니저도 메모리에 딱 하나만 존재해야 할 것이다. 그래야만 메인메뉴에서 게임으로도 씬을 변경할 수 있고,
게임에서 게임오버씬으로도 변경할 수 있기 때문이다.
이렇게 오로지 한개의 메모리만 지시하는 클래스가 필요할 수 있는데, 이걸 디자인패턴에서 싱글톤 패턴이라고 한다.
===== 싱글톤 패턴 만들기 =====
==== 1. congif.h 파일 ====
config.h 파일을 만들기로 하자
#pragma once
#ifndef __CONFIG__
#define __CONFIG__
// 로고, 메인메뉴, 게임 화면 등 각각의 씬
enum class Scene
{
logo = 0, titleMenu, game, scoreScene
};
// 전역적으로 사용할 싱글톤 클래스
class SceneManager
{
private:
SceneManager() {}
static SceneManager* instance;
public:
static SceneManager* GetInstance()
{
if (instance != nullptr)
{
return instance;
}else
{
instance = new SceneManager();
return instance;
}
}
bool gameExit;
Scene scene;
};
#endif
싱글톤을 어떻게 만들었는지 살펴보자.
SceneManager의 생성자인 SceneManager()를 Private:에 넣어놨다.
그리고 instace를 static으로 생성하여 프로그램이 끝날때까지 생명을 불어넣었다.
생성자를 private에 넣었으므로 아무렇게나 생성할 수 없다. 단지 GetInstance()함수를 불러 올 수 밖에 없다.
그런데 GetInstance()함수에서는 기존에 instace가 있으면 기존 instace를 불러오고, instance가 없을 때에만 새로 생성하게 만들었다.
따라서 어디에서 언제 불러오든 SceneManager의 instance는 유일하게 된다.
==== 2. 싱글톤 구현해 보기 ====
=== 가. 널포인터 선언 ====
main.cpp에서 main함수의 바깥에서 다음과 같이 SceneManager의 instance를 nullptr로 비어준다.
SceneManager* SceneManager::instance = nullptr;
이것을 그냥 config.h에서 구현해 보려고 하였는데 계속하여 오류가 났다. 왜 위의 코드를 main.cpp에서만 구현해야 하는지 아는 사람은 댓글을 달아주었으면 한다.
=== 나. 싱글톤 확인해 보기 ===
이제 config.h를 game.h에 인클루드 한 후에((game.h에만 인클루드하면 된다. main.cpp는 game.h도 인클루드 하므로 main.cpp는 자연히 config.h를 인클루드 한다. ))
main.cpp와 game.cpp에 Scnemanager를 다음과 같이 여럿 선언해 보자
== (1) main.cpp ==
SceneManager* a = SceneManager::GetInstance();
SceneManager* b = SceneManager::GetInstance();
== (2) game.cpp ==
SceneManager* c = SceneManager::GetInstance();
SceneManager* d = SceneManager::GetInstance();
== 각각의 주소를 확인해 보기 ==
이제 각각의 cpp파일에서 다음 코드로 각 인스턴스의 포인터 주소를 확인해 보자
printf("a instance address : %p\n", a);
이런 식으로 확인해 보자.
그러면 main.cpp에서 선언한 instance든, game.cpp에서 선언한 instance든, 모두 동일한 주소를 가지고 있음을 확인할 수 있다.
==== 3. 게임 종료 기능 구현하기 ====
그럼 이제 싱글톤으로 좀 더 실용적인 기능을 만들도록 하자.
우리는 게임 루프를 raylib의 내장 함수인 WindowShouldClose()로 돌렸다.
그런데 이러면 ESC키를 누르면 무조건 게임이 종료된다. 그런데 우리는 게임화면에서 ESC키를 누르면 정지화면이 나오고,
다시 정지상태에서 ESC를 누른상태에서만 게임이 종료되게 할 것이다. 통상의 게임을 이런 식으로 작동하기 때문이다.
=== 가. 게임 루프를 SceneManager 내부 변수로 바꾸기 ===
SceneManager에 boolean으로 gameExit를 선언했다는 것을 기억하자.
gameExit가 false인 상태에서는 계속하여 프로그램이 돌아가도록 코드를 짜보자.
main.cpp에서 main함수 내에 다음과 같이 코딩하였다.
SceneManager* menuManager = SceneManager::GetInstance();
menuManager->gameExit = false;
Game game {screenWidth, screenHeight, "Flappy Bird"};
//--------------------------------------------------------------------------------------
// Main game loop
while (!menuManager->gameExit) // Detect window close button or ESC key
{
game.Tick();
}
while문에 GameShouldClose()대신에 menuManager((이전에는 'a'라는 이름으로 선언하였으나 가독성을 위해 menuManager로 바꾸었다)) 내의 변수인 gameExit를 이용하였다.
=== 나. game.cpp 에서 gameExit 값 바꾸기 ===
일단 클래스의 바깥에서 다음과 같이 SceneManager 클래스의 인스턴스를 하나 선언한다.
SceneManager* Manager = SceneManager::GetInstance();
그 다음에 update()메서드를 다음과 같이 바꾸었다.
void Game::Update()
{
// Update
switch (gamestate)
{
case Gamestate::pause : // 정지로직
if (IsKeyPressed(KEY_UP)) gamestate = Gamestate::game;
if (gamestate == Gamestate::pause && IsKeyPressed(KEY_ESCAPE) ) Manager->gameExit = true; // 정지상태일때, ESC 누르면 종료
break;
case Gamestate::game: // 게임로직
SetExitKey(KEY_F9);
bird.Update();
if (IsKeyPressed(KEY_UP)) bird.Jump();
if (IsKeyPressed(KEY_ESCAPE)) gamestate = Gamestate::pause;
break;
default:
break;
}
}
main.cpp와 달리 game.cpp에서 SceneManager클래스의 인스턴스의 이름은 'Manager'이다((main.cpp에서는 'menuManager'였다)). 그런데 Manager->gameExit 변수를 game.cpp에서 변경시켰는데,
main.cpp에서 while문을 탈출하고 게임이 끝났다.
즉, 싱글톤이 잘 작동함을 알 수 있다.
===== 로고 화면 만들기 =====
==== 1. 로고 클래스 만들기 ====
=== 가. logo.h ===
게임 클래스와 별반 다를 바 없다. 다만 Init() 함수를 따로 만들고 생성자에는 아무 것도 안 만들었음을 유의하라.
#pragma once
#include
#include "raylib.h"
#include "config.h"
class Logo
{
public :
void Init(int width, int height, std::string title, int maxTimer);
void Tick();
int timer;
int MaxTimer;
private:
void Draw();
void Update();
};
int형으로 타이머를 변수로 설정하여 설정된 타이머 시간이 지나면 게임화면으로 넘어가게 할 것이다.
Init함수를 선언한 것은 logo 객체가 SceneManager의 Scene상황에 따라 다르게 하기 위함이다.
따라서 Game 클래스에서도 생성자는 비워두고 Init로 기존 생성자에 있던 코드를 옮기자.
=== 나. logo.cpp ===
#include "logo.h"
SceneManager* logoManager = SceneManager::GetInstance();
void Logo::Init(int width, int height, std::string title, int maxTimer)
{
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
InitWindow(width, height, title.c_str());
timer = 0;
MaxTimer = maxTimer;
}
void Logo::Tick()
{
BeginDrawing();
Update();
Draw();
EndDrawing();
if (timer >= MaxTimer) CloseWindow();
}
void Logo::Update()
{
timer++; // 타이머는 BeginDrawing과 End Erawing사이에 있어야만 설정된 FPS대로 숫자가 올라간다.
}
void Logo::Draw()
{
ClearBackground(DARKBLUE);
DrawText("Flappy Bird Game!", GetScreenWidth() / 2 - 160, GetScreenHeight() / 2 - 40, 40, DARKBROWN);
}
game.cpp와 별반 다를바 없다. 다만 지정된 시간을 지나면 CloseWindow()를 호출하여 로고 창을 닫게 하였다. 이렇게 하지 않으면 창이 두개가 된 상태가 된다.
여기에서 FPS를 60으로 했음을 상기하자. 따라서 1초에 60번을 돌려야 한다.
이러한 FPS는 BeginDrawing과 EndDrawing사이에만 이루어진다. 따라서 초당 60번을 의도하려면 타이머를 BeginDrawing과 EndDrawing사이에 넣어야 한다.
==== 2. 구현하기 ====
=== 가. 클래스 인스턴스 만들기 ===
main.cpp 파일 내에서 사용할 것이므로 상당 부분에 다음과 같이 logo와 game클래스를 생성한다.
Logo logo = Logo();
Game game = Game();
=== 나. SceneManager에 마지막 씬을 저장하는 변수 설정하기 ===
첫째, config.h에 Scene자료형으로 lastSce이란 변수를 설정하다.
둘쨰, main.cpp에 다음과 같이 lastSce에 임의의 Scene을 할당해 준다.
menuManager->lastSce = Scene::scoreScene;
이렇게 하는 이유는 씬이 바뀔 때마다 logo와 game클래스의 Init()함수를 한번만 불러오게 하려는 의도이다.
logo와 game클래스의 Init()에는 SetTargetFPS()함수와 InitWindow()함수를 집어 넣었다. 이런 함수들은 한번만 불러와야 한다.
따라서 while문에서 한번만 불러올 수 있게 트릭이 필요하다.
=== 다. 게임화면 변수 만들기 ===
게임화면을 로고화면과 다르게 설정하기 위하여 main.cpp 상단에 다음과 같이 따로 변수를 만들어주었다.
const int GamescreenWidth = 1200;
const int GamescreenHeight = 900;
=== 라. 메인메뉴 루프 만들기 ===
씬이 바뀔 때에는 초기화를 한번하고, 씬이 안 바뀌면 계속하여 루프를 돌리게 해야 할 것이다.
그러면 다음 코드와 같이 될 것이다.
// Main game loop
while (!menuManager->gameExit) // 게임 루프
{
switch (menuManager->scene)
{
case Scene::logo : // 로고 화면
if (menuManager->lastSce != menuManager->scene) // 로고화면으로 전환시에 로고 클래스 초기화 하기
{
logo.Init(screenWidth, screenHeight, "Flappy Bird", 120);
menuManager->lastSce = menuManager->scene; // 마지막 씬을 현재 씬으로 저장하여 앞으로 씬 전환이 아니라고 알려주기
}else
{
if(logo.timer < logo.MaxTimer)
{
logo.Tick(); // 설정된 시간 동안 루프를 돌린다.
}else
{
menuManager->scene = Scene::game; // 설정된 시간을 넘기면 게임 화면으로 넘어간다.
}
}
break;
case Scene::game :
if (menuManager->lastSce != menuManager->scene) // 다른 씬에서 게임화면으로 전환하면 클래스를 초기화한다.
{
game.Init(GamescreenWidth, GamescreenHeight, "Flappy Bird"); // 게임화면을 초기화한다. 로고화면과 다르다는 것을 알리기 위해 화면을 크게 했다.
menuManager->lastSce = menuManager->scene; // 마지막 씬도 현재 씬과 똑같다고 설정한다.
}else
{
game.Tick(); // 게임 루프를 돌린다.
}
break;
default :
break;
}
}
게임화면 창은 로고화면 창과 다르다는 것을 알리기 위해 게임화면 크기는 로고화면 크기와 다르게 하였다.
창이 2개 뜨고 있다는 것을 보고 싶으면, logo.cpp에서 CloseWindow()부분을 없애면 확인할 수 있다.
==== 3. 정리 ====
{{:raylib:flappybirdlogo화면.png?600|Flappy Bird로고 화면 }}
여기까지 만들면 로고화면이 나오고 2초 후에 게임화면으로 들어가는 것을 확인할 수 있다.
여기까지의 소스코드는 다음과 같다.
{{ :raylib:mainmenu_chap1.zip |Flappy Bird Main Menu Chapter1}}
===== 창을 단일화 하기 =====
위에서는 WindowIniti()함수를 이용해서 로고하면에서 창을 만들고, 다시 죽인 다음에 게임화면에서 창을 새로 만들었다.
그런데 이렇게 하면 화면이 깜빡거려서 보기에 안 좋다.
로고화면과 게임화면의 크기를 달리하는 것만 구현하면 되고 화면을 죽였다가 새롭게 할 필요는 없을 것이다. 왜냐하면 화면을 매번 새롭게 다시 그릴 것이기 때문이다.
화면 크기를 재설정하는 레이라이브 함수는 SetWindowSize()이다.
그리고 화면의 타이틀을 재설정한느 레이라이브 함수는 SetWindowTitle()이다.
이를 이용하여 InitWindow()와 CloseWindow()를 main.cpp에 넣고 game.cpp에서는 화면 창 크기를 바꾸도록 하자.
이렇게 고친 전체 소스파일은 다음과 같다.
이제 이를 이용하여 다음으로 넘어갈 것이다.
{{ :raylib:mainmenu_chap2_begin.zip |Raylib Main Menu Chapter2_begin File}}
===== 다음 강의 =====
다음에는 [[raylib:gui_와_메인메뉴|GUI와 메인메뉴]]를 해볼 것이다.