사용자 도구

사이트 도구


raylib:flappybird:flappy_bird_게임_장애물_만들기
flappy bird 게임 장애물 만들기

  

플래피 버드 장애물 컬리젼 및 리셋

준비

장애물 클래스 만들기

1. 개념

플래피 버드에서는 파이프 장애물이 존재한다. 이를 네모난 박스로 구조화하면 다음과 같다.

장애물 컨셉

위와 아래의 장애물 좌표를 구하려면 위의 장애물 높이와, 아래 장애물 높이, 그리고 중간 빈 공간의 높이가 필요하다.

위의 장애물 높이와 중간 빈 공간의 높이를 정해 놓으면 아래 장애물 높이는 전체 화면 높이에서 빼면 될 것이다.

굳이 서식으로 해보자면

아래장애물 높이 = 전체 화면 높이 - (위 장애물 높이 + 중간 빈 공간의 높이)

이다.

2. obstacle.h

obstacle.h
#pragma once
#include "raylib.h"
 
enum class ObstacleType 
{
    top = 0, bottom, both
};
 
typedef struct Box 
{
    int x; 
    int y;
    int bWidth;
    int bHeight;
} Box ;
 
class Obstacle {
    public: 
 
    void Init();
    void Update();
    void Draw();
    int GetPipePosition();
 
    private:
    Box upPipe;
    Box downPipe;
    int upPipeHeight; 
    int width;
    int middleHeight;
    int speed = 2; 
    ObstacleType type = ObstacleType::top;
 
};

장애물은 네모난 박스로 그릴 것이므로 int형의 박스 구조체를 만들었다. 그리고 장애물의 종류를 설정했다. 위에 달린 장애물, 아래에 달린 장애물 위와 아래 모두에 달린 장애물의 세 가지 종류를 만들었다.

3. obstacle.cpp

obstacle.cpp
#include "obstacle.h"
 
void Obstacle::Init()
{
    int typeValue = GetRandomValue(0, 2);   // 장애물 타입 0은 top, 1은 bottom, 2는 both
    type = ObstacleType(typeValue);  // 장애물 타입을 랜덤하게 설정함
    width = GetRandomValue(100, 150);  // 파이프 너비는 위와 아래 파이프가 동일 하다 
    upPipeHeight = GetRandomValue(250, 400);  // 위 파이프의 높이 
    middleHeight = GetRandomValue(300, 400);  // 위와 아래 파이프 사이의 공간 
 
    upPipe = {GetScreenWidth(), 0, width, upPipeHeight}; 
 
    downPipe = {GetScreenWidth(), upPipeHeight + middleHeight, width, GetScreenHeight() - (upPipeHeight + middleHeight)};
}
 
void Obstacle::Update()
{
    upPipe.x -= speed;
    downPipe.x -= speed;
}
 
// 파이프가 화면 왼쪽끝을 넘어가면 지우기 위해 왼쪽 끝의 값을 가져옴
 
int Obstacle::GetPipePosition()
{
    return upPipe.x + upPipe.bWidth;
}
 
void Obstacle::Draw()
{
    if (type == ObstacleType::top) {
        DrawRectangle(upPipe.x, upPipe.y, upPipe.bWidth, upPipe.bHeight, DARKGREEN);
    }else if (type == ObstacleType::bottom) {
        DrawRectangle(downPipe.x, downPipe.y, downPipe.bWidth, downPipe.bHeight, DARKGREEN);
    }else {
        DrawRectangle(upPipe.x, upPipe.y, upPipe.bWidth, upPipe.bHeight, DARKGREEN);
        DrawRectangle(downPipe.x, downPipe.y, downPipe.bWidth, downPipe.bHeight, DARKGREEN);
    }
}

가. Init() 함수

장애물은 랜덤하게 위, 아래, 양쪽을 나오게 했다. 따라서 GetRandomValue()함수를 이용하여 랜덤 값을 처음에 만들어 준다.

각 장애물(파이프)의 높이도 랜덤하게 만들 것이므로 랜덤 하게 변수를 설정하게 했다. 이후에 위와 아래 파이프의 상자를 그리기 위하여 4개의 값을 요소로 하는 구조체에 넣어주게 하였다.

나. Update() 함수

왼쪽으로만 움직이게 할 것이므로 x 값만 바꿔주면 된다.

다. GetPipePosition() 함수

Game 클래스에서 파이프가 화면에 왼쪽 끝으로 완전히 가게 되면 파이프를 제거하는 로직을 만들 것이다.

그래서 파이프가 왼쪽 끝으로 가는지를 리턴하는 함수를 만들었다. 파이프의 오른쪽 끝이 0이 되게 만들어야 하므로 x+width값을 리턴하게 하였다.

라. Draw() 함수

각 상황에 따라 달리 그린다. 현재 상태가 top이면 위에 있는 파이프만, 그리고 bottom이면 아래에 있는 파이프만 그리는 방식이다.

Game 클래스에서 구현하기

1. 전역 변수 선언하기

장애물로 사용할 변수를 게임클래스 전역에서 사용할 것이다. 따라서 game.cpp 최상단에 다음과 같이 장애물 벡터 배열을 선언하였다.

장애물(파이프)의 전체 갯수에 대하여 동적으로 선언할 것이므로 벡터를 이용하였다.

// 장애물 변수 선언
std::vector<Obstacle> pipes;
int timer = 0;
int maxTimer = 120;

vector를 사용할 것이므로

#include <vector>

를 미리 선언해야 한다.

2. 장애물 초기화하기

Gaeme::Init()에는 다음과 같이 장애물을 초기화한다.

    // 장애물 초기화하기 
    Obstacle pipe = Obstacle();
    pipe.Init();     
    pipes.push_back(pipe); 
    maxTimer = GetRandomValue(120, 240);
  1. 벡터 배열에서 요소를 추가하는 것은 push_back을 이용한다. 그러면 동적 배열에 요소가 하나씩 들어간다.
  2. maxTimer는 다음 장애물이 생성될 텀이다. timer가 maxTimmer 만족하면 다음 장애물을 만들게 할 것이다.

3. Update() 로직

가. 장애물을 새롭게 만드는 로직

초당 60번의 프레임을 돌리게 하였으므로 60프레임이면 1초다.

게임이 Update루프를 돌면서 maxTimer를 만족하면 새롭게 장애물을 만들게 하였다.

            // 장애물 로직 시작 
            // 타이머를 만족하면 새로운 파이프 장애물을 만든다. 
            timer++;
            if (timer >= maxTimer) 
            {
                timer = 0; 
                maxTimer = GetRandomValue(120, 240); 
                Obstacle newPipe;
                newPipe.Init();
                pipes.push_back(newPipe);
            }

나. 장애물 제거하는 로직

업데이트 루프를 돌다가 장애물이 왼쪽 끝에 가면 사라지게 만들어야 한다. 장애물을 무한정으로 생성하면 메모리는 계속 쌓이기만 할 것이기 때문에 왼쪽 끝에 다다라서 소용 없는 장애물은 배열에서 제거해야 하기 때문이다.

벡터에서 요소를 제거하는 것은 다음 코드와 같이 하면 된다.

            // 전체 파이프를 루프 돌려서 업데이트 메서드 실행
            for (unsigned int i =0; i < pipes.size(); i++)
            {
                pipes[i].Update();
 
                if (pipes[i].GetPipePosition() <= 0) 
                {
                    pipes.erase(pipes.begin() + i);  // 장애물이 왼쪽 끝에 가면 배열에서 제거 
                }
            } 
            // 장애물 로직 끝

4. Draw() 로직

장애물은 하나가 아니므로 For 루프를 돌려서 장애물을 모두 돌려야 한다. 다음과 같이 짜면 된다.

            // 전체 파이프를 루프 돌려서 드로우 메서드 실행
            for (unsigned int j =0; j < pipes.size();j++)
            {
                pipes[j].Draw();
            }
 
            DrawText(TextFormat("Pipes : %d", pipes.size()), GetScreenWidth() - 200, 40, 20, DARKGRAY);

전체 장애물(파이프)가 몇 개가 있는지를 확인하기 위해 DrawText() 메서드도 이용하였다. 게임을 실행하여 장애물이 왼쪽 끝에 다다르면 숫자가 사라지는지 확인해 보자.

5. 결과

다음 그림과 같이 랜덤한 시간이 지나면 장애물이 만들어져서 오른쪽에서 왼쪽으로 이동하고, 왼쪽 끝에 다다르면 장애물이 배열에서 사라지는 것을 알 수 있다.

플래피버드 장애물 생성 및 제거

장애물을 통과하면 점수를 올리기

1. 변수 추가하기

장애물을 통과하면 점수를 올리게 하겠다. 점수를 game.cpp에 선언하자.

// 플레이어 점수와 생명
int score = 0;
int life = 3;

생명은 향후에 플레이어가 장애물에 부딪히면 감소하게 만들 변수이다. 지금 당장은 필요치 않다.

2. 장애물 통과 로직 만들기

가. 플레이어의 현재위치를 Rectangle 값으로 반환하기

bird.cpp에 다음과 같이 플레이어의 현재 위치를 Rectangle 값으로 반환하는 함수를 만들자

(물론, bird.cpp에 다음 함수를 public으로 선언해야 할 것이다.)

// 플레이어의 컬리젼 네모 값을 리턴한다. 
Rectangle Bird::GetPosition()
{
    return {pos.x - halfLength, pos.y - halfLength, halfLength * 2, halfLength * 2};  
}

플레이어를 네모로 만들었으므로 네모의 각 점을 반환하는 함수이다.

물론 지금 단계에서는 파이프를 지나가는 것만 판단할 것이므로 플레이어의 x 값만 반환해도 된다. 하지만 향후에 플레이어와 파이프가 충돌하는 로직도 만들 것이므로 플레이어의 네개의 점을 모두 반환하는 함수로 만들었다.

나. 플레이어와 파이프의 x 값을 비교하는 함수를 만들기

obstacle.cpp에 다음과 같이 x 값을 비교하는 함수를 만들자(물론 obstacle.h에 public으로 함수를 선언하도록 하자).

// 새가 파이프를 무사히 건넜는지 확인
bool Obstacle::checkPassing(Rectangle bird)
{
    if (!isPassed) {
     if (bird.x > upPipe.x + upPipe.bWidth) 
     {
        isPassed = true;
        return isPassed;
 
    }
    }
 
    return false;
}

플레이어의 x값과 파이프의 오른쪽 x값(x+width)을 비교하여 플레이어가 파이프를 지나치면 true를 리턴한다.

여기에서 중요한 것은, isPassed라는 변수를 boolean으로 선언하여 딱 한번만 로직이 실행되게 한 것이다.

파이프는 플레이어를 지나친 이후에도 화면끝으로 이동하기 전까지는 계쏙하여 왼쪽으로 움직인다. 따라서 플레이어를 지나치자마자

계속하여 true값을 리턴할 것이다. 이러면 점수가 계속하여 올라가므로 딱 한번만 true값을 리턴하게 한 것이다.

3. game.cpp에 구현하기

가. update() 메서드

game.cpp내에서 pipes 루프 구간 내에 다음의 코드를 삽입하자

                // 장애물을 패스해서 점수획득 
                if (pipes[i].checkPassing(bird.GetPosition())) score++;

이렇게 하면 장애물을 통과할 때 점수가 딱 한번 오르게 작동하는 것을 알 수 있다.

이제 다음번에는 플레이어와 장애물 간의 충돌 판정을 하게 할 것이다.

나. draw() 메서드

점수와 생명력을 숫자로 보여주자

            DrawText(TextFormat("Score : %d", score), GetScreenWidth() - 200, 60, 20, DARKGRAY);
            DrawText(TextFormat("Lifes : %d", life), GetScreenWidth() - 200, 80, 20, DARKGRAY);

장애물과의 컬리젼 만들기

1. 기본 개념

플레이어가 장애물에 부딪히면 생명력을 잃게 하는 로직을 짜보자.

일단 플레이어도 네모고 장애물도 네모이므로 Raylib에서 기본적으로 제공하는 CheckCollisionRec()함수를 이용한다는 생각은 누구나 할 것이다.

단순하게 플레이어가 코인을 먹으면 코인이 사라지게 하는 로직은 이런 식으로 짜면 된다. 그런데 장애물은 플레이어가 부딪혀도 사라지게 하지는 않을 것이다. 그러면 장애물과 플레이어가 겹치고 있는 동안은 계속하여 생명력이 감소하게 된다. 우리는 이것을 일회만 감소하게 만들어야 한다.

그렇다면 장애물을 한번 부딪히면 플레이어의 생명력이 감소함과 동시에 일정 시간 동안에는 플레이어가 컬리젼 충돌을 받지 않도록 만들어야 한다.

이러한 기본 개념을 머릿속에 넣으면 로직을 짤 수 있을 것이다.

2. obstacle.cpp

플레이어와 장애물의 컬리젼 체크를 해서 결과값을 리턴하게 만들자.

// 새가 파이프와 부딪히는지 체크
bool Obstacle::CheckCollide(Rectangle bird)
{
 
    Rectangle topRect = {float(upPipe.x), float(upPipe.y), float(upPipe.bWidth), float(upPipe.bHeight)}; 
    Rectangle bottomRect = {float(downPipe.x), float(downPipe.y), float(downPipe.bWidth), float(downPipe.bHeight)};
 
    if (type == ObstacleType::top) {
       bCollide = CheckCollisionRecs(bird, topRect);
    }else if (type == ObstacleType::bottom) {
        bCollide = CheckCollisionRecs(bird, bottomRect);
    }else {
        bCollide = CheckCollisionRecs(bird, topRect) || CheckCollisionRecs(bird, bottomRect);
    }
 
    return bCollide;
}

장애물이 top, bottom, both의 3가지 유형이 있으므로 각 유형마다 컬리젼 체크를 하게 해주면된다.

obstacle.h에 함수를 선언하는 것을 잊지 말도록 하자. 이건 앞으로도 쭉 그렇다.

3. 플레이어 로직

가. bird.h

플레이어에게는 현재 충격을 받을지 여부를 리턴해주고, 충격을 받을지 말지를 세팅해주는 함수를 선언하자. 외부에서 받을 것이므로 public으로 선언한다.

    void SetInvincible(bool flag);
    bool GetInvincible();

또한 플레이어가 일정한 시간이 되면 다시 충돌 판정을 받게 할 것이므로 다음의 값을 private에 넣어준다.

    bool isInvincible = false; 
    int timer = 0;
    int MaxTimer = 80;

MaxTimer는 무적상태를 의미한다. 우리는 80프레임을 무적상태로 설정했다.

나. bird.cpp

(1) 현재 충돌을 받을지 여부

다음과 같이 현재 플레이어가 충돌을 받을지 여부를 세팅하고, 이 값을 리턴하는 코드를 구현하자

void Bird::SetInvincible(bool flag)
{
    isInvincible = flag;
}
 
bool Bird::GetInvincible()
{
    return isInvincible;
}
(2) 일정한 시간이 지나면 다시 충돌을 받게 해주기

Update() 메서드에 다음과 같이 코드를 넣으면 플레이어가 다시 일정한 시간이 지나면 충돌을 받게 해줄 수 있다.

void Bird::Update()
{
    downSpeed += GRAVITY;
    pos.y += downSpeed;
 
    CheckCollision();
 
    if (isInvincible)
    {
        timer++;
        if (timer > MaxTimer) 
        {
            timer = 0;
            isInvincible = false;
        }
    }
}
(3) 플레이어 상태에 따라 플레이어를 달리 그리기

다음과 같이 플레이어 상태에 따라 플레이어의 색상을 달리했다.

// 플레이어 그리기 
void Bird::Draw()
{
    if (isInvincible)
    {
        DrawRectangle(pos.x - halfLength, pos.y - halfLength, halfLength * 2, halfLength * 2, GRAY);  // 플레이어가 무적일 떄 
    }else
    {
        DrawRectangle(pos.x - halfLength, pos.y - halfLength, halfLength * 2, halfLength * 2, DARKGREEN); // 평상시 
    }
}

4. game.cpp

이제 이것을 게임로직에서 구현할 것이다. 플레이어가 장애물과 부딪히면 잠시 무적인 상태가 되면서 생명력이 1이 깍이고, 다시 일정한 시간이 지나면 다시 무적이 풀리게 구현하면 된다.

전체 파이프 루프 함수 내에 다음과 같이 코드를 넣으면 된다.

                // 장애물에 부딪히는지 여부 
                if (!bird.GetInvincible())
                {
                    if (pipes[i].CheckCollide(bird.GetPosition())) 
                    {
                        life--;
                        bird.SetInvincible(true);
                    }
                }

플레이어가 무적이 아닌 상태에서 각 장애물들과의 충돌 여부를 체크한 후, 충돌하면 생명력을 1 감소시키고 플레이어를 무적으로 세팅해주는 코드다.

5. 결론

이렇게 하면 일단 게임의 기본 골격은 갖추었다. 플레이어는 장애물을 부딪히면 생명력이 깍이고 잠시 무적 상태가 된다.

게임 리셋하기

게임 리셋하기는 사실 조금만 머리쓰면 매우 쉽다. 플레이어와 장애물을 모두 초기화하면 된다.

game.cpp에 다음과 같이 코드를 구현하면 된다.

1. 리셋함수

다음과 같이 플레이어와 장애물을 초기화하는 리셋함수를 만들었다.

// 게임 리셋하기
void Game::Reset()
{
    life = 3; 
    score = 0;
    bird.Init();
    pipes.clear();
 
    // 장애물 초기화하기 
    Obstacle pipe = Obstacle();
    pipe.Init();   
    maxTimer = GetRandomValue(120, 240);
}

2. 리셋함수 불러오기

Upate() 메서드 내에서 게임로직 스위치 문의 맨 끝에 다음과 같이 생명력이 0보다 작을 떄에는 리셋함수를 불러오게 하자.

            if (life <= 0) Reset();  // 생명력이 0이하면 게임 리셋 

3. 결론

이제 다음과 같이 게임의 대략적인 윤곽이 잡혔다.

플래피 버드 장애물 컬리젼 및 리셋

지금까지의 소스코드는 다음과 같다.

플래피버드 장애물 만들고 충돌 로직 구현하기

이제 다음번에는 텍스쳐를 이용하여 게임을 좀 더 이쁘게 만들어 보자

다음은 플래피버드 게임에 그림입히기를 해보자

로그인하면 댓글을 남길 수 있습니다.

raylib/flappybird/flappy_bird_게임_장애물_만들기.txt · 마지막으로 수정됨: 2023/11/20 00:00 저자 이거니맨