★スライドパズル ゲームシーン1★


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

タイトルシーンで「難易度」選択をしましたが、「難易度」によってシーンを変えるのは得策とは言えません。

「3×3の9分割」でも「4×4の16分割」でも動作するように工夫して作っていきます。

まずは、画像を分割するための準備を始めましょう。


画像の分割


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

private:

    const int PUZZLE_WIDTH;  //元画像の幅
    const int PUZZLE_HEIGHT; //元画像の高さ

    int m_panelWidth;  //パネル1枚の幅
    int m_panelHeight; //パネル1枚の高さ
};

「SceneGame.cpp」のコンストラクタ関数で定数の初期値を代入しましょう。

//=============================================================================
// コンストラクタ
// 引 数:Engine* エンジンクラスのアドレス
//=============================================================================
SceneGame::SceneGame(Engine *pEngine) : Scene(pEngine)
    , PUZZLE_WIDTH(480)
    , PUZZLE_HEIGHT(480)
{

}

「Start関数」でパネル1枚の幅と高さを計算します。

//=============================================================================
// シーンの実行時に1度だけ呼び出される開始処理関数
//=============================================================================
void SceneGame::Start()
{
    m_panelWidth = PUZZLE_WIDTH / m_gameData.m_split;
    m_panelHeight = PUZZLE_HEIGHT / m_gameData.m_split;
}

「m_gameData.m_split」が「3」の場合

  480 / 3 = 160

になり、「4」の場合、

  480 / 4 = 120

となります。

※3でも4でも割り切れるよう、幅も高さも「480ピクセル」にしてあります。


分割した画像の転送


実際には1枚の画像ですが、上で計算したパネルの幅と高さを使って分割転送したいと思います。

そのためには、色々と工夫が必要になります。

「3×3の9分割」の場合、↓のようなパネルに分割されます。

イメージに書いてある通り、それぞれのパネルに番号を付けます。

この番号を元にパネルの転送元座標を計算して転送します。


「3×3」の場合は「9個」、「4×4」の場合は「16個」の領域が必要になりますので、「new」を使って領域を確保します。

イメージは2次元配列ですが、「new」や「malloc」を使って領域を確保するのに2次元配列は面倒なので、1次元配列を2次元配列のように使う方法を使います。

「SceneGame.h」を開き、領域確保のためのポインタを追加します。

private:

    const int PUZZLE_WIDTH;  //元画像の幅
    const int PUZZLE_HEIGHT; //元画像の高さ

    int m_panelWidth;  //パネル1枚の幅
    int m_panelHeight; //パネル1枚の高さ

    int *m_pField; //フィールド
};

「SceneGame.cpp」のコンストラクタ関数で初期値を代入します。

//=============================================================================
// コンストラクタ
// 引 数:Engine* エンジンクラスのアドレス
//=============================================================================
SceneGame::SceneGame(Engine *pEngine) : Scene(pEngine)
    , PUZZLE_WIDTH(480)
    , PUZZLE_HEIGHT(480)
    , m_pField(NULL)
{

}

「Start関数」で領域を確保し、「0」からの連番を代入します。

//=============================================================================
// シーンの実行時に1度だけ呼び出される開始処理関数
//=============================================================================
void SceneGame::Start()
{
    m_panelWidth = PUZZLE_WIDTH / m_gameData.m_split;
    m_panelHeight = PUZZLE_HEIGHT / m_gameData.m_split;

    int size = m_gameData.m_split * m_gameData.m_split;

    m_pField = new int[size];

    for (int i = 0; i < size; i++) {
        m_pField[i] = i;
    }
}

領域確保をした場合エラーチェックが必要でしたが、「例外」を使っていますのでとりあえず大丈夫です。

ただ、解放コードは書いておかないと「メモリリーク」が発生しますので、「Exit関数」に解放コードを追加します。

//=============================================================================
// シーンの終了時に呼び出される終了処理関数
//=============================================================================
void SceneGame::Exit()
{
    if (m_pField) {
        delete[] m_pField;
        m_pField = NULL;
    }
}

これで↓のイメージ通りの配列が出来上がりました。


この番号から画像を転送する方法を説明します。

番号と転送元座標の関係を表にしてみましょう。

「3×3の9分割」のケースです。

番号左座標上座標
00
1600
3200
0160
160160
320160
0320
160320
320320

基本はトランプの番号とスートからカードを選ぶ方法と変わりません。

左座標は、

  番号 % 分割数 * パネルの幅

で計算出来ますし、上座標は、

  番号 / 分割数 * パネルの高さ

で計算出来ます。

番号左座標上座標
0 % 3 * 160 = 00 / 3 * 160 = 0
1 % 3 * 160 = 1601 / 3 * 160 = 0
2 % 3 * 160 = 3202 / 3 * 160 = 0
3 % 3 * 160 = 03 / 3 * 160 = 160
4 % 3 * 160 = 1604 / 3 * 160 = 160
5 % 3 * 160 = 3205 / 3 * 160 = 160
6 % 3 * 160 = 06 / 3 * 160 = 320
7 % 3 * 160 = 1607 / 3 * 160 = 320
8 % 3 * 160 = 3208 / 3 * 160 = 320

この計算式を元に画像を転送しますので「Draw関数」にコードを追加しましょう。

転送先座標は、前回の宝探しと同じように考えます。

また、転送する際に「Blt関数」に渡す画像ファイル名定数は、タイトルで共用データに入れてあるはずです。

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

            SetRect(&m_sour,
                (m_pField[i * m_gameData.m_split + j] % m_gameData.m_split) * m_panelWidth,
                (m_pField[i * m_gameData.m_split + j] / m_gameData.m_split) * m_panelHeight,
                (m_pField[i * m_gameData.m_split + j] % m_gameData.m_split) * m_panelWidth + m_panelWidth,
                (m_pField[i * m_gameData.m_split + j] / m_gameData.m_split) * m_panelHeight + m_panelHeight);

            SetRect(&m_dest,
                j * m_panelWidth,
                i * m_panelHeight,
                j * m_panelWidth + m_panelWidth,
                i * m_panelHeight + m_panelHeight);

            m_pEngine->Blt(&m_dest, m_gameData.m_strGraphicName, &m_sour);
        }
    }
}

実行してみてください。

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


シャッフルしてみる


このプログラムがちゃんと動くかどうかシャッフルしてみましょう。

「Update関数」にシャッフルのコードを追加しますが、まずはトランプの時と同じようにシャッフルしてみます。

//=============================================================================
// シーンの実行時に1度だけ呼び出される開始処理関数
//=============================================================================
void SceneGame::Start()
{
    m_panelWidth = PUZZLE_WIDTH / m_gameData.m_split;
    m_panelHeight = PUZZLE_HEIGHT / m_gameData.m_split;

    int size = m_gameData.m_split * m_gameData.m_split;

    m_pField = new int[size];

    for (int i = 0; i < size; i++) {
        m_pField[i] = i;
    }

    int idx1, idx2;

    int work;

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

        idx1 = rand() % size;
        idx2 = rand() % size;

        work = m_pField[idx1];
        m_pField[idx1] = m_pField[idx2];
        m_pField[idx2] = work;
    }
}

実行してみましょう。

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

出来ているようです。


シャッフル出来ているように見えますが、ランダムにシャッフルした場合ゲームをクリア出来ないケースが出てくる事があります。

スライドパズルの場合、元の形からスライドさせてシャッフルする事でクリア出来ない状況を回避出来ますので、そのように作り直しましょう。

シャッフル回数も定数化しますので、「SceneGame.h」を開き定数を追加します。

private:

    const int PUZZLE_WIDTH;  //元画像の幅
    const int PUZZLE_HEIGHT; //元画像の高さ

    const int SHUFFLE; //シャッフル回数

    int m_panelWidth;  //パネル1枚の幅
    int m_panelHeight; //パネル1枚の高さ

    int *m_pField; //フィールド
};

「SceneGame.cpp」のコンストラクタ関数で初期値を代入します。

//=============================================================================
// コンストラクタ
// 引 数:Engine* エンジンクラスのアドレス
//=============================================================================
SceneGame::SceneGame(Engine *pEngine) : Scene(pEngine)
    , PUZZLE_WIDTH(480)
    , PUZZLE_HEIGHT(480)
    , SHUFFLE(100)
    , m_pField(NULL)
{

}

適当に「100回」にしました。


さて、シャッフルですが、きちんとスライドさせて配置を変えるため色々考えてみます。

スライドさせるとは、空いている場所へパネルを移動させる事です。

そこで、空いている場所を決めます。

番号が「0」の箇所を「空いている場所」としますので、「Draw関数」にコードを追加します。

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

            if (m_pField[i * m_gameData.m_split + j] != 0) {

                SetRect(&m_sour,
                    (m_pField[i * m_gameData.m_split + j] % m_gameData.m_split) * m_panelWidth,
                    (m_pField[i * m_gameData.m_split + j] / m_gameData.m_split) * m_panelHeight,
                    (m_pField[i * m_gameData.m_split + j] % m_gameData.m_split) * m_panelWidth + m_panelWidth,
                    (m_pField[i * m_gameData.m_split + j] / m_gameData.m_split) * m_panelHeight + m_panelHeight);

                SetRect(&m_dest,
                    j * m_panelWidth,
                    i * m_panelHeight,
                    j * m_panelWidth + m_panelWidth,
                    i * m_panelHeight + m_panelHeight);

                m_pEngine->Blt(&m_dest, m_gameData.m_strGraphicName, &m_sour);
            }
        }
    }
}

実行してみます。

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

1箇所空きました。


初期状態は左上のパネルが「0」番ですので、そこを起点に乱数を使ってシャッフルします。

「スライド」させるとは、空の場所と隣のパネルを「入れ替える」事で実現できます。

空の場所を起点に「上下左右」をランダムで決定し、入れ替えるプログラムを作ります。

配列の添え字を超えないように、色々と配慮したプログラムを書きますので、よく見て考えてみてください。

//=============================================================================
// シーンの実行時に1度だけ呼び出される開始処理関数
//=============================================================================
void SceneGame::Start()
{
    m_panelWidth = PUZZLE_WIDTH / m_gameData.m_split;
    m_panelHeight = PUZZLE_HEIGHT / m_gameData.m_split;

    int size = m_gameData.m_split * m_gameData.m_split;

    m_pField = new int[size];

    for (int i = 0; i < size; i++) {
        m_pField[i] = i;
    }

    int x1, y1;
    int x2, y2;

    int work;

    int shuffle_num = m_gameData.m_split * SHUFFLE;

    x1 = y1 = x2 = y2 = 0;

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

        switch (rand() % 4) {

        case 0:
            if (y1 - 1 >= 0) {
                x2 = x1;
                y2 = y1 - 1;
            }
            break;

        case 1:
            if (x1 + 1 < m_gameData.m_split) {
                x2 = x1 + 1;
                y2 = y1;
            }
            break;

        case 2:
            if (y1 + 1 < m_gameData.m_split) {
                x2 = x1;
                y2 = y1 + 1;
            }
            break;

        case 3:
            if (x1 - 1 >= 0) {
                x2 = x1 - 1;
                y2 = y1;
            }
            break;
        }

        work = m_pField[y1 * m_gameData.m_split + x1];
        m_pField[y1 * m_gameData.m_split + x1] = m_pField[y2 * m_gameData.m_split + x2];
        m_pField[y2 * m_gameData.m_split + x2] = work;

        x1 = x2;
        y1 = y2;
    }
}

2次元配列の座標を見立てて「x1, y1, x2, y2」を用意しました。

シャッフル回数は、分割数にシャッフル回数を掛けていますが根拠はありません・・・

番号「0」の箇所を中心に「上下左右」をランダムに決めます。

配列の範囲外でなければ「x2, y2」に入れ替える座標を入れて、パネルを交換しています。

交換した場所が「空(番号が0)」になるので、最後に「x1, y1」に「x2, y2」を代入して繰り返します。

ややこしいかも知れませんが、トレースしてアルゴリズムを理解してください。

では、実行してみましょう。

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

「見た目」は変わっていませんが、ちゃんとクリア出来るようになっています。

続きは次回にしましょう。


次へ

戻る

目次へ