PR

4日目 テトリミノの落下と回転【JavaScriptのタイマー処理】

テトリスを作ろう

はじめに

スマホだけを使って、一週間でテトリスを作っていきます。

1日目 キャンバスの設定と描写
2日目 フィールドの設定と描写
3日目 テトリミノの設定と描写
▶︎4日目 テトリミノの落下と回転
5日目 スマホでの操作と画面固定
6日目 テトリミノの当たり判定
7日目 ラインを消す処理

パソコンがなくても、スマホだけでwebアプリケーションの開発ができます。

プログラミングに興味がある人やプログラミングの勉強を始めたばかりの人に向けて、一からわかりやすく解説していきます。

4日目でやること

前回は、テトリミノ(テトリスのブロック)を多次元配列の変数で設定して、フィールド上に描画するところまでやりました。

今回は、テトリミノ(テトリスのブロック)の落下処理と、画面をタップしたときにテトリミノが回転するところまでをやっていきます。

4日目 テトリミノの落下と回転

全体のソースコード

まずは完成したソースコードをご覧ください。

HTMLのコード

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
  <title>テトリス</title>
</head>

<body>
  <div class="main">
    <canvas id="canvas01"></canvas>
  </div>
  <script src="script.js"></script>
</body>

</html>

CSSのコード

html {
  height: 100%;
}

body {
  height: 100%;
  margin: 0;
}

.main {
  height: 100%;
  text-align: center;
}

#canvas01 {
  height: calc(100% - 8px);
}

JavaScriptのコード

// ============ 変数 ============

// フィールドサイズ
const FIELD_X = 10   // 横のマスの数
const FIELD_Y = 20   // 縦のマスの数
const MINO_SIZE = 30 // マスの大きさ

// テトリミノが落ちる速度
const DROP_SPEED = 500

// キャンバスの設定
let canvas = document.getElementById("canvas01")
let conText = canvas.getContext("2d")
canvas.width = MINO_SIZE * FIELD_X
canvas.height = MINO_SIZE * FIELD_Y
canvas.style.border = "4px solid #050505"

// 色の設定
const COLOR = [
    "#F2F2F2",   // フィールド背景
    "#C8C8C8",   // 枠の線
    "#00F2F2",   // Iミノ 水色
    "#F2F200",   // Oミノ 黄色
    "#00F200",   // Sミノ 黄緑
    "#F20000",   // Zミノ 赤
    "#0000F2",   // Jミノ 青
    "#F2A200",   // Lミノ オレンジ
    "#F200F2"    // Tミノ 紫
]

// フィールドの初期設定
let field = []
for (let i = 0; i < FIELD_Y; i++) {
  field[i] = []
  for (let j = 0; j < FIELD_X; j++) {
    field[i][j] = 0
  }
}

// テトリミノの初期設定
let mino_x = Math.ceil((FIELD_X / 2) - 2)
let mino_y = 0

let mino_type = Math.trunc(Math.random() * 7)
let mino_angle = 0

let minoshapes = [
// mino_type_I
  // mino_angle_0
  [[
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0]
  ],
  // mino_angle_90
  [
    [0,0,0,0],
    [0,0,0,0],
    [2,2,2,2],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0]
  ],
  // mino_angle_270
  [
    [0,0,0,0],
    [0,0,0,0],
    [2,2,2,2],
    [0,0,0,0]
  ]],
// mino_type_O
  // mino_angle_0
  [[
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_S
  // mino_angle_0
  [[
    [0,4,4,0],
    [4,4,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,4,0,0],
    [0,4,4,0],
    [0,0,4,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,4,4,0],
    [4,4,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,4,0,0],
    [0,4,4,0],
    [0,0,4,0],
    [0,0,0,0]
  ]],
// mino_type_Z
  // mino_angle_0
  [[
    [5,5,0,0],
    [0,5,5,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,0,5,0],
    [0,5,5,0],
    [0,5,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [5,5,0,0],
    [0,5,5,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,0,5,0],
    [0,5,5,0],
    [0,5,0,0],
    [0,0,0,0]
  ]],
// mino_type_J
  // mino_angle_0
  [[
    [0,0,6,0],
    [0,0,6,0],
    [0,6,6,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,6,0,0],
    [0,6,6,6],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,6,6,0],
    [0,6,0,0],
    [0,6,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [6,6,6,0],
    [0,0,6,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_L
  // mino_angle_0
  [[
    [0,7,0,0],
    [0,7,0,0],
    [0,7,7,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,7,7,7],
    [0,7,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,7,7,0],
    [0,0,7,0],
    [0,0,7,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,0,7,0],
    [7,7,7,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_T
  // mino_angle_0
  [[
    [8,8,8,0],
    [0,8,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,8,0,0],
    [8,8,0,0],
    [0,8,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,8,0,0],
    [8,8,8,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,8,0,0],
    [0,8,8,0],
    [0,8,0,0],
    [0,0,0,0]
  ]]
]


// ============ 関数 ============

// フィールドの表示
function display() {
  for (let i = 0; i < FIELD_Y; i++) {
    for (let j =0; j < FIELD_X; j++) {
      // 塗りつぶしの四角を描画
      conText.fillStyle = COLOR[field[i][j]]
      conText.fillRect(MINO_SIZE * j, MINO_SIZE * i, MINO_SIZE, MINO_SIZE)

      // 輪郭の四角を描画
      conText.strokeStyle = COLOR[1]
      conText.lineWidth = 1
      conText.strokeRect(MINO_SIZE * j, MINO_SIZE * i, MINO_SIZE, MINO_SIZE)
    }
  }
}

// テトリミノの表示
function drawmino() {
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (minoshapes[mino_type][mino_angle][i][j] > 1) {
        // 新しい座標を定義
        let px = MINO_SIZE * (mino_x + j)
        let py = MINO_SIZE * (mino_y + i)

        // 塗りつぶしの四角を描画
        conText.fillStyle = COLOR[minoshapes[mino_type][mino_angle][i][j]]
        conText.fillRect(px, py, MINO_SIZE, MINO_SIZE)

        // 輪郭の四角を描画
        conText.strokeStyle = COLOR[1]
        conText.lineWidth = 1
        conText.strokeRect(px, py, MINO_SIZE, MINO_SIZE)
      }
    }
  }
}

// テトリミノの落下
function dropmino() {
  // 動けるなら落下
  mino_y += 1
  display()
  drawmino()
}


// ========= スマホ操作 ==========

canvas.addEventListener("touchend", SwipeEnd)

function SwipeEnd() {
  // 動けるなら回転
  mino_angle = (mino_angle + 1) % 4
  display()
  drawmino()
}


// ============ 実行 ============

// 初期画面の表示
display()
drawmino()
setInterval(dropmino, DROP_SPEED)

JavaScriptの解説

テトリミノの落下処理

一定時間ごとにテトリミノの1マス下へ移動する処理をします。

テトリミノの1マス下へ移動する処理

// テトリミノの落下
function dropmino() {
  // 動けるなら落下
  mino_y += 1
  display()
  drawmino()
}

function dropmino() { .. }

関数名を dropmino にして、{ } 内に実行する処理を定義します。dropmino() で関数を呼び出すことができます。

mino_y += 1

テトリミノを描画する y座標「mino_y」に1を足します。

つまり、テトリミノが1つ下のマスに移動することになります。

「mino_y += 1」は、「mino_y = mino_y + 1」と同じです。

また、「mino_y++」や「++mino_y」も同じく変数の値に1を増やす演算となります。

mino_y = mino_y + 1
mino_y += 1
mino_y++
++mino_y

変数の値に 1を加算するだけなら、結果はすべて同じです。

display()

フィールドを表示する関数を呼び出します。

drawmino()

テトリミノを表示する関数を呼び出します。

新しいフィールドを描画することで元のテトリミノが消えて、新しい位置にテトリミノを描画します。

そうすることで、テトリミノが落下しているように見えます。

同じ処理を一定時間ごとに繰り返す

setInterval(dropmino, DROP_SPEED)

setIntervalでタイマー処理を行う

setInterval(繰り返し実行したい関数名, 経過するたびに関数を呼び出す時間)

経過するたびに関数を呼び出す時間は「 1000 」で 1秒です。

例:「 500 」なら 0.5秒ごとに繰り返し処理します。

setInterval(dropmino, DROP_SPEED)

変数DROP_SPEED は 500なので、関数dropminoで 0.5秒ごとにテトリミノが1つ下のマスに移動します。

スマホ操作(タップした時の処理)

// ========= スマホ操作 ==========

canvas.addEventListener("touchend", SwipeEnd)

function SwipeEnd() {
  // 動けるなら回転
  mino_angle = (mino_angle + 1) % 4
  display()
  drawmino()
}

タップした時に関数を呼び出す「addEventListener」

対象の要素.addEventListener(特定の操作, 呼び出される関数名)

対象の要素に特定の操作が行われた時に、呼び出される関数を設定します。

よく使う特定の操作

タッチイベント
“touchstart”:タップしたとき(画面に指が触れたとき)
“touchmove”:スワイプしたとき(触れている指が動いたとき)
“touchend”:タップが終わったとき(触れている指が離れたとき)

マウスイベント
“click”:マウスボタンをクリックしたとき
“mousedown”:マウスボタンを押したとき
“mousemove”:マウスカーソルが移動したとき
“mouseup”:マウスボタンを離したとき

canvas.addEventListener(“touchend”, SwipeEnd)

キャンバスの要素上でタップが終わった時に、関数SwipeEndでテトリミノを回転させます。

function SwipeEnd() { .. }

関数名を SwipeEnd にして、{ } 内に実行する処理を定義します。SwipeEnd() で関数を呼び出すことができます。

mino_angle = (mino_angle + 1) % 4

テトリミノの回転した形の変数mino_angleの値を 0 → 1 → 2 → 3 → 0 →・・と変更します。

mino_angleの値に 1を足して、4になったら 0に戻します。

「 % 」は、割り算の余りを求める計算式です。

0 % 4 = 0
1 % 4 = 1
2 % 4 = 2
3 % 4 = 3
4 % 4 = 0
5 % 4 = 1
  :
  :
display()

フィールドを表示する関数を呼び出します。

drawmino()

テトリミノを表示する関数を呼び出します。

新しいフィールドを描画することで元のテトリミノが消えて、回転したテトリミノを新たに描画します。

そうすることで、テトリミノが回転しているように見えます。

最後にJavaScript全体のソースコードを載せておきます。

// ============ 変数 ============

// フィールドサイズ
const FIELD_X = 10   // 横のマスの数
const FIELD_Y = 20   // 縦のマスの数
const MINO_SIZE = 30 // マスの大きさ

// テトリミノが落ちる速度
const DROP_SPEED = 500

// キャンバスの設定
let canvas = document.getElementById("canvas01")
let conText = canvas.getContext("2d")
canvas.width = MINO_SIZE * FIELD_X
canvas.height = MINO_SIZE * FIELD_Y
canvas.style.border = "4px solid #050505"

// 色の設定
const COLOR = [
    "#F2F2F2",   // フィールド背景
    "#C8C8C8",   // 枠の線
    "#00F2F2",   // Iミノ 水色
    "#F2F200",   // Oミノ 黄色
    "#00F200",   // Sミノ 黄緑
    "#F20000",   // Zミノ 赤
    "#0000F2",   // Jミノ 青
    "#F2A200",   // Lミノ オレンジ
    "#F200F2"    // Tミノ 紫
]

// フィールドの初期設定
let field = []
for (let i = 0; i < FIELD_Y; i++) {
  field[i] = []
  for (let j = 0; j < FIELD_X; j++) {
    field[i][j] = 0
  }
}

// テトリミノの初期設定
let mino_x = Math.ceil((FIELD_X / 2) - 2)
let mino_y = 0

let mino_type = Math.trunc(Math.random() * 7)
let mino_angle = 0

let minoshapes = [
// mino_type_I
  // mino_angle_0
  [[
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0]
  ],
  // mino_angle_90
  [
    [0,0,0,0],
    [0,0,0,0],
    [2,2,2,2],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0],
    [0,2,0,0]
  ],
  // mino_angle_270
  [
    [0,0,0,0],
    [0,0,0,0],
    [2,2,2,2],
    [0,0,0,0]
  ]],
// mino_type_O
  // mino_angle_0
  [[
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,3,3,0],
    [0,3,3,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_S
  // mino_angle_0
  [[
    [0,4,4,0],
    [4,4,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,4,0,0],
    [0,4,4,0],
    [0,0,4,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,4,4,0],
    [4,4,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,4,0,0],
    [0,4,4,0],
    [0,0,4,0],
    [0,0,0,0]
  ]],
// mino_type_Z
  // mino_angle_0
  [[
    [5,5,0,0],
    [0,5,5,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,0,5,0],
    [0,5,5,0],
    [0,5,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [5,5,0,0],
    [0,5,5,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,0,5,0],
    [0,5,5,0],
    [0,5,0,0],
    [0,0,0,0]
  ]],
// mino_type_J
  // mino_angle_0
  [[
    [0,0,6,0],
    [0,0,6,0],
    [0,6,6,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,6,0,0],
    [0,6,6,6],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,6,6,0],
    [0,6,0,0],
    [0,6,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [6,6,6,0],
    [0,0,6,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_L
  // mino_angle_0
  [[
    [0,7,0,0],
    [0,7,0,0],
    [0,7,7,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,7,7,7],
    [0,7,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,7,7,0],
    [0,0,7,0],
    [0,0,7,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,0,7,0],
    [7,7,7,0],
    [0,0,0,0],
    [0,0,0,0]
  ]],
// mino_type_T
  // mino_angle_0
  [[
    [8,8,8,0],
    [0,8,0,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_90
  [
    [0,8,0,0],
    [8,8,0,0],
    [0,8,0,0],
    [0,0,0,0]
  ],
  // mino_angle_180
  [
    [0,8,0,0],
    [8,8,8,0],
    [0,0,0,0],
    [0,0,0,0]
  ],
  // mino_angle_270
  [
    [0,8,0,0],
    [0,8,8,0],
    [0,8,0,0],
    [0,0,0,0]
  ]]
]


// ============ 関数 ============

// フィールドの表示
function display() {
  for (let i = 0; i < FIELD_Y; i++) {
    for (let j =0; j < FIELD_X; j++) {
      // 塗りつぶしの四角を描画
      conText.fillStyle = COLOR[field[i][j]]
      conText.fillRect(MINO_SIZE * j, MINO_SIZE * i, MINO_SIZE, MINO_SIZE)

      // 輪郭の四角を描画
      conText.strokeStyle = COLOR[1]
      conText.lineWidth = 1
      conText.strokeRect(MINO_SIZE * j, MINO_SIZE * i, MINO_SIZE, MINO_SIZE)
    }
  }
}

// テトリミノの表示
function drawmino() {
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (minoshapes[mino_type][mino_angle][i][j] > 1) {
        // 新しい座標を定義
        let px = MINO_SIZE * (mino_x + j)
        let py = MINO_SIZE * (mino_y + i)

        // 塗りつぶしの四角を描画
        conText.fillStyle = COLOR[minoshapes[mino_type][mino_angle][i][j]]
        conText.fillRect(px, py, MINO_SIZE, MINO_SIZE)

        // 輪郭の四角を描画
        conText.strokeStyle = COLOR[1]
        conText.lineWidth = 1
        conText.strokeRect(px, py, MINO_SIZE, MINO_SIZE)
      }
    }
  }
}

// テトリミノの落下
function dropmino() {
  // 動けるなら落下
  mino_y += 1
  display()
  drawmino()
}


// ========= スマホ操作 ==========

canvas.addEventListener("touchend", SwipeEnd)

function SwipeEnd() {
  // 動けるなら回転
  mino_angle = (mino_angle + 1) % 4
  display()
  drawmino()
}


// ============ 実行 ============

// 初期画面の表示
display()
drawmino()
setInterval(dropmino, DROP_SPEED)

次回は「5日目 スマホでの操作と画面固定」です。