【中級者向け】第五回 メモ帳だけでゲーム作ってみた〈ぷよぷよ編〉

2020年10月13日

今回も、メモ帳だけを使って、あの有名なパズルゲームの「ぷよぷよ」をつくっていきたいと思います。

前回の記事をまだ読んでいない方は、ぜひこちらを先に読んでください!


今回作るもの

前回は、ぷよを回転できるようにしましたね!

  1. ぷよぷよのフィールドを作ろう!
  2. ぷよを上から落としてみよう!
  3. ぷよを操作できるようにしてみよう!
  4. ぷよを回転できるようにしてみよう! ←前回
  5. ぷよが下に落ちるようにしよう! 今回
  6. 連鎖をできるようにしよう!
  7. 次のぷよが何なのかわかるようにしてみよう!

そして今回は、前回置いたぷよが浮いてしまう問題がありましたが、下に落ちるようにして解決しましょう。

また、次回第六回の連鎖の準備も同時にしていきたいと思います!

完成イメージは以下の通りです。

画像だけではわかりませんが、しっかり置いたぷよが下に落ちるようになっています!


コードを書いていこう!

ここからは、前回と同様にメモ帳にコードを書いていきましょう!

また、後半半分は次回の準備になるので、わかりずらいかもしれませんが、ゆっくり進めていきましょう!


変数と関数を作る

ではまず初めに、今回必要になってくる変数と関数を作っていきましょう。

まずは変数を作りましょう。
今回は2つ作ります。

…
let down_speed = 0.02;

let stop = false;

let rensa_finish = true;

function downPuyo() {
…

まず1つ目の、「stop」というのは、ぷよが下に落ちている間や、連鎖をしているときに操作ができないようにするためにあります。

ぷよが落ちている間や連鎖中は「true」、それ以外は「false」にして使います。

そして2つ目の「rensa_finish」というのは名前の通りかもしれませんが、連鎖が終わったかどうかをチェックするときに使います。

連鎖が終われば、「rensa_finish」が「true」になると同時に、「stop」が「true」になるようなコードにします。

今回主に使うのは「stop」で、「rensa_finish」は今回は使いませんが、次回使うのでどういう意味かだけは覚えておきましょう。

次に関数を作っていきましょう。

function downPuyo() {
…
省略
…
}

function checkField() {

}

function fallPuyo(){

}

function erasePuyo(){

}

function nextPuyo() {

}

function movePuyo(e){
…

この4つの関数を作りましょう。

まず、最初の「checkField」は、フィールドをチェックするもので、ぷよが置かれるとまずこの関数の仕事を行います。

そして「fallPuyo」でぷよが下に落ちる処理をします。

次に「erasePuyo」で4つつながっているぷよが消えるようにします。
ですが、これは連鎖になってくるので次回にコードを詳しく書いていきます。

そして、連鎖が終われば最後の「nextPuyo」で、ぷよの位置を上にリセットするなど次のぷよの処理をします。

今はまだ何も処理を書いていないのでわかりずらいかもと思うので、分からなくても次に進めましょう!


ぷよが落ちている間や連鎖中に操作できないようにする

では、次にぷよが設置され、下に落ちるまでの間、操作ができないようにしましょう。

これには最初に作った変数を使いましょう。

「stop」という変数が、「false」の時だけ操作できるようにすればいいですね。

なので、「movePuyo」の中にif文を一個組み込むだけでOKです。

こんな感じ。

function movePuyo(e){
  if (stop == false) {
    if (e.key == "ArrowDown" || e.key == "s") {
      down_speed = 0.2;
    }else if (e.key == "ArrowRight" || e.key == "d") {
      …中略…
    }else if (e.key == "x" || e.key == "o") {
      let temp_x = Math.round(Math.cos((direction - 90) / 360 * 2 * Math.PI));
      let temp_y = -Math.round(Math.sin((direction - 90) / 360 * 2 * Math.PI));
      if (position[0] + temp_x >= 0 && position[0] + temp_x <= 5 && position[1] + temp_y >= 0 && position[1] + temp_y <= 11) {
        if (field[Math.ceil(position[1]) + temp_y][position[0] + temp_x] == 0) {
          child = [temp_x, temp_y];
          direction = direction - 90;
        }
      }
    }
  }
}

こんな感じで、「if (stop == false)」で大きくくくれば大丈夫です。

また、操作しなくても勝手に下に動かすというシステムなので、それも止めるために「downPuyo」の中にも同じように書き込みます。

function downPuyo() {
  if (stop == false) {
    position[1] += down_speed;

    if (position[1] >= 11 || position[1] + child[1] >= 11) {
      field[Math.floor(position[1])][Math.floor(position[0])] = color[1];
      field[Math.floor(position[1] + child[1])][Math.floor(position[0] + child[0])] = color[0];

      position = [2, 0];
      child = [0, 1];
      color = [Math.floor(Math.random() * 4 + 1), Math.floor(Math.random() * 4 + 1)];
      direction = 270;
    }else if (field[Math.floor(position[1]) + 1][Math.floor(position[0])] > 0 || field[Math.floor(position[1] + child[1]) + 1][Math.floor(position[0] + child[0])] > 0) {
      field[Math.floor(position[1])][Math.floor(position[0])] = color[1];
      field[Math.floor(position[1] + child[1])][Math.floor(position[0] + child[0])] = color[0];

      position = [2, 0];
      child = [0, 1];
      color = [Math.floor(Math.random() * 4 + 1), Math.floor(Math.random() * 4 + 1)];
      direction = 270;
    }
  }
  drawField();
}

さっきと同じで大きくくくるだけですね

まだ「stop」を「true」にするコードは書いていませんので特に変化はありませんので次に進めましょう。


連鎖中に上から落ちてくるぷよを表示させない

今回は連鎖のコードは組まないので少しわかりずらいですが、連鎖中やぷよが下に落ちている間に上から落ちてくるぷよを表示させないために、先ほどと同様に「stop」を使ったif文を1つ追加しましょう。

function drawField() {
  CONTEXT.clearRect(0, 0, 300, 600);
  if (stop == false) {
    switch (color[0]) {
      case 1:
      CONTEXT.fillStyle = "red";
      break;
      case 2:
      CONTEXT.fillStyle = "yellow";
      break;
      case 3:
      CONTEXT.fillStyle = "blue";
      break;
      case 4:
      CONTEXT.fillStyle = "green";
      break;
    }
    CONTEXT.fillRect((position[0] + child[0]) * 50, (position[1] + child[1]) * 50, 50, 50);

    switch (color[1]) {
      case 1:
      CONTEXT.fillStyle = "red";
      break;
      case 2:
      CONTEXT.fillStyle = "yellow";
      break;
      case 3:
      CONTEXT.fillStyle = "blue";
      break;
      case 4:
      CONTEXT.fillStyle = "green";
      break;

    }
    CONTEXT.fillRect(position[0] * 50, position[1] * 50, 50, 50);

  }
…省略…
}

このように、落ちてくるぷよを表示させるコードの部分にif文で大きく囲めばいいですね!

今回はぷよが落ちてくるときだけに影響しますが、次回以降では連鎖中にも影響するようなコードとなっています。


ぷよを下に落とすコードを書く

では次に本題のぷよを下に落とすコードを考えて行きましょう。

いきなりすべてのマスで考えるのは難しいので、まずは一番左の縦のフィールドだけ考えてみましょう。

簡単に図で書いて見ました。

ぷよを落とすには、下のマスが空欄だったら一個下に落とすということを繰り返せばいいですね。

そのことがわかったうえで下のコードを見てみましょう。

for (let j = 1; j < 12; j++) {
  if (field[j][0] == 0) {
    field[j][0] = field[j - 1][0];
    field[j - 1][0] = 0;
  }
}

これで下が空欄だと下に落とすコードができました。

ですがこれだけでは少し不十分です。

なぜなら今のコードだと、1つしかブロックを下に落とすことができないからです。

さっきのコードだとこうなる

これを直すには、下に落とすときに上のぷよも一緒に下に落としてあげないといけません。

なので、コードはこうなります。

for (let j = 1; j < 12; j++) {
  if (field[j][i] == 0) {
    for (let l = j; l > 0 ; l--) {
      field[l][i] = field[l - 1][i];
      field[l - 1][i] = 0;
    }
  }
}

lという数字が、例えば、j が 3 の時に、l = 3 ⇒ 2 ⇒ 1 と減っていくようになっています。

*「l–」は「l++」の逆の意味で、1つずつ減らしていくということです。

後は、これをforループでもう一度囲んで、他の列も同じようにすればいいですね。

また、ぷよを落とした後にゲーム画面を更新するために「drawField」も書いておきましょう。

function fallPuyo() {
  for (let i = 0; i < 6; i++) {
    for (let j = 1; j < 12; j++) {
      if (field[j][i] == 0) {
        for (let l = j; l > 0 ; l--) {
          field[l][i] = field[l - 1][i];
          field[l - 1][l] = 0;
        }
      }
    }
  }
  drawField();
}

これで、ぷよを下に落とすコードは完成しました!

少し難しいかもしれませんが、forループがどのように処理しているかを図や言葉で書くと分かりやすいかもしれませんよ。


「nextPuyo」の処理を書く

では次に「nextPuyo」の処理を先に書いていきたいと思います。

前回までぷよが置かれた瞬間に次のぷよを一番上に設定しなおしていましたが、今回からぷよが落ちるまで待たないといけないです。

なので、「nextPuyo」という関数に次のぷよの設定を書き込み、もともと次のぷよの設定をしていた部分には「checkField」が実行されるように変えましょう。

function downPuyo() {
  if (stop == false) {
    position[1] += down_speed;

    if (position[1] >= 11 || position[1] + child[1] >= 11) {
      field[Math.floor(position[1])][Math.floor(position[0])] = color[1];
      field[Math.floor(position[1] + child[1])][Math.floor(position[0] + child[0])] = color[0];

      stop = true;

      checkField();
    }else if (field[Math.floor(position[1]) + 1][Math.floor(position[0])] > 0 || field[Math.floor(position[1] + child[1]) + 1][Math.floor(position[0] + child[0])] > 0) {
      field[Math.floor(position[1])][Math.floor(position[0])] = color[1];
      field[Math.floor(position[1] + child[1])][Math.floor(position[0] + child[0])] = color[0];

      stop = true;

      checkField();
    }
  }
  drawField();
}

…
省略
…

function nextPuyo() {
  position = [2, 0];
  child = [0, 1];
  color = [Math.floor(Math.random() * 4 + 1), Math.floor(Math.random() * 4 + 1)];
  direction = 270;
}

「checkField」の処理を書く

では次に、さっき出てきた「checkField」の処理を書きたいと思います。

「checkField」では、

何秒後かに「fallPuyo」でぷよを下に落とす

何秒後かに「erasePuyo」で4つつながっているぷよを消す

もし連鎖が終わっていれば次のぷよを用意する
もし連鎖が終わっていなければ、何秒後かにまた「checkField」を実行する(つまり一番上に戻る)

といった順番で処理をしていきます。

そしてこの「何秒後に○○をさせる」というコードを作るのに、

setTimeout(処理する内容, 何ミリ秒後に処理するか);

というものを使います。

*何ミリ秒という新しい単位が出ていますね。1000ミリ秒で1秒です。
 今回は500ミリ秒に設定するので、0.5秒ということですね。

では早速「setTimeout」を使ってコードを書いて見ます。

function checkField() {
  setTimeout(fallPuyo, 500);
  setTimeout(function(){
    erasePuyo();
    if (rensa_finish == true) {
      nextPuyo();
      stop = false;
    }else {
      setTimeout(checkField, 500);
    }
  }, 1000);
}

ぷよを落とすのを0.5秒後に、そしてもし連鎖が終わっていれば1秒後に新しいぷよの設定をして、まだ連鎖が続いているなら「checkField」をその0.5秒後に呼び出すようにしています。

これで今回のコードは完成です!

使ってない関数などがありますが、それは次回の連鎖のためにあります。

なので今回のぷよを時間経過で落とす処理だけを簡単に理解していればOKです!

また、ちょっとずつゲームができてきましたね!

次回でぷよぷよの醍醐味の連鎖を追加して、とうとう遊べるようになりますよ!


うまくいかなかった方向け

うまくいかなかった方は、まずエラーが出ていないか確認しましょう!

エラーの確認方法については、別の記事で詳しくまとめたので、ぜひそちらを参考にしてください!

それでもうまくいかなければ、下のサンプルコードをダウンロードして、自分のコードを比較してみましょう!


今回はここまで!

というわけで第五回はここまでとしたいと思います!

少しずつですが完成に近づいてきていましたね!

次回は、とうとうぷよぷよの醍醐味の「連鎖」をできるようにしていきたいと思います!

というわけで長くなってしまいましたが、

最後まで読んでいただきありがとうございました!

ご感想などあれば、コメントやTwitterにどしどしお寄せください!

第六回はこちらから!