레이블이 SDL인 게시물을 표시합니다. 모든 게시물 표시
레이블이 SDL인 게시물을 표시합니다. 모든 게시물 표시

2015년 2월 16일 월요일

[일기] SDL 틱탁톡

출처 : http://www.sdltutorials.com/sdl-tutorial-tic-tac-toe

 지금까지 게임 개발을 위한 기초를 세웠다. 지금까지 공통된 루틴을 다룰 기본 구조를 구축했고, 이벤트를 다루기 위한 구체적인 클래스를 만들었고 또한 어느정도 표면을 다루기 위한 클래스도 만들었다고 한다. 본 장에서는 이들을 모두 합할 것이다. 즉, 모두 이용하여 빌게*츠가 만들었다던 틱택톡 게임을 작성하는 것을 목적으로 한다. 걱정 말길 바란다. 간단하게 만들어낼 수 있다. 마지막 작업물을 기초로 작업을 계속해 나가자.

 첫째로 필요한 작업은 게임에 대한 계획을 세우는 것이다. 경험을 통해 우리는 틱택톡 게임이 O 또는 X를 그릴 수 있는 3x3 크기의 격자를 갖고 있다는 것을 알고 있을 것이다. 즉, 우리는 3 개의 그래픽 요소가 필요한데, 격자와, X 그리고 O 표시를 위한 요소들이다. 우리는 여러 개의 X, Y 요소가 필요없는데 컴퓨터로 원하는 만큼 많이 그릴 것이기 때문이다. 첫 작업을 끝내도록 하자. 격자는 600x600으로 준비하고 따라서 그 크기의 1/3인 200x200 픽셀 크기로 X와 O를 준비한다.

>http://www.sdltutorials.com/Data/Posts/103/grid.png
>http://www.sdltutorials.com/Data/Posts/103/x.png
>http://www.sdltutorials.com/Data/Posts/103/o.png

이미지는 준비되었다. 이제 프로그램으로 이들을 적재해 보자. CApp.h를 열어서 다음과 같이 변경하자. Test Surface를 제거하고 3 개의 표면(surface)를 추가한다.

#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

#include "CEvent.h"
#include "CSurface.h"

class CApp : public CEvent {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

    private:
        SDL_Surface*    Surf_Grid;

        SDL_Surface*    Surf_X;
        SDL_Surface*    Surf_O;

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

                void OnExit();

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif

그리고 CApp.cpp를 열어서 역시 편집한다. Test Surface를 제거하고, 3 개의 표면을 추가한다.

#include "CApp.h"

CApp::CApp() {
    Surf_Grid = NULL;
    Surf_X = NULL;
    Surf_O = NULL;

    Surf_Display = NULL;

    Running = true;
}

int CApp::OnExecute() {
    if(OnInit() == false) {
        return -1;
    }

    SDL_Event Event;

    while(Running) {
        while(SDL_PollEvent(&Event)) {
            OnEvent(&Event);
        }

        OnLoop();
        OnRender();
    }

    OnCleanup();

    return 0;
}

int main(int argc, char* argv[]) {
    CApp theApp;

    return theApp.OnExecute();
}


 CApp_OnCleanup.cpp 에도 변경이 필요하다는 사실을 잊지 말자. 전에 했던 것과 같이, Test Surface를 없애고 세 줄을 다음과 같이 추가한다.

#include "CApp.h"

void CApp::OnCleanup() {
    SDL_FreeSurface(Surf_Grid);
    SDL_FreeSurface(Surf_X);
    SDL_FreeSurface(Surf_O);
    SDL_FreeSurface(Surf_Display);
    SDL_Quit();
}

이제 3개의 이미지를 위한 세 개의 표면이 준비되었다. 메모리에 적재를 해야 하므로 CApp_OnInit.cpp를 열어서 수정을 좀 해야할 것 같다. 이전의 test surface는 역시 제거하도록 하자. 그리고 세 표면을 메모리에 적재하는 작업을 추가한다. 이미지 파일명을 정확하게 기술하도록 하자. 또한 600x600 크기로 윈도우 창의 크기를 변경하자. 무슨 말인고 하니, 격자 이미지 크기가 600x600이므로 창의 크기도 적합하게 맞추어 빈 공간(여백)이 생기지 않게 하려 함이다.

#include "CApp.h"

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

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

    if((Surf_Grid = CSurface::OnLoad("./gfx/grid.bmp")) == NULL) {
    return false;
    }

    if((Surf_X = CSurface::OnLoad("./gfx/x.bmp")) == NULL) {
    return false;
    }

    if((Surf_O = CSurface::OnLoad("./gfx/o.bmp")) == NULL) {
    return false;
    }

    return true;
}

파일명이 바뀌었다. 여기 예에서는 ./gfx/fksms 폴더를 만들어 여기에 이미지들을 다 집어넣었다. 게임 커질수록 폴더로 정리해 나가면 편리해 진다고 한다. 그렇기에, 여기서부터는 모든 이미지는 gfx 폴더에 넣도록 하겠다. 화면에 격자를 이제 표현해보자. CApp_OnRender.cpp를 열고 test surface를 격자를 그리도록 변경해 보자.

#include "CApp.h"

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

    SDL_Flip(Surf_Display);
}

컴파일을 시도하여 성공한다면 화면에 격자가 나타날 것이다. 여기에는 표면을 사용하기 위한 5 단계가 구성된다.

 > 표면을 선언하고
 > 널로 초기화하며
 > 메모리에 적재후
 > 화면에 그려낸후
 > 그것을 해제한다

이 5 단계를 연습하는 것은 좋은 경험이 된다. 나중에 이 중 적어도 한 개 과정을 깜박하게 되면 문제가 발생한다. 즉, 익숙해져야 발생가능한 문제를 예방할 수 있다. 가령 2단계를 놓치면 정의되지 않은 동작이 발생하며 자원 해제를 잊는다면 메모리 누수가 발생한다.

 여기서 사용하는 이미지에 뭔가 이상한 점이 발견된다. X와 O를 나타내는 이미지는 분홍색의 배경을 포함한다. 여기는 이유가 있는데, 이 색이 투명성을 구현하는데 기반이 될 것이기 때문이다. 즉, 분홍색이 쓰인 부분이, 투명해진다. 분홍색을 투명하게 할 것이다. SDL은 이를 하는 간단한 함수 SDL_SetColorKey를 제공한다. 이를 적용하기 위해, 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);

        static bool Transparent(SDL_Surface* Surf_Dest, int R, int G, int B);
};

#endif

 이제 함수를 구현하자. CSurface.cpp 를 만들고 기능을 추가하라.

bool CSurface::Transparent(SDL_Surface* Surf_Dest, int R, int G, int B) {
    if(Surf_Dest == NULL) {
        return false;
    }

    SDL_SetColorKey(Surf_Dest, SDL_SRCCOLORKEY | SDL_RLEACCEL, SDL_MapRGB(Surf_Dest->format, R, G, B));

    return true;
}

3개의 추가 인수들에 주목하자. 세 개의 색깔을 나타내기 위하여 쓰인다. 즉 투명색이 될 색상을 지정한다. 고로, 사실상 투명을 적용할 색이 분홍색이어야 할 필요는 없다. 예로 적색을 투명색으로 만들고자 한다면, 세 개의 인수를 255, 0, 0으로 만들기만 하면 된다.

 이 함수는 첫째로 전달된 첫번째 표면 매개변수가 유효한지를 검사한다. 만일 그러하다면 투명색을 위한 색상을 지정한다. SDL_SetColorKey에는 투명색으로 변경하고자 하는 색상이 있는 표면(surface)을 첫째 인수로 넣으면 된다. 둘째 인수부터는 몇몇의 플래그 변수가 들어가는데, SDL이 연산을 어떻게 처리할지를 지시하는데 쓰인다. 셋째 인수는 투명색으로 만들 색생을 입력한다. 위에 적용된 플래그들은 기본적인 사항으로 첫째 플래그는 색상 키(color key)를 인자로 전달된 표면에 적용하도록 하고 둘째 플래그는 이를 적용할 때 RLE 가속을 사용하도록 지시하는 역할을 한다(이는 기본적으로 나중에 더 고속으로 그림을 그리도록 시도한다). 셋째 인수는 다소 복잡하다. 여기에 SDL_MapRGB가 색상을 만들기 위해 사용되었다. SDL_MapRGB 는 표면과 RGB 값을 인자로 취한다. 그리고 전달받은 표면에 가장 유사한 포맷으로 색상을 근사시킨다. 이게 왜 유용한가 하면, 모든 표면은 같은 색상표를 갖지 않기 때문이다. 옛 사용가능한 색상이 적었던 NES시대를 기억하면 된다. 같은 아이디어를 여기에도 적용하는데, SDL_MapRGB는 색상(color)을 취하여 표면 팔레트에 있는 색상과 가장 가까운 색상으로 매치시킨다.

 아직 이해가 잘 안가지만 새 함수에 우리의 표면들을 적용해보자. CApp_OnInit.cpp를 열어서 다음과 같이 변경한다.

#include "CApp.h"

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

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

    if((Surf_Grid = CSurface::OnLoad("./gfx/grid.bmp")) == NULL) {
        return false;
    }

    if((Surf_X = CSurface::OnLoad("./gfx/x.bmp")) == NULL) {
        return false;
    }

    if((Surf_O = CSurface::OnLoad("./gfx/o.bmp")) == NULL) {
        return false;
    }

    CSurface::Transparent(Surf_X, 255, 0, 255);
    CSurface::Transparent(Surf_O, 255, 0, 255);

    return true;
}

 표면(그릴 이미지를 가진)들을 위한 모든 것들이 준비되었다. 다음 순서는 X와 O를 그리는 방법을 파악하는 것이다. 여기서 이들은 격자 내의 임의 위치에나 막 그려져서는 안된다. 같은 곳에 그려져서도 안된다. 따라서 9칸의 배열을 만든다. 이 배열 내의 값들은 격자의 각 셀의 상태를 나타낸다. 고로 0은 좌상단 칸을 나타내고 1은 중앙상측을 나타내고 2는 우상단, 3은 중앙좌측 순으로 나타나게 된다. CApp.h에 추가한다.

#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

#include "CEvent.h"
#include "CSurface.h"

class CApp : public CEvent {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

    private:
        SDL_Surface*    Surf_Grid;

        SDL_Surface*    Surf_X;
        SDL_Surface*    Surf_O;

    private:
        int        Grid[9];

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

            void OnExit();

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif

 각 칸은 3개의 값을 가질 수 있다고 가정한다. 3이라는 숫자는 공백, X, O를 각각 나타내기 위해 정해지었다. 세개니까. 구체적으로 말해 각각을 0, 1, 2로 생각하기로 하자. 그런데 숫자는 읽기 어려우니 enum을 사용할 것이다. 다음과 같이 CApp.h에 추가해보자. 단, 위치는 Grid 배열 바로 밑이다.

enum {
    GRID_TYPE_NONE = 0,
    GRID_TYPE_X,
    GRID_TYPE_O
};

 여기까지 언급하던 파일들의 모든 코드 내용을 실었었다. 그러나 이제부터는 스스로 코드의 위치를 파악하기를 기대한다. 이는 사실 이전까지 코드의 일부만을 변경하든 전체를 변경하든 중간에 코드를 삽입하는 경우에 그 위치를 알리기 위해 코드 전체를 올려야만 하는 경우가 생겼었다. 다시 말해 이제는 부분 코드만 올리고 어디에 기재해야 할지를 알리는 방식으로 바꾸려 한다. 물론 필요하다면 전체 코드를 올릴 것이다.

 이제 각 격자의 셀이 어떤 상태인지를 알 수 있게 되었으므로 이제 해야할 것은 게임판(격자)을 초기화하는 것이다. CApp.h 파일 내부 바닥에 다음을 추가하자.

public:
    void Reset();

CApp.cpp를 열고 아래 코드를 메인 함수 바로 직전에(위에) 끼워 넣는다.

void CApp::Reset() {
    for(int i = 0;i < 9;i++) {
        Grid[i] = GRID_TYPE_NONE;
    }
}

이 반복문은 격자의 모든 셀을 GRID_TYPE_NONE으로 설정한다. 즉 모든 셀은 비어있다고 선언하는 것이다. 프로그램이 시작된 극초반에 이렇게 작업을 하기 위해, CApp_OnInit.cpp파일에 함수 호출문을 만든다.

//...

CSurface::Transparent(Surf_X, 255, 0, 255);
CSurface::Transparent(Surf_O, 255, 0, 255);

Reset();

지금까진 좋다. 다음에 필히 해야할 것은 화면에 X와 O를 배치하는 능력을 겸비하는 것이다. 새 함수를 만들자. 이 함수는 그 능력을 기능으로 갖는다. CApp.h를 다시 열어서 Reset 함수 바로 밑에 추가하자.

void SetCell(int ID, int Type);

후, 다시 CApp.cpp 열자. 그리고 이거 추가해.

void CApp::SetCell(int ID, int Type) {
    if(ID < 0 || ID >= 9) return;
    if(Type < 0 || Type > GRID_TYPE_O) return;

    Grid[ID] = Type;
}

이 함수는 인자 두 개를 가져가는데, 변화시킬 셀 번호를 첫 인자로, 변화할 상태를 두 번째로 넘기면 된다. 여기에 조건을 두어야 하는데, 제약 1은 배열의 경계를 벗어나서는 안된다는 것이다(그렇게 되면 프로그램은 박살난다). 그리고 제약 2는 우리가 두 번째로 전달한 타입이 적합한 것이어야 한다는 것이다. 무척 단순하지만 이것만 지켜도 훌륭하다. X, O를 그리는 기능을 구현하자. CApp_OnRender를 열면 된다. 격자(grid) 다음에 밑의 코드를 추가하자.

for(int i = 0;i < 9;i++) {
    int X = (i % 3) * 200;
    int Y = (i / 3) * 200;

    if(Grid[i] == GRID_TYPE_X) {
        CSurface::OnDraw(Surf_Display, Surf_X, X, Y);
    }else
    if(Grid[i] == GRID_TYPE_O) {
        CSurface::OnDraw(Surf_Display, Surf_O, X, Y);
    }
}

아 이건 뭔가 좀 지금까지 해온 것보다 복잡한 듯하다. 첫째로, 격자의 각 셀을 반복하여 접근한다. 둘째로, grid ID를 X축 좌표나 Y축 좌표로 변환하는 단계를 거친다. 이것은 2개의 구분된 방법으로 처리한다. X좌표는 i 변수의 나머지가 3인 것을 취하면 얻어지며, 3으로 나눈 몫을 취하면 Y좌표가 얻어진다. 여기에 각 셀의 크기인 200을 곱하여 좌표를 얻어낸다. 이는 자주 사용되므로 이해를 충분히 하길 장려한다.

 다음으로 할 일은 셀의 타입을 확인하고 그 셀을 표면에 적합한 위치에 그려내는 것이다.

그려야 할 표면을 판단하고 적용하기 위해 사용자와 컴퓨터와 의사소통할 방법이 필요하다. 이를 우리는 마우스 이벤트를 통해서 적용할 것인데, 사용자가 셀을 클릭한다면 그 셀을 적절하게 그릴 것이다. 이제 이를 처리하기 위한 CEvent 함수 중 하나를 오버로딩할 필요가 있다. CApp.h를 열어서 다음 함수를 OnEvent 바로 밑에 추가하되, OnExit 위에 둔다.

void OnLButtonDown(int mX, int mY);

CApp_OnEvent.cpp를 열고 아래를 적어낸다.

void CApp::OnLButtonDown(int mX, int mY) {
    int ID    = mX / 200;
    ID = ID + ((mY / 200) * 3);

    if(Grid[ID] != GRID_TYPE_NONE) {
        return;
    }

    if(CurrentPlayer == 0) {
        SetCell(ID, GRID_TYPE_X);
        CurrentPlayer = 1;
    }else{
        SetCell(ID, GRID_TYPE_O);
        CurrentPlayer = 0;
    }
}

첫째로, 여기서는 우리가 ID를 축 좌표로 변환했던 과정의 역과정을 처리한다. 즉 좌표로부터 ID를 얻어낸다. 그리고 해당 셀이 아직 공백인지 확인하는데, 공백이 아니라 X나 O로 채워져 있다면 바로 함수를 빠져나온다. 공백이라면 어느 플레이어의 차례인지를 확인해서 그 셀을 적절히 채운다(X 또는 O를). CApp.h를 열고 grid 배열 밑에다가 변수를 추가한다.

int CurrentPlayer;

또한 CApp.cpp에 이 변수를 기본값으로 초기화시킨다.

CApp::CApp() {
    CurrentPlayer = 0;

    Surf_Grid = NULL;
    Surf_X = NULL;
    Surf_O = NULL;

    Surf_Display = NULL;

    Running = true;
}

프로그램을 컴파일하면 어느정도 잘 동작하는 버전의 틱탁톡 프로그램이 보일 것이다. 축하한다.

이제 나머지는 스스로 작성해야 한다. 여기는 게임의 기초 틀을 만들었을 뿐이다. 그리고 대부분이 완성되었다. 더 나아가 X가 이겼는지, O가 이겼는지 아니면 비겼는지를 필요하다면 추가적인 이미지까지 동원하여 구성해보길 바란다. 자신이 있다면 AI까지 작성하여 플레이어를 상대하는 컴퓨터를 제작해 보길 바란다.

2015년 2월 15일 일요일

[일기] SDL EVENTS

일기. 출처 - http://www.sdltutorials.com/sdl-events

게임 개발의 기초와 문맥을 같이 하는 것이 이벤트이다. 핑퐁게임에서부터 난감할 정도로 복잠한 PC 게임과 콘솔 게임까지 모든 비디오게임들은 플레이어와 상호작용하기 위해 이벤트를 사용한다. 이벤트는 키보드, 마우스, 조이스틱, 게임패드 등으로부터 올 수도 있고 혹은 운영체제로부터 올 수도 있다. 게임에서 사용자와 적절하게 상호작용하기 위하여 이벤트가 어떻게 동작하는지를 이해하는 것은 분명 중요하다. 이제 이벤트를 사용할 준비가 되었다. 그러나 윈도우 창을 닫는 이벤트만을 살펴볼 것이며 동시에 사용자로부터 이벤트를 어떻게 받아내는지도 알아볼 것이다.

알아차렸을지 모르지만, 각각의 튜토리얼들은 이전의 것으로부터 구축되고 있다. 따라서 이번에도 역시 이전의 튜토리얼의 결과 코드를 사용하여 확장해 나갈 것이다. 모든 이벤트를 추적하고 이 이벤트들을 근사한 함수들에게 전달하기 위해 새 클래스를 하나 만들어 보자. CEvent.h 파일과 CEvent.cpp 파일을 만든다. 이 두 파일은 이벤트를 다루고 적절한 함수를 호출할 것이다. CApp은 이 클래스를 상속받도록 만들어, 만일 우리가 특정 이벤트를 추적하고자 한다면 단순하게 함수를 오버라이드하기만 하면 되도록 만들 것이다.

헤더 파일은 다음과 같다.

#ifndef _CEVENT_H_
    #define _CEVENT_H_

#include <SDL.h>

class CEvent {
    public:
        CEvent();

        virtual ~CEvent();

        virtual void OnEvent(SDL_Event* Event);

        virtual void OnInputFocus();

        virtual void OnInputBlur();

        virtual void OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode);

        virtual void OnKeyUp(SDLKey sym, SDLMod mod, Uint16 unicode);

        virtual void OnMouseFocus();

        virtual void OnMouseBlur();

        virtual void OnMouseMove(int mX, int mY, int relX, int relY, bool Left,bool Right,bool Middle);

        virtual void OnMouseWheel(bool Up, bool Down);    //Not implemented

        virtual void OnLButtonDown(int mX, int mY);

        virtual void OnLButtonUp(int mX, int mY);

        virtual void OnRButtonDown(int mX, int mY);

        virtual void OnRButtonUp(int mX, int mY);

        virtual void OnMButtonDown(int mX, int mY);

        virtual void OnMButtonUp(int mX, int mY);

        virtual void OnJoyAxis(Uint8 which, Uint8 axis, Sint16 value);

        virtual void OnJoyButtonDown(Uint8 which, Uint8 button);

        virtual void OnJoyButtonUp(Uint8 which, Uint8 button);

        virtual void OnJoyHat(Uint8 which, Uint8 hat, Uint8 value);

        virtual void OnJoyBall(Uint8 which, Uint8 ball, Sint16 xrel, Sint16 yrel);

        virtual void OnMinimize();

        virtual void OnRestore();

        virtual void OnResize(int w,int h);

        virtual void OnExpose();

        virtual void OnExit();

        virtual void OnUser(Uint8 type, int code, void* data1, void* data2);
};

#endif

클래스가 꽤 무겁다. 이제 CPP 파일을 열고 다음을 코딩하자.

#include "CEvent.h"

CEvent::CEvent() {
}

CEvent::~CEvent() {
    //Do nothing
}

void CEvent::OnEvent(SDL_Event* Event) {
    switch(Event->type) {
        case SDL_ACTIVEEVENT: {
            switch(Event->active.state) {
                case SDL_APPMOUSEFOCUS: {
                    if ( Event->active.gain )    OnMouseFocus();
                    else                OnMouseBlur();

                    break;
                }
                case SDL_APPINPUTFOCUS: {
                    if ( Event->active.gain )    OnInputFocus();
                    else                OnInputBlur();

                    break;
                }
                case SDL_APPACTIVE:    {
                    if ( Event->active.gain )    OnRestore();
                    else                OnMinimize();

                    break;
                }
            }
            break;
        }

        case SDL_KEYDOWN: {
            OnKeyDown(Event->key.keysym.sym,Event->key.keysym.mod,Event->key.keysym.unicode);
            break;
        }

        case SDL_KEYUP: {
            OnKeyUp(Event->key.keysym.sym,Event->key.keysym.mod,Event->key.keysym.unicode);
            break;
        }

        case SDL_MOUSEMOTION: {
            OnMouseMove(Event->motion.x,Event->motion.y,Event->motion.xrel,Event->motion.yrel,(Event->motion.state&SDL_BUTTON(SDL_BUTTON_LEFT))!=0,(Event->motion.state&SDL_BUTTON(SDL_BUTTON_RIGHT))!=0,(Event->motion.state&SDL_BUTTON(SDL_BUTTON_MIDDLE))!=0);
            break;
        }

        case SDL_MOUSEBUTTONDOWN: {
            switch(Event->button.button) {
                case SDL_BUTTON_LEFT: {
                    OnLButtonDown(Event->button.x,Event->button.y);
                    break;
                }
                case SDL_BUTTON_RIGHT: {
                    OnRButtonDown(Event->button.x,Event->button.y);
                    break;
                }
                case SDL_BUTTON_MIDDLE: {
                    OnMButtonDown(Event->button.x,Event->button.y);
                    break;
                }
            }
            break;
        }

        case SDL_MOUSEBUTTONUP:    {
            switch(Event->button.button) {
                case SDL_BUTTON_LEFT: {
                    OnLButtonUp(Event->button.x,Event->button.y);
                    break;
                }
                case SDL_BUTTON_RIGHT: {
                    OnRButtonUp(Event->button.x,Event->button.y);
                    break;
                }
                case SDL_BUTTON_MIDDLE: {
                    OnMButtonUp(Event->button.x,Event->button.y);
                    break;
                }
            }
            break;
        }

        case SDL_JOYAXISMOTION: {
            OnJoyAxis(Event->jaxis.which,Event->jaxis.axis,Event->jaxis.value);
            break;
        }

        case SDL_JOYBALLMOTION: {
            OnJoyBall(Event->jball.which,Event->jball.ball,Event->jball.xrel,Event->jball.yrel);
            break;
        }

        case SDL_JOYHATMOTION: {
            OnJoyHat(Event->jhat.which,Event->jhat.hat,Event->jhat.value);
            break;
        }
        case SDL_JOYBUTTONDOWN: {
            OnJoyButtonDown(Event->jbutton.which,Event->jbutton.button);
            break;
        }

        case SDL_JOYBUTTONUP: {
            OnJoyButtonUp(Event->jbutton.which,Event->jbutton.button);
            break;
        }

        case SDL_QUIT: {
            OnExit();
            break;
        }

        case SDL_SYSWMEVENT: {
            //Ignore
            break;
        }

        case SDL_VIDEORESIZE: {
            OnResize(Event->resize.w,Event->resize.h);
            break;
        }

        case SDL_VIDEOEXPOSE: {
            OnExpose();
            break;
        }

        default: {
            OnUser(Event->user.type,Event->user.code,Event->user.data1,Event->user.data2);
            break;
        }
    }
}

void CEvent::OnInputFocus() {
    //Pure virtual, do nothing
}

void CEvent::OnInputBlur() {
    //Pure virtual, do nothing
}

void CEvent::OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode) {
    //Pure virtual, do nothing
}

void CEvent::OnKeyUp(SDLKey sym, SDLMod mod, Uint16 unicode) {
    //Pure virtual, do nothing
}

void CEvent::OnMouseFocus() {
    //Pure virtual, do nothing
}

void CEvent::OnMouseBlur() {
    //Pure virtual, do nothing
}

void CEvent::OnMouseMove(int mX, int mY, int relX, int relY, bool Left,bool Right,bool Middle) {
    //Pure virtual, do nothing
}

void CEvent::OnMouseWheel(bool Up, bool Down) {
    //Pure virtual, do nothing
}

void CEvent::OnLButtonDown(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnLButtonUp(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnRButtonDown(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnRButtonUp(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnMButtonDown(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnMButtonUp(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnJoyAxis(Uint8 which,Uint8 axis,Sint16 value) {
    //Pure virtual, do nothing
}

void CEvent::OnJoyButtonDown(Uint8 which,Uint8 button) {
    //Pure virtual, do nothing
}

void CEvent::OnJoyButtonUp(Uint8 which,Uint8 button) {
    //Pure virtual, do nothing
}

void CEvent::OnJoyHat(Uint8 which,Uint8 hat,Uint8 value) {
    //Pure virtual, do nothing
}

void CEvent::OnJoyBall(Uint8 which,Uint8 ball,Sint16 xrel,Sint16 yrel) {
    //Pure virtual, do nothing
}

void CEvent::OnMinimize() {
    //Pure virtual, do nothing
}

void CEvent::OnRestore() {
    //Pure virtual, do nothing
}

void CEvent::OnResize(int w,int h) {
    //Pure virtual, do nothing
}

void CEvent::OnExpose() {
    //Pure virtual, do nothing
}

void CEvent::OnExit() {
    //Pure virtual, do nothing
}

void CEvent::OnUser(Uint8 type, int code, void* data1, void* data2) {
    //Pure virtual, do nothing
}

거 무지 기네... 그렇지만 모든 이벤트가 처리되어야 하므로 어쩔 수 없었다. 이제 할일은 SDL_Event 포인터를 얻는 것이다. 그리고 이벤트 타입에 따라 스위칭을 하고 그리고 적절한 함수를 호출한다. 코드가 길어 보인다. 위는 꽤 잦게 발생하는 이벤트다.

모든게 준비되었으니, CApp.h를 열고 상위 두개 파일을 연결시키자.

#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

#include "CEvent.h"
#include "CSurface.h"

class CApp : public CEvent {
    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

컴파일은 모두 잘된다. 우리는 준비된 이벤트 클래스를 갖고 있다. 이제 이벤트를 우리의 새 이벤트 동작 구조(방식)에 연결시켜보자. CApp_OnEvent.cpp를 열고 다음과 같이 편집한다.

#include "CApp.h"

void CApp::OnEvent(SDL_Event* Event) {
    CEvent::OnEvent(Event);
}

이벤트 구조체를 새 클래스에 전달하고 있다. 위 코드는 정확한 함수 호출을 보장할 것이다. 이제 특정 이벤트를 확인하고자 할 때, 함수를 오버라이딩하기만 하면 된다. SDL_QUIT 이벤트를 체크하는 기능을 없앴으므로 새 함수를 대신 사용해보자. CApp.h를 다시 열고 다음 함수를 추가하자.

#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

#include "CEvent.h"
#include "CSurface.h"

class CApp : public CEvent {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

        SDL_Surface*    Surf_Test;

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

        void OnExit();

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif

OnExit 메서드는 SDL_QUIT 이벤트를 다룬다. 우리는 이제 프로토타입을 가졌으므로 이것이 무슨 일을 할지 정의해보자. CApp_OnEvent.cpp를 열고서 다음을 추가한다.

#include "CApp.h"

void CApp::OnEvent(SDL_Event* Event) {
    CEvent::OnEvent(Event);
}

void CApp::OnExit() {
    Running = false;
}

다시 컴파일하고, 실행시켜보라. 이제 전에 닫히지 않던 응용프로그램을 종료할 수 있을 것이다.

 확인가능한 다양한 이벤트를 살펴보기를 추천한다. 나중에 게임에서 이와 같은 이벤트 중 일부를 사용할 것이다. 단지 지금은 이들과 친숙해져야 한다.

[일기] 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의 좌표 체계가 어떻게 구축되는지, 얼마나 자주 이를 사용할지에 대해 세심한 주의를 기울여야 한다.

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

2014년 7월 1일 화요일

[일기] SDL 트루 타입 폰트 TTL(True Type Font) 튜토리얼 정리.

라이브러리가 또한 필요하다고 한다. 이것 만드는 사람 대단하다. 나도 그 중 한 명이 되고 싶은데 멀었다.

 http://www.libsdl.org/projects/SDL_ttf/

여기서 다운 받는다.

전에서와 마찬가지로 설정을 마치고......(dll 이동 및 lib 추가; 본인은 VS2012 사용...)

SDL_ttf 는 속도와 질의 Trade-off를 가지고 다양하게 텍스트를 렌더링할수 있는 방법을 몇 가지 제공한다고 한다. 가령, 유니코드, UTF-8, 글리프와 같은....

TTF_RenderText_Blended를 사용할 것이다.

SDL_Color로 텍스트의 색상도 지정할 수 있다.


불행하게도(?), SDL_ttf는 표면 즉, Surface에만 렌더링을 수행할 수 있다. 이에 문자를 출력하는 단계는 대략 다음과 같다.

 1. 렌더러 생성.
 2. 텍스트 생성.
 3. 텍스트로 표면 생성.
 4. 표면으로 텍스처 생성.
 5. 화면에 출력.

TTF_OpenFont로 사용할 폰트를 불러온다.

초기화는 TTF_Init()으로 하되 성공시 0을 리턴한다.

------------------------------------------------------------------------

위 단계를 하나로 밀집한 함수를 만든다.

DL_Texture* MY_RenderText(const std::string &msg, const std::string &fontFile,
  SDL_Color color, int fontSize, SDL_Renderer *ren){
TTF_Font *font = TTF_OpenFont(fontFile.c_str(), fontSize);
if(font == nullptr){
MY_LogSDLError(std::cout, "TTF_OpenFont");
return nullptr;
}

SDL_Surface* surf = TTF_RenderText_Blended(font, msg.c_str(), color);
if(surf == nullptr){
TTF_CloseFont(font);
MY_LogSDLError(std::cout, "TTF_RenderText");
return nullptr;
}

SDL_Texture* tex = SDL_CreateTextureFromSurface(ren, surf);
if(tex == nullptr) {
MY_LogSDLError(std::cout, "CreateTexture");
}

SDL_FreeSurface(surf);
TTF_CloseFont(font);
return tex;
}

이를 이용해서 출력을 시도해 본다.

int recordTutorialFour(int argc, char* argv[]){

if( TTF_Init() != 0 ){
MY_LogSDLError(std::cout, "TTF_Init");
return 1;
}

SDL_Init(SDL_INIT_EVERYTHING);

SDL_Window* win = SDL_CreateWindow("Font", 0, 0, WIDTH_OF_SCREEN, HEIGHT_OF_SCREEN, SDL_WINDOW_SHOWN);
SDL_Renderer *ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);


const std::string resPath = getResourcePath();

SDL_Color color = { 255, 255, 255, 255 };
SDL_Texture *image = MY_RenderText("TTF fonts are cool!", resPath + "sample.ttf",
color, 64, ren);
if (image == nullptr){
return 1;
}

int iW, iH;
SDL_QueryTexture(image, NULL, NULL, &iW, &iH);
int x = WIDTH_OF_SCREEN / 2 - iW / 2;
int y = HEIGHT_OF_SCREEN / 2 - iH / 2;


//Note: This is within the program's main loop
SDL_RenderClear(ren);
//We can draw our message as we do any other texture, since it's been
//rendered to a texture
MY_RenderTexture(image, ren, x, y);
SDL_RenderPresent(ren);

SDL_Delay(4000);

SDL_DestroyTexture(image);
SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
SDL_Quit();
        TTF_Quit();

return 0;
}

int main(int argc, char* argv[]){

return recordTutorialFour(argc, argv);
}

sample.ttf는 훈하얀고양이R 폰트.


[일기] SDL 이미지 부분 출력하기.

SDL의 RenderCopy에서 세 번째 인자를 지정함으로 손쉽게 가능하다.

손쉽게 가능하다.

void renderTexture(SDL_Texture *tex, SDL_Renderer *ren, SDL_Rect dst,
SDL_Rect *clip = nullptr)
{
SDL_RenderCopy(ren, tex, clip, &dst);
}


void renderTexture(SDL_Texture *tex, SDL_Renderer *ren, int x, int y,
SDL_Rect *clip = nullptr)
{
SDL_Rect dst;
dst.x = x;
dst.y = y;
if (clip != nullptr){
dst.w = clip->w;
dst.h = clip->h;
}
else
SDL_QueryTexture(tex, NULL, NULL, &dst.w, &dst.h);

renderTexture(tex, ren, dst, clip);
}

[일기] SDL 이벤트 다루기 틀. 내용 없음.

Message-driven.

Handling Events 라는 주제로 학습했다.

SDL_PollEvent 함수를 호출하면 되는데, 익숙한 반복문 형태가 나타나게 된다.

메시지 가져오고 처리하고를 무한 반복.

위 함수의 인자는 SDL_Event 이다.

아래는 튜토리얼 사이트의 예제 코드.

while (SDL_PollEvent(&e)){

if (e.type == SDL_QUIT)
quit = true;

if (e.type == SDL_KEYDOWN)
quit = true;

if (e.type == SDL_MOUSEBUTTONDOWN)
quit = true;
}

좀 더 완성된 형태라는 예제 코드.

//구조

SDL_Event e;
bool quit = false;
while (!quit){
while (SDL_PollEvent(&e)){
if (e.type == SDL_QUIT)
quit = true;
if (e.type == SDL_KEYDOWN)
quit = true;
if (e.type == SDL_MOUSEBUTTONDOWN)
quit = true;
}
//장면 렌더링
SDL_RenderClear(renderer);
renderTexture(image, renderer, x, y);
SDL_RenderPresent(renderer);
}

[일기] SDL에서 png 이미지 출력하기.

1. 확장 라이브러리가 필요하다.

 SDL_image 가 그것이다.

본인은 Windows에서 Visual Studio를 사용하므로 SDL_image를 http://www.libsdl.org/projects/SDL_image/ 에서 다운받고, 필요한 lib를 추가하는 반면, dll 또한 system32 폴더에 위치시켰다.





 포함될 헤더는 SDL_image.h 하나 뿐이다.

이전 튜토리얼 2에서 배운 함수를 변경한다.

 SDL_Texture* MY_LoadTexture(const std::string &file, SDL_Renderer *ren){
SDL_Texture *tex = IMG_LoadTexture(ren, file.c_str());
if (tex == nullptr)
MY_LogSDLError(std::cout, "LoadTexture");
return tex;
}

또한 다른 렌더링 함수를 오버로딩 한다.

void MY_RenderTexture(SDL_Texture *tex, SDL_Renderer *ren, int x, int y, int w, int h){
//우리가 위치시킬 곳을 지정한다. 폭과 높이도 멋대로. 알아서 수축하고 팽창한다.
SDL_Rect dst;
dst.x = x;
dst.y = y;
dst.w = w;
dst.h = h;
SDL_RenderCopy(ren, tex, NULL, &dst);
}

void MY_RenderTexture(SDL_Texture *tex, SDL_Renderer *ren, int x, int y){
int w, h;
SDL_QueryTexture(tex, NULL, NULL, &w, &h);
My_RenderTexture(tex, ren, x, y, w, h);
}


SDL_image를 새로 영입(?)했기 때문에 이를 초기화해야 한다. 본인은 PNG만 출력하면 되었으므로,

IMG_Init을 호출하되, IMG_INIT_PNG만을 플래그로 하여 인자로 전달한다. SDL_image에서는 이미지 로딩 시 자동으로 필요한 이미지를 초기화하는데, 이는 시간적 지연을 발생시키므로 미리 로딩한다. 사용된 함수는 현재 초기화된 이미지 로더의 비트마스크를 리턴한다.

//main

if ( (IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG) != IMG_INIT_PNG ) {
         MY_LogSDLError(std::cout, "IMG_Init");
         return 1;
}

이제 출력해보자. 아래의 코드는 튜토리얼 2에서와 동일하다. 경로 제외.

const std::string resPath = getResourcePath();
SDL_Texture *background = loadTexture(resPath + "background.png", ren);
SDL_Texture *foreground= loadTexture(resPath + "foreground.png", ren);
//Make sure they both loaded ok
if (background == nullptr || foreground == nullptr)
return 4;

이미지 출처 : http://www.willusher.io/sdl2%20tutorials/2013/08/18/lesson-3-sdl-extension-libraries/


int recordTutorialThree(int argc, char* argv[]){
if(SDL_Init(SDL_INIT_EVERYTHING) != 0){
MY_LogSDLError(std::cout, "SDL_Init");
return 1;
}

SDL_Window *win = SDL_CreateWindow("Tutorial 2", 100, 100,
WIDTH_OF_SCREEN, HEIGHT_OF_SCREEN, SDL_WINDOW_SHOWN);

if(win == nullptr){
MY_LogSDLError(std::cout, "CreateWindow");
return 2;
}
SDL_Renderer *ren = SDL_CreateRenderer(win, -1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if(ren == nullptr) {
MY_LogSDLError(std::cout, "CreateRenderer");
return 3;
}

const std::string resPath = getResourcePath();
SDL_Texture* background = MY_LoadTexture(resPath + "background.png", ren);
SDL_Texture* foreground = MY_LoadTexture(resPath + "imaged.png", ren);


SDL_RenderClear(ren); //지우기

int bW, bH;
SDL_QueryTexture(background, NULL, NULL, &bW, &bH);
MY_RenderTexture(background, ren, 0, 0);
MY_RenderTexture(background, ren, bW, 0);
MY_RenderTexture(background, ren, 0, bH);
MY_RenderTexture(background, ren, bW, bH);

//실제 그린다.
int iW, iH;
SDL_QueryTexture(foreground, NULL, NULL, &iW, &iH);
int x = WIDTH_OF_SCREEN / 2 - iW / 2;
int y = HEIGHT_OF_SCREEN / 2 - iH / 2;
MY_RenderTexture(foreground, ren, x, y);

SDL_RenderPresent(ren); //화면에 출력
SDL_Delay(2000); //2초 대기


if(background == nullptr || foreground == nullptr){
return 4;
}

SDL_DestroyTexture(background);
SDL_DestroyTexture(foreground);
SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
SDL_Quit();
 IMG_Quit();

return 0;
}

int main(int argc, char* argv[]){

return recordTutorialThree(argc, argv);
}

위는 테스트 코드.

아래는 결과.


[일기] SDL 초기 TUTORIAL 연습 2

이번 찾아본 강의의 목적은 SDL을 이용하여 화면에 이미지를 출력하는 내용이다.

 1. 화면에 나타낼 윈도우의 크기를 미리 상수로 잡아놓는다.


[그림 1]

 2. 이후 에러 출력을 담당할 함수를 만들었다.



[그림 2] [그림 3]

 3. 그리고 텍스처를 불러오는 함수를 만드는데...

======================================================================
SDL에서 윈도우를 그리기 위해서는 SDL_CreateWindow 를 호출하면 된다. 인자는 6개로,

 타이틀명
 좌상단 좌표 x
 좌상단 좌표 y
 폭
 높이
 깃발(flag)

이다.

 여기서 6번째 인자로 SDL_WINDOW_SHOWN 을 이용하면 화면에 나타난다. null을 리턴할 경우, 에러가 발생한 것으로 간주할 수 있다.

 SDL_Window *win = SDL_CreateWindow("Title", 0, 0, 300, 500, SDL_WINDOW_SHOWN);

 화면에 무엇을 그리기 위해 SDL에서는 Renderer가 필요하다. 이는 SDL_CreateRenderer 를 이용하면 된다. 인자는 3개로

 윈도우 포인터
 렌더링 드라이버 인덱스
 렌더링 정렬(?) 플래그

이다.

 SDL_Renderer *ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);

 여기서 플래그는 모르고 넘어갔다. 문제 발생시 nullptr를 리턴한다.

비트맵을 로딩해야 한다. 이는 SDL_LoadBMP를 통해서 하며, 인자는 1개로 파일 경로 문자열이다. SDL_Surface*를 리턴한다.

 SDL_Surface *bmp = SDL_LoadBMP("my.bmp");

문제가 있는 경우 nullptr이 리턴된다.

 문제는 우리가 필요한 것은 텍스처라는 놈이다라는 것이다.  이는 SDL_CreateTextureFromSurface를 통해 해결한다. 인자는 2개로 렌더러와 이미지 표면(surface)이다.

SDL_Texture* tex = SDL_CreateTextureFromSurface(ren, bmp);

역시 문제가 발생하면 nullptr이 리턴된다.

위 모든 경우에 있어 문제의 원인의 상세는 SDL_GetError()를 통해 얻을 수 있다.

SDL_FreeSurface를 통해 더 이상 사용않는 표면의 메모리를 해제할 수 있다.

 SDL_FreeSurface(bmp);

SDL_DestroyRenderer를 통해 더 이상 사용않는 렌더러를 소멸시킬 수 있다.

 SDL_DestroyRenderer(ren);

SDL_DestroyWindow를 통해 더 이상 사용않는 윈도우를 소멸시킬 수 있다.

 SDL_DestroyWindow(win);

 그림을 화면에 뿌리기 위해서 다음 순서의 작업이 추천된다.

 1. 렌더러를 깨끗하게 만든다.

  SDL_RenderClear(ren);

 2. 렌더러에 이미지를 복사한다.

 SDL_RenderCopy(ren, tex, NULL, NULL); //함수 상세는 이후에...

 3. 화면에 고속복사한다.

 SDL_RenderPresent(ren);

 * SDL_Delay를 이용하면 ms 단위의 프로그램 정지(Sleep)가 가능하다.

 SDL_Delay(2000); //2초 기다린다.

========================================================================

*반드시 모든 일을 끝내고 나서는 할당한 메모리를 해제한다.

SDL_DestroyTexture(tex)
SDL_DestroyRenderer(ren)
SDL_DestroyWindow(win)
SDL_Quit()

....가 기본 내용인데.. 이를 심화해서 나갔다.

반복적인 작업이므로 한꺼번에 하도록 함수로 만들었다.


[그림 4]

4. 그림을 출력하기 위한 함수를 만드는데....

  렌더러에 이미지를 복사할때, 복사 영역을 설정할 수 있다. 이때 SDL_Rect 를 이용하게 된다.

 SDL_Rect는 단순히 x, y, w, h 즉, 좌상단 x좌표, 좌상단 y좌표, 폭, 높이를 갖는다.

 * SDL_QueryTexture를 이용하면 우리가 불러온 텍스처의 폭과 높이를 알 수 있다.

 SDL_Rect dst;
 ...
 SDL_QueryTexture(tex, NULL, NULL, &dst.w, &dst.h);

이를 이용하여 복사 영역을 알아낸 후, 렌더러에 이미지를 복사한다.

 SDL_RenderCopy(ren, tex, NULL, &dst);

 만일 4번째 인자에 nullptr가 들어가면 자동으로 텍스처의 폭과 높이를 맞추어준다. 그리고
출력 좌상단 좌표는 0, 0이 된다.

...가 기본 내용이므로 이를 묶어버렸다.


[그림 5]


5. 윈도우, 렌더러 만들었다.


[그림 6]

6. 지금까지 가져온 것들을 기반으로 이미지를 가져와 화면에 출력한다.

 이미지를 2개 가져오는데 하나는 배경으로, 나머지 하나는 전경으로 사용한다.




[그림 7] [그림 8] [그림 9] [그림 10]

이미지 출처 : http://www.livescience.com/34143-blue-moon-phrase.html
http://www.twitterevolutions.com/tree-and-a-moonlight-twitter-background/

결과]


[그림 11]


2014년 6월 30일 월요일

[일기] SDL 초기 튜토리얼 연습 1

http://www.willusher.io/sdl2%20tutorials/2014/06/16/postscript-0-properly-finding-resource-paths/

아주 좋은 사이트가 있다.

경로를 다음과 같이 잡으라는데


Lessons/
    bin/
        executables in here
    res/
        Lesson1/
            Lesson 1's assets are stored here
    src/
        Lesson1/
            Lesson 1's source code
    include/
        Headers shared between lessons

무시하고 다음과 같이 진행했다.

 First/
        bin/
              executables
        res/
              Recored's assets
        src/
              source codes

 바로 유용한 함수 하나를 사이트에서 제공했다.

#ifndef RES_PATH_H
#define RES_PATH_H

#include <iostream>
#include <string>
#include <SDL.h>

/*
 * Get the resource path for resources located in res/subDir
 * It's assumed the project directory is structured like:
 * bin/
 *  the executable
 * res/
 *  Lesson1/
 *  Lesson2/
 *
 * Paths returned will be Lessons/res/subDir
 */
std::string getResourcePath(const std::string &subDir = ""){
//We need to choose the path separator properly based on which
//platform we're running on, since Windows uses a different
//separator than most systems
#ifdef _WIN32
const char PATH_SEP = '\\';
#else
const char PATH_SEP = '/';
#endif
//This will hold the base resource path: Lessons/res/
//We give it static lifetime so that we'll only need to call
//SDL_GetBasePath once to get the executable path
static std::string baseRes;
if (baseRes.empty()){
//SDL_GetBasePath will return NULL if something went wrong in retrieving the path
char *basePath = SDL_GetBasePath();
if (basePath){
baseRes = basePath;
SDL_free(basePath);
}
else {
std::cerr << "Error getting resource path: " << SDL_GetError() << std::endl;
return "";
}
//We replace the last bin/ with res/ to get the the resource path
size_t pos = baseRes.find_last_of("bin") - 2;
baseRes = baseRes.substr(0, pos) + "res" + PATH_SEP;
}
//If we want a specific subdirectory path in the resource directory
//append it to the base path. This would be something like Lessons/res/Lesson0
return subDir.empty() ? baseRes : baseRes + subDir + PATH_SEP;
}

#endif

- 출처 : http://www.willusher.io/sdl2%20tutorials/2014/06/16/postscript-0-properly-finding-resource-paths/



헤더 파일 생성 후 위 코드 복사 붙여넣기.


메인 함수에 코딩.


결과 확인.

결론.
  
  현재 작업경로를 얻어오는 방법을 알게 되었다...(?)

 보통은
#include <Windows.h>
 std::string MyTi_ExePath() {
    char buffer[MAX_PATH];
    GetModuleFileName( NULL, buffer, MAX_PATH );
    std::string::size_type pos = std::string( buffer ).find_last_of( "\\/" );
    return std::string( buffer ).substr( 0, pos);
}

이렇게 얻는다.

[일기] SDL 설정.

심플 다이렉트미디어 레이어(Simple DirectMedia Layer), SDL는 C 프로그래밍 언어로 짜여진 크로스플랫폼 멀티미디어 라이브러리이다. 이 라이브러리는 비디오, 오디오, 사용자 입력 등의 계층을 추상화하여, 리눅스, 마이크로소프트 윈도, 맥 OS X 등 여러 운영 체제에서 실행이 가능하도록 한다. 이 라이브러리는 비디오, 이벤트, 디지털 오디오, CD-ROM, 사운드, 스레드, 공유 객체 불러오기, 네트워킹, 타이머를 관리한다.

 - 위키피디아.

 사용 한번 해보고자 찾아보았다. 여러 사이트를 뒤진 결과 많은 IDE에서의 사용법이 나와 있는데, 환경 한계상 다 도전하지는 못하고 Visual Studio에서만 먼저 도전했다.

 SDL은 https://www.libsdl.org/에서 다운받는다. 2.0이 있다.

다운 받은 파일의 압축을 풀어보면 .h 파일들과 .lib 그리고 .dll 파일이 있다.

 이에 .dll은 실행 파일과 같은 경로에 둬 버렸고(보통은 system32에 넣으라 한다.) 프로젝트 설정에서 추가 lib 디렉터리를 lib 파일이 있는 디렉터리로 지정해 놓았다.

 추가 종속성에 .lib를 추가했다.

 내가 위 글을 읽어 이해가 안 될테니 사진으로 기록해 보려 한다.


콘솔 응용프로그램으로 프로젝트 하나 생성해봤다.


빈프로젝트로.



속성 들어가봤다.


[링커] [입력] 에서 추가 종속성을 편집했다.



위와 같이 추가했다.


위에서 입력한 라이브러리를 찾기 위해 위 입력한 파일이 존재하는 경로를 추가했다.


그곳이 여기이므로


추가하고 확인 눌렀다.


.dll 파일을 일부로 프로젝트 실행 파일이 위치할(지금은 Debug 쪽에) 곳에 미리 이동시켰다.



그러더니 컴파일 성공했다.