사용자 도구

사이트 도구


raylib: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에 구현하기

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

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

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

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

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

raylib/flappy_bird_게임_장애물_만들기.1697289239.txt.gz · 마지막으로 수정됨: 2023/10/14 22:13 (바깥 편집)