HardCoding/SDL

[SDL]Simple DirectMedia Layer API 사용하기 -2

싸이on 2008. 9. 17. 11:29

Simple DirectMedia Layer API 사용하기

라이브러리 초기화
라이브러리를 동적으로 로드하고 초기화하기 위해서는 SDL_Init() 를 사용한다. 이 함수는 활성화하고자 하는 부분에 해당하는 플래그들을 취한다. :

SDL_INIT_AUDIO
SDL_INIT_VIDEO
SDL_INIT_CDROM
SDL_INIT_TIMER
라이브러리의 사용을 마쳤으면 SDL_Quit() 를 통해 라이브러리를 정리한다.

예제:
#include <stdlib.h>
#include "SDL.h"

main(int argc, char *argv[])
{
    if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 ) {
        fprintf(stderr, "Unable to init SDL: %s\n", SDL_GetError());
        exit(1);
    }
    atexit(SDL_Quit);

    ...
}

 Tip:
SDL은 시스템 라이브러리들이 기본적으로 위치한 디렉토리로부터 SDL 라이브러리를 로드한다. 애플리케이션이 배포될 때 함께 제공된 동적 라이브러리들이 별도의 디렉토리로부터 로드되도록 하려면 SDL_SetLibraryPath() 함수를 사용한다. 


비디오(Video)
비디오 모드의 선택과 설정 (쉬운방법)
여러분의 맘에 드는 bit-depth 와 해상도를 선택하여 설정한다!

 팁 #1:
SDL_GetVideoInfo() 함수를 사용해서 하드웨어에 의해 지원되는 가장 빠른 비디오 depth 를 얻을 수 있다.
팁 #2:
SDL_ListModes() 함수를 사용해서 특정 bit-depth 하에 지원되는 비디오 해상도의 목록을 얻을 수 있다.
 
예제:
{ SDL_Surface *screen;

    screen = SDL_SetVideoMode(640, 480, 16, SDL_SWSURFACE);
    if ( screen == NULL ) {
        fprintf(stderr, "Unable to set 640x480 video: %s\n", SDL_GetError());
        exit(1);
    }
}


화면에 점찍기
화면에 그리는 작업은 그래픽 프레임버퍼에 직접 쓴뒤 화면 갱신 함수를 호출함으로써 수행된다.

 팁:
만약 화면에 그릴 것이 많다면, 그리기 전에 (필요하다면) 화면을 한번 잠그고 갱신될 영역의 목록을 유지하다가 디스플레이를 갱신하기 전에 다시 화면을 푸는 것이 가장 좋은 방법이다. 

예제:
무작위 포맷의 화면에 점찍기

#include "SDL_endian.h" /* 엔디안-종속적인 24 bpp 모드를 위해 사용 */

void DrawPixel(SDL_Surface *screen, int x, int y, Uint8 R, Uint8 G, Uint8 B)
{
    Uint32 color = SDL_MapRGB(screen->format, R, G, B);

    if ( SDL_MUSTLOCK(screen) ) {
        if ( SDL_LockSurface(screen) < 0 ) {
            return;
        }
    }
    switch (screen->format->BytesPerPixel) {
        case 1: { /* 8-bpp 라고 가정 */
            Uint8 *bufp;

            bufp = (Uint8 *)screen->pixels + y*screen->pitch + x;
            *bufp = color;
        }
        break;

        case 2: { /* 아마 15-bpp 아니면 16-bpp */
            Uint16 *bufp;

            bufp = (Uint16 *)screen->pixels + y*screen->pitch/2 + x;
            *bufp = color;
        }
        break;

        case 3: { /* 느린 24-bpp mode, 보통 사용되지 않는다 */
            Uint8 *bufp;

            bufp = (Uint8 *)screen->pixels + y*screen->pitch + x * 3;
            if(SDL_BYTEORDER == SDL_LIL_ENDIAN) {
                bufp[0] = color;
                bufp[1] = color >> 8;
                bufp[2] = color >> 16;
            } else {
                bufp[2] = color;
                bufp[1] = color >> 8;
                bufp[0] = color >> 16;
            }
        }
        break;

        case 4: { /* 아마 32-bpp */
            Uint32 *bufp;

            bufp = (Uint32 *)screen->pixels + y*screen->pitch/4 + x;
            *bufp = color;
        }
        break;
    }
    if ( SDL_MUSTLOCK(screen) ) {
        SDL_UnlockSurface(screen);
    }
    SDL_UpdateRect(screen, x, y, 1, 1);
}


이미지의 로딩과 디스플레이
SDL은 사용자 편의를 위한 SDL_LoadBMP() 이라는 하나의 이미지 로딩 루틴을 제공한다. 이미지 로딩을 위한 라이브러리는 SDL 데모 모음에서 발견할 수 있다.

SDL_BlitSurface() 를 사용해서 그래픽 프레임버퍼로 이미지를 blit 하면 화면에 디스플레이할 수 있다. SDL_BlitSurface() 는 자동적으로 blit 사각형을 클립(clip)하는데, 이 blit 사각형은 SDL_BlitSurface()에 넘겨져 바뀐 화면의 영역이 갱신되도록 한다.

 팁 #1:
만약 여러번 그려질 이미지를 로딩하려면, 이미지를 스크린의 포맷으로 변환하여 blit 속도를 향상시킬 수 있다. SDL_DisplayFormat() 함수가 이러한 변환을 해 준다.
팁 #2:
많은 스프라이트 이미지들은 투명한 배경을 가진다. SDL_SetColorKey() 함수를 사용해서 투명한 blit(컬러키 blit)을 할 수 있다.
 

예제:
void ShowBMP(char *file, SDL_Surface *screen, int x, int y)
{
    SDL_Surface *image;
    SDL_Rect dest;

    /* BMP 파일을 서페이스로 읽어들임 */
    image = SDL_LoadBMP(file);
    if ( image == NULL ) {
        fprintf(stderr, "Couldn't load %s: %s\n", file, SDL_GetError());
        return;
    }

    /* 화면 서페이스에 Blit.
       서페이스는 이 시점에서 잠궈져있지 않아야 한다. */
    dest.x = x;
    dest.y = y;
    dest.w = image->w;
    dest.h = image->h;
    SDL_BlitSurface(image, NULL, screen, &dest);

    /* 화면의 변화된 부분을 갱신 */
    SDL_UpdateRects(screen, 1, &dest);

    SDL_FreeSurface(image);
}


이벤트(Events)
이벤트 기다리기
SDL_WaitEvent() 함수를 사용해서 이벤트를 기다린다.

 팁:
SDL 은 국제 키보드를 지원한다. 키 이벤트를 유니코드와 유사한 코드로 바꾸어 event.key.keysym.unicode 에 넣는다. 이러한 작업은 약간의 프로세싱 오버헤드를 감수해야 하기 때문에, SDL_EnableUNICODE() 가 활성화되어야만 사용된다. 

예제:
{
    SDL_Event event;

    SDL_WaitEvent(&event);

    switch (event.type) {
        case SDL_KEYDOWN:
            printf("The %s key was pressed!\n",
                   SDL_GetKeyName(event.key.keysym.sym));
            break;
        case SDL_QUIT:
            exit(0);
    }
}


이벤트를 폴링(Polling)하기
SDL_PollEvent() 함수를 사용해서 이벤트를 폴링한다(검사한다).

 팁:
SDL_PeepEvents() 함수에 SDL_PEEKEVENT 를 넘겨 이벤트를 삭제하지 않고도 이벤트 큐에서 이벤트를 확인할 수 있다. 

예제:
{
    SDL_Event event;

    while ( SDL_PollEvent(&event) ) {
        switch (event.type) {
            case SDL_MOUSEMOTION:
                printf("Mouse moved by %d,%d to (%d,%d)\n",
                       event.motion.xrel, event.motion.yrel,
                       event.motion.x, event.motion.y);
                break;
            case SDL_MOUSEBUTTONDOWN:
                printf("Mouse button %d pressed at (%d,%d)\n",
                       event.button.button, event.button.x, event.button.y);
                break;
            case SDL_QUIT:
                exit(0);
        }
    }
}


이벤트 상태를 폴링하기(Polling)
이벤트를 직접적으로 처리하는 것 뿐만 아니라, 각 이벤트 타입은 애플리케이션 이벤트 상태를 체크할 수 있는 함수를 제공한다. 만약 이 함수를 독점적으로 사용하면, SDL_EventState() 함수로 모든 이벤트를 무시하고, SDL_PumpEvents() 함수를 주기적으로 호출하여 애플리케이션 이벤트 상태를 갱신해야만 한다.
 
 팁:
SDL_ShowCursor() 를 사용해서 시스템 마우스 커서를 감추거나 보이게 할 수 있다. 

예제:
{
    SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
}

void CheckMouseHover(void)
{
    int mouse_x, mouse_y;

    SDL_PumpEvents();

    SDL_GetMouseState(&mouse_x, &mouse_y);
    if ( (mouse_x < 32) && (mouse_y < 32) ) {
        printf("Mouse in upper left hand corner!\n");
    }
}


사운드(Sound)
오디오 장치 열기
여러분은 여러분의 오디오 데이타를 혼합하고 그것을 오디오 스트림으로 보내는 콜백 함수를 만들어야 한다. 그 다음엔, 원하는 오디오 포맷과 속도를 선택하고, 오디오 장치를 연다.

SDL_PauseAudio(0) 를 호출할때까지 오디오는 실제로 재생되지 않는다. 이 함수는 여러분의 콜백 함수가 실행되기 전에, (필요하다면) 다른 오디오 초기화작업을 수행할 수 있도록 한다. 사운드 출력을 마친 다음, SDL_CloseAudio() 함수를 사용해 오디오 출력을 닫아야 한다.

 팁:
만약 여러분의 애플리케이션이 다른 오디오 포맷을 처리할 수 있다면, 두번째 SDL_AudioSpec 포인터를 SDL_OpenAudio() 에 보내 실제 하드웨어 오디오 포맷을 취하도록 해야 한다. 만약 두번째 포인터를 NULL로 남겨놓는다면, 오디오 데이타는 실행시에 하드웨어 오디오 포맷으로 변환될 것이다. 

예제:
#include "SDL.h"
#include "SDL_audio.h"
{
    extern void mixaudio(void *unused, Uint8 *stream, int len);
    SDL_AudioSpec fmt;

    /* 16-bit 스테레오 오디오를 22Khz 로 설정한다. */
    fmt.freq = 22050;
    fmt.format = AUDIO_S16;
    fmt.channels = 2;
    fmt.samples = 512;        /* 게임을 위해 적당한 값 */
    fmt.callback = mixaudio;
    fmt.userdata = NULL;

    /* 오디오 장치를 열고 사운드를 재생하기 시작한다! */
    if ( SDL_OpenAudio(&fmt, NULL) < 0 ) {
        fprintf(stderr, "Unable to open audio: %s\n", SDL_GetError());
        exit(1);
    }
    SDL_PauseAudio(0);

    ...

    SDL_CloseAudio();
}


사운드 로딩과 재생
SDL 은 사용자 편의를 위해 하나의 사운드 로딩 루틴인 SDL_LoadWAV() 를 제공한다. 사운드를 로딩한 뒤에 SDL_ConvertAudio() 를 사용해서 사운드 출력에 맞는 오디오 포맷으로 변환하여 여러분의 혼합(mixing) 함수에 사용될 수 있게 해야 한다.

 팁:
SDL 오디오 기능은 저수준 소프트웨어 오디오 믹서를 위해 설계되었다. 완벽한 믹서 구현 예제는 LGPL 라이센스 하에 SDL 데모 모음을 통해 제공된다. 

예제:
#define NUM_SOUNDS 2
struct sample {
    Uint8 *data;
    Uint32 dpos;
    Uint32 dlen;
} sounds[NUM_SOUNDS];

void mixaudio(void *unused, Uint8 *stream, int len)
{
    int i;
    Uint32 amount;

    for ( i=0; i        amount = (sounds[i].dlen-sounds[i].dpos);
        if ( amount > len ) {
            amount = len;
        }
        SDL_MixAudio(stream, &sounds[i].data[sounds[i].dpos], amount, SDL_MIX_MAXVOLUME);
        sounds[i].dpos += amount;
    }
}

void PlaySound(char *file)
{
    int index;
    SDL_AudioSpec wave;
    Uint8 *data;
    Uint32 dlen;
    SDL_AudioCVT cvt;

    /* 비어있는(또는 끝난) 사운드 슬롯을 찾는다. */
    for ( index=0; index        if ( sounds[index].dpos == sounds[index].dlen ) {
            break;
        }
    }
    if ( index == NUM_SOUNDS )
        return;

    /* 사운드 파일을 로드하여 22kHz의 16-bit 스테레오로 변환한다. */
    if ( SDL_LoadWAV(file, &wave, &data, &dlen) == NULL ) {
        fprintf(stderr, "Couldn't load %s: %s\n", file, SDL_GetError());
        return;
    }
    SDL_BuildAudioCVT(&cvt, wave.format, wave.channels, wave.freq,
                            AUDIO_S16,   2,             22050);
    cvt.buf = malloc(dlen*cvt.len_mult);
    memcpy(cvt.buf, data, dlen);
    cvt.len = dlen;
    SDL_ConvertAudio(&cvt);
    SDL_FreeWAV(data);

    /* 사운드 데이타를 슬롯에 넣는다(곧바로 재생된다). */
    if ( sounds[index].data ) {
        free(sounds[index].data);
    }
    SDL_LockAudio();
    sounds[index].data = cvt.buf;
    sounds[index].dlen = cvt.len_cvt;
    sounds[index].dpos = 0;
    SDL_UnlockAudio();
}


CD-ROM 오디오
사용하기 위해 시디롬 드라이브를 열기
SDL_CDNumDrives() 함수를 통해 시스템에서 사용할 수 있는 시디롬 드라이브 갯수를 알 수 있으며, SDL_CDOpen() 를 사용해서 그 중 하나를 사용할 수 있다.

시스템 디폴트 시디롬은 항상 드라이브 0 이다. 만약 드라이브에 디스크가 없더라도 사용하기 위해 드라이브를 열 수 있다.

SDL_CDStatus() 함수를 사용해서 드라이브의 상태를 확인해야 한다. 시디롬 드라이브의 사용을 마쳤다면 SDL_CDClose() 함수를 사용해서 닫는다.

 팁:
SDL_CDName() 함수를 사용해서 시디롬 드라이브의 시스템 독립적인 이름을 얻을 수 있다. 

예제:
{
    SDL_CD *cdrom;

    if ( SDL_CDNumDrives() > 0 ) {
        cdrom = SDL_CDOpen(0);
        if ( cdrom == NULL ) {
            fprintf(stderr, "Couldn't open default CD-ROM: %s\n" SDL_GetError());
            return;
        }

        ...

        SDL_CDClose(cdrom);
    }
}


시디롬 재생하기
시디롬 드라이브는 MSF 포맷(분/초/프레임, mins/secs/frames) 또는 직접적으로 프레임을 사용해서 시간을 지정한다. 한 프레임은 시디 시간의 표준 단위이며, 1/75 초에 해당한다. SDL 은 트랙길이와 오프셋을 지정할 때 MSF 포맷 대신에 프레임을 사용하지만, FRAMES_TO_MSF() 과 MSF_TO_FRAMES() 매크로를 사용해서 변환할 수 있다.

SDL 은 SDL_CDStatus() 를 호출할 때까지 SDL_CD 구조의 트랙 정보를 갱신하지 않기 때문에, 항상 시디를 재생하기 전에 SDL_CDStatus() 를 사용해서 드라이브에 시디가 있는지 또, 어떤 트랙이 가용한지를 확인해야 한다. 트랙 인덱스는 첫번째 트랙이 0 부터 시작한다는 것을 주의하자.

SDL 은 시디롬을 재생하기 위해 두가지 함수를 제공한다. SDL_CDPlayTracks() 를 사용해서 시디상의 특정 트랙을 재생할 수도 있고 SDL_CDPlay() 를 사용해서 절대 프레임 오프셋을 재생할 수도 있다.

SDL은 시디삽입 또는 재생완료를 자동으로 감지하는 기능을 제공하지 않는다. 이러한 상황을 감지하기 위해서는, SDL_CDStatus() 를 사용해 주기적으로 드라이브의 상태를 확인해야 한다. 이 함수들이 시디의 목차를 읽기 때문에 짧은 루프안에서 계속적으로 호출되지 않도록 해야 한다.

 팁:
여러분은 cdrom->tracks[track].type 를 보고 SDL_AUDIO_TRACK 인지 SDL_DATA_TRACK 인지 비교하여 어떤 트랙이 오디오 트랙이며 어떤 트랙이 데이타 트랙인지 확인해야 한다. 

예제:
void PlayTrack(SDL_CD *cdrom, int track)
{
    if ( CD_INDRIVE(SDL_CDStatus(cdrom)) ) {
        SDL_CDPlayTracks(cdrom, track, 0, track+1, 0);
    }
    while ( SDL_CDStatus(cdrom) == CD_PLAYING ) {
        SDL_Delay(1000);
    }
}


쓰레드(Threads)
간단한 쓰레드 만들기
SDL_CreateThread() 에 여러분의 함수를 넘겨줌으로써 쓰레드를 만들 수 있다. SDL_CreateThread()가 성공적으로 리턴된다면, 여러분의 함수는 이제 애플리케이션의 나머지 부분동안에 동시에 수행된다. 자신의 실행 컨텍스트(스택, 레지스터, 등등)를 가지며, 애플리케이션의 나머지 부분에 의해 사용되는 메모리와 파일 핸들을 액세스할 수도 있다.

 팁:
SDL_CreateThread() 의 두번째 인수는 쓰레드 함수에 대한 인수로 넘겨진다. 이 인수를 통해 스택상에 데이타 자체를 넘겨줄 수도 있고, 쓰레드에 의해 사용될 데이타에 대한 포인터로 사용할 수도 있다. 

예제:
#include "SDL_thread.h"

int global_data = 0;

int thread_func(void *unused)
{
    int last_value = 0;

    while ( global_data != -1 ) {
        if ( global_data != last_value ) {
            printf("Data value changed to %d\n", global_data);
            last_value = global_data;
        }
        SDL_Delay(100);
    }
    printf("Thread quitting\n");
    return(0);
}

{
    SDL_Thread *thread;
    int i;

    thread = SDL_CreateThread(thread_func, NULL);
    if ( thread == NULL ) {
        fprintf(stderr, "Unable to create thread: %s\n", SDL_GetError());
        return;
    }

    for ( i=0; i<5; ++i ) {
        printf("Changing value to %d\n", i);
        global_data = i;
        SDL_Delay(1000);
    }

    printf("Signaling thread to quit\n");
    global_data = -1;
    SDL_WaitThread(thread, NULL);
}


리소스에 대한 액세스 동기화
뮤텍스(mutex)를 생성하고 잠금(SDL_mutexP())과 해제(SDL_mutexV())를 통해 하나 이상의 쓰레드가 하나의 리소스를 액세스하는 제한할 수 있다.

 팁:
하나이상의 쓰레드에 의해 액세스할 수있는 모든 데이타는 뮤텍스(mutex)에 의해 보호되어져야 한다. 

예제:
#include "SDL_thread.h"
#include "SDL_mutex.h"

int potty = 0;
int gotta_go;

int thread_func(void *data)
{
    SDL_mutex *lock = (SDL_mutex *)data;
    int times_went;

    times_went = 0;
    while ( gotta_go ) {
        SDL_mutexP(lock);    /* potty 를 잠근다 */
        ++potty;
        printf("Thread %d using the potty\n", SDL_ThreadID());
        if ( potty > 1 ) {
            printf("Uh oh, somebody else is using the potty!\n");
        }
        --potty;
        SDL_mutexV(lock);
        ++times_went;
    }
    printf("Yep\n");
    return(times_went);
}

{
    const int progeny = 5;
    SDL_Thread *kids[progeny];
    SDL_mutex  *lock;
    int i, lots;

    /* 동기화를 위한 lock 생성 */
    lock = SDL_CreateMutex();

    gotta_go = 1;
    for ( i=0; i        kids[i] = SDL_CreateThread(thread_func, lock);
    }

    SDL_Delay(5*1000);
    SDL_mutexP(lock);
    printf("Everybody done?\n");
    gotta_go = 0;
    SDL_mutexV(lock);

    for ( i=0; i        SDL_WaitThread(kids[i], &lots);
        printf("Thread %d used the potty %d times\n", i+1, lots);
    }
    SDL_DestroyMutex(lock);
}


타이머(Timers)
현재 시간을 천분의 1초 단위로 얻는다
SDL_GetTicks() 는 지난 임의의 시간으로부터 지금까지 몇 밀리세컨드(milliseconds)가 지났는지 알려준다.

 팁:
일반적으로, 게임을 구현할 때 프레임률보다는 시간에 기반하여 게임상의 객체를 움직이는 것이 더 낫다. 그렇게 하면 빠른 시스템이나 느린 시스템이나 동일한 속도로 게임을 즐길 수 있다. 

예제:
#define TICK_INTERVAL    30

Uint32 TimeLeft(void)
{
    static Uint32 next_time = 0;
    Uint32 now;

    now = SDL_GetTicks();
    if ( next_time <= now ) {
        next_time = now+TICK_INTERVAL;
        return(0);
    }
    return(next_time-now);
}


지정된 밀리세컨드만큼 기다리기
SDL_Delay() 는 몇 밀리세컨드만큼 기다리게 한다.

SDL이 지원하는 운영체제들은 멀티태스킹 운영체제이기 때문에, 애플리케이션이 요구된 시간만큼 정확히 지연된다는 보장을 할 수 없다. 이 함수는 특정 시간에 다시 진행되어야 하는 경우보다는 잠깐동안 정지해있어야 하는 경우에 더 적합하다.

 탑:
대부분의 운영체제들은 약 10ms 정도의 스케줄러 시간조각(timeslice)을 가진다. SDL_Delay(1) 는 현재의 시간조각에 해당하는 시간조각을 희생하여 다른 쓰레드가 실행될 수 있도록 사용된다. 이 함수는 아주 빨리 돌아가는 루프의 쓰레드가 있을 때 다른 쓰레드(오디오 같은)도 함께 실행되어야 한다면 중요하다. 

예제:
{
    while ( game_running ) {
        UpdateGameState();
        SDL_Delay(TimeLeft());
    }
}
 

엔디안 비의존성(Endian independence)
현재 시스템의 엔디안(endianness)을 결정하기
C 전처리기(preprocessor)는 SDL_BYTEORDER 를 현재 시스템의 바이트 순서(byte order)에 따라 SDL_LIL_ENDIAN 또는 SDL_BIG_ENDIAN 로 정의한다.

리틀 엔디안(little endian) 시스템은 아래와 같은 순서로 데이타를 디스크에 쓴다 :
     [lo-bytes] [hi-bytes]
빅 엔디안(big endian) 시스템은 아래와 같은 순서로 데이타를 디스크에 쓴다 :
     [hi-bytes] [lo-bytes]

 팁:
x86시스템들은 리틀 엔디안이며, PPC 시스템들은 빅 엔디안이다.
 
예제:
#include "SDL_endian.h"

#if SDL_BYTEORDER == SDL_LIL_ENDIAN
#define SWAP16(X)    (X)
#define SWAP32(X)    (X)
#else
#define SWAP16(X)    SDL_Swap16(X)
#define SWAP32(X)    SDL_Swap32(X)
#endif


다른 엔디안의 시스템들간의 데이타 변환
SDL 은 SDL_endian.h 에 빠른 매크로들을 제공한다. SDL_Swap16() 와 SDL_Swap32() 함수는 데이타의 엔디안을 스왑(swap)시킨다. 또한 특정 엔디안의 데이타를 로컬 시스템의 엔디안으로 변환시키는 매크로도 제공한다.

 팁:
만약 시스템의 바이트-순서(byte-order)만 알고 싶고, 모든 스왑 함수들은 필요없다면, SDL_endian.h 대신에 SDL_byteorder.h 를 인클루드하라. 

예제:
#include "SDL_endian.h"

void ReadScanline16(FILE *file, Uint16 *scanline, int length)
{
    fread(scanline, length, sizeof(Uint16), file);
    if ( SDL_BYTEORDER == SDL_BIG_ENDIAN ) {
        int i;
        for ( i=length-1; i >= 0; --i )
            scanline[i] = SDL_SwapLE16(scanline[i]);
    }
}

 http://www.libsdl.org/