マインスイーパーはマウスの左右ボタンを同時に押すと周りのパネルが一気に開くという操作があります。
ただ、いつでもどこでも開くのではなく、きちんと条件があります。
1.0以外の数字のパネルの上で同時押し 2.そのパネルの周りに数字と同じ数の「旗」が立っている |
「旗」がきちんと爆弾のパネルに立てられていた場合、開いていなかった周りのパネルが開きます。
下の図のマウスカーソルの場所で、左右のボタンを同時押しすると、
閉じていた下の3枚のパネルが一気に開きます。
また↓のように、パネルの数字と「旗」の数が異なる場合は開きません。
さらに、爆弾では無い箇所に「旗」を立てていた場合、
同時押しをすると、ゲームオーバーになります。
「SceneGame.cpp」の「Update関数」に同時押しのコードを追加します。
//============================================================================= // シーンの実行時に繰り返し呼び出される更新処理関数 //============================================================================= void SceneGame::Update() { if (m_pEngine->GetMouseButton(DIK_LBUTTON) && m_pEngine->GetMouseButton(DIK_RBUTTON)) { POINT point = m_pEngine->GetMousePosition(); } if (m_pEngine->GetMouseButtonSync(DIK_LBUTTON)) { POINT point = m_pEngine->GetMousePosition(); if (m_mine.Open(point)) { m_gameData.m_bClear = false; m_nowSceneData.Set(Common::SCENE_EXIT, false, NULL); } } if (m_pEngine->GetMouseButtonSync(DIK_RBUTTON)) { POINT point = m_pEngine->GetMousePosition(); m_mine.SwitchFlag(point); } if (m_mine.IsClear()) { m_gameData.m_bClear = true; m_nowSceneData.Set(Common::SCENE_EXIT, false, NULL); } } |
ここでは、「GetMouseButtonSync関数」ではなく「GetMouseButton関数」を使っています。
「GetMouseButtonSync関数」は、ボタンを押した瞬間を判定します。
「瞬間」なので、きっちり同時に押さなければ反応しません。
「GetMouseButton関数」は、ボタンが押されているかどうかだけを調べますので、こちらを使います。
パネルを一気に開くための関数を追加します。
クラスも長くなってきたので、必要な関数のプロトタイプ宣言を一気に行います。
class Mine { public: Mine(); ~Mine(); //フィールドの初期化 void Initialize(const int row, const int col, const int bombMax); //フィールドの描画 void Draw(Engine *pEngine); //パネルを開く bool Open(const POINT &refPoint); //旗を切り替える void SwitchFlag(const POINT &refPoint); //クリアしたか? bool IsClear() const; //周りのパネルを開く bool OpenAround(const POINT &refPoint); private: const int ADJUST_X; //調整用左座標 const int ADJUST_Y; //調整用上座標 const int PANEL_SOUR_X; //パネルの転送元左座標 const int PANEL_SOUR_Y; //パネルの転送元上座標 const int HIDE_SOUR_X; //閉じた状態の転送元左座標 const int HIDE_SOUR_Y; //閉じた状態の転送元上座標 const int FLAG_SOUR_X; //旗の転送元左座標 const int FLAG_SOUR_Y; //旗の転送元上座標 const int PANEL_WIDTH; //パネルの幅 const int PANEL_HEIGHT; //パネルの高さ enum { NONE, //爆弾なし BOMB = 9, //爆弾 }; struct Field { int m_number; //周りにある爆弾の数(0〜8)、爆弾は9 bool m_bOpen; //オープンフラグ bool m_bFlag; //旗フラグ }; Field *m_pField; //フィールド int m_row; //縦方向のパネル数 int m_col; //横方向のパネル数 int m_bombMax; //爆弾の最大数 RECT m_sour; RECT m_dest; //フィールドのクリア void ClearField(); //爆弾のセット void SetBomb(); //爆弾の個数のセット void SetNumber(); //周りにある爆弾の数を数える int GetAroundBombNum(const int x, const int y); //フィールド外か? bool IsOutsideField(const POINT &refPoint) const; //隣接したパネルを開く(再帰) void OpenAdjacentPanel(const int x, const int y); //パネルが開かれている数字(1〜8)か? bool IsOpenNumber(const int index) const; //パネルの番号を取得する int GetNumber(const int index) const; //周り8パネルにある旗の数を数える int GetAroundFlagNum(const int x, const int y) const; //周りのパネルを開く bool OpenAround(const int x, const int y); }; |
publicメンバ関数として「OpenAround関数」を用意しました。
そこから呼び出されるprivateメンバ関数を4つ作ります。
「IsOpenNumber関数」は、「開かれているパネルで1〜8までの数字」のパネルかどうかを調べます。
「GetNumber関数」は、「パネルの数字を取得する」関数です。
「GetAroundFlagNum関数」は「周りにある旗の数を返す」関数です。
パネルの数字と旗の数が一致した場合「OpenAround関数」を呼び出します。
追加したpublicメンバ関数と同じ名前ですが、引数が異なるので問題ありません。
先にprivateメンバ関数から作っていきます。
では「Mine.cpp」の「OpenAdjacentPanel関数」の下に「IsOpenNumber関数」の本体を作ります。
引数の「index」は一次元配列の添え字で、呼び出し元の「OpenAround関数」で計算する予定です。
//パネルが開かれている数字(1〜8)か?
bool Mine::IsOpenNumber(const int index) const
{
if (m_pField[index].m_bOpen) {
if (m_pField[index].m_number > NONE && m_pField[index].m_number < BOMB) {
return true;
}
}
return false;
}
|
開いたパネルで、0(NONE)より大きく、9(BOMB)より小さければtrueを返します。
「IsOpenNumber関数」の下に「GetNumber関数」を追加します。
こちらも引数は「index」です。
//パネルの番号を取得する
int Mine::GetNumber(const int index) const
{
return m_pField[index].m_number;
}
|
「GetNumber関数」の下に「GetAroundFlagNum関数」を追加します。
引数の「x」「y」は2次元配列としての座標です。
これも「OpenAround関数」で計算して渡されるように作ります。
中身は「GetAroundBombNum関数」とほぼ同じです。
//周り8パネルにある旗の数を数える
int Mine::GetAroundFlagNum(const int x, const int y) const
{
int counter = 0;
for (int i = -1; i < 2; i++) {
for (int j = -1; j < 2; j++) {
int wx = x + j;
int wy = y + i;
if (wx >= 0 && wx < m_col && wy >= 0 && wy < m_row) {
if (m_pField[wy * m_col + wx].m_bFlag) {
counter++;
}
}
}
}
return counter;
}
|
「GetAroundFlagNum関数」の下に「OpenAround関数」を追加します。
これは「OpenAdjacentPanel関数」のような内容になりますが、爆弾ではないパネルに「旗」を立てていた場合、爆弾のパネルを開くことになりますので、ゲームオーバーかどうかの判定も行うように作ります。
//周りのパネルを開く
bool Mine::OpenAround(const int x, const int y)
{
for (int i = -1; i < 2; i++) {
for (int j = -1; j < 2; j++) {
int wx = x + j;
int wy = y + i;
if (wx >= 0 && wx < m_col && wy >= 0 && wy < m_row) {
int index = (y + i) * m_col + wx;
if (!m_pField[index].m_bOpen && !m_pField[index].m_bFlag) {
m_pField[index].m_bOpen = true;
if (m_pField[index].m_number == BOMB) {
return true;
}
if (m_pField[index].m_number == NONE) {
OpenAdjacentPanel(wx, wy);
}
}
}
}
}
return false;
}
|
戻り値がtrueであった場合は「爆弾を開いた」と言う意味になります。
また、開いた先が「0」のパネルと言う可能性もありますので、再帰関数を呼び出せるようにもなっています。
これでprivate関数は作り終えました。
では、publicメンバ関数の「OpenAround関数」を「IsClear関数」の下に追加します。
//周りのパネルを開く
bool Mine::OpenAround(const POINT &refPoint)
{
if (IsOutsideField(refPoint)) {
return false;
}
int x = (refPoint.x - ADJUST_X) / PANEL_WIDTH;
int y = (refPoint.y - ADJUST_Y) / PANEL_HEIGHT;
int index = y * m_col + x;
if (IsOpenNumber(index)) {
int number = GetNumber(index);
int flag = GetAroundFlagNum(x, y);
if (number == flag) {
if (OpenAround(x, y)) {
return true;
}
}
}
return false;
}
|
これまでに作ったprivateメンバ関数を呼び出しています。
privateメンバ関数の「OpenAround関数」がtrueを返してきた場合、trueを返してゲームオーバーになった事を伝えます。
最後に「SceneGame.cpp」を開き「Update関数」から呼び出します。
//============================================================================= // シーンの実行時に繰り返し呼び出される更新処理関数 //============================================================================= void SceneGame::Update() { if (m_pEngine->GetMouseButton(DIK_LBUTTON) && m_pEngine->GetMouseButton(DIK_RBUTTON)) { POINT point = m_pEngine->GetMousePosition(); if (m_mine.OpenAround(point)) { m_gameData.m_bClear = false; m_nowSceneData.Set(Common::SCENE_EXIT, false, NULL); } } if (m_pEngine->GetMouseButtonSync(DIK_LBUTTON)) { POINT point = m_pEngine->GetMousePosition(); if (m_mine.Open(point)) { m_gameData.m_bClear = false; m_nowSceneData.Set(Common::SCENE_EXIT, false, NULL); } } if (m_pEngine->GetMouseButtonSync(DIK_RBUTTON)) { POINT point = m_pEngine->GetMousePosition(); m_mine.SwitchFlag(point); } if (m_mine.IsClear()) { m_gameData.m_bClear = true; m_nowSceneData.Set(Common::SCENE_EXIT, false, NULL); } } |
実行して確認してみましょう。
<実行結果 クライアント領域のみ>
ここをクリックすればクリアです。
同時押しに慣れるとクリアまでのスピードが上がります。
最後に設定を少し変えてみましょう。
ゲームのシーンとして組み込むのではなく単純に数値を変えてみるだけです。
「SceneGame.cpp」の「Start関数」を開きましょう。
//============================================================================= // シーンの実行時に1度だけ呼び出される開始処理関数 //============================================================================= void SceneGame::Start() { m_pEngine->AddTexture(TEXTURE_MINE); m_mine.Initialize(12, 16, 30); } |
フィールドの幅を16、高さを12、爆弾の数を30個にしました。
それでは、実行してみましょう。
<実行結果 クライアント領域のみ>
要は上の部分を変更すれば、難易度が変えられます。
タイトルシーンで難易度を選ぶなど色々出来そうです。
また、画像を小さく作り直す事で、もっとたくさんのパネルを配置する事も出来ます。
マインスイーパーの作成はこれで終わりです。
実際のマインスイーパーと比べると時間の計測などまだまだ作りこむ余地がありますが、皆さんでチャレンジしてみてください。