今回は、Windowsに付属していたゲームでおなじみの「マインスイーパー」を作りたいと思います。
「マイン(Mine)」は英語で色々な意味がありますが、この場合のマインは「地雷」や「機雷」を意味します。
「スイーパー(Sweeper)」は「掃除機」や「掃除人」を意味しますので、「地雷除去ゲーム」と言えば分かりやすいでしょう。
ゲームのルールを知らない人向けに、少しだけ説明します。
ゲーム画面は↓のようなイメージです。
フィールドが四角いパネルで区切られており、どこかに「地雷」が仕掛けられています。
「地雷」のパネルを開かないよう、パネルをクリックしていきます。
ヒントとして、「地雷」ではないパネルには、自分の周り8マスに何個の「地雷」があるか数字が書いてあります。
これを手掛かりに、「地雷」以外のパネルを全て開くとクリアとなります。
上の図では、☆が付いている箇所が「地雷」と思われるパネルです。
ただし、実際のゲームと全く同じものを作るつもりは無く、今回もゲームシーンのみ作ります。
今まで通り、プラスアルファは皆さんで考えて工夫してみてください。
今回使用する画像は、↓の画像です。
「地雷」ではなく「爆弾」の絵ですから、以降は「爆弾」と呼びます。
上段のパネル用の絵は、パネルの周り8マスに何個の「爆弾」があるか表示するためのものです。
最大8個存在する可能性があるので、0(数字表示なし)から8まで用意しました。
9番目の位置に「爆弾」がある事も覚えておいてください。
下の段は、開いていないパネルと、「爆弾」かもしれないパネルに置いておくための目印(旗)です。
画像サイズは「512×512ピクセル」です。
※プロジェクトフォルダの「Resource\\Textureフォルダ」に「Mine.png」と言うファイル名で保存してください。
「GameBase.h」を開き、ファイル名を定数化します。
//-----------------------------------------------------------------------------
//ゲーム中で使用するテクスチャ、BGM、SE、フォントのパス付ファイル名を書きます。
//-----------------------------------------------------------------------------
namespace KeyString
{
const std::string TEXTURE_MINE = "Resource\\Texture\\Mine.png";
}
|
「SceneGame.cpp」の「Start関数」で画像を追加します。
//=============================================================================
// シーンの実行時に1度だけ呼び出される開始処理関数
//=============================================================================
void SceneGame::Start()
{
m_pEngine->AddTexture(TEXTURE_MINE);
}
|
「Exit関数」で解放しても構いませんが、シーンを追加し画像を使う方は解放しなくても良いです。
今回もクラスオブジェクトを追加して、メンバ変数や定数、関数を使って作ります。
これまでの手順を参考に「Mine.h」と「Mine.cpp」をプロジェクトに追加してください。
↑こうなっていればOKです。
先に「GameBase.h」を開き、ヘッダファイルをインクルードしておきます。
//-----------------------------------------------------------------------------
// オブジェクトのヘッダファイルをインクルードします。
//-----------------------------------------------------------------------------
#include "Object\\Mine\\Mine.h"
|
「Mine.h」を開き、Mineクラスの枠組みだけ作っておきましょう。
class Mine { public: private: }; |
今回のクラスは、今までのクラスと比べると大き目のクラスになる予定です。
少しずつ付け加えていきますので、理解しながら進めてください。
まずは、フィールドをどのように作るか考えます。
これまでの説明を元に格納したいデータを考えると、「爆弾かどうか」や「周りに何個爆弾があるか」「パネルが開かれているか」など色々思いつきます。
「パネルが開かれているかどうか」はフラグで対応出来そうです。
「爆弾かどうか」「周りの爆弾の数」は数値で管理出来るよう画像を用意しています。
これまでの演習を理解していれば、この並びを見ればデータが分かると思います。
データ 対応する絵 0 爆弾なし 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 爆弾
転送元座標の計算もこれで簡単に出来ます。
とりあえず、ここまでの情報を元に列挙体と構造体を宣言します。
class Mine {
public:
private:
enum {
NONE, //爆弾なし
BOMB = 9, //爆弾
};
struct Field {
int m_number; //周りにある爆弾の数(0〜8)、爆弾は9
bool m_bOpen; //オープンフラグ
};
};
|
列挙体は「0」と「9」だけ用意しました。
他の数は宣言しても使う事がありません・・・
次にフィールドについて考えます。
実際の「マインスイーパー」はフィールドの大きさと爆弾の数で難易度が変えられるようになっています。
それを考えると、フィールドの大きさは変更可能でなければなりません。
ポインタ変数を用意して「new」で領域確保が出来るようにします。
class Mine { public: private: enum { NONE, //爆弾なし BOMB = 9, //爆弾 }; struct Field { int m_number; //周りにある爆弾の数(0〜8)、爆弾は9 bool m_bOpen; //オープンフラグ }; Field *m_pField; //フィールド }; |
ポインタ変数にはコンストラクタ関数でNULLを代入しますので、コンストラクタ関数の宣言を加えます。
class Mine { public: Mine(); private: enum { NONE, //爆弾なし BOMB = 9, //爆弾 }; struct Field { int m_number; //周りにある爆弾の数(0〜8)、爆弾は9 bool m_bOpen; //オープンフラグ }; Field *m_pField; //フィールド }; |
「Mine.cpp」を開き、コンストラクタ関数の本体を作りましょう。
Mine::Mine() : m_pField(NULL) { } |
フィールドの初期化は、「Initialize関数」を付け加えて行う事にします。
この関数は、フィールドの大きさを引数で受け取るように作ります。
イメージは2次元配列ですので、縦方向のサイズと横方向のサイズを受け取るように作ります。
「Mine.h」を開き、「Initialize関数」と必要な変数を追加します。
class Mine { public: Mine(); //フィールドの初期化 void Initialize(const int row, const int col); private: enum { NONE, //爆弾なし BOMB = 9, //爆弾 }; struct Field { int m_number; //周りにある爆弾の数(0〜8)、爆弾は9 bool m_bOpen; //オープンフラグ }; Field *m_pField; //フィールド int m_row; //縦方向のパネル数 int m_col; //横方向のパネル数 }; |
「Mine.cpp」を開き、コンストラクタ関数の下に「Initialize関数」本体を追加します。
//フィールドの初期化
void Mine::Initialize(const int row, const int col)
{
m_row = row;
m_col = col;
m_pField = new Field[m_row * m_col];
}
|
領域の確保を行った場合、必ず解放を行う必要がありますからデストラクタ関数を追加しましょう。
「Mine.h」を開き、宣言を書きます。
class Mine { public: Mine(); ~Mine(); //フィールドの初期化 void Initialize(const int row, const int col); private: enum { NONE, //爆弾なし BOMB = 9, //爆弾 }; struct Field { int m_number; //周りにある爆弾の数(0〜8)、爆弾は9 bool m_bOpen; //オープンフラグ }; Field *m_pField; //フィールド int m_row; //縦方向のパネル数 int m_col; //横方向のパネル数 }; |
「Mine.cpp」を開き、コンストラクタ関数の下にデストラクタ関数を追加します。
Mine::~Mine() { if (m_pField) { delete[] m_pField; m_pField = NULL; } } |
これでメモリリークの対応が出来ました。
続いて、フィールドに初期値「爆弾無し(NONE)」を格納した後で「爆弾」をセットします。
一気に続けて書きたいところですが、ここで「privateメンバ関数」を読んでください。
メンバ関数も機能ごとに分割し、他から呼び出す必要がなければprivateメンバ関数にするべきです。
ここでも「爆弾無しを格納する」関数と「爆弾をセットする」関数に分割して作成してみましょう。
「Mine.h」を開き、フィールドをクリア(爆弾無しをセット)する関数「ClearField関数」を追加します。
class Mine { public: Mine(); ~Mine(); //フィールドの初期化 void Initialize(const int row, const int col); private: enum { NONE, //爆弾なし BOMB = 9, //爆弾 }; struct Field { int m_number; //周りにある爆弾の数(0〜8)、爆弾は9 bool m_bOpen; //オープンフラグ }; Field *m_pField; //フィールド int m_row; //縦方向のパネル数 int m_col; //横方向のパネル数 //フィールドのクリア void ClearField(); }; |
「Mine.cpp」を開き、「Initialize関数」の下に「ClearField関数」本体を追加します。
//フィールドのクリア
void Mine::ClearField()
{
const int FIELD_MAX = m_row * m_col;
for (int i = 0; i < FIELD_MAX; i++) {
m_pField[i].m_number = NONE;
}
}
|
繰り返し条件に書く「m_row * m_col」を定数として先に計算しておきました。
意味が無いように見えますが、計算式をまとめておくとプログラムが見やすくなります。
また、オープンフラグは今のところ無視しています。
後で改めて設定しましょう。
「Initialize関数」から「ClearField関数」を呼び出しましょう。
//フィールドの初期化 void Mine::Initialize(const int row, const int col) { m_row = row; m_col = col; m_pField = new Field[m_row * m_col]; ClearField(); } |
とりあえず、ビルドしてエラーが無いかどうかチェックしてください。
続きは次回にしましょう。