第7章 キー操作を制御する

7−4 キーを押している間、一定間隔で弾を撃つには?

現在の自キャラは、画面上に1発だけ弾を撃つことができる。では、複数の弾を撃つにはどうすればよいだろうか?例えばスペースキーを押すと弾が出て、画面上に表示できる弾の数を最大5個にする場合、次のようになる。

  1. 弾の構造体変数を要素数5の配列にし、初期化する。
  2. スペースキーを押したとき、lifeが0である弾を配列から探し、その弾を発射する。すべての弾のlifeが1の場合、何もしない(5つ以上の弾は発射できない)。
  3. 画面からはみ出した弾のlifeを0にし、再利用する。

このようにプログラムを修正する。

(a) 自ショット用の配列を宣言し、初期化処理を修正する

まず、common.hに、弾の最大数を宣言する。

#define SHOT_MAX 5

次に、game.cppで宣言している自ショット用構造体変数を配列として宣言する。

static STATUS MyShot[SHOT_MAX]; // 自ショット

次に、自ショットの初期化処理を次のように修正する。

//-----------------------------------------------------------------------------
// 関数名 : GameInit()
// 機能概要: スタート処理初期化
//-----------------------------------------------------------------------------
void GameInit(HWND hWnd)
{
    int     i;

    //------------------------------------------------------- 各変数の初期化
    /* 自キャラ(カーソルキーで移動) */
    StatusInit(&MyChar, 0, 0, 40, 40, 310, 230, 2, 2, 5, STOP, nowTickCount, 5, 0, 1);
    /* 自ショット(スペースキーで発射、最大SHOT_MAX発) */
    StatusInit(&MyShot[0], 392, 64, 398, 80, 0, 0, 0, 5, 1, STOP, nowTickCount, 1, 0, 0);
    for (i=1 ; i<SHOT_MAX ; i++)
        MyShot[i] = MyShot[0];

    //------------------------------------------------------- フレームナンバーセット
    g_FrameNo = GAME_READY;

}

※for文を0から回してStatusInit関数を毎回呼び出すと(理論上は)処理が微妙に重くなるため、構造体変数同士で値をコピーしている。

(b) 自ショットの描画処理を修正する

自ショットのうち、lifeが1であるものを描画するよう、ScreenOut関数を修正する。

//-----------------------------------------------------------------------------
// 関数名 : ScreenOut()
// 機能概要: ゲーム画像描画処理
//-----------------------------------------------------------------------------
static void ScreenOut(void)
{
    HRESULT hRet;
    int i;

    /* ゲーム画面用背景 */
    hRet = g_pDDSBack->BltFast(0, 0, g_pDDSGame, &ScreenRect, DDBLTFAST_NOCOLORKEY);
    if (hRet != DD_OK)
        return;

    /* 自キャラ */
    hRet = g_pDDSBack->BltFast(MyChar.x, MyChar.y, g_pDDSChara, 
                               &MyChar.rect[MyChar.direction][MyChar.AnimeNo], DDBLTFAST_SRCCOLORKEY);
    if (hRet != DD_OK)
        return;

    /* 自ショット */
    for (i=0 ; i<SHOT_MAX ; i++)
    {
        if (MyShot[i].life > 0)
        {
            hRet = g_pDDSBack->BltFast(MyShot[i].x, MyShot[i].y, g_pDDSChara, 
                              &MyShot[i].rect[MyShot[i].direction][MyShot[i].AnimeNo], DDBLTFAST_SRCCOLORKEY);
           if (hRet != DD_OK)
               return;
        }
    }

}

(c) 自ショットの移動処理を修正する

スペースキーを押したときに、使っていない配列(lifeが0)を探すよう、CharMove関数を修正する。

//-----------------------------------------------------------------------------
// 関数名 : CharMove()
// 機能概要: キャラクタ移動処理
//-----------------------------------------------------------------------------
static void CharMove(void)
{
    int i;
  ・
  ・
  ・
    /* 自ショットの発射 */
    if (KeyTbl[VK_SPACE] & 0x80)
    {
        for (i=0 ; i<SHOT_MAX ; i++)
        {
            if (MyShot[i].life == 0)
            {
                MyShot[i].life = 1;
                MyShot[i].x = MyChar.x;
                MyShot[i].y = MyChar.y - MyShot[i].height;
                break;
            }
        }
    }

    /* 自ショットの移動 */
    for (i=0 ; i<SHOT_MAX ; i++)
    {
        if (MyShot[i].life > 0)
            MyShot[i].y -= MyShot[i].move_y;
    }

}

(d) 自ショットとゲーム画面との当たり判定を修正する

自ショットが配列になったので、それぞれに対してゲーム画面との当たり判定を行うよう、ScreenHitCheck関数を修正する。

//-----------------------------------------------------------------------------
// 関数名 : ScreenHitCheck()
// 機能概要: キャラクタがゲーム画面をはみ出している時の処理
//-----------------------------------------------------------------------------
static void ScreenHitCheck(void)
{
    int i;
  ・
  ・
  ・
    /* 自ショットとゲーム画面(画面全体)をチェック */
    for (i=0 ; i<SHOT_MAX ; i++)
    {
        if (MyShot[i].y < ScreenRect.top)
            MyShot[i].life = 0;
    }

}

7-4(a)〜7-4(d)確認(必須問題)

4-1〜4-4の修正を行い、自ショットが連続で撃てるかどうかを確認する。

[ 実行結果サンプル ]

問題点

4-1〜4-4の修正では、スペースキーが押されていれば自ショットを発射するため、ゲームループが回るたびに発射してしまう。実際の連射では、一定間隔(0.2秒ごとなど)に弾が発射されるようにしなければならない。

(e) 一定間隔で自ショットが発射されるよう修正する

一定間隔で自ショットを発射するには、前回ショットを発射してからの経過時間を調べ、一定の時間が経過したら新しい弾を発射するような仕組みを組み込めばよい。これは、カーソルの移動と同じ考え方で作成できる。

まず、common.hに、ショットの間隔を決めるマクロと、ショット発射後の経過時間を格納する変数を用意する。

//=============================================================================
// プロジェクト内で共通に読み込まれるヘッダ・ファイル
//=============================================================================
  ・
  ・
  ・
/* キャラクタ情報用 */
#define DIRECTION_MAX   5       // キャラクタの向き情報の最大数
#define ANIMATION_MAX   5       // キャラクタのアニメーション数の最大
#define ANIMATION_TIME  500     // キャラクタアニメーションの単位(ミリ秒)
#define READY_TIME              3000    // READY状態の時間(ミリ秒)
#define SHOT_MAX                5       // 画面上に表示できる弾の最大
#define SHOT_TIME       200     // キャラクタアニメーションの単位(ミリ秒)

//-----------------------------------------------------------------------------
// 列挙型
//-----------------------------------------------------------------------------
/* キャラクタの向きを格納する変数の型 */
typedef enum tarKEYSTATE {
    STOP, LEFT, RIGHT, UP, DOWN
} KEYSTATE;

//-----------------------------------------------------------------------------
// 構造体
//-----------------------------------------------------------------------------
/* キャラクタ・ステータス情報 */
typedef struct tarSTATUS {
    int x, y; // 表示座標
    RECT rect[DIRECTION_MAX][ANIMATION_MAX]; // 画像の矩形
    RECT hitrect; // 当たり判定用の矩形
    int move_x, move_y; // 移動量
    int width, height; // キャラクタの幅と高さ
    KEYSTATE direction; // 現在向いている方向(0:STOP 1:LEFT 2:RIGHT 3:UP 4:DOWN)
    DWORD AnimeTime; // アニメーション後経過時間
    int AnimeMax; // アニメーション数
    int AnimeNo; // 現在のアニメーション番号
    int life; // キャラクタの状態
    DWORD ShotTime; // ショット発射後経過時間
} STATUS;
  ・
  ・
  ・

キャラクタ構造体にメンバが増えたため、Game.cpp内のキャラクタデータ初期化関数を次のように修正する(プロトタイプ宣言も修正すること)。

//-----------------------------------------------------------------------------
// 関数名 : 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, // 方向の数と、方向の初期値
    DWORD AnimeTime, int AnimeMax, int AnimeNo, // 経過時間、最大表示数、表示番号
    int life, // キャラクタの状態
    DWORD ShotTime // ショット発射後経過時間
)
{
  ・
  ・
  ・
    // キャラクタの状態
    status->life = life;
    // ショット発射後経過時間
    status->ShotTime = ShotTime;

}

//-----------------------------------------------------------------------------
// プロトタイプ宣言(ソース内でしか使わないもの)
//-----------------------------------------------------------------------------
static void ScreenOut(void);
static void CharMove(void);
static void CharAnime(void);
static void ScreenHitCheck(void);
static void CharHitCheck(void);
static bool HitCheck(RECT, RECT);
static void StatusInit(STATUS*, int, int, int, int, int, int, int, int, int, KEYSTATE, DWORD, int, int, int, DWORD);

同様に、StatusInit関数を呼び出す処理を、次のように修正する。

//-----------------------------------------------------------------------------
// 関数名 : GameInit()
// 機能概要: スタート処理初期化
//-----------------------------------------------------------------------------
void GameInit(HWND hWnd)
{
    int i;

    //------------------------------------------------------- 各変数の初期化
    /* 自キャラ(カーソルキーで移動) */
    StatusInit(&MyChar, 0, 0, 40, 40, 310, 230, 2, 2, 5, STOP, nowTickCount, 5, 0, 1, 0);
    /* 自ショット(スペースキーで発射、最大SHOT_MAX発) */
    StatusInit(&MyShot[0], 392, 64, 398, 80, 0, 0, 0, 5, 1, STOP, nowTickCount, 1, 0, 0, 0);
  ・
  ・
  ・

}

あとは、一定時間経過(SHOT_TIME)後にスペースキーの判定を行うようにすればよい。CharMove関数を次のように修正する。

//-----------------------------------------------------------------------------
// 関数名 : CharMove()
// 機能概要: キャラクタ移動処理
//-----------------------------------------------------------------------------
static void CharMove(void)
{
  ・
  ・
  ・
    /* 自ショットの発射 */
    if ( nowTickCount - MyChar.ShotTime >= SHOT_TIME )
    {
        MyChar.ShotTime = nowTickCount;
        if (KeyTbl[VK_SPACE] & 0x80)
        {
            for (i=0 ; i<SHOT_MAX ; i++)
            {
                if (MyShot[i].life == 0)
                {
                    MyShot[i].life = 1;
                    MyShot[i].x = MyChar.x;
                    MyShot[i].y = MyChar.y - MyShot[i].height;
                    break;
                }
            }
        }
    }

    /* 自ショットの移動 */
    for (i=0 ; i<SHOT_MAX ; i++)
    {
        if (MyShot[i].life > 0)
            MyShot[i].y -= MyShot[i].move_y;
    }

}

※一定時間(SHOT_TIME)が経過するまで、スペースキーのチェックを行わないようになる。これにより、一定間隔での連射が可能になる。

確認(必須問題)

7-4(a) 〜 7-4(d)までのプログラムに7-4(e)の修正を行い、自ショットが0.2秒間隔で連射されるかどうかを確認する。

[ 実行結果サンプル ]


[ TOP ]