목차

플래피 버드 그림 입히기

  

스타팅 파일

플레이어 텍스쳐 입히기

사실 텍스쳐드로잉은 이미 메뉴 만들기에서 한번 해본 적 있다. 메뉴 만들기에 구현해 놓은 코드들을 bird 클래스에 가져오면 된다.

1. bird.h

private 선언 부분에 다음과 같이 텍스쳐를 선언하게 만들었다.

    // 텍스처 로드하기 
    // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required)
    Texture2D birdImage;        // Texture loading;  
    float halfLengthH = 18; 
    float halfLengthW = 25; 

애셋 폴더에 있는 새의 이미지가 가로 크기는 50픽셀이고 세로크기는 35이다. 따라서 가로를 반으로 나눈 값은 25로 했고, 세로를 반으로 나눈 값은 18로 했다.

이미 메뉴파일에서도 birdImage란 이름으로 선언했던 적이 있다. 따라서 컴파일을 하면 오류가 날 것이다. 메뉴파일과 다른 이름으로 Texture2D를 선언해도 되지만 나는 메뉴 파일에서는 기존에 쓰던 텍스쳐를 지우리고 했으므로 똑같은 이름을 사용했다. 만약 메뉴파일을 수정하기 싫은 사람은 birdImage가 아니라 다른 이름으로 선언해야 한다.

2. bird.cpp

가. Init()함수

다음과 같이 텍스쳐를 불러온다.

void Bird::Init()
{
    // 최초의 포지션 정하기
    pos = {60, float(GetScreenHeight()) / 2} ;
// 그림으로 넣기
    birdImage = LoadTexture("Assets/birddrawing.png");        // Texture loading
}

나. Draw()함수

다음과 같이 DrawTexture 함수를 불러오면 된다. DrawTexture는 현재 포지션인 x와 y값만 가져오면 된다.

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

DrawTexure()함수는 단순히 그림이 그려질 위치만 인자로 받는다. 그런데 이 그림이 그려질 위치는 왼쪽 위이므로, 이에 대한 조정이 필요하다 .

우리는 pos.x를 플레이어의 정중앙으로 잡았으므로 왼쪽시작점은 pos.x에서 halfLengthW일 것이다. 이런식으로 조정하면 된다.

다. 텍스쳐 언로드

객체가 소멸될 때 텍스쳐도 언로드 하는 것을 잊지 말자

Bird::~Bird()
{
    UnloadTexture(birdImage);       // Texture unloading
}

장애물 그림 입히기

1. obstacle.h

똑같이 Texture2D를 priavte에 선언한다.

     // 텍스처 로드하기 
    // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required)
    Texture2D UpPipeImage;        // Texture loading;  
    Texture2D DownPipeImage;        // Texture loading;  
 
    Rectangle topRectImg;
    Rectangle bottomRectImg;

그림 파일에서 가져올 텍스쳐 부분도 설정해야 하므로, 가져올 부분을 Rectangle로 선언하였다.

2. obstacle.cpp

가. 기본 개념

에셋폴더에는 가로 100, 세로 350 픽셀짜리 위쪽 파이프와 아래쪽 파이프가 있다.

우리는 가로 너비는 그대로 하고 세로 너비는 랜덤하게 설정하였다. 그렇다면 각 랜덤하게 나오는 파이프에 걸맞게 텍스쳐도 해당 부분을 가져와야 한다.

파이프 위치 계산하기

만약 위쪽 파이프가 세로 높이가 250이라고 해보자.

그렇다면 아래를 향하고 있는 천장 파이프는 세로 100픽셀부터 350픽셀까지 250픽셀을 읽게 된다. 이를 수식으로 하면 이렇게 정리 할 수 있다.

천장 파이프 그림 파일: (350-파이프 높이) ~ (파이프 높이) 

자 이를 토대로 코드를 짜보자.

나. Init()함수

    DownPipeImage = LoadTexture("Assets/Pipe-up350.png");      // 아래쪽에 있는 파이프이다. 에셋의 파일명은 위쪽을 바라보는 의미에서 pipe-up이다. 
    UpPipeImage = LoadTexture("Assets/Pipe-down350.png");      // 위쪽에 있는 파이프이다. 에셋의 파일명은 아래쪽을 바라보는 의미에서 pipe-up이다. 
 
    topRectImg = {0.0f, 350.0f - float(upPipe.bHeight), 100.0f, float(upPipe.bHeight)};   // 위쪽 파이프 그림에서 가져올 부분이다. 
    bottomRectImg = {0.0f, 0.0f, 100, float(downPipe.bHeight)};  // 아래쪽 파이프 그림에서 가져올 부분이다.  
 

천장파이프는 y축 시작점이 (350 - 파이프 높이)이고 끝점이 (파이프 높이)임을 알 수 있다.

이렇게 하면 필요한 텍스쳐를 가져올 수 있다.

다. Draw() 메서드

다음과 같이 DrawTextureRec()함수를 이용하였다. Init()함수에서 에셋에서 가져올 파이프의 해당 부분 영역을 구하였으므로 이를 원본소스에서 가져온 후 지정한 위치에 그리는 함수이다.

// 드로잉 메서드 
void Obstacle::Draw()
{
    if (type == ObstacleType::top) {
        // DrawRectangle(upPipe.x, upPipe.y, upPipe.bWidth, upPipe.bHeight, DARKGREEN);
        DrawTextureRec(UpPipeImage, topRectImg, Vector2{float(upPipe.x), float(upPipe.y)}, WHITE);  // 천장 파이프 그리기 
    }else if (type == ObstacleType::bottom) {
        // DrawRectangle(downPipe.x, downPipe.y, downPipe.bWidth, downPipe.bHeight, DARKGREEN);
        DrawTextureRec(DownPipeImage, bottomRectImg, Vector2{float(downPipe.x), float(downPipe.y)}, WHITE ); // 바닥 파이프 그리기 
    }else {
        DrawTextureRec(UpPipeImage, topRectImg, Vector2{float(upPipe.x), float(upPipe.y)}, WHITE);
        DrawTextureRec(DownPipeImage, bottomRectImg, Vector2{float(downPipe.x), float(downPipe.y)}, WHITE );  // 천장과 바닥 파이프 그리기 
    }
}

라. 텍스쳐 언로드

텍스쳐를 그래픽램에 계속 쌓이게만 하면 안되므로 필요 없는 텍스쳐 에셋은 그래픽램에 지워주어야 한다.

(1) obstacle.cpp

다음과 같이 텍스쳐를 언로드 하는 함수를 public으로 구현해 준다(헤더파일에는 선언해주는 것도 잊지말자).

void Obstacle::UnloadPipeTexture()
{
    UnloadTexture(DownPipeImage);       // Texture unloading
    UnloadTexture(UpPipeImage);       // Texture unloading
}
(2) game.cpp

파이프가 화면 왼쪽으로 가면 텍스쳐를 언로드 하는 함수를 불러와 주면 된다.

다음과 같이 배열에서 파이르를 지우기 전에 언로드를 먼저 불러와 주자

                // 장애물이 화면 밖으로 나가면 배열에서 제거
                if (pipes[i].GetPipePosition() <= 0) 
                {
                    pipes[i].UnloadPipeTexture();
                    pipes.erase(pipes.begin() + i);   // 장애물이 왼쪽 끝에 가면 배열에서 제거 
                }

참고로, 원래는 Obstacle클래스의 소멸자에서 UnloadTexture를 불러왔었는데 이렇게 하면 Obstacle객체가 생성되자마자 텍스쳐가 Unload된다. 왜 이렇게 되는지는 잘 모르겠다. 아마도 C++ 문법을 좀 더 공부해 본 후에 그 이유를 찾아 보아야 할 것 같다. 아는 사람은 댓글로 달아주면 좋겠다.

배경화면 입히기

game.cpp 에서 Draw()메서드 내에서 가장 먼저 배경화면을 그리면 그 위에 플레이어와 파이프가 그려질 것이다.

따라서 배경화면을 입히는 것은 어려운 것이 아니다.

다만 우리 에셋 폴더에 있는 background.png 파일은 800*600 사이즈이다. 그런데 우리는 스크린 사이즈를 1200 * 900으로 하였다.

딱 50%씩 더 커진 것이다.

이럴 때에는 어떻게 해야 할까? 배경화면을 1200 * 900으로 맞춰서 그려주는 것이 가장 좋겠지만, 이번에는 원본 소스를 확대하는 함수를 한번 사용해 보자.

아래 코드를 game.cpp에서 Draw() 메서드 내에서 최사안에 넣어주면 된다.

            DrawTexturePro(backgroundImage, Rectangle{0, 0, 800, 600}, Rectangle{0, 0, 1200, 800}, Vector2{0, 0}, 0, WHITE);

DrawTexurePro()함수는 원본 소스의 원하는 영역을 가져와서 타겟이 되는 영역을 원본 소스보다 키울 수 있다. 위와 같이 대상 크기를 더 늘려주면 우리는 굳이 background.png를 수정하지 않고도 배경화면을 그릴 수 있다.

소멸자에는 UnloadTexture()함수로 텍스쳐를 그래픽 램에서 지워주는 것도 잊지 말자.

결론

다음과 같이 그림을 입히니 이제 진짜 게임에 가까워졌다.

플래피 버드 그림 입히기

지금까지의 소스파일이다.

플래피버드 그림 입히기 소스파일

다음번에는 오디오를 구현해 보자

플래피버드에 사운드 입히기