SDL2 で矩形を書いてアニメーションさせる

矩形を描く

 矩形を描くには、SDL_RenderDrawRect() 関数、または SDL_RenderFillRect() 関数を使います。SDL_RenderDrawRect() 関数は矩形の境界線を、SDL_RenderFillRect() 関数は内部を塗りつぶした矩形を描きます。

 どちらも引数に、レンダラと矩形を表す構想体へのポインタを渡します。

SDL_Rect 構造体

 矩形を表す構造体は、SDL_Rect です。メンバに矩形の左上の座標を持つ x, y そして、矩形の幅の w、矩形の高さの h があります。

矩形を描く

 例えば、矩形の左上のスクリーン座標が (0, 0) で幅が 40 高さが 30 の矩形を白い線で描くには、次のようにします。

    SDL_Rect rect;
    ・
    ・
    SDL_SetRenderDrawColor(renderer, 0xff, 0xff, 0xff, SDL_ALPHA_OPAQUE);
    rect.x = 0;
    rect.y = 0;
    rect.w = 40;
    rect.h = 30;
    SDL_RenderDrawRect(renderer, &rect);

 また、矩形の左上のスクリーン座標が (0, 210) で幅が 40 高さが 30 の水色で塗りつぶした矩形を描くには、次のようにします。

    SDL_Rect rect;
    ・
    ・
    SDL_SetRenderDrawColor(renderer, 0, 0xff, 0xff, SDL_ALPHA_OPAQUE);
    rect.x = 0;
    rect.y = 210;
    rect.w = 40;
    rect.h = 30;
    SDL_RenderFillRect(renderer, &rect);

 この 2 つを描くと下図のようになります。

矩形の描画

アニメーションさせる

 アニメーションさせるには、定期的に描画する必要があります。このような用途に、SDL2 はタイマーを持っています。

タイマーを使う

 タイマーは、SDL_AddTimer() 関数で追加することができます。

 この関数は、指定のミリ秒が経過した後に別スレッドのコールバック関数を呼ぶように設定します。ここで問題になるのは、「別スレッド」ということです。

 何が問題かと言うと、レンダラが問題になります。SDL_CreateRenderer() を実行したスレッドと同一のスレッドでなければ、そのレンダラに対して描画を行えません。つまり、タイマーのコールバック関数で直接描画を行うためには、コールバック関数以外からの描画を諦める必要があります。簡単なプログラムでは、これでもいいでしょう。

 スレッドの問題を回避するには、コールバック関数の中で、ユーザイベントを発生させ、イベントループから、描画関数を呼び出す方法があります。この手順を説明していきます。

タイマーの設定

 SDL_AddTimer() 関数の引数は、ミリ秒で指定したタイマー時間、コールバック関数、コールバック関数に渡すパラメータのポインタです。戻り値はタイマー ID を返します。

 200 ミリ秒間隔で、draw_timer() という関数を呼び出します。draw_timer() には、レンダラを渡すようにします。以下のようになります。

    SDL_Renderer *renderer;
    SDL_TimerID my_timer_id;
    ・
    ・
    my_timer_id = SDL_AddTimer(200, draw_timer, (void *)renderer);

 タイマー ID はタイマーを削除する際に、SDL_RemoveTimer() 関数に渡します。

コールバック関数

 コールバック関数はユーザイベントを発生させるので、以下のようになります。ほとんどの場合、userevent.data1 に設定する関数の変更以外の修正は必要はありません。この例では、イベントループ内で、draw() 関数を呼び出すようにしています。

Uint32 draw_timer(Uint32 interval, void *param)
{
    SDL_Event event;
    SDL_UserEvent userevent;

    /* コールバックでSDL_USEREVENTイベントをキューに入れる。
    このコールバック関数は一定の周期で再び呼ばれる */

    userevent.type = SDL_USEREVENT;
    userevent.code = 0;
    userevent.data1 = &draw;
    userevent.data2 = param;

    event.type = SDL_USEREVENT;
    event.user = userevent;

    SDL_PushEvent(&event);
    return(interval);
}

イベントループ内で処理する

 イベントループ内で、ユーザイベントを処理します。

    switch(event.type)
    {
        ・
        ・
        case SDL_USEREVENT: {
            void (*p) (void*) = event.user.data1;
            p(event.user.data2);
            break;
        }
        ・
        ・
    }

アニメーションを描画する

 draw() 関数内で、呼び出される毎に違う位置に矩形を描画すれば動いているように見えます。全てまとめると、以下のようなソースになります。

#include <stdio.h>
#include <SDL.h>

void *draw(void *param) {
    SDL_Renderer *renderer;
    SDL_Rect rect;
    static int d = 0;

    renderer = (SDL_Renderer *)param;

    /* 描画 */
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    SDL_SetRenderDrawColor(renderer, 0xff, 0xff, 0xff, SDL_ALPHA_OPAQUE);

    /* 矩形描画 */
    rect.x = 0 + d;
    rect.y = 0 + d*3/4;
    rect.w = 40;
    rect.h = 30;
    SDL_RenderDrawRect(renderer, &rect);
    SDL_SetRenderDrawColor(renderer, 0, 0xff, 0xff, SDL_ALPHA_OPAQUE);
    rect.y = 210 - d*3/4;
    SDL_RenderFillRect(renderer, &rect);

    d = (d + 10) %290;

    /* バッファをウィドウに反映 */
    SDL_RenderPresent(renderer);
}

Uint32 draw_timer(Uint32 interval, void *param)
{
    SDL_Event event;
    SDL_UserEvent userevent;

    /* コールバックでSDL_USEREVENTイベントをキューに入れる。
    このコールバック関数は一定の周期で再び呼ばれる */

    userevent.type = SDL_USEREVENT;
    userevent.code = 0;
    userevent.data1 = &draw;
    userevent.data2 = param;

    event.type = SDL_USEREVENT;
    event.user = userevent;

    SDL_PushEvent(&event);
    return(interval);
}

int main(int argc, char* argv[]){
    SDL_Event event;
    SDL_Window *window;
    SDL_Renderer *renderer;
    SDL_TimerID my_timer_id;
    
    int quit_flg = 1;

    /* 初期化 */
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr, "SDL_Init(): %s\n", SDL_GetError());
        exit(1);
    }

    /* ウィンドウ作成 */
    window = SDL_CreateWindow("SDL Test", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 320, 240, SDL_WINDOW_OPENGL);
    if( window == NULL ) {
        printf("Can not create window\n");
        exit(1);
    }
    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
    if( renderer == NULL ) {
        printf("Can not create renderer\n");
        exit(1);
    }
    /* 強制終了時に SDL_Quit() を呼ぶ */
    atexit(SDL_Quit);

    /* タイマー設定 */
    my_timer_id = SDL_AddTimer(200, draw_timer, (void *)renderer);

    /* イベントループ */
    while(quit_flg) {
        while( SDL_PollEvent(&event) ) {
            switch (event.type) {
            /* タイマー処理 */
            case SDL_USEREVENT: {
                void (*p) (void*) = event.user.data1;
                p(event.user.data2);
                break;
            }

            /* 終了 */
            case SDL_KEYDOWN:
            case SDL_QUIT:
                quit_flg = 0;
                break;
            }
        }
        /* イベントがない場合、少し待つ */
        SDL_Delay(50);
    }
    if( my_timer_id ) SDL_RemoveTimer(my_timer_id);
    if (renderer) SDL_DestroyRenderer(renderer);
    if (window) SDL_DestroyWindow(window);

    SDL_Quit();

    return 0;
}
矩形のアニメーション

 上のようなアニメーションになると思います。

SDL2

Posted by sirius