Handling the Keyboard

キーボード関係の構造体

キーボードに関するデータタイプについて慣れておくと、この チュートリアルの内容も理解しやすくなりますので、先に そちらから説明します。

SDLKey

SDLKeyは、SDL/include/SDL_keysym.hで定義されている 列挙型で、詳細はこちらにあります。 SDLKeyのシンボルはそれぞれ1つのキーを 示しており、例えば、 SDLK_a は キーボードの'a'キーを、 SDLK_SPACE はスペースバーを指しています。

SDLMod

SDLModはSDLKeyとよく似た列挙型ですが、 キーではなくモディファイヤ(CTRL、ALT、SHIFTなど)を表しています。 モディファイヤのシンボルの完全なリストはこちらにあります。 SDLModの値は ANDをとることで、いくつかのモディファイヤの組み合わせを表現することができます。

SDL_keysym

typedef struct{
  Uint8 scancode;
  SDLKey sym;
  SDLMod mod;
  Uint16 unicode;
} SDL_keysym;

SDL_keysym構造体はキーを押したり離したりといった 状態を管理します。 scancodeフィールドはハード固有の値で、 特にそれを知りたいと思わなければ無視できるものです。 symフィールドはSDLKeyの 値で、押されたり離されたりしたキーの値が入っています。 modフィールドはキーが押されたり離されたり したときのモディファイヤの状態を表していますので、 KMOD_NUM | KMOD_CAPS | KMOD_LSHIFTという 状態は、NumlockとCAPS、それから左のSHIFTがすべて 押されている(か、ロックキーが有効になっている)状態を表しています。 それから、 unicodeフィールドは、キーの16ビットunicode表現が 格納されます。

Note: このフィールドはSDL_keysymが 押されたときだけ有効であるということを理解し、注意しておく必要が あるでしょう。 unicodeの値はキーが押されたときしか意味を持ちません。 unicodeの値は国際化された文字をあらわすもので、その文字はキーを押したときに しか生成されないからです。 unicodeに関するより詳しい情報は www.unicode.orgで得ることができます。

Note: unicodeの変換はSDL_EnableUNICODE関数を呼び出すことで有効になります。

SDL_KeyboardEvent

typedef struct{
  Uint8 type;
  Uint8 state;
  SDL_keysym keysym;
} SDL_KeyboardEvent;

SDL_KeyboardEvent構造体は、 (その名の通り)キーボードイベントをあらわします。 SDL_Event共用体の keyメンバは、このSDL_KeyboardEvent構造体です。typeフィールドは、 キーを離したときのイベント(SDL_KEYUP)なのか、 キーを押したときのイベント(SDL_KEYDOWN)なのかを 表します。 stateフィールドは、大変冗長なのですが、 typeフィールドと同じ情報を返します。 ただし返す値が異なります(SDL_RELEASEDSDL_PRESSED)。 keysym フィールドは、 押したり離したりしたイベントを発生したキーを表します(上で説明しました)。

キーボードイベントを読み出す

キーボードイベントをイベントキューから読み出すのは とてもシンプルな作業です(イベントキューと、その利用のしかたについては こちらを参照してください)。 while()ループの中で SDL_PollEventを使って イベントキューからイベントを読み出し、 switchを使ってSDL_KEYUPSDL_KEYDOWNイベントをチェックします。つまり以下のような感じです:

Example 3-10. キーボードイベント読み出し


  SDL_Event event;
  .
  .
  /* イベントを取り出します。キューにイベントがないと */
  /* SDL_PollEvent() は0を返すので、ループを抜けることにします */
  while( SDL_PollEvent( &event ) ){
    /* 関心があるのはSDL_KEYDOWN と SDL_KEYUP の2つのイベントだけです */
    switch( event.type ){
      case SDL_KEYDOWN:
        printf( "キーが押されたことを検出しました\n" );
        break;

      case SDL_KEYUP:
        printf( "キーが離されたことを検出しました\n" );
        break;

      default:
        break;
    }
  }
  .
  .

これはとても単純な例で、 押されたり離されたりしたキーに関する情報はまったく処理されていません。 以下の例では、まったく両極端の例、 つまりキーボードイベントに関して扱いうるすべての情報を 表示する例をお見せします。

より深く調査する

イベントを扱うためにはSDL_Initで初期化を済ませ、SDL_SetVideoModeでビデオモードを設定する必要があります。 さらに、全ての情報を得るためには他にも2つの関数を呼び出す必要があります。 つまり、SDL_EnableUNICODE(1)を呼び出して、 unicode変換を有効にし、 SDL_GetKeyNameを 使うことで、SDLKeyの値を、出力可能な形式に 変換します。

Note: unicodeにおいて、0x80以下の値はそのままASCIIの値とすることが できることは知っておいたほうがよいでしょう。下の例ではその 事実を使用しています。

Example 3-11. キーイベント情報を解釈する


    #include "SDL.h"

    /* プロトタイプ宣言 */
    void PrintKeyInfo( SDL_KeyboardEvent *key );
    void PrintModifiers( SDLMod mod );

    /* main */
    int main( int argc, char *argv[] ){
        
        SDL_Event event;
        int quit = 0;
        
        /* SDLを初期化します */
        if( SDL_Init( SDL_INIT_VIDEO ) < 0){
            fprintf( stderr, "SDLを初期化できませんでした: %s\n", SDL_GetError() );
            exit( -1 );
        }

        /* ビデオモードをセットします */
        if( !SDL_SetVideoMode( 320, 200, 0, 0 ) ){
            fprintf( stderr, "ビデオモードをセットできませんでした: %s\n", SDL_GetError() );
            SDL_Quit();
            exit( -1 );
        }

        /* unicode変換を有効にします */
        SDL_EnableUNICODE( 1 );

        /* SDL_QUIT イベントが起きるまでループします */
        while( !quit ){

            /* イベントを取り出します */
            while( SDL_PollEvent( &event ) ){
                
                switch( event.type ){
                    /* キーボードイベント */
                    /* PrintKeyInfo() にイベントを渡します */
                    case SDL_KEYDOWN:
                    case SDL_KEYUP:
                        PrintKeyInfo( &event.key );
                        break;

                    /* SDL_QUIT イベント (ウインドウが閉じられた) */
                    case SDL_QUIT:
                        quit = 1;
                        break;

                    default:
                        break;
                }

            }

        }

        /* 後始末 */
        SDL_Quit();
        exit( 0 );
    }

    /* キーイベントに関する全ての情報を表示します */
    void PrintKeyInfo( SDL_KeyboardEvent *key ){
        /* キーが押されたのか離されたのか? */
        if( key->type == SDL_KEYUP )
            printf( "キーが離されました:- " );
        else
            printf( "キーが押されました:- " );

        /* ハード固有のスキャンコードを表示します */
        printf( "スキャンコード: 0x%02X", key->keysym.scancode );
        /* キーの名前を表示します */
        printf( ", 名前: %s", SDL_GetKeyName( key->keysym.sym ) );
        /* unicodeの変換結果を表示したいのですが、その前に */
        /* キーが押されたイベントであることを確認します */
        /* (キーが離されたイベントではunicodeの情報を得られない */
        /* ことを思い出してください) */
        if( key->type == SDL_KEYDOWN ){
            /* unicodeの値が 0x80 未満だったら、*/
            /* その値は (char)unicodeとすることで */
            /* 表示可能な文字を得ることができます */
            printf(", Unicode: " );
            if( key->keysym.unicode < 0x80 && key->keysym.unicode > 0 ){
                printf( "%c (0x%04X)", (char)key->keysym.unicode,
                        key->keysym.unicode );
            }
            else{
                printf( "? (0x%04X)", key->keysym.unicode );
            }
        }
        printf( "\n" );
        /* モディファイヤの情報を表示します */
        PrintModifiers( key->keysym.mod );
    }

    /* モディファイヤの情報を表示します */
    void PrintModifiers( SDLMod mod ){
        printf( "モディファイヤ: " );

        /* 何もなければそのままその旨を表示してリターンします */
        if( mod == KMOD_NONE ){
            printf( "該当なし\n" );
            return;
        }

        /* それぞれのSDLModの値をチェックします */
        /* みっともないですが他によい方法がありません */
        if( mod & KMOD_NUM ) printf( "NUMLOCK " );
        if( mod & KMOD_CAPS ) printf( "CAPSLOCK " );
        if( mod & KMOD_LCTRL ) printf( "左CTRL " );
        if( mod & KMOD_RCTRL ) printf( "右CTRL " );
        if( mod & KMOD_RSHIFT ) printf( "右SHIFT " );
        if( mod & KMOD_LSHIFT ) printf( "左SHIFT " );
        if( mod & KMOD_RALT ) printf( "右ALT " );
        if( mod & KMOD_LALT ) printf( "左ALT " );
        if( mod & KMOD_CTRL ) printf( "CTRL " );
        if( mod & KMOD_SHIFT ) printf( "SHIFT " );
        if( mod & KMOD_ALT ) printf( "ALT " );
        printf( "\n" );
    }

ゲーム向けの入力処理

ひとつの基本的なポイントを理解しないまま、 キーボードイベントをゲームやインタラクティブなアプリケーションで利用している 例をみかけます。

キーボードイベントはキーが押されたり離されたりしてキーの状態が 変化したときにだけ発生するものです。逆もまた 同様です。

エイリアンの画像があり、それをカーソルキーを使って動かしたいといった 状況を思いうかべてください: 左矢印を押せば彼を左の方向に動かすことができ、 下矢印を押せば下方向に動かすことができるようにしたいと思うでしょう。 以下のコードを試してみてください。ハイライトで示した部分が、 多くの人が間違いを犯している部分です。


    /* エイリアンのスクリーン上の座標 */
    int alien_x=0, alien_y=0;
    .
    .
    /* SDL、ビデオモードの初期化処理 */
    .
    /* メインループ */
    /* イベントのチェック */
    while( SDL_PollEvent( &event ) ){
        switch( event.type ){
            /* キーが押されたかどうかをチェック */
            case SDL_KEYDOWN:
                /* SDLKeyの値を調べて位置を変えます */
                switch( event.key.keysym.sym ){
                    case SDLK_LEFT:
                        alien_x -= 1;
                        break;
                    case SDLK_RIGHT:
                        alien_x += 1;
                        break;
                    case SDLK_UP:
                        alien_y -= 1;
                        break;
                    case SDLK_DOWN:
                        alien_y += 1;
                        break;
                    default:
                        break;
                }
            }
        }
    }
    .
    .
ちょっと見ただけでは、このコードで問題なく動作すると思えるかもしれませんが、 それは正しくありません。 キーボードイベントはキーの状態が変わったときにだけ発生しますので、 エイリアンを100ピクセル左に動かすためには、 左矢印を100回押さなくてはいけないことになります。

この問題を回避するためには、エイリアンの位置を変化させるために イベントを使わないということになります。 イベントは、別の処理でエイリアンを動かすときに参照するフラグを設定する ために使います。 例えば以下のような処理になります:

Example 3-12. 正しいゲームの処理


    /* エイリアンのスクリーン上の座標 */
    int alien_x=0, alien_y=0;
    int alien_xvel=0, alien_yvel=0;
    .
    .
    /* SDL、ビデオモードの初期化処理 */
    .
    /* メインループ */
    /* イベントのチェック */
    while( SDL_PollEvent( &event ) ){
        switch( event.type ){
            /* キーが押されたかどうかチェック */
            case SDL_KEYDOWN:
                /* SDLKeyの値から速度を変化させる */
                switch( event.key.keysym.sym ){
                    case SDLK_LEFT:
                        alien_xvel = -1;
                        break;
                    case SDLK_RIGHT:
                        alien_xvel =  1;
                        break;
                    case SDLK_UP:
                        alien_yvel = -1;
                        break;
                    case SDLK_DOWN:
                        alien_yvel =  1;
                        break;
                    default:
                        break;
                }
                break;
            /* x と yの速度を戻すためにSDL_KEYUP もチェックする */
            /* 必要があります。 ただし、速度を0にするべきでない */
            /* ときに間違って0にしないように気をつける必要があります */
            case SDL_KEYUP:
                switch( event.key.keysym.sym ){
                    case SDLK_LEFT:
                        /* エイリアンが確かに左に動いているかどうか */
                        /* をチェックします。動いていれば速度を0に */
                        /* します。もしエイリアンが右に動いていて */
                        /* 右矢印が押されつづけていたときは、速度を */
                        /* いじってはいけません */
                        if( alien_xvel < 0 )
                            alien_xvel = 0;
                        break;
                    case SDLK_RIGHT:
                        if( alien_xvel > 0 )
                            alien_xvel = 0;
                        break;
                    case SDLK_UP:
                        if( alien_yvel < 0 )
                            alien_yvel = 0;
                        break;
                    case SDLK_DOWN:
                        if( alien_yvel > 0 )
                            alien_yvel = 0;
                        break;
                    default:
                        break;
                }
                break;
            
            default:
                break;
        }
    }
    .
    .
    /* エイリアンの位置を更新します */
    alien_x += alien_xvel;
    alien_y += alien_yvel;

見てお分かりの通り、 alien_xvel and alien_yvelという2つの変数を追加しました。 これらはエイリアンの移動方向を表し、 キーが押されたり離されたりしたときに更新するべき値となります。