2015년 2월 15일 일요일

[일기] SDL 좌표 그리고 블럭전송(BLIT)

일기. 출처. http://www.sdltutorials.com/sdl-coordinates-and-blitting

 첫 예제를 이용하여, SDL 표면(surface)의 세계를 자세히 탐구해본다. 지난 과정에서 설명하려한 것과 같이 SDL의 표면은 기본적으로는 메모리에 저장된 이미지이다. 공백의 320x240 픽셀 표면을 생각해보자. SDL의 좌표(coordinate) 체계를 묘사하자면 다음과 같다.

>http://www.sdltutorials.com/Data/Posts/105/coords.jpg

 이 좌표 체계는 우리가 친숙하게 봐왔던 좌표계와 상당히 다르다. Y 좌표는 밑으로 갈수록 증가하며 X 좌표는 오른쪽으로 갈수록 증가한다. 화면에 이미지를 적절하게 그리게 위해서는 SDL 좌표 체계를 이해해야만 한다.

 이미 준비된 주표면(main surface)을 가지고 있으니 이제 그 위에 이미지를 그리는 방법만 알면 된다. 이때 그리는 과정을 블럭전송(Blitting)이라 하며 여기서 우리는 단지 하나의 이미지를 다른 곳으로 전송한다. 그러나 이러한 작업을 진행하기 전에 반드시 메모리로 그릴 이미지를 미리 적재할 수 있어야 한다. SDL은 이를 위해 SDL_LoadBMP의 손쉬운 함수를 제공한다. 이를 이용한 이미지 적재를 위한 의사 코드는 다음과 같을 것이다.

SDL_Surface* Surf_Temp;

if((Surf_Temp = SDL_LoadBMP("image.bmp")) == nullptr) {
    //에러 처리
}

꽤 단순하다. SDL_LoadBMP 함수는 하나의 인수를 필요로 하는데 우리가 적재하고자 하는 파일의 이름을 넘겨주면 된다. 그러면 표면(Surface)를 반환해준다. 만일 널(NULL)을 반환한 경우, 파일을 찾을 수 없거나 파일이 손상되었다거나 등의 다른 에러가 있다고 생각하면 된다. 불행하게도 효율성 측면에서 이 함수는 그리 훌륭하지 못하다. 종종 적재된 이미지는 출력 픽셀과는 다른 픽셀 포맷을 가지기 때문인데, 화면에 이미지를 그리면, 성능 저하, 색상 결손 등의 결과가 초래된다. 하지만 SDL은 이에 해결하기 위해 SDL_DisplayFormat을 준비해 놓았다. 이 함수는 적재된 표면(surface)를 인자로 받으면 출력 모니터와 같은 포맷을 사용하는 새로운 표면(surface)를 반환해 준다.

 그렇다면 위에서 설명한 대로 이미지를 적재하고 그리는 기능을 하는, 재사용 가능한 클래스를 만들어보자. SDL Tutorial 1의 코드를 기초로 하되, 다음 코드들은 새로운 두개의 파일 CSurface.h와 CSurface.cpp에 추가하자. CSurface.h를 열고 다음을 작성한다.

 #ifndef _CSURFACE_H_
    #define _CSURFACE_H_

#include <SDL.h>

class CSurface {
    public:
        CSurface();

    public:
        static SDL_Surface* OnLoad(char* File);
};

#endif

 우리는 간단하게 정적 메서드 OnLoad를 만들었다. 이 메서드는 표면(surface)를 적재하는 기능을 할 것이다. 이제 CSurface.cpp를 연다.

#include "CSurface.h"

CSurface::CSurface() {
}

SDL_Surface* CSurface::OnLoad(char* File) {
    SDL_Surface* Surf_Temp = NULL;
    SDL_Surface* Surf_Return = NULL;

    if((Surf_Temp = SDL_LoadBMP(File)) == NULL) {
        return NULL;
    }

    Surf_Return = SDL_DisplayFormat(Surf_Temp);
    SDL_FreeSurface(Surf_Temp);

    return Surf_Return;
}

이와 같이 작성한다. 여기서 집고 넘어갈 두 가지 사항이 있다. 첫째로, 항상 포인터를 만들 때는 0 또는 NULL로 초기화를 시키는 것을 잊지 말길 바란다. 많은 문제들은 이를 인지하지 못한 데에서 기인하는 경우가 많다. 둘째로, SDL_DisplayFormat 함수는 새 표면(Surface)를 반환하지만 절대 기존의 표면을 변경하지 않는다. 즉, 이 함수가 새 표면을 만들어내므로 우리는 기존 표면(리소스 자원)을 해제해 주어야 한다. 그렇지 않으면 메모리 누수가 발생하는 것은 당연지사다.

 이제 메모리에 표면을 적재하는 방법을 기술했으니 다른 표면에 적재된 상(image)들을 그리는 방법도 작성해보자. SDL이 이미지를 적재하기 위해 함수를 제공해 준 것처럼 그리기(Blit)를 위해서도 함수를 제공한다. SDL_BlitSurface 함수가 그것이다. 이 함수는 SDL_LoadBMP 함수만큼 사용법이 간단하지 않지만 (그렇다 해도,) 충분히 단순하다. 다시 CSurface.h를 열고 다음과 같이 메서드 프로토타입을 추가한다.

#ifndef _CSURFACE_H_
    #define _CSURFACE_H_

#include <SDL.h>

class CSurface {
    public:
        CSurface();

    public:
        static SDL_Surface* OnLoad(char* File);

        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);
};

#endif

그리고 CSurface.cpp 파일에는 다음을 추가한다.

#include "CSurface.h"

CSurface::CSurface() {
}

SDL_Surface* CSurface::OnLoad(char* File) {
    SDL_Surface* Surf_Temp = NULL;
    SDL_Surface* Surf_Return = NULL;

    if((Surf_Temp = SDL_LoadBMP(File)) == NULL) {
        return NULL;
    }

    Surf_Return = SDL_DisplayFormat(Surf_Temp);
    SDL_FreeSurface(Surf_Temp);

    return Surf_Return;
}

bool CSurface::OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y) {
    if(Surf_Dest == NULL || Surf_Src == NULL) {
        return false;
    }

    SDL_Rect DestR;

    DestR.x = X;
    DestR.y = Y;

    SDL_BlitSurface(Surf_Src, NULL, Surf_Dest, &DestR);

    return true;
}

가장 먼저, OnDraw 메서드에 선언된 형식인수들을 보자. 2개의 표면과 두개의 int 정수타입으로 구성된다. 첫번째 표면은 자신 위에 그림이 그려질 표면이며 두 번째 표면은 대상 표면으로 우리가 그릴 표면이다. 기본적으로 우리는 Surf_Src를 Surf_Dest 위에 올려놓는다. X, Y 변수는 Surf_Dest 상의 그림이 그려질 위치를 나타낼 때 쓰인다.

 메서드 초기에는 표면들이 유효한지를 검사하며 그렇지 않을 경우 거짓을 리턴한다. 다음에는 SDL_Rect가 나타나는데 이것은 SDL에서 x, y, w, h를 가지는 기본 구조체이다. 즉 이 구조체로 사각형을 나타낸다. 현재 우리의 주된 관심은 어디에 그릴지이며 어느 크기로 그려지는가는 관심 밖이다. 따라서 우리는 X, Y 좌표만을 이용한다. SDL_BiltSurface에 두 번째 인자로 들어있는 NULL은 SDL_Rect를 위한 매개변수이며 이 매개변수에 대해서는 추후 논의할 것이다.

 마지막 부분에서, 이미지를 그리기 위해 실제로 함수를 호출하며 참을 반환시키고 있다.

이제 모든 것이 제대로 동작하는지 확인하기 위해 테스트 코드를 만들어본다. CApp.h를 열고 새 표면을 만든 다음에 만들었던 CSurface.h를 포함시킨다.

#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

#include "CSurface.h"

class CApp {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

        SDL_Surface*    Surf_Test;

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif

또한, 생성자에서 표면을 NULL로 초기화시킨다.

CApp::CApp() {
    Surf_Test = NULL;
    Surf_Display = NULL;

    Running = true;
}

그리고 자원 해제 코드를 작성하도록 한다.
#include "CApp.h"

void CApp::OnCleanup() {
    SDL_FreeSurface(Surf_Test);
    SDL_FreeSurface(Surf_Display);
    SDL_Quit();
}

이제 이미지를 적재한다. CApp_Oninit.cpp를 열고 표면을 적재하기 위한 다음 코드를 추가한다.

#include "CApp.h"

bool CApp::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        return false;
    }

    if((Surf_Display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
        return false;
    }

    if((Surf_Test = CSurface::OnLoad("image.bmp")) == NULL) {
        return false;
    }

    return true;
}

image.bmp를 실제 가지고 있는 파일 이름으로 바꿔야 할 것이다. 파일이 없다면 그림판을 열어 아무거나 재빨리 그리고 실행파일과 같은 폴더 안에 저장시켜도 된다. 적재까지 했으니 그려보자. CApp_OnRender.cpp을 열고 다음과 같이 코딩한다.

#include "CApp.h"

void CApp::OnRender() {
    CSurface::OnDraw(Surf_Display, Surf_Test, 0, 0);

    SDL_Flip(Surf_Display);
}

아, 새 함수 SDL_Flip이 등장하였다. 이 함수는 기본적으로 버퍼를 비우고 Surf_Display 표면 객체를 화면에 출력하는 기능을 수행한다. 흠; 더블 버퍼링을 쓴다. 이것은 메모리에 이미지를 그리는 과정이며 그리고 나서 최종적으로 모든 것을 화면에 그려낸다. 만일 이 기능을 사용하지 않을 경우, 화면에서 이미지가 깜박깜박거리는 현상을 겪을 것이다. SDL_DOUBLEBUF 플래그를 기억할련지 모르겠다. 이 플래그가 더블버퍼를 활성화시킨다.

 코드를 컴파일하되, 모든 것이 잘 작성되었는지 확인하라. 정상적으로 수행되었다면 화면 좌상단 구석에서 추가한 이미지가 보여야 한다. 만일 보인다면 축하한다. 실제 게임에 한 발짝 다가갔다. 만일 보이지 않는다면, 이미지 파일이 실행파일과 같은 폴더 내에 있는지 확인해보라. 또한 타당한 이미지 파일이어야 한다.



 이제 좀 더 진도를 심화해보자. 화면에 모든 이미지를 그리는 것은 충분히 훌륭하다. 그러나 종종 이미지의 부분만을 그려야 할 때도 있다. 이를 위해 타일셋(tileset) 이미지를 준비해봤다.

>http://www.sdltutorials.com/Data/Posts/105/tileset.png

비록 하나의 단일 이미지이지만, 이 이미지의 부분만을 그리길 원한다. CSurface.h로 돌아와서 다음과 같이 코딩해보자.

#ifndef _CSURFACE_H_
    #define _CSURFACE_H_

#include <SDL.h>

class CSurface {
    public:
        CSurface();

    public:
        static SDL_Surface* OnLoad(char* File);

        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);

        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H);
};

#endif

 또 CSurface.cpp로 돌아가서 다음을 추가한다. (OnDraw 메서드를 오버로딩하는 것이다. 덮어쓰는게 아니다)

bool CSurface::OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H) {
    if(Surf_Dest == NULL || Surf_Src == NULL) {
        return false;
    }

    SDL_Rect DestR;

    DestR.x = X;
    DestR.y = Y;

    SDL_Rect SrcR;

    SrcR.x = X2;
    SrcR.y = Y2;
    SrcR.w = W;
    SrcR.h = H;

    SDL_BlitSurface(Surf_Src, &SrcR, Surf_Dest, &DestR);

    return true;
}

초기에 작성한 메서드와 기본적으로 구조가 같다. 우리가 중간에 새로운 SDL_Rect를 추가한 것을 제외한다면 말이다. 원본 사각형 구조체(source rect)는 그릴 이미지의 부분 영역을 지정하여 목적지에 그 부분을 덮어쓰도록 하기 위해 쓰였다. 만일 0, 0, 50, 50을 구조체의 멤버로 각각 지정했다면 이미지에서 좌상단 기준 50x50 픽셀 사각 영역만큼을 그리게 된다.

>http://www.sdltutorials.com/Data/Posts/105/draw2.jpg

이 메서드도 테스트해본다. CApp._OnRender.cpp를 열고 다음과 같이 코딩하면 된다.

#include "CApp.h"

void CApp::OnRender() {
    CSurface::OnDraw(Surf_Display, Surf_Test, 0, 0);
    CSurface::OnDraw(Surf_Display, Surf_Test, 100, 100, 0, 0, 50, 50);

    SDL_Flip(Surf_Display);
}

그러면 그림이 좌상단으로 부터 100, 100 떨어진 위치에 이미지의 부분 영역만이 그려진 것을 볼 수 있을 것이다.

 지금까지의 함수들이 어떠한 원리로 동작하고 SDL의 좌표 체계가 어떻게 구축되는지, 얼마나 자주 이를 사용할지에 대해 세심한 주의를 기울여야 한다.

다음은 이벤트에 대해서 고찰한다.

댓글 없음:

댓글 쓰기