★宝探しゲーム ゲームシーン1★


続いてゲームシーンを作りましょう。

画像はタイトルシーンで追加してあり、解放していませんのでそのまま使えます。


フィールドの作成と描画


宝探しゲーム」は2次元配列でフィールドを作り、その中に設置された「宝」を探すゲームでした。

まずは「SceneGame.h」を開き、privateに2次元配列を追加します。

private:

    enum { ROW = 10, COL = 10, };
    int m_field[ROW][COL];
};

次に「SceneGame.cpp」を開き、「Start関数」でフィールドを初期化します。

//=============================================================================
// シーンの実行時に1度だけ呼び出される開始処理関数
//=============================================================================
void SceneGame::Start()
{
    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; j++) {
            m_field[i][j] = 0;
        }
    }
}

「0」で初期化しましたが、これは「選択していない状態」と言う事です。

と言う説明をしなければならないので、定数化しましょう。

今回は状態を3つに分けます。

0 未選択

1 選択済み

2 宝

「SceneGame.h」を開き、privateに列挙体を追加します。

private:

    enum {
        UNCHECKED, //未選択
        CHECKED,   //選択済み
        TREASURE,  //宝
    };

    enum { ROW = 10, COL = 10, };
    int m_field[ROW][COL];
};

「SceneGame.cpp」の「Start関数」も修正します。

//=============================================================================
// シーンの実行時に1度だけ呼び出される開始処理関数
//=============================================================================
void SceneGame::Start()
{
    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; j++) {
            m_field[i][j] = UNCHECKED;
        }
    }
}

フィールドの状態を描画したいと思いますので画像を確認します。

ぴぽや」さんからお借りした、マップチップ用画像です。

左が「未選択」、真ん中が「選択済み」、右が「宝」となっています。

画像サイズは、全て「32×32ピクセル」です。

フィールドの要素の値を元に転送元座標を計算して転送します。


転送元座標の計算


基本はトランプと同じです。

フィールド画像左座標
未選択0
選択済み32
64

この表を元に計算式を組み立てると、

  左座標 = フィールドの値 × チップの幅(32)

となります。

「SceneGame.h」に定数を追加しますが、今回はチップの幅なども定数にしてみます。

private:

    const int CHIP_X;      //チップの左端の座標
    const int CHIP_Y;      //チップの上の座標
    const int CHIP_WIDTH;  //チップの幅
    const int CHIP_HEIGHT; //チップの高さ

    enum {
        UNCHECKED,
        CHECKED,
        TREASURE,
    };

    enum { ROW = 10, COL = 10, };
    int m_field[ROW][COL];
};

「SceneGame.cpp」を開き、コンストラクタ関数で初期値を設定します。

//=============================================================================
// コンストラクタ
// 引 数:Engine* エンジンクラスのアドレス
//=============================================================================
SceneGame::SceneGame(Engine *pEngine) : Scene(pEngine)
    , CHIP_X(0)
    , CHIP_Y(480)
    , CHIP_WIDTH(32)
    , CHIP_HEIGHT(32)
{

}

「Draw関数」で転送元座標の設定だけ追加します。

//=============================================================================
// シーンの実行時に繰り返し呼び出される描画処理関数
//=============================================================================
void SceneGame::Draw()
{
    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; j++) {

            SetRect(&m_sour,
                CHIP_X + m_field[i][j] * CHIP_WIDTH,
                CHIP_Y,
                CHIP_X + m_field[i][j] * CHIP_WIDTH + CHIP_WIDTH,
                CHIP_Y + CHIP_HEIGHT);
        }
    }
}

定数に慣れていない場合、一見ややこしくなったように見えるかも知れません。

左座標の計算式を値に直してみると、

0 + m_field[i][j] * 32

です。

なぜ、わざわざ「0」を定数にして足しているかと言うと、元画像の配置が変わった時のためです。

様々な変更があったとしても、出来るだけ少ない修正で済むように考えておきましょう。


転送先座標の設定


次は転送先です。

とりあえず↓のように、画面の左上にフィールドを表示したいと思います。

C言語編でも何度もこのようなフィールドを表示してきました。

チップの幅と高さも分かっていますので、そこまで難しい事ではありません。

転送先の画像を図にしてみます。

左座標はチップの幅(32ピクセル)、上座標はチップの高さ(32ピクセル)ずつ増えています。

これを2重ループの添え字を使って計算します。

「Draw関数」に転送先座標と「Blt関数」を追加しましょう。

//=============================================================================
// シーンの実行時に繰り返し呼び出される描画処理関数
//=============================================================================
void SceneGame::Draw()
{
    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; j++) {

            SetRect(&m_sour,
                CHIP_X + m_field[i][j] * CHIP_WIDTH,
                CHIP_Y,
                CHIP_X + m_field[i][j] * CHIP_WIDTH + CHIP_WIDTH,
                CHIP_Y + CHIP_HEIGHT);

            SetRect(&m_dest, 
                j * CHIP_WIDTH,
                i * CHIP_HEIGHT,
                j * CHIP_WIDTH + CHIP_WIDTH, 
                i * CHIP_HEIGHT + CHIP_HEIGHT);

            m_pEngine->Blt(&m_dest, TEXTURE_TREASURE, &m_sour);
        }
    }
}

左座標は「j * 幅」、上座標は「i * 高さ」で変化させながら転送します。

実行して確認してください。

<実行結果 クライアント領域のみ>


領域の選択


ここからはマウスを使って、フィールドを選択出来るようにします。

これも「数当て」の時にやった事の応用で出来ます。

まずは、マウスの左ボタンを押したかどうか調べ、押していればマウスの座標を取得します。

「Update関数」に書きましょう。

//=============================================================================
// シーンの実行時に繰り返し呼び出される更新処理関数
//=============================================================================
void SceneGame::Update()
{
    if (m_pEngine->GetMouseButtonSync(DIK_LBUTTON)) {

        POINT point = m_pEngine->GetMousePosition();
    }
}

次にフィールドが表示されいている範囲かどうか調べます。

範囲を数値で表すと、

座標数値
0
0
320
320

ですが、右は「チップの幅 × 横方向のチップ数」つまり

CHIP_WIDTH * COL

ですし、左は「チップの高さ × 縦方向のチップ数」

CHIP_HEIGHT * ROW

と定数で表す事が出来ます。

では、「Update関数」に追加しましょう。

//=============================================================================
// シーンの実行時に繰り返し呼び出される更新処理関数
//=============================================================================
void SceneGame::Update()
{
    if (m_pEngine->GetMouseButtonSync(DIK_LBUTTON)) {

        POINT point = m_pEngine->GetMousePosition();

        if (point.x >= 0 && point.x < CHIP_WIDTH * COL && point.y >= 0 && point.y < CHIP_HEIGHT * ROW) {

        }
    }
}

まだ動きが無いので、実行しても何も変わりません。


選択のための座標変換


「数当て」の時と同じく、取得したマウス座標から計算式を使って座標を変換します。

今回は、マウス座標から2次元配列の添え字を求めなければなりません。

くどいですが横方向の対応を表にしてみましょう。

point.xの範囲欲しい添え字
0 〜 310
32 〜 631
64 〜 952
96 〜 1273
128 〜 1594
160 〜 1915
192 〜 2236
224 〜 2557
256 〜 2878
288 〜 3199

前と同じですが、次の式で添え字が求められます。

  point.x / 32

整数として計算すれば、小数点以下は切り捨てられます。

縦方向も同じですから、「Update関数」にクリックした箇所を「選択済み」にするコードを追加しましょう。

//=============================================================================
// シーンの実行時に繰り返し呼び出される更新処理関数
//=============================================================================
void SceneGame::Update()
{
    if (m_pEngine->GetMouseButtonSync(DIK_LBUTTON)) {

        POINT point = m_pEngine->GetMousePosition();

        if (point.x >= 0 && point.x < CHIP_WIDTH * COL && point.y >= 0 && point.y < CHIP_HEIGHT * ROW) {

            int x = point.x / CHIP_WIDTH;
            int y = point.y / CHIP_HEIGHT;

            m_field[y][x] = CHECKED;
        }
    }
}

実行してフィールドをクリックしてみましょう。

<実行結果 クライアント領域のみ>

クリックしたところに「穴」が開いていけば成功です。


宝の設定


次は「宝」を設定します。

C言語編の時は1つだけセットしましたが、冷静に見ると100分の1個ですよね・・・

正直途中で飽きてしまいます。

と言う訳で、今回は「宝」を10個に増やしましょう。

まずは「宝」の個数を定数化します。

「SceneGame.h」を開き、privateに定数を追加します。

private:

    const int CHIP_X;
    const int CHIP_Y;
    const int CHIP_WIDTH;
    const int CHIP_HEIGHT;

    const int TREASURE_MAX; //宝の個数

    enum {
        UNCHECKED,
        CHECKED,
        TREASURE,
    };

    enum { ROW = 10, COL = 10, };
    int m_field[ROW][COL];
};

「SceneGame.cpp」を開きコンストラクタ関数で初期値を入れます。

//=============================================================================
// コンストラクタ
// 引 数:Engine* エンジンクラスのアドレス
//=============================================================================
SceneGame::SceneGame(Engine *pEngine) : Scene(pEngine)
    , CHIP_X(0)
    , CHIP_Y(480)
    , CHIP_WIDTH(32)
    , CHIP_HEIGHT(32)
    , TREASURE_MAX(10)
{

}

「Start関数」で乱数を使って「宝」をセットしましょう。

//=============================================================================
// シーンの実行時に1度だけ呼び出される開始処理関数
//=============================================================================
void SceneGame::Start()
{
    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; j++) {
            m_field[i][j] = UNCHECKED;
        }
    }

    for (int i = 0; i < TREASURE_MAX; i++) {

        int idxX = rand() % COL;
        int idxY = rand() % ROW;

        m_field[idxY][idxX] = TREASURE;
    }
}

10回繰り返しながら、乱数で縦横の添え字を計算し、「宝」をセットしました。

とりあえず、実行してみましょう。

<実行結果 クライアント領域のみ>

色々とマズイところがありますね・・・

分かりやすいのは「宝が見えている」と言う事です。

分かりづらいのは「宝が9個しかない」と言う事です。

※常に9個ではなく、何度か実行すると10個では無い時があります。


問題点の対応


先に「宝が9個しかない」方を対処します。

原因は「同じ乱数が発生し、宝がセット済みの場所に宝をセットしている」事です。

重複しないように、宝がセットされているかどうかを確認するように変更します。

//=============================================================================
// シーンの実行時に1度だけ呼び出される開始処理関数
//=============================================================================
void SceneGame::Start()
{
    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; j++) {
            m_field[i][j] = UNCHECKED;
        }
    }

    int idxX;
    int idxY;

    for (int i = 0; i < TREASURE_MAX; i++) {

        do {

            idxX = rand() % COL;
            idxY = rand() % ROW;

        } while (m_field[idxY][idxX] == TREASURE);

        m_field[idxY][idxX] = TREASURE;
    }
}

do〜while文を使う事で対応しました。

実行して確認してください。

<実行結果 クライアント領域のみ>


もう1つの問題「宝が見えている」への対応は色々考える必要があります。

C言語編の時は「宝」であった場合は「未選択」を表示する事で対応していました。

ルールによっては、この方法が使えないケースもありますので、ルールをはっきりさせましょう。

「宝」を1つ見つければクリア!では面白くないので、「宝」を5個見つけるまでに、何回クリックしたかを競う事にしましょう。

このルールの場合、「宝」を5個見つけるまで終わらないので「見つけた宝」は表示し続ける必要があります。

「見つけた宝」と「見つかっていない宝」を区別する方法が無ければ、↑のような事は出来ません。

フィールドを構造体にして、メンバ変数を増やす事で対応してみましょう。


「SceneGame.h」を開き、privateで構造体を宣言し、これまでのフィールド配列を変更します。

private:

    const int CHIP_X;
    const int CHIP_Y;
    const int CHIP_WIDTH;
    const int CHIP_HEIGHT;

    const int TREASURE_MAX;

    enum {
        UNCHECKED,
        CHECKED,
        TREASURE,
    };

    //フィールド構造体
    struct Field {
        int m_status;     //状態
        bool m_bTreasure; //宝フラグ
    };

    enum { ROW = 10, COL = 10, };
    Field m_field[ROW][COL];
};

これによって「SceneClear.cpp」にエラーが発生していますので直しましょう。

「Start関数」から直します。

//=============================================================================
// シーンの実行時に1度だけ呼び出される開始処理関数
//=============================================================================
void SceneGame::Start()
{
    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; j++) {
            m_field[i][j].m_status = UNCHECKED;
            m_field[i][j].m_bTreasure = false;
        }
    }

    int idxX;
    int idxY;

    for (int i = 0; i < TREASURE_MAX; i++) {

        do {

            idxX = rand() % COL;
            idxY = rand() % ROW;

        } while (m_field[idxY][idxX].m_bTreasure);

        m_field[idxY][idxX].m_bTreasure = true;
    }
}

宝フラグには最初に「false」を入れておきます。

その後、乱数を使って、10カ所だけ「true」に変えます。

「Update関数」も直します。

//=============================================================================
// シーンの実行時に繰り返し呼び出される更新処理関数
//=============================================================================
void SceneGame::Update()
{
    if (m_pEngine->GetMouseButtonSync(DIK_LBUTTON)) {

        POINT point = m_pEngine->GetMousePosition();

        if (point.x >= 0 && point.x < CHIP_WIDTH * COL && point.y >= 0 && point.y < CHIP_HEIGHT * ROW) {

            int x = point.x / CHIP_WIDTH;
            int y = point.y / CHIP_HEIGHT;

            m_field[y][x].m_status = CHECKED;
        }
    }
}

最後は「Draw関数」です。

//=============================================================================
// シーンの実行時に繰り返し呼び出される描画処理関数
//=============================================================================
void SceneGame::Draw()
{
    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; j++) {

            SetRect(&m_sour,
                CHIP_X + m_field[i][j].m_status * CHIP_WIDTH,
                CHIP_Y,
                CHIP_X + m_field[i][j].m_status * CHIP_WIDTH + CHIP_WIDTH,
                CHIP_Y + CHIP_HEIGHT);

            SetRect(&m_dest, 
                j * CHIP_WIDTH,
                i * CHIP_HEIGHT,
                j * CHIP_WIDTH + CHIP_WIDTH, 
                i * CHIP_HEIGHT + CHIP_HEIGHT);

            m_pEngine->Blt(&m_dest, TEXTURE_TREASURE, &m_sour);
        }
    }
}

とりあえず実行してみましょう。

<実行結果 クライアント領域のみ>

まだ「宝」は表示されませんが、動作する事だけ確認してください。


残りは次回に続きます。


次へ

戻る

目次へ