목차

  

기본 템플릿을 통해 플래피 버드를 만들기로 하자.

아래 템프릿을 다운받아서 VS CODE를 열자

템플릿 파일

중력에 의해 내려오는 새 만들기

1. 개념

새가 내려오게 할 것이다. 중력 가속도에 따라 내려오게 할 것이다.

매 프레임마다 속도는 중력에 따라 변화할 것이므로, 가속도를 먼저 설정해주고 매프레임마다 가속도를 더하게 하면 될 것이다.

2. bird.h

다음과 같이 기본 선언을 하였다. 내려오는 속도는 매 프레임마다 가속도를 더해주는 것이므로 맨 처음에는 0이다.

그리고 새의 위치는 raylib에서 기본으로 제공해주는 vector2를 이용하였다.

bird.h
#include "raylib.h"
 
#define GRAVITY 0.2f;  // 중력 값 : 가속도의 값이다.  한 프레임당 가속도이다. 
 
class Bird {
    public: 
 
    void Init();
    void Update();
    void Jump();
    void Draw();
 
    private:
    Vector2 pos; 
    float downSpeed = 0;
    float jumpSpeed = 6; 
    float halfLength = 20; 
 
    void CheckCollision();
};

2. bird.cpp

다음과 같이 기본 각 선언된 함수를 정의하였다. 여기까지는 코드만 보고도 이해하는데 어려움이 업을 것이다.

bird.cpp
#include "bird.h"
 
void Bird::Init()
{
    pos = {40, float(GetScreenHeight()) / 2} ;
}
 
void Bird::Update()
{
    downSpeed += GRAVITY;
    pos.y += downSpeed;
 
    CheckCollision();
}
 
void Bird::CheckCollision()
{
    if (pos.y - halfLength <= 0) pos.y = halfLength; 
 
    if (pos.y + halfLength >= GetScreenHeight()) pos.y = GetScreenHeight() - halfLength;
}
 
void Bird::Jump()
{
    downSpeed = 0; 
    pos.y -= jumpSpeed;
}
 
void Bird::Draw()
{
    DrawRectangle(pos.x - halfLength, pos.y - halfLength, halfLength * 2, halfLength * 2, DARKGREEN);
}

3. game.cpp

이제 이것을 game.cpp에 각 구현을 하자. 다음 코드만으로 충분히 이해가 갈 것이다.

다만 Game클래스가 선언되고 나서 전체 스크린의 값을 알 수 있으므로, Game클래스가 생성되고 나서 bird.Init()를 호출해야 한다는 것을 확인하자.

game.cpp
#include "game.h"
#include "bird.h"
 
Bird bird;
 
Game::Game(int width, int height, std::string title) 
{
    SetTargetFPS(60);               // Set our game to run at 60 frames-per-second
    InitWindow(width, height, title.c_str()); 
 
    bird.Init();
}
 
Game::~Game()
{
    CloseWindow();                  // Close window and OpenGL context
} 
 
bool Game::GameShouldClose() const
{
    return WindowShouldClose();
}
 
void Game::Tick()
{
    BeginDrawing();
    Update();
    Draw();
    EndDrawing();
}
 
void Game::Update()
{
    // Update
    //----------------------------------------------------------------------------------
    // TODO: Update your variables here
    //---------------------------------------------------------------------------------- 
    bird.Update();
 
    if (IsKeyPressed(KEY_UP)) bird.Jump(); 
}
 
void Game::Draw()
{ 
    // Draw
    //----------------------------------------------------------------------------------
 
        ClearBackground(BLACK);
        bird.Draw();
 
    //----------------------------------------------------------------------------------
}

기존과 달리, IsKeyDown()함수가 아니라 IsKeyPressed()함수를 이용하였다. IsKeyDown()함수는 키가 눌려지고 있는 상태면 계속 점프 함수를 호출하므로 게임이 너무 쉬워진다. 따라서 키를 누른 상태에서는 점프가 한번만 되게 바꿨다.

4. 점프 함수 수정 하기

기존에는 점프를 하면 바로 위치를 위로 올리기로 하였다. 그런데 이러면 순간이동을 하는 것으로 보이기 때문에 매우 어색하다.

점프버튼을 누르면 자연스럽게 위로 올라갔다가 내려오게 하자.

플레이어(새)는 매 프레임마다 중력을 받게 되어 있으므로 속도를 위쪽 방향으로 설정해주면 매프레임마다 속도가 떨어지면서 궁극적으로는 아래로 내려오게 될 것이다.

따라서 다음과 같이만 바꿔주면 된다.

여기서에서 점프스피드는 6정도가 알맞다. 450픽셀에서 6정도면 매우 작은 숫자같은데, 왜 적당한 숫자가 나오는지는 잘 모르겠다.

void Bird::Jump()
{
    downSpeed = -jumpSpeed; 
}

게임스테이트와 일시 정지 화면 만들기

1. 개념

이번에는 기본 UI도 같이 만들어 볼 것이다. 우선 가장 쉬운 것이 일시 정지 화면일 것이다.

일시 정지 화면을 만들려면 게임스테이트를 만들어야 한다.

즉,

(1) 정지상태에서는 게임을 움직이지 않게 하고 정지메뉴를 보여주고 
(2) 게임 상태에서는 어떤 키를 누르면 정지 메뉴로 들어가기 

이렇게 로직을 짜주어야 하기 때문이다. 따라서 게임스테이트란 개념이 필요하다.

게임스테이트를 만들어보자. 이번에는 아주 간단하게 pause와 game이란 2개의 게임스테이트만 만들어 볼 것이다.

2. 게임스테이트

가. 게임스테이트 선언

game.h에 다음과 같이 두개의 게임스테이트 열거자 클래스를 만들자

enum class Gamestate
{
    pause, game
};

나. update 함수 내에서 게임스테이트 분기하기

일단 game.cpp 맨 상단에 다음과 같이 gamestate를 선언해 준다.

Gamestate gamestate;

이후 업데이트 메서드를 다음과 같이 코딩해주면, 게임스테이트에 따라 update가 분기되게 할 수 있을 것이다.

void Game::Update()
{
    // Update
    switch (gamestate)
    {
        case Gamestate::pause :    // 정지로직 
 
            if (IsKeyPressed(KEY_UP)) gamestate = Gamestate::game;
            break;
 
        case Gamestate::game:   // 게임로직
 
            bird.Update();
            if (IsKeyPressed(KEY_UP)) bird.Jump(); 
 
            if (IsKeyPressed(KEY_ESCAPE)) gamestate = Gamestate::pause;
            break; 
 
        default: 
 
        break;
    }
}

다. 게임스테이트에 따라 화면 그리기를 달리 해주기

Draw() 메서드도 각 상태에 따라 화면을 달리 그려줘야 할 것이다. 그러면 다음과 같이 될 것이다.

void Game::Draw()
{ 
    // Draw
    switch(gamestate)
    {
        case Gamestate::pause :   // 정지화면 그리기 
            ClearBackground(BLACK); 
            DrawRectangle(GetScreenWidth() / 2 - 160, GetScreenHeight() / 2 - 60, 320, 120, DARKBROWN);
            DrawText("Press Arrow Up  if you continue", GetScreenWidth() / 2 - 120, GetScreenHeight() / 2 -20, 20, MAGENTA);
            bird.Draw();
            break;
 
        case Gamestate::game:  // 게임화면 그리기 
            ClearBackground(BLACK); 
            DrawText("Pause : Press ESC", GetScreenWidth() - 200, 20, 20, DARKGRAY);
            bird.Draw();
            break;
    }
}

우리는 이 게임에서 화살표 위키만 사용할 것이므로 정지화면에서 게임을 재개할 때에는 화살표 위키를 사용하게 하였다.

그리고 정지화면으로 갈 때에는 ESC키를 누르게 설정하였다. 통상의 게임은 ESC키로 일시 정지를 하기 때문이다.

3. 게임 종료 키 새로 선언하기

가. 문제점

우리는 기존에 레이라이브의 기본 함수인 WindowShouldClose()를 이용하여 게임을 무한 루프 시켰다.

WindowShouldClose()는 ESC키를 받으면 true를 리턴하는 함수이다.

그런데 우리는 이것을 최상단에서 루프시키므로, ESC키를 누르면 게임이 종료될 수 밖에 없다.

따라서 이것을 고쳐야 한다.

나. 게임 루프 수정

다음과 같이 WindowShouldClose()를 없애고, ESC키는 게임이 정지상태일 때에만 true를 반환하게 하였다.

bool Game::GameShouldClose() const
{
    if (gamestate == Gamestate::pause && IsKeyPressed(KEY_ESCAPE) )  // 정지상태일 때에만 ESC로 종료
        return true;
 
    if (gamestate == Gamestate::game) 
    {
        return false; 
    }
 
    return false;
}

다. 결론

여기까지 하면, 게임상태에 따라 어떻게 로직을 분기시키는지 이해가 갈 것이다.

지금까지의 소스파일은 다음과 같다.

Flappy Bird Chapter1

다음에는 싱글톤과 로고화면을 만들어 볼 것이다.