MTI Engineer Blog

エムティーアイ エンジニアブログ

素のHTML, CSS, JavaScriptで0から始めるWeb開発【HTML&JavaScript基礎編】

こんにちは、エムティーアイの井上です。今回はこれからWeb開発を始めたいという方向けにWeb開発の基礎となる部分を紹介します。

このシリーズのゴールはWeb開発に必要な3つの言語 HTML, CSS, JavaScript の仕組みを理解し書けるようになることにあります。HTML, CSS, JavaScriptでテトリスを作りながらそれぞれの仕組みをご紹介いたします。 全体で4部構成になっており今回はその第3弾【HTML&JavaScript基礎編】となります。

f:id:mti-hackers:20171117141649p:plain

一定時間で繰り返す

JavaScriptでは画面上のさまざまな要素を書き換えることができます。第1弾では文字を書き換えましたね。 今回は一定時間ごとに繰り返す処理を書いてみます。 main.jsを次のように書き加えてみましょう

document.getElementById("hello_text").textContent = "はじめてのJavaScript";

var count = 0;
setInterval(function() {
  // 何回目かを数えるために変数countを1ずつ増やす
  count++;
  document.getElementById("hello_text").textContent = "はじめてのJavaScript(" + count + ")"; // 何回目かを文字にまとめて表示する
}, 1000);

まず var count = 0; の部分でcountという変数を定義します。 そしてsetIntervalというのは一定の間隔で繰り返すための関数です。 setInterval(動かしたい関数, 繰り返す間隔(ms)); という書き方で繰り返しの処理を行うことができます。 今回の例ではcountを1ずつ増やしてタイトルに表示しています。

それではこの繰り返しを用いてブロックが1秒間に1マス落ちるようにしましょう!

第2弾で制作したゲーム盤の一つ一つは<td>タグでしたね。 まずHTML上の<td>タグをすべてJavaScriptから取得します。

document.getElementById("hello_text").textContent = "はじめてのJavaScript";

var count = 0;
setInterval(function() {
  count++;
  document.getElementById("hello_text").textContent = "はじめてのJavaScript(" + count + ")";
  var td_array = document.getElementsByTagName("td"); // 200個の要素を持つ配列
}, 1000);

このときtd_arrayには全部で200マス分の<td>タグが格納されています。 格納されている順番は次の通りです。

f:id:mti-hackers:20171117142748j:plain

このままではtd_arrayの何番目がどのマスを示しているかわかりずらいですね… 2次元配列にして座標のように扱えるよう変形してみましょう!

document.getElementById("hello_text").textContent = "はじめてのJavaScript";

var count = 0;
setInterval(function() {
  count++;
  document.getElementById("hello_text").textContent = "はじめてのJavaScript(" + count + ")";
  var td_array = document.getElementsByTagName("td");
  var cells = [];
  var index = 0;
  for (var row = 0; row < 20; row++) {
    cells[row] = []; // 配列のそれぞれの要素を配列にする(2次元配列にする)
    for (var col = 0; col < 10; col++) {
      cells[row][col] = td_array[index];
      index++;
    }
  }
}, 1000);

これはfor文を用いてcells[何行目][何列目]と指定できるようになります。 cellsの指し示す中身は以下の通りになります。 (あまりに多いので一部のみ掲載します)

f:id:mti-hackers:20171117142802j:plain

cellsの中に入っているのはタグそのものを指し示すためクラスを付けたりなくしたりできます! これを活用することでそれぞれのマスを示す<td>タグのクラスを一つ下のマスを示す<td>に移すことであたかもブロックが落ちているように見せます! 流れとしては 1. 一番下の行を見る 2. クラスを空にする 3. 下から2番目の行を見る 4. もしもクラスが振られていたら一行下のマスにクラスを写して自分のクラスを空にする 5. 4を一番上の行に達するまで繰り返す となります。 コードにするとこうなります。

document.getElementById("hello_text").textContent = "はじめてのJavaScript";

var count = 0;
setInterval(function() {
  count++;
  document.getElementById("hello_text").textContent = "はじめてのJavaScript(" + count + ")";
  var td_array = document.getElementsByTagName("td");
  var cells = [];
  var index = 0;
  for (var row = 0; row < 20; row++) {
    cells[row] = [];
    for (var col = 0; col < 10; col++) {
      cells[row][col] = td_array[index];
      index++;
    }
  }
  // 一番下の行のクラスを空にする
  for (var i = 0; i < 10; i++) {
    cells[19][i].className = "";
  }
  // 下から二番目の行から繰り返しクラスを下げていく
  for (var row = 18; row >= 0; row--) {
    for (var col = 0; col < 10; col++) {
      if (cells[row][col].className !== "") {
        cells[row + 1][col].className = cells[row][col].className;
        cells[row][col].className = "";
      }
    }
  }
}, 1000);

これで1秒ごとにブロックが落ちていくように見えましたか?

さて、これから

  • ランダムにブロックを生成したり
  • ブロックが積み重なるようにしたり
  • そろった行を削除したり
  • 矢印キーでブロックを移動させたり

していくわけですが それぞれをただ書き続けているとプログラムが読みにくくなってしまいますね…

ここで関数を使ってまとめていきます!

現状のJavaScriptを関数を使って整理してみましょう! 現在行っている処理は大きく分けて二つありますね?

  1. ゲーム盤の状態を2次元配列にまとめる
  2. ブロック(色がついているマス)を一つ下に移動させる

この二つを関数としてloadTablefallBlocksにまとめます。 全体の構成としてはこのようになります。

var cells; // ゲーム盤を示す変数

loadTable(); // ゲーム盤を読み込む
setInterval(function () {
  fallBlocks(); // ブロックを落とす
}, 1000);

/* ------ ここから下は関数の宣言部分 ------ */

function loadTable() {
  // ゲーム盤を変数cellsにまとめる
}

function fallBlocks() {
  // ブロックを落とすプログラムを記述する
}

※関数の中の実装は省略しています

それぞれの関数の中身はすでに実装したコードの一部となります。 しかし、関数でまとめることで上記の全体像のように流れが分かりやすくなりますね。

実際に関数の中身を実装すると次のようになります。

document.getElementById("hello_text").textContent = "はじめてのJavaScript";

var count = 0;

var cells;

loadTable();
setInterval(function () {
  count++;
  document.getElementById("hello_text").textContent = "はじめてのJavaScript(" + count + ")";
  fallBlocks();
}, 1000);

/* ------ ここから下は関数の宣言部分 ------ */

function loadTable() {
  cells = [];
  var td_array = document.getElementsByTagName("td");
  var index = 0;
  for (var row = 0; row < 20; row++) {
    cells[row] = [];
    for (var col = 0; col < 10; col++) {
      cells[row][col] = td_array[index];
      index++;
    }
  }

}

function fallBlocks() {
  // 一番下の行のクラスを空にする
  for (var i = 0; i < 10; i++) {
    cells[19][i].className = "";
  }
  // 下から二番目の行から繰り返しクラスを下げていく
  for (var row = 18; row >= 0; row--) {
    for (var col = 0; col < 10; col++) {
      if (cells[row][col].className !== "") {
        cells[row + 1][col].className = cells[row][col].className;
        cells[row][col].className = "";
      }
    }
  }
}

それではこれから記述するプログラムをあらかじめ機能ごとに関数に分けて記述してみましょう!

document.getElementById("hello_text").textContent = "はじめてのJavaScript";

var count = 0;

var cells;

loadTable();
setInterval(function () {
  count++;
  document.getElementById("hello_text").textContent = "はじめてのJavaScript(" + count + ")";
  if (hasFallingBlock()) { // 落下中のブロックがあるか確認する
    fallBlocks();// あればブロックを落とす
  } else { // なければ
    deleteRow();// そろっている行を消す
    generateBlock();// ランダムにブロックを作成する
  }
}, 1000);

/* ------ ここから下は関数の宣言部分 ------ */

function loadTable() {
  /* 省略 */
}

function fallBlocks() {
  /* 省略 */
}

function hasFallingBlock() {
  // 落下中のブロックがあるか確認する
  return true;
}

function deleteRow() {
  // そろっている行を消す
}

function generateBlock() {
  // ランダムにブロックを生成する
}

function moveRight() {
  // ブロックを右に移動させる
}

function moveLeft() {
  // ブロックを左に移動させる
}

もちろん現状では中身を実装していないので何も動きませんが、 これから何を実装するかはわかりやすくなりましたね。

それではまず初めに落下中のブロックがあるかを判定する関数を実装します。 この関数があることで、 - ブロックを落とすのか あるいは - そろった行を消して新しいブロックを生成するのか を切り替えることが可能になります。

実装としては落下中かどうかをフラグ(変数)で持ちブロックを落とすときに判断します。 ブロックが一番下の行に達した時に落下中のフラグはfalseになります。 落下中のフラグは変数isFallingで保持します。

/* 省略 */

function fallBlocks() {
  // 一番下の行にブロックがあれば落下中のフラグをfalseにする
  for (var i = 0; i < 10; i++) {
    if (cells[19][i].className !== "") {
      isFalling = false;
      return; // 一番下の行にブロックがいるので落とさない
    }
  }
  // 下から二番目の行から繰り返しクラスを下げていく
  for (var row = 18; row >= 0; row--) {
    for (var col = 0; col < 10; col++) {
      if (cells[row][col].className !== "") {
        cells[row + 1][col].className = cells[row][col].className;
        cells[row][col].className = "";
      }
    }
  }
}

var isFalling = true;
function hasFallingBlock() {
  // 落下中のブロックがあるか確認する
  return isFalling;
}

/* 省略 */

これで一番下までブロックが来た時にブロックが落ちなくなりましたね?

それでは次にランダムにブロックを生成する関数を実装しましょう!

ブロックにはいくつかのパターンがあり、それぞれのパターンには決められたクラスが存在します。 これらをif文ですべて分岐していたら大変なのでJavaScriptのObjectを使って管理します。 Objectとは配列に似た機能を用い複数のデータをまとめるときに使用します。 今回の場合、「パターン名」「クラス名」「ブロックの配置パターン」をまとめることになります。 配列との違いはそれぞれの要素を番号ではなく名前で呼び出せることにあります。 例えば配列の場合 hoge[0],hoge[1]と呼び出していましたが、 Objectの場合 hoge["title"],hoge["desc"]と呼び出せます。 こちらの方がコードが読みやすいですね。 それでは「パターン名」「クラス名」「ブロックの配置パターン」を持ったObjectを定義します。

document.getElementById("hello_text").textContent = "はじめてのJavaScript";

var count = 0;

var cells;

// ブロックのパターン
var blocks = {
  i: {
    class: "i",
    pattern: [
      [1, 1, 1, 1]
    ]
  },
  o: {
    class: "o",
    pattern: [
      [1, 1], 
      [1, 1]
    ]
  },
  t: {
    class: "t",
    pattern: [
      [0, 1, 0], 
      [1, 1, 1]
    ]
  },
  s: {
    class: "s",
    pattern: [
      [0, 1, 1], 
      [1, 1, 0]
    ]
  },
  z: {
    class: "z",
    pattern: [
      [1, 1, 0], 
      [0, 1, 1]
    ]
  },
  j: {
    class: "j",
    pattern: [
      [1, 0, 0], 
      [1, 1, 1]
    ]
  },
  l: {
    class: "l",
    pattern: [
      [0, 0, 1], 
      [1, 1, 1]
    ]
  }
};

loadTable();
/* 以下省略 */

i型(横一列)のブロックを例に説明します。 まずi型のブロックのみを表す部分は以下の部分です。

// ブロックのパターン
var blocks = {
  i: {
    class: "i",
    pattern: 1, 1, 1, 1
  },
  /* 省略 */
};

まずi : { ... }となっている塊がi型ブロック自身を示しており、blocks["i"]で呼び出すことができます。 そしてclass: "i"という要素はblocks["i"].classで呼び出すことができその値は"i"になります。 同様にpatternに関してもblocks["i"].patternで呼び出すことができます。 patternは2次元配列を用いて1の部分にはブロックを置き、0のところにはブロックを置かないものとしてブロックの配置パターンを定義しています。 i型の場合は横一列に4ブロック並ぶので

[
  [1, 1, 1, 1]
]

t型なら

[
  [0, 1, 0],
  [1, 1, 1]
]

となります。

このブロックパターンからランダムにパターンを選出しゲーム盤の一番上に配置します。 この処理は3つの処理に分けて考えることができますね。 コメントアウトで記述すると下記のとおりです。

function generateBlock() {
  // ランダムにブロックを生成する
  // 1. ブロックパターンからランダムに一つパターンを選ぶ
  // 2. 選んだパターンをもとにブロックを配置する
  // 3. 落下中のブロックがあるとする
}

それではまずブロックパターンからランダムに一つパターンを選びます。

function generateBlock() {
  // ランダムにブロックを生成する
  // 1. ブロックパターンからランダムに一つパターンを選ぶ
  var keys = Object.keys(blocks);
  var nextBlockKey = keys[Math.floor(Math.random() * keys.length)];
  var nextBlock = blocks[nextBlockKey];
  // 2. 選んだパターンをもとにブロックを配置する
  // 3. 落下中のブロックがあるとする
}

JavaScriptのObjectはそれぞれの名前(String)で要素を指定するため一工夫必要です。 まず初めに名前の配列を取得します(1行目) 次に名前の一覧から要素をランダムに選びます(2行目) 最後にランダムに選んだ名前をもとに要素を取得します(3行目) 2行目のランダムに配列の要素を取得している部分では0から1までの乱数を生成するMath.random()に配列が何個あるか(keys.length)を掛け、それをMath.floor()によって小数点以下を切りすてることで選んでいます。

次に選んだパターンからブロックを配置します。 このときブロックがゲーム盤の中央上部に登場するよう左から4番目のマスから配置していきます。

function generateBlock() {
  // ランダムにブロックを生成する
  // 1. ブロックパターンからランダムに一つパターンを選ぶ
  var keys = Object.keys(blocks);
  var nextBlockKey = keys[Math.floor(Math.random() * keys.length)];
  var nextBlock = blocks[nextBlockKey];
  // 2. 選んだパターンをもとにブロックを配置する
  var pattern = nextBlock.pattern;
  for (var row = 0; row < pattern.length; row++) {
    for (var col = 0; col < pattern[row].length; col++) {
      if (pattern[row][col]) {
        cells[row][col + 3].className = nextBlock.class;
      }
    }
  }
  // 3. 落下中のブロックがあるとする
}

これでブロックが一番下まで達したときに新たにブロックが生成されるようになります。 しかし、このままでは落下中のブロックがあるとは思わず、1秒経った時にまたブロックを生成し続けてしまいます。 それを防ぐために、isFallingtrueにしておきましょう。

function generateBlock() {
  // ランダムにブロックを生成する
  // 1. ブロックパターンからランダムに一つパターンを選ぶ
  var keys = Object.keys(blocks);
  var nextBlockKey = keys[Math.floor(Math.random() * keys.length)];
  var nextBlock = blocks[nextBlockKey];
  // 2. 選んだパターンをもとにブロックを配置する
  var pattern = nextBlock.pattern;
  for (var row = 0; row < pattern.length; row++) {
    for (var col = 0; col < pattern[row].length; col++) {
      if (pattern[row][col]) {
        cells[row][col + 3].className = nextBlock.class;
      }
    }
  }
  // 3. 落下中のブロックがあるとする
  isFalling = true;
}

さて、これで一見正しく動きそうですが、実は動きません… なぜかというと、現在のフローでは

f:id:mti-hackers:20171117142824j:plain

このときの条件である一番下の行にブロックが達したかですが、どのブロックでも一番下にあれば新しくブロックを生成してしまうため理想的な動きではありません。 そこで生成したブロックに番号を振り、落下中だったブロックが落ちなくなったら新しくブロックを生成するようにします。

まず、この実装のために最初から配置していたブロックをなくします。 (index.htmlからclass="*"を削除します)

そして、最初の段階ではisFalling(落ちているブロック)はないはずなのでfalseで初期化します。

var isFalling = false;
function hasFallingBlock() {
  // 落下中のブロックがあるか確認する
  return isFalling;
}

次にブロックを生成する際に番号を振り、その番号をfallingBlockNumという変数で保持します。

var fallingBlockNum = 0;
function generateBlock() {
  // ランダムにブロックを生成する
  // 1. ブロックパターンからランダムに一つパターンを選ぶ
  var keys = Object.keys(blocks);
  var nextBlockKey = keys[Math.floor(Math.random() * keys.length)];
  var nextBlock = blocks[nextBlockKey];
  var nextFallingBlockNum = fallingBlockNum + 1;
  // 2. 選んだパターンをもとにブロックを配置する
  var pattern = nextBlock.pattern;
  for (var row = 0; row < pattern.length; row++) {
    for (var col = 0; col < pattern[row].length; col++) {
      if (pattern[row][col]) {
        cells[row][col + 3].className = nextBlock.class;
        cells[row][col + 3].blockNum = nextFallingBlockNum;
      }
    }
  }
  // 3. 落下中のブロックがあるとする
  isFalling = true;
  fallingBlockNum = nextFallingBlockNum;
}

そして、fallBlocks関数ではただブロックを落とすだけではなく、1マス落としていいのかどうか正しく判定する必要が出てきました。 (これまでのプログラムでは落としていいかどうかはほとんど判定していない) ただし、チェックしなければいけない条件はいたってシンプルです。

  1. 底についていないか?
  2. 1マス下に別のブロックがないか?

これを実装します。

function fallBlocks() {
  // 1. 底についていないか?
  for (var col = 0; col < 10; col++) {
    if (cells[19][col].blockNum === fallingBlockNum) {
      isFalling = false;
      return; // 一番下の行にブロックがいるので落とさない
    }
  }
  // 2. 1マス下に別のブロックがないか?
  for (var row = 18; row >= 0; row--) {
    for (var col = 0; col < 10; col++) {
      if (cells[row][col].blockNum === fallingBlockNum) {
        if (cells[row + 1][col].className !== "" && cells[row + 1][col].blockNum !== fallingBlockNum){
          isFalling = false;
          return; // 一つ下のマスにブロックがいるので落とさない
        }
      }
    }
  }
  // 下から二番目の行から繰り返しクラスを下げていく
  for (var row = 18; row >= 0; row--) {
    for (var col = 0; col < 10; col++) {
      if (cells[row][col].blockNum === fallingBlockNum) {
        cells[row + 1][col].className = cells[row][col].className;
        cells[row + 1][col].blockNum = cells[row][col].blockNum;
        cells[row][col].className = "";
        cells[row][col].blockNum = null;
      }
    }
  }
}

これでだいぶゲームらしくなってきたのではないのでしょうか?

それでは次回以降で

  • そろった行を消す
  • キーボードでブロックを操作する
  • 積みあがったらゲームオーバーとする

といった機能の説明をいたします。

今回の記事でJavaScriptの動きがだいぶつかめてきたでしょうか?

次回、いよいよゲームを完成させましょう!