第6章 キャラクタ表示の様々なテクニック

6−2 移動方向によって表示する画像を変える

カーソルキーが何も押されていないときは「静止画像」を、右が押されていたら「右向きの画像」というように、移動方向によって表示する画像を切り替えるにはどうすればよいだろうか?
いろいろな方法があるが、ここでは配列を使った方法を紹介する。手順は次の通り。

  1. キャラクタ情報に、「現在向いている方向を格納する変数」「向きごとの矩形を格納する配列」を持つ。
  2. キャラクタ情報初期化処理で、向きごとの矩形情報(静止、上、下、左、右)を配列に格納する。
  3. 初期化処理時、向きの個数と、向きの初期値をセットする。
  4. キャラクタ画像描画時、向いている方向によって画像の矩形(配列の要素番号)を変える。
  5. ゲーム処理時、カーソルキーの状態を調べ、キャラクタ情報に格納する。

(a) キャラクタ構造体の修正

common.h内に作成したキャラクタ構造体を次のように修正する。

typedef struct tarSTATUS {
    int x, y;      // 表示座標
    RECT rect[5];   // 画像の矩形
    RECT hitrect;    // 当たり判定用の矩形
    int move_x, move_y; // 移動量
    int width, height;  // キャラクタの幅と高さ
    int direction;   // 現在向いている方向(0:STOP 1:LEFT 2:RIGHT 3:UP 4:DOWN)
} STATUS;

(b) 初期化変数の修正

方向分の矩形をセットできるよう、Game.cpp内を修正する。

  1. キャラクタ情報初期化関数「StatusInit」のプロトタイプ宣言を次のように修正(引数を2つ増やす)。
    static void StatusInit(STATUS*, int, int, int, int, int, int, int, int, int, int);
  2. キャラクタ情報初期化関数「StatusInit」の本体を次のように修正。
    //-----------------------------------------------------------------------------
    // 関数名 : StatusInit()
    // 機能概要: ステータス情報初期化
    //-----------------------------------------------------------------------------
    static void StatusInit(
        STATUS *status,                            // キャラクタ構造体変数のポインタ
        int left, int top, int right, int bottom,  // 画像の矩形(一枚目)
        int x, int y,                              // 表示座標の初期値
        int move_x, int move_y,                    // 移動量の初期値
        int direction_count, int direction   // 方向の数と、方向の初期値
    )
    {
        int i;
    
        // キャラクタの幅と高さ
        status->width = right - left;
        status->height = bottom - top;
        // キャラクタ表示位置
        status->x = x;
        status->y = y;
        // キャラクタの移動量
        status->move_x = move_x;
        status->move_y = move_y;
        // 向きの数と、向きに対する矩形
        for (i=0 ; i<direction_count ; i++)
        {
            status->rect[i].left = left;
            status->rect[i].top = top + status->height * i;
            status->rect[i].right = status->rect[i].left + status->width;
            status->rect[i].bottom = status->rect[i].top + status->height;
        }
        status->direction = direction;
    
    }

(c) 初期化処理

初期化用関数を呼び出す際、「向きの数」「向きの初期値」を与える。

/* 自キャラ(カーソルキーで移動) */
StatusInit(&MyChara, 0, 0, 40, 40, 310, 230, 1, 1, 5, 0);
/* ゴキブリ(動かない) */
StatusInit(&Goki, 64, 394, 96, 426, 400, 300, 0, 0, 1, 0);

(d) 方向別にキャラクタ画像を表示

画像表示時、方向の数値により、表示する矩形を切り替える。

/* 天使 */
hRet = g_pDDSBack->BltFast(MyChara.x, MyChara.y, g_pDDSChara, &MyChara.rect[MyChara.direction], DDBLTFAST_SRCCOLORKEY);
if (hRet != DD_OK)
    return;

/* ゴキブリ */
hRet = g_pDDSBack->BltFast(Goki.x, Goki.y, g_pDDSChara, &Goki.rect[Goki.direction], DDBLTFAST_SRCCOLORKEY);
if (hRet != DD_OK)
    return;

(e) 向きの情報を格納する

カーソルキーの状態によって向きを変えるため、移動処理を次のように修正する。

//-----------------------------------------------------------------------------
// 関数名 : CharMove()
// 機能概要: キャラクタ移動処理
//-----------------------------------------------------------------------------
static void CharMove(void)
{
    /* 天使の移動 */
    MyChara.direction = 0;

    if (KeyTbl[VK_LEFT] & 0x80)
    {
        MyChara.x -= MyChara.move_x;
        MyChara.direction = 1;
    }
    if (KeyTbl[VK_RIGHT] & 0x80)
    {
        MyChara.x += MyChara.move_x;
        MyChara.direction = 2;
    }
    if (KeyTbl[VK_UP] & 0x80)
    {
        MyChara.y -= MyChara.move_y;
        MyChara.direction = 3;
    }
    if (KeyTbl[VK_DOWN] & 0x80)
    {
        MyChara.y += MyChara.move_y;
        MyChara.direction = 4;
    }

}

■6-2確認1

(a) 〜 (e)全ての修正を行い、正常に動くかどうかを確認する。

[ 実行結果サンプル ]

(f) ちょっとしたテクニック

方向別に表示画像が変わるようにはなったが、1がLEFTであるとか3がUPであるなど、数値に意味をつけて覚えるのは分かりずらい。そこで、enum型を利用して次のように改造すると、見やすくなる。

  1. common.hを次のように修正
    //-----------------------------------------------------------------------------
    // 列挙型
    //-----------------------------------------------------------------------------
    /* キャラクタの向きを格納する変数の型 */
    typedef enum tarKEYSTATE {
        STOP, LEFT, RIGHT, UP, DOWN
    } KEYSTATE;
    
    //-----------------------------------------------------------------------------
    // 構造体
    //-----------------------------------------------------------------------------
    /* キャラクタ・ステータス情報 */
    typedef struct tarSTATUS {
        int x, y;            // 表示座標
        RECT rect[5];        // 画像の矩形
        RECT hitrect;        // 当たり判定用の矩形
        int move_x, move_y;  // 移動量
        int width, height;   // キャラクタの幅と高さ
        KEYSTATE direction; // 現在向いている方向(0:STOP 1:LEFT 2:RIGHT 3:UP 4:DOWN)
    } STATUS;
  2. Game.cppのキャラクタ情報初期化関数の最後の引数のデータ型をintからKEYSTATEに変更(プロトタイプ宣言と関数本体)
    //-----------------------------------------------------------------------------
    // プロトタイプ宣言(ソース内でしか使わないもの)
    //-----------------------------------------------------------------------------
    static void StatusInit(STATUS*, int, int, int, int, int, int, int, int, int ,KEYSTATE);
    
    
    
    //-----------------------------------------------------------------------------
    // 関数名 : StatusInit()
    // 機能概要: ステータス情報初期化
    //-----------------------------------------------------------------------------
    static void StatusInit(
        STATUS *status,                            // キャラクタ構造体変数のポインタ
        int left, int top, int right, int bottom,  // 画像の矩形(一枚目)
        int x, int y,                              // 表示座標の初期値
        int move_x, int move_y,                    // 移動量の初期値
        int direction_count, KEYSTATE direction    // 方向の数と、方向の初期値
    )
    
  3. 初期化で与える値を、0からSTOPに変更(自キャラと敵キャラ)
    void GameInit(HWND hWnd)
    {
        //------------------------------------------------------- 各変数の初期化
            /* 自キャラ(カーソルキーで移動) */
            StatusInit(&MyChara, 0, 0, 40, 40, 310, 230, 1, 1, 5, STOP);
            /* ゴキブリ(動かない) */
            StatusInit(&Goki, 64, 394, 96, 426, 400, 300, 0, 0, 1, STOP);
    
  4. CharaMove関数で、0〜4をそれぞれ「STOP」「LEFT」「RIGHT」「UP」「DOWN」に変更
    //-----------------------------------------------------------------------------
    // 関数名 : CharMove()
    // 機能概要: キャラクタ移動処理
    //-----------------------------------------------------------------------------
    static void CharMove(void)
    {
        /* 天使の移動 */
        MyChara.direction = STOP;
    
        if (KeyTbl[VK_LEFT] & 0x80)
        {
            MyChara.x -= MyChara.move_x;
            MyChara.direction = LEFT;
        }
        if (KeyTbl[VK_RIGHT] & 0x80)
        {
            MyChara.x += MyChara.move_x;
            MyChara.direction = RIGHT;
        }
        if (KeyTbl[VK_UP] & 0x80)
        {
            MyChara.y -= MyChara.move_y;
            MyChara.direction = UP;
        }
        if (KeyTbl[VK_DOWN] & 0x80)
        {
            MyChara.y += MyChara.move_y;
            MyChara.direction = DOWN;
        }

これにより、0〜4の数値ではなく、STOP、LEFTなど意味のある言葉でプログラムできる。マクロ文(#define)でも同様に記述できるが、マクロは1行で1つの値しか指定できないため、行が無駄に増える。

※enum型についてはC言語教科書176ページを参照

■6-2確認2

(f) のようにプログラムを修正し、正常に動くかどうかを確認する。

■補足

方向別の矩形は一定の規則によって並んでいるため、方向別の矩形を配列に持たなくても、方向の数値から計算で求めることができる。
例えば静止画像の矩形がMyChar.rectに、方向がMyChar.directionにそれぞれ格納されている場合、描画時に表示するべき矩形を求めれば、配列を持つことなく表示できる。

【描画時に計算する例】

RECT rc;
rc.top = MyChar.top + MyChar.height * MyChar.direction;
rc.left = MyChar.left;
rc.bottom = rc.top + MyChar.height;
rc.right = MyChar.right;

hRet = g_pDDSBack->BltFast(MyChara.x, MyChara.y, g_pDDSChara, &rc, DDBLTFAST_SRCCOLORKEY);

しかし、この方法は描画処理が行われるたびに計算を行わなければならないため、CPUに負荷をかけてしまうことになる。ゲームループのたびに計算させていたのではスピードが落ちてしまう可能性があるので、ゲームループ中はなるべく余計な計算をさせないようなプログラミングを心がけるべきである。
ただし、配列を多く持たなければならないため、メモリは余計に消費してしまう。


[ TOP ]