Processingで「マインスイーパ」を作ろう
はじめに
みなさんこんにちは。ゆーてぃーです。今回は、Processingというプログラムソフトを用いてパズルゲームの1種であるマインスイーパを作り、マス目を用いたゲーム制作で必要なことを学んでいきましょう!
マインスイーパを作る
使用環境
・Windows11
・Processing 4.3
⓪製作方法
プログラムを記載するほか、作り方の説明やプログラムの意味などを記述していきます。プログラムは2つの「…」の間に挟まれてる部分を追加していくことで順番に作ることができます。
①ゲームの基本設定
それではさっそくマインスイーパを作っていきましょう!新規ファイルを作成すると、何も書かれてない状態になっています。この状態で左上にある三角マークの実行ボタンを押すと、何も出力されていないと思います。この状態からいろいろ追加していきます。
int bomb=10;
int []board=new int[100];
void setup() {
size(1000, 1000);
}
爆弾の数を示す変数bomb、盤面の情報を保存する配列boardを宣言します。変数bombは、数値を書き換えることで爆弾の数を変更できます。配列boardには各マスが数字なのか爆弾なのかを保存します。各マスが数字なら0から8、爆弾なら9を配列に格納します。
void setup(){}を記入します。Processingで繰り返し描画を実行するときには、void setup()とvoid draw()を使用します。void setup()内には初期設定を、void draw()内には繰り返し描画する動作を記入します。void内で宣言した変数・配列はその中でしか使用できません。size関数を使い、1000×1000の出力画面を作ります。実行してみると、さっきよりもはるかに大きい出力画面が表示されていると思います。
②盤面を表示する
マスを開けていくことがマインスイーパの主な作業なので、マスを配置します。void setup()の下にvoid draw()とその中身を書いていきます。
void setup(){
//省略
}
...
void draw() {
for (int y=0; y<10; y++) {
for (int x=0; x<10; x++) {
fill(200);
rect(x*100, y*100, 100, 100);
}
}
}
...
①で宣言した配列boardの要素数が100なので、10×10=100個のマスを描画します。x座標を100ずつ変えて10個配置し、y座標を100変えてまた配置していく動作を10回繰り返すので、for文の中にfor文を入れます。
fill関数を使い色を灰色(グレースケールで200)にします。最後にrect関数を用いて幅100の四角を描画します。Processingでは左上が座標(0,0)で右側、下側が正なのでx,y座標共に+100を増やしていきます。実行すると10×10の灰色のマスが表示されます。
③爆弾設置
盤面の配列と描画ができたので、次は盤面の内部処理を作っていきましょう!void setup()内に以下のプログラムを追加します。
void setup(){
size(1000,1000);
...
int r;
while (bomb>0) {
r=(int)random(0, 99);
if (board[r]==0) {
board[r]=9;
bomb--;
}
}
...
}
マインスイーパではプレイするたびに爆弾の位置が変わります。ここでは乱数を使用して実装していきます。random関数を使い0から99の範囲で乱数を発生させます。整数以外にも5.7など小数を含んだ数値も発生されてしまうので、変数rに代入する際に(int)を付け加えて整数(int型)に変換します。配置する爆弾の個数回、配列boardの乱数番目の要素を爆弾に対応する9に書き換えれば済むように感じますが、少し注意が必要です。乱数はランダムに数を発生させるため、同じ数が複数回発生する場合があります。その場合設置したい爆弾の数と乱数によって配置される爆弾の数が合わなくなってしまいます。
ではどうすればいいのか。ただ爆弾設置を10回繰り返すのではなく、乱数の値がすでに爆弾を設置した場所になった時は設置をせず、10回設置し終わるまで繰り返すようにします。爆弾を設置できた時に変数bombの値を1減らし、bombの値が0になるまで繰り返すwhile文を繰り返し処理に採用します。繰り返し内では、乱数で指定された配列の位置に爆弾が無いことをif文で判断し、爆弾を設置(配列の要素を9に)します。この処理を行うと、爆弾(9)が10個設置されてることが分かります。
④爆弾以外のマスの数字を計算
爆弾が設置できたので、残りのマスに周りの爆弾の数を配列に格納していきます。③で書いた処理の続きに以下のプログラムを追加します。
void setup(){
size(1000,1000);
int r;
while(bomb>0){
//省略
}
}
...
for (int i=0; i<100; i++) {
if (board[i]!=9) {
if ((i%10!=9)&&(board[i+1]==9)) { //右に爆弾があるか
board[i]++;
}
if ((i%10!=0)&&(board[i-1]==9)) { //左に爆弾があるか
board[i]++;
}
if ((i>=10)&&(board[i-10]==9)) { //上に爆弾があるか
board[i]++;
}
if ((i<90)&&(board[i+10]==9)) { //下に爆弾があるか
board[i]++;
}
if ((i%10!=9)&&(i>=10)&&(board[i-9]==9)) { //右上に爆弾があるか
board[i]++;
}
if ((i%10!=9)&&(i<90)&&(board[i+11]==9)) { //右下に爆弾があるか
board[i]++;
}
if ((i%10!=0)&&(i>=10)&&(board[i-11]==9)) { //左上に爆弾があるか
board[i]++;
}
if ((i%10!=0)&&(i<90)&&(board[i+9]==9)) { //左下に爆弾があるか
board[i]++;
}
}
}
...
}
まずfor文で100回繰り返し、すべてのマスに対して処理を実行します。爆弾のマスは無視していいので、if(board[i]!=9)で分岐させます。その後、対象としたマスの周り8マスそれぞれに爆弾があるか判断してそのマスに対応する配列の数値に+1します。周り8マスを指定する方法は、以下の画像の通りです。
ただしこのままでは、正しく動作しません。各マスの番号は左上から右下に向かって増えていくため、右端に行くと次の段の左端に進みます。そのため、右端で上記の図のi+1方法を使うと、さらに右にはマスがないのに次の段の左端を参照してしまいます。これは上下左右すべてに言えます。なので、各if文にiの範囲を設定します。
・上を参照しないときは画像の赤の部分以外なので、i>=10
・下を参照しないときは画像の青の部分以外なので、i<90
・右を参照しないときは画像の黄の部分以外なので、i%10!=9
・左を参照しないときは画像の緑の部分以外なので、i%10!=0
となります。
⑤クリックしたマスの位置を計算する
次は、クリックしたときの処理を作っていきます。まず、以下の変数、配列をvoid setup()の前に宣言します。
...
int []pressed=new int[100];
int clickPlace;
...
void setup(){
//省略
各マスの状態を配列pressedに保存します。0=押したことがない、1=押したことがあるという感じです。変数clickPlaceにはマウスの座標を元にクリックしたマスが何番目かを保存します。座標から番号を計算する方法は、以下の通りです。
void draw(){
...
if (mousePressed&&0<=mouseX&&mouseX<=999&&0<=mouseY&&mouseY<=999) {
clickPlace=mouseY/100*10+mouseX/100;
if (pressed[clickPlace]==0) {
pressed[clickPlace]=1;
}
}
...
for (int y=0; y<10; y++) {
for (int x=0; x<10; x++) {
//省略
}
マスの描画処理の前に記入します。最初のif文の条件に含まれているmousePressedを使うことで、マウスがクリックされたか判断できます。一緒に書かれているmouseX、mouseYの条件は、マウスが出力画面内に入っているか判断します。これがないと、画面外でクリックした際にほかのマスがクリックされてしまいます。
1マスあたり縦横の長さ100なので、mouseY/100*10+mouseX/100を使うことでマスの番号を取得することができます。例えば、以下の画像の12の場合、X座標は200~299、Y座標は100~199なので、100~199/100*10+200~299/100=10+2=12となります。
マスの番号が取得できたあとは、マスがすでに押されているか判断し、押されてなければ配列を1にします。
⑥爆弾・数字を出力する
今は押されてないマスしか描画していないので、押されたマスの描画も行います。void draw()内にあるfill(200)…の前にif文を追加して2種類のマスの描画を分岐させます。fill(255)の255は、グレースケールで白です。
void draw(){
//省略
for (int y=0; y<10; y++) {
for (int x=0; x<10; x++) {
...
if (pressed[y*10+x]==1) {
fill(255);
rect(x*100, y*100, 100, 100);
} else {
...
fill(200);
rect(x*100, y*100, 100, 100);
...
}
...
}
}
}
マス自体の描画が完成したので、爆弾と爆弾の数を示す数字を出力できるようにしていきます。どちらも文字を使用するので、文字の初期設定を行います。void setup()内に文字のサイズを指定するtextSize(100)を記入します。
void setup(){
...
textSize(100);
...
size(1000,1000);
//省略
続いて、文字を出力していきます。
if (pressed[y*10+x]==1) {
fill(255);
rect(x*100, y*100, 100, 100);
...
switch(board[y*10+x]) {
case 0:
break;
case 1:
fill(0, 0, 255);
text("1", x*100+25, (y+1)*100-20);
break;
case 2:
fill(0, 255, 0);
text("2", x*100+25, (y+1)*100-20);
break;
case 3:
fill(255, 0, 0);
text("3", x*100+25, (y+1)*100-20);
break;
case 4:
fill(57, 16, 123);
text("4", x*100+25, (y+1)*100-20);
break;
case 5:
fill(168, 68, 60);
text("5", x*100+25, (y+1)*100-20);
break;
case 6:
fill(86, 20, 92);
text("6", x*100+25, (y+1)*100-20);
break;
case 7:
fill(70, 191, 189);
text("7", x*100+25, (y+1)*100-20);
break;
case 8:
fill(239, 136, 190);
text("8", x*100+25, (y+1)*100-20);
break;
case 9:
fill(0);
text("*", x*100+25, (y+1)*100+10);
break;
}
...
} else {
fill(200);
rect(x*100, y*100, 100, 100);
}
}
配列に保存されている数字0~9(9は爆弾)に対してそれぞれ処理を行いたいので、switch文を使用します。xとyのfor文内での実行なので、配列の要素を参照するための番号はy*10+xです。0の場合は何も表示しないので処理は無し、1~8は数字を、爆弾(9)の場合は*を爆弾として表示します。各case後にはbreakを付けることを忘れずに!
⑦クリア処理等を実装する
これでゲームの機能が一通り完成したので、最後にクリア、ゲームオーバーの処理を作ります。まずはクリックしたマスの数を保存する変数scoreとゲームの状態を保存する変数sceneを宣言します。
...
int score=100-bomb;
int scene=0;
...
void setup(){
変数scoreは爆弾の数を省いたマスの数なので、すべてのマスの数-爆弾の数にします。変数sceneはゲーム中、ゲーム終了後を分けるために使います。
void draw(){
//省略
pressed[clickPlace]=1;
...
if (board[mouseY/100*10+mouseX/100]==9) {
scene=1;
} else {
score--;
if (score==0) {
scene=2;
}
}
...
}
}
for (int y=0; y<10; y++) {
for (int x=0; x<10; x++) {
爆弾を押してしまったらsceneを1に、爆弾以外のマスを全てクリックできたらsceneを2にします。その数字ごとに結果を表示するため以下のプログラムをvoid draw()内の最後に記述します。
void draw(){
for (int y=0; y<10; y++) {
for (int x=0; x<10; x++) {
//省略
} else {
fill(200);
rect(x*100, y*100, 100, 100);
}
}
...
if (scene==1) {
fill(70, 0, 80);
text("gameover", 275, 530);
noLoop();
} else if (scene==2) {
fill(100, 200, 0);
text("gameclear", 275, 530);
noLoop();
}
...
}
scene=1は爆弾が押された場合なので、ゲームオーバーの文字を出力させます。scene=2は爆弾以外のマスを全てクリックした場合なので、ゲームクリアの文字を出力させます。出力後はnoLoopを使ってdraw関数の繰り返しを止めます。
すべて記入し終わったら、実行しましょう。各マスに周りの爆弾の数が正確に表示され、爆弾を押したときは「gameover」、爆弾以外のすべてのマスをクリックしたときは「gameclear」と表示され、ほかのマスをクリックしても何も実行されなければ完成です!うまくいかない場合はプログラムを見直しましょう。比較したい方は、以下のサンプルプログラムを参照してください。
int bomb=10;
int []board=new int[100];
int []pressed=new int[100];
int clickPlace;
int score=100-bomb;
int scene=0;
void setup() {
textSize(100);
size(1000, 1000);
int r;
while (bomb>0) {
r=(int)random(0, 99);
if (board[r]==0) {
board[r]=9;
bomb--;
}
}
for (int i=0; i<100; i++) {
if (board[i]!=9) {
if ((i%10!=9)&&(board[i+1]==9)) { //右に爆弾があるか
board[i]++;
}
if ((i%10!=0)&&(board[i-1]==9)) { //左に爆弾があるか
board[i]++;
}
if ((i>=10)&&(board[i-10]==9)) { //上に爆弾があるか
board[i]++;
}
if ((i<90)&&(board[i+10]==9)) { //下に爆弾があるか
board[i]++;
}
if ((i%10!=9)&&(i>=10)&&(board[i-9]==9)) { //右上に爆弾があるか
board[i]++;
}
if ((i%10!=9)&&(i<90)&&(board[i+11]==9)) { //右下に爆弾があるか
board[i]++;
}
if ((i%10!=0)&&(i>=10)&&(board[i-11]==9)) { //左上に爆弾があるか
board[i]++;
}
if ((i%10!=0)&&(i<90)&&(board[i+9]==9)) { //左下に爆弾があるか
board[i]++;
}
}
}
}
void draw() {
if (mousePressed&&0<=mouseX&&mouseX<=999&&0<=mouseY&&mouseY<=999) {
clickPlace=mouseY/100*10+mouseX/100;
if (pressed[clickPlace]==0) {
pressed[clickPlace]=1;
if (board[mouseY/100*10+mouseX/100]==9) {
scene=1;
} else {
score--;
if (score==0) {
scene=2;
}
}
}
}
for (int y=0; y<10; y++) {
for (int x=0; x<10; x++) {
if (pressed[y*10+x]==1) {
fill(255);
rect(x*100, y*100, 100, 100);
switch(board[y*10+x]) {
case 0:
break;
case 1:
fill(0, 0, 255);
text("1", x*100+25, (y+1)*100-20);
break;
case 2:
fill(0, 255, 0);
text("2", x*100+25, (y+1)*100-20);
break;
case 3:
fill(255, 0, 0);
text("3", x*100+25, (y+1)*100-20);
break;
case 4:
fill(57, 16, 123);
text("4", x*100+25, (y+1)*100-20);
break;
case 5:
fill(168, 68, 60);
text("5", x*100+25, (y+1)*100-20);
break;
case 6:
fill(86, 20, 92);
text("6", x*100+25, (y+1)*100-20);
break;
case 7:
fill(70, 191, 189);
text("7", x*100+25, (y+1)*100-20);
break;
case 8:
fill(239, 136, 190);
text("8", x*100+25, (y+1)*100-20);
break;
case 9:
fill(0);
text("*", x*100+25, (y+1)*100+10);
break;
}
} else {
fill(200);
rect(x*100, y*100, 100, 100);
}
}
if (scene==1) {
fill(70, 0, 80);
text("gameover", 275, 530);
noLoop();
} else if (scene==2) {
fill(100, 200, 0);
text("gameclear", 275, 530);
noLoop();
}
}
}
おわりに
今回のマインスイーパはとても簡易的なものなので、本家とは異なる部分があります。その部分も再現したい!という方は、発展として自分で考えてみてください。私が挙げられるものは
・爆弾のマスとしておく旗の設置処理
・数字が0のマスを押したときは数字が1以上のマスまで周りを開ける処理
・最初にクリックするマスが必ず爆弾以外になるようにする処理
です。
製作を通して、マスと配列の関係は理解できたでしょうか。この関係を利用することで、オセロや将棋、チェスといったマスを使ったほかのゲームも作れると思います。ぜひぜひ頭の片隅に残しておいてください。
ここまで読んでくれてありがとうございました。またいつか、別の記事で会いましょう。
この記事が気に入ったらサポートをしてみませんか?