jsでブロックを落として揃えるゲームを作る

  • 20
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

今日のトピック

Javascriptでclassをゴリゴリと使ってブロックを落として列を揃えて消すゲームを作っていきます!国民的ゲームのテ◯◯◯ですね、はい!
盤の大きさは、10 × 21です。ブロックの形に関しては本家のものを、そのまま使わさせてもらいます!

アジェンダ

  1. 下準備
  2. 盤の準備(表示サイド)
  3. ブロックの作成
  4. 盤の準備1(実装サイド)
  5. インターフェース実装
  6. 盤の準備2(実装サイド)
  7. 完成!

Let' try!

下準備

STEP1 ファイルの作成

次のファイルを準備しておきましょう!

  • index.html
  • css/application.css
  • js/application.js インターフェース系を担当します。
  • js/Board.js Boardクラスを記述します。ゲーム盤を表しています。
  • js/Block.js Blockクラスを作ってブロックの基本性質を記述します。
  • js/Blocks.js Blockクラスを継承して様々なブロックを作成します。

index.htmlを次のようにしておきます。

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>テスト</title>
  <script src="/js/Block.js"></script>
  <script src="/js/Blocks.js"></script>
  <script src="/js/Board.js"></script>
  <script src="/js/application.js"></script>
  <link rel="stylesheet" href="/css/application.css"></link>
</head>
<body>
</body>
</html>

STEP2 仕様の確認

ゲーム盤の情報は、裏側では文字列として管理します。
最初の10文字は盤の一番上の行、次の10文字はその下の行という形で表していこうと思います。空白文字は何もないマスということにして、アルファベットA-Gまでがブロックのあるマスということにしておきます。この情報を元に、htmlで作った盤の上に出力をしていきます。

盤を準備する

10 × 21のマスを手で書くのは面倒なので、jsで書き出してしまいましょう!

STEP1 盤を書き出すコードの作成

js/application.js
window.onload = () => {
  let table = document.createElement("table");
  let thead = document.createElement("thead");
  let tbody = document.createElement("tbody");
  let cells = [];
  for(let i = 0; i < 21; ++i) {
    let tr = document.createElement("tr");

    for(let j = 0; j < 10; ++j) {
      let td = document.createElement("td");
      cells.push(td);
      tr.appendChild(td);
    }
    tbody.appendChild(tr);
  }
  table.appendChild(thead);
  table.appendChild(tbody);
  document.body.appendChild(table);

特に解説事項がないので次にいきます!

STEP2 デザインを整える

このままでは、ブロック間に隙間ができてしまうのでcssでどうにかします。

css/application.css
table {
  border-collapse: collapse;
}

td {
  width: 20px;
  height: 20px;
  margin: 0px;
  padding: 0px;
  border: 1px solid #000;
}

border-collapse: collapse;とすることでtableタグのセル間の隙間を埋めることができます。(が、marginとpaddingが、さらに隙間を作っていたので消します)

ここまでできるとhtml上に盤ができているはずです。

ブロックを作る

STEP1 Blockクラスを作ろう!

ブロックって形以外同じ性質ですよねー!(回転するとか、文字に起こさなきゃいけないとか)
ということでそれらの基本性質をまとめたBlockクラスを作っておくと様々なブロックで使いまわせて楽ですね!

js/Block.js
class Block {
  constructor(...placementArray) {
    this.placement = placementArray.map(line => line.split(""));
    this.width = Math.max.apply(null, this.placement.map(line => line.length));
    this.height = this.placement.length;
  }

  rotateClockwise() {
    let newPlacement = [];

    for(let i = 0; i < this.width; ++i) {
      newPlacement[i] = this.placement.map(line => line[i] || " ").reverse();
    }

    this.placement = newPlacement;
    this.width = Math.max.apply(null, this.placement.map(line => line.length));
    this.height = this.placement.length;
  } 

  rotateCounterclockwise() {
    let newPlacement = [];
    let test = [];

    for(let i = 0; i < this.width; ++i) {
      newPlacement[i] = this.placement.map(line => line[i] || " ");
    }

    this.placement = newPlacement.reverse();
    this.width = Math.max.apply(null, this.placement.map(line => line.length));
    this.height = this.placement.length;
  }

  getPlacement() {
    return this.placement;
  }

  toString(paddingLeft = 0, lineLength = 0) {
    let placementChain = "";
    let l = 0;

    for(let i = 0; i < this.placement.length; ++i, l = 0) {
      for(let j = 0; j < paddingLeft; ++j) {
        placementChain += " ";
        ++l;
      }
      placementChain += this.placement[i].join("");
      l += this.placement[i].length;
      for(; l < lineLength; ++l) {
        placementChain += " ";
      }
    }

    return placementChain;
  }
}

ほとんどメソッド名のままのメソッドなので細かな解説は割愛します。
ブロックの配置情報は2次元配列で記録してあります。
constructor(...placementArray)とすることで宣言時に引数を可変長にすることができます!(可変長引数は配列で値が受け渡されます。便利)
ちなみにreverseメソッドは配列を逆順にするメソッドなのですが、破壊的なメソッドのようなので注意してください。
コード中に出てくるline => line[i]みたいな表記はfunction(line) { return line[i]アロー関数によって略して、(line) => { return line[i] }
さらに引数が1つだと()が省略でき、文が1行だと{}が省略できるので、このような形になっています!

STEP2 いろいろなブロックを作る

継承しちゃいましょう!prototypeでゴリゴリやってた時代よりはるかに楽になりましたね!

js/Blocks.js
class Block1 extends Block {
  constructor() {
    super(
      "A", 
      "A",
      "A",
      "A"
    );
  }
}

class Block2 extends Block {
  constructor() {
    super(
      "BB", 
      " B",
      " B"
    );
  }
}

class Block3 extends Block {
  constructor() {
    super(
      "CC", 
      "C ",
      "C "
    );
  }
}

class Block4 extends Block {
  constructor() {
    super(
      "DD ",
      " DD"
    );
  }
}

class Block5 extends Block {
  constructor() {
    super(
      " EE",
      "EE "
    );
  }
}

class Block6 extends Block {
  constructor() {
    super(
      " F ",
      "FFF"
    );
  }
}

class Block7 extends Block {
  constructor() {
    super(
      "GG",
      "GG"
    );
  }
}

もはや解説することがないので次にいきます!

盤の準備1

とりあえずゲーム盤のクラスを仮組しておきましょう!

STEP1 必要なメソッドを準備

とりあえずどんなメソッドが必要か書き出しておきます!

js/Board.js
class Board {
  constructor() {
    this.status = [];
    for(let i = 0; i < 21; ++i) {
      this.status[i] = [];
      for(let j = 0; j < 10; ++j) {
        this.status[i][j] = " ";
      }
    }
    this.fallingBlock = null;
    this.fallingBlockPos = [0, 5];
  }

  start() {
  }

  generateNewBlock() {
  }

  update() {
  }

  putBlock() {
  }

  fallBlock() {
  }

  canFallBlock() {
  }

  deleteLines() {
  }

  ifOverwrapBlocks() {
  }

  rotateBlockClockwiseIfPossible() {
  }

  rotateBlockCounterclockwiseIfPossible() { 
  }

  moveBlockToRightIfPossible() {
  }

  moveBlockToLeftIfPossible() {
  }

  toString() {
  }
}

だいたいメソッド名のままです。少し仕様を説明すると、ifOverwrapBlocksは落下中のブロックと盤に固定されたブロックが重なっていないかを確認するメソッドにします!これは、ブロックを動かしたり回転したりする時には、必ず呼び出しておくことにします!もし重なってしまった場合には、その操作を取り消すような仕様にします。
updateメソッドは、一定間隔で実行されるメソッドで、ブロックを固定したり生成したり落としたりを行います
fallingBlockプロパティは落ちているブロックのオブジェクトを、fallingBlockPosプロパティは落ちているブロックの座標を指すことにします!
statusプロパティが盤のどこが空いているのか、ブロックがあるのかを記憶する変数になっています。

STEP2 ざっくり作ろう!

とりあえずメソッドの中身を書いていきましょう!

js/Board.js
class Board {
  constructor() {
    this.status = [];
    for(let i = 0; i < 21; ++i) {
      this.status[i] = [];
      for(let j = 0; j < 10; ++j) {
        this.status[i][j] = " ";
      }
    }
    this.fallingBlock = null;
    this.fallingBlockPos = [0, 5];
  }

  start() {
    update();
  }

  generateNewBlock() {
    let r = Math.floor(Math.random() * 7);

    switch(r) {
      case 0:
        this.fallingBlock = new Block1();
        break;
      case 1:
        this.fallingBlock = new Block2();
        break;
      case 2:
        this.fallingBlock = new Block3();
        break;
      case 3:
        this.fallingBlock = new Block4();
        break;
      case 4:
        this.fallingBlock = new Block5();
        break;
      case 5:
        this.fallingBlock = new Block6();
        break;
      case 6:
        this.fallingBlock = new Block7();
        break;
    }
    this.fallingBlockPos = [0, 5];
  }

  update() {
    if(this.fallingBlock == null) {
      this.generateNewBlock();
      if(this.ifOverwrapBlocks()) {
        console.log("ゲームオーバ");
        return;
      }
    } else if(this.canFallBlock()) {
      this.fallBlock();
    } else {
      this.putBlock();
      this.deleteLines();
    }
    setTimeout(() => { this.update() }, 150);
  }

  putBlock() {
  }

  fallBlock() {
    this.fallingBlockPos[0] += 1;
  }

  canFallBlock() {
    this.fallBlock();
    if(this.ifOverwrapBlocks()) {
      this.fallingBlockPos[0] -= 1;
      return false;
    } else {
      this.fallingBlockPos[0] -= 1;
      return true;
    }
  }

  deleteLines() {
    for(let i = 0; i < this.status.length; ++i) {
      if(this.status[i].filter(line => line != " ").length == 10) {
        this.status.splice(i, 1);
        this.status.unshift("          ".split(""));
      }
    }
  }

  ifOverwrapBlocks() {
  }

  rotateBlockClockwiseIfPossible() {
    this.fallingBlock.rotateClockwise();
    if(this.ifOverwrapBlocks()) {
      this.fallingBlock.rotateCounterclockwise();
    }
  }

  rotateBlockCounterclockwiseIfPossible() { 
    this.fallingBlock.rotateCounterclockwise();
    if(this.ifOverwrapBlocks()) {
      this.fallingBlock.rotateClockwise();
    }
  }

  moveBlockToRightIfPossible() {
    this.fallingBlockPos[1] += 1;
    if(this.ifOverwrapBlocks()) {
      this.fallingBlockPos[1] -= 1;
    }
  }

  moveBlockToLeftIfPossible() {
    this.fallingBlockPos[1] -= 1;
    if(this.ifOverwrapBlocks()) {
      this.fallingBlockPos[1] += 1;
    }
  }

  toString() {
  }
}

とりあえず、ほとんど何も考えずに書ける部分が埋まりました!
filterメソッドは、配列の中から条件に合う要素だけを残すことができるメソッドです!(引数に関数を入れて返り値がtrueのものが残ります)このメソッドは破壊的では、ありません。

STEP3 toStringメソッドを作ろう

座標計算ゴリゴリ・・・

js/Board.js(抜粋)
toString() {
  let statusChain = [];
  let margin = this.fallingBlockPos[0] * 10;
  let tmp = (this.fallingBlock != null) ? this.fallingBlock.toString(this.fallingBlockPos[1], 10) : "";

  for(let i = 0; i < this.status.length; ++i) {
    for(let j = 0; j < this.status[i].length; ++j) {
      if(i * 10 + j >= margin && i * 10 + j < margin + tmp.length && tmp[i * 10 + j - margin] != " ") {
        statusChain[i * 10 + j] = tmp[i * 10 + j - margin];
      } else {
        statusChain[i * 10 + j] = this.status[i][j];
      }
    }
  }

  return statusChain.join("");
}

this.statusが盤の情報を持っているのですが、落下中のブロックはここに書かずthis.fallingBlockに持たせているのでうまく組み合わせる必要があります。

一旦インターフェース系

このままではデバッグが行いにくいので、ユーザからの入力を受け付け盤にブロックが表示されるようにします!

js/application.js
window.onload = () => {
  let table = document.createElement("table");
  let thead = document.createElement("thead");
  let tbody = document.createElement("tbody");
  let board = new Board();
  let cells = [];

  for(let i = 0; i < 21; ++i) {
    let tr = document.createElement("tr");

    for(let j = 0; j < 10; ++j) { 
      let td = document.createElement("td");
      cells.push(td);
      tr.appendChild(td);
    }
    tbody.appendChild(tr);
  }
  table.appendChild(thead);
  table.appendChild(tbody);
  document.body.appendChild(table);

  board.start();
  document.onkeydown = (e) => {
    if(e.keyCode == 37) {
      board.moveBlockToLeftIfPossible();
      e.preventDefault();
    } else if(e.keyCode == 38) {
      board.rotateBlockClockwiseIfPossible();
      e.preventDefault();
    } else if(e.keyCode == 39) {
      board.moveBlockToRightIfPossible();
      e.preventDefault();
    } else if(e.keyCode == 40) {
      board.rotateBlockCounterclockwiseIfPossible();
      e.preventDefault();
    }
  }

  setInterval(() => {
    let tmp = board.toString();

    for(let i = 0; i < tmp.length; ++i) {
      switch(tmp[i]) {
        case "A":
          cells[i].style.backgroundColor = "#ff0000";
          break;
        case "B":
          cells[i].style.backgroundColor = "#00ff00";
          break;
        case "C":
          cells[i].style.backgroundColor = "#0000ff";
          break;
        case "D":
          cells[i].style.backgroundColor = "#ffff00";
          break;
        case "E":
          cells[i].style.backgroundColor = "#ff00ff";
          break;
        case "F":
          cells[i].style.backgroundColor = "#00ffff";
          break;
        case "G":
          cells[i].style.backgroundColor = "#000000";
          break;
        default:
          cells[i].style.backgroundColor = "transparent";
      }
    }
  }, 10);
}

10msごとにtoStringメソッドを呼び出して、盤の情報を書き出しています。ブロックごとに文字を変えているので、それを元に表示の色を変えています!

盤の準備2

STEP1 ブロックにあたり判定をつける。

これも座標計算ゴリゴリ・・・

js/Board.js(抜粋)
ifOverwrapBlocks() {
  let block = this.fallingBlock.getPlacement();
  for(let i = 0; i < block.length; ++i) {
    for(let j = 0; j < block[i].length; ++j) {
      let pos = this.fallingBlockPos;
      let y = pos[0] + i, x = pos[1] + j;

      if(y < 0 || y > 20 || x < 0 || x > 9 || (block[i][j] != " " && this.status[y][x] != " ")) {
        return true;
      }
    }
  }
  return false;
}

ブロックが動かせるかどうかは、このifOverwrapBlocksメソッドで計算します!ブロックを動かすことで重なりあうと判定した場合には、その動作をキャンセルすることにします!

STEP2 動かなくなったブロックを止める!

putBlockメソッドを実装して、落下できなくなったブロックを固定します!

js/Board.js(抜粋)
putBlock() {
  let block = this.fallingBlock.getPlacement();
  let pos = this.fallingBlockPos;

  for(let i = 0; i < block.length; ++i) {
    for(let j = 0; j < block[i].length; ++j) {
      if(block[i][j] != " ") {
        this.status[pos[0] + i][pos[1] + j] = block[i][j];
      }
    }
  }
  this.fallingBlock = null;
}

気合いで書き出しています!

完成

スクリーンショット 2016-10-02 23.42.07.png

これで一応完成です!得点とかは、適当に実装してみてください!