【JavaScript】200行で作るテトリスのレシピ【HTML5】

おっ立ち野郎が2015年1月4日19:16:56に投稿しました。

JavaScript

34

お気に入り

準備中

Views

このレシピで作るもの

世界的に有名なゲームであるテトリスをJavaScript(HTML5)で作っていきます。

2012年にDionysis Zindrosさんが教育目的で公開したテトリスのコードを使っていきます。動画 -> YouTube

元のコードのバグは修正済みです。

JavaScript + HTML + CSSで作るので、特殊なソフトウェアを必要とせず、このページを見ている環境があれば今すぐにでもテトリスを作ることができます。


目次

準備するもの

今回必要なものは以下のようになります。

  • Webページを読み取れるブラウザ
  • HTMLやJavaScriptを記述できる何か(最低限は、メモ帳

つくっていくもの

今回のレシピで作っていくファイルは以下のようになります。

  • index.html
  • style.css
  • tetris.js
  • render.js
  • controller.js

下準備

作業用ディレクトリを作る

js-tetrisというようなディレクトリを作ってください。

この中にどんどんファイルを追加していきテトリスを作っていきます。

効果音をDLする

名前を保存で以下の効果音ファイルをダウンロードしてください https://raw.githubusercontent.com/ottatiyarou/canvas-tetris/master/sound/pop.ogg

作業フォルダにsoundというディレクトリを作りその中にpop.oggとして保存してください

style.cssを作成する

作業ディレクトリの直下にstyle.cssを作成します

canvas {
    display: block;
    margin: auto;
    border: 1px solid black;
}

display: block;margin: auto;でキャンバスをセンタリングします。

block要素にmargin: autoを適用するとセンタリングできるのでこの二行となっています。

border: 1px solid black;では黒色の実線を1pxで描くことを示しています。

以上でCSSファイルの作成は終了です

index.htmlを作る

作業ディレクトリの直下にindex.htmlを以下のように記述してください。

<!DOCTYPE html>
<html>
    <head>
        <title>HTML5 Tetris</title>
        <link rel='stylesheet' href='style.css' />
    </head>
    <body>
        <audio id="clearsound" src="sound/pop.ogg" preload="auto"></audio>
        <canvas width='300' height='600'></canvas>
        <script src='js/tetris.js'></script>
        <script src='js/controller.js'></script>
        <script src='js/render.js'></script>
    </body>
</html>

おそらくここではまだJavaScriptファイルやCSSファイルを作成していないので以下のようにエラーがでてきます。

スクリーンショット 2014-12-31 14.27.09.png

ここからはこれらのファイルを作成していきます。

tetris.jsを書いていく

流れ

ブロックを落とすたびにnewGame関数を呼び出します。

グローバル変数を書いていく

基本的にコメントの通りです。

var COLS = 10, ROWS = 20;  // 横10、縦20マス
var board = [];  // 盤面情報
var lose;  // 一番上までいっちゃったかどうか
var interval;  // ゲームを実行するタイマーを保持する変数
var current; // 今操作しているブロックの形
var currentX, currentY; // 今操作しているブロックの位置

// 操作するブロックのパターン
var shapes = [
    [ 1, 1, 1, 1 ],
    [ 1, 1, 1, 0,
      1 ],
    [ 1, 1, 1, 0,
      0, 0, 1 ],
    [ 1, 1, 0, 0,
      1, 1 ],
    [ 1, 1, 0, 0,
      0, 1, 1 ],
    [ 0, 1, 1, 0,
      1, 1 ],
    [ 0, 1, 0, 0,
      1, 1, 1 ]
];

// ブロックの色
var colors = [
    'cyan', 'orange', 'blue', 'yellow', 'red', 'green', 'purple'
];

01.png

ページが読み込まれた時の処理を書こう

新しいゲームを始める関数, newGameを作っていきます。

tetris.jsファイルの一番下に以下を書いてください。

function newGame() {
  clearInterval(interval);  // ゲームタイマーをクリア
  init();  // 盤面をまっさらにする
  newShape();  // 操作ブロックをセット
  lose = false;  // 負けフラッグ
  interval = setInterval( tick, 250 );  // 250ミリ秒ごとにtickという関数を呼び出す
}

newGame();
  • いつでもこのコードはtetris.jsの一番下に来るようにしてください。
  • 一番初めに読み込まれた時、ゲームオーバーになった時にnewGame関数は呼び出されます。
  • ゲーム自体はtickという関数が250ミリ秒毎に呼ばれて実行されていきます。
  • 新しくゲームを始める場合はtickを呼び出すタイマー, intervalをクリアします。

盤面を空にする関数を書こう

盤面をリセットする関数を書きます。

今回は盤面上では

  • 0: 何もない
  • 1~: ブロック

を表します。

ブロックが1~なのは、それぞれの番号が色を表すからです。

// 盤面を空にする
function init() {
  for ( var y = 0; y < ROWS; ++y ) {
    board[ y ] = [];
    for ( var x = 0; x < COLS; ++x ) {
      board[ y ][ x ] = 0;
    }
  }
}

y, xは左上が(0,0)を表します。yの正負が高校の数学とは逆となっています。

新しい操作ブロックをセットする関数を書こう

  • ブロックのパターンであるshapesからランダムにパターンを取り出し、currentにコピー(セット)していきます。最後に盤面の上にくるよう位置をセットしています。
  • 操作ブロックは4 x 4マスの中で表現されます。
  • 空のマスは0, 色のマスは1以上としてセットします。

02.png

// shapesからランダムにブロックのパターンを出力し、盤面の一番上へセットする
function newShape() {
  var id = Math.floor( Math.random() * shapes.length );  // ランダムにインデックスを出す
  var shape = shapes[ id ];
  // パターンを操作ブロックへセットする
  current = [];
  for ( var y = 0; y < 4; ++y ) {
    current[ y ] = [];
    for ( var x = 0; x < 4; ++x ) {
      var i = 4 * y + x;
      if ( typeof shape[ i ] != 'undefined' && shape[ i ] ) {
        current[ y ][ x ] = id + 1;
      }
      else {
        current[ y ][ x ] = 0;
      }
    }
  }
  // ブロックを盤面の上のほうにセットする
  currentX = 5;
  currentY = 0;
}

メインループ処理を書いていこう

ゲームが始まると250秒毎に呼び出されていく関数、tickを書いていきます。

  1. 操作ブロックを下へ1つずらし、
  2. 操作ブロックが着地したら消去処理、ゲームオーバー判定を行います
function tick() {
  // 1つ下へ移動する
  if ( valid( 0, 1 ) ) {
    ++currentY;
  }
  // もし着地していたら(1つしたにブロックがあったら)
  else {
    freeze();  // 操作ブロックを盤面へ固定する
    clearLines();  // ライン消去処理
    if (lose) {
      // もしゲームオーバなら最初から始める
      newGame();
      return false;
    }
    // 新しい操作ブロックをセットする
    newShape();
  }
}

これから以下の関数を書いていきます。

  • validはその方向へ移動できるかどうかを返す関数です
  • freeze関数は操作ブロックを盤面へ固定する関数です
  • clearLinesはブロックを消去できるかどうか判別し、できるなら処理する関数です。

valid関数を書こう

その方向へ操作ブロックを移動できるかどうかを返す関数です。

基本的に現在の操作ブロックがその方向(offsetX, offsetY)に動いたら、というものを判定しますが、newCurrentという引数を取った場合、そのブロックがその方向に動いたらというものを判定します。

以下の場合falseを返し、そうでない場合はtrueを返します。

  • 移動先が盤面外だった場合
  • 移動先に既に色のマスが合った場合

もし操作ブロックが盤面の上にあったらゲームオーバーにします。loseフラッグをtrueにします。

※ Bugfixしました

// 指定された方向に、操作ブロックを動かせるかどうかチェックする
// ゲームオーバー判定もここで行う
function valid( offsetX, offsetY, newCurrent ) {
  offsetX = offsetX || 0;
  offsetY = offsetY || 0;
  offsetX = currentX + offsetX;
  offsetY = currentY + offsetY;
  newCurrent = newCurrent || current;
  for ( var y = 0; y < 4; ++y ) {
    for ( var x = 0; x < 4; ++x ) {
      if ( newCurrent[ y ][ x ] ) {
        if ( typeof board[ y + offsetY ] == 'undefined'
             || typeof board[ y + offsetY ][ x + offsetX ] == 'undefined'
             || board[ y + offsetY ][ x + offsetX ]
             || x + offsetX < 0
             || y + offsetY >= ROWS
             || x + offsetX >= COLS ) {
                    if (offsetY == 1 && offsetX - currentX == 0 && offsetY - currentY == 1) {
                        console.log('game over');
                        lose = true; // もし操作ブロックが盤面の上にあったらゲームオーバーにする
                    }
               return false;
             }
      }
    }
  }
  return true;
}

freeze関数を書こう

freeze関数は操作ブロックを盤面へセットする関数です。操作ブロックが着地する際に呼び出されます。

// 操作ブロックを盤面にセットする関数
function freeze() {
  for ( var y = 0; y < 4; ++y ) {
    for ( var x = 0; x < 4; ++x ) {
      if ( current[ y ][ x ] ) {
        board[ y + currentY ][ x + currentX ] = current[ y ][ x ];
      }
    }
  }
}

消去処理、clearLines関数を書こう

clearLines関数はfreeze関数が呼び出された直後に実行されます。処理流れとしては以下となります。

  • 一行が揃っている場所を調べる
  • 揃っていたらサウンドをならし、その上にあったブロックを1つずつしたへずらす(消去)

一行が揃っているかどうかはrowFilled変数に代入します。

// 一行が揃っているか調べ、揃っていたらそれらを消す
function clearLines() {
  for ( var y = ROWS - 1; y >= 0; --y ) {
    var rowFilled = true;
    // 一行が揃っているか調べる
    for ( var x = 0; x < COLS; ++x ) {
      if ( board[ y ][ x ] == 0 ) {
        rowFilled = false;
        break;
      }
    }
    // もし一行揃っていたら, サウンドを鳴らしてそれらを消す。
    if ( rowFilled ) {
      document.getElementById( 'clearsound' ).play();  // 消滅サウンドを鳴らす
      // その上にあったブロックを一つずつ落としていく
      for ( var yy = y; yy > 0; --yy ) {
        for ( var x = 0; x < COLS; ++x ) {
          board[ yy ][ x ] = board[ yy - 1 ][ x ];
        }
      }
      ++y;  // 一行落としたのでチェック処理を一つ下へ送る
    }
  }
}

ここまでで描画、操作以外の処理を書くことが出来ました。

render.js(描画処理)を書いていこう

これまででは何もcanvasに表示されないのでrender.jsに描画処理を書いていきます。
これらの処理はtetris.jsのメインループ処理とは完全に独立してループしていきます。

グローバル変数を書く

canvasを扱う際にはコンテクストが必要です。canvas.getContext('2d')で取得することができます。

キャンバスのサイズ、マスのサイズをセットしています。

/*
 現在の盤面の状態を描画する処理
 */
var canvas = document.getElementsByTagName( 'canvas' )[ 0 ];  // キャンバス
var ctx = canvas.getContext( '2d' ); // コンテクスト
var W = 300, H = 600;  // キャンバスのサイズ
var BLOCK_W = W / COLS, BLOCK_H = H / ROWS;  // マスの幅を設定

描画処理を書こう

render関数は盤面と操作ブロックを描画する関数です。30ms毎に呼び出されます。

処理の流れとしては

  1. 一度キャンバスをまっさらにし
  2. 盤面を描画
  3. 操作ブロックを描画

という3つの流れとなります。

// 盤面と操作ブロックを描画する
function render() {
  ctx.clearRect( 0, 0, W, H );  // 一度キャンバスを真っさらにする
  ctx.strokeStyle = 'black';  // えんぴつの色を黒にする

  // 盤面を描画する
  for ( var x = 0; x < COLS; ++x ) {
    for ( var y = 0; y < ROWS; ++y ) {
      if ( board[ y ][ x ] ) {  // マスが空、つまり0ではなかったら
        ctx.fillStyle = colors[ board[ y ][ x ] - 1 ];  // マスの種類に合わせて塗りつぶす色を設定
        drawBlock( x, y );  // マスを描画
      }
    }
  }

  // 操作ブロックを描画する
  for ( var y = 0; y < 4; ++y ) {
    for ( var x = 0; x < 4; ++x ) {
      if ( current[ y ][ x ] ) {
        ctx.fillStyle = colors[ current[ y ][ x ] - 1 ];  // マスの種類に合わせて塗りつぶす色を設定
        drawBlock( currentX + x, currentY + y );  // マスを描画
      }
    }
  }
}

// 30ミリ秒ごとに状態を描画する関数を呼び出す
setInterval( render, 30 );

マスが空白の部分は0と指定してあります。それを回避するために色マスは一度プラス1され、色を読み込むときにそこから-1します。

x, yの部分へマスを描画する処理を書こう

x, yの部分に1マス分だけマスを描画する関数です。先ほどのrender関数の上へ書いてください。

// x, yの部分へマスを描画する処理
function drawBlock( x, y ) {
  ctx.fillRect( BLOCK_W * x, BLOCK_H * y, BLOCK_W - 1 , BLOCK_H - 1 );
  ctx.strokeRect( BLOCK_W * x, BLOCK_H * y, BLOCK_W - 1 , BLOCK_H - 1 );
}

指定した色で1マス分を描きます。

以上で描画処理を実装することができました。

操作ブロックを動かそう

このままですとただブロック積もっていくのを見るというムービーになってしまいます。なので操作処理を付け加えましょう。

controller.jsの作成

操作処理はcontroller.jsへ書いていきます。

document.body.onkeydownというものに関数を指定した場合、どれかキーボードが押された場合に呼び出されます。押されたキーボードの種類は数字としてe.keyCodeに代入されるので、それを見ていきます。

矢印キーの番号に名前をつけていきます。これらのキーのどれかが押された場合はkeyPressという関数を呼び出していきます。これは後ほどtetris.jsに書いていきます。

/*
 キーボードを入力した時に一番最初に呼び出される処理
 */
document.body.onkeydown = function( e ) {
  // キーに名前をセットする
  var keys = {
    37: 'left',
    39: 'right',
    40: 'down',
    38: 'rotate'
  };

  if ( typeof keys[ e.keyCode ] != 'undefined' ) {
    // セットされたキーの場合はtetris.jsに記述された処理を呼び出す
    keyPress( keys[ e.keyCode ] );
    // 描画処理を行う
    render();
  }
};

操作が処理を終えた後は描画関数を呼び出して画面をすぐに更新します。

keyPress関数を作成しよう!

キーボードが押された時の処理を書いていきます。tetris.jsに書いていきます。

上が押された時は回転し、それ以外の時はその方向へ操作ブロックをずらします。ずらす場合はvalid処理をはさみます。

操作ブロックを回転させる処理を実行するrotate関数は後ほど書いていきます。

// キーボードが押された時に呼び出される関数
function keyPress( key ) {
  switch ( key ) {
  case 'left':
    if ( valid( -1 ) ) {
      --currentX;  // 左に一つずらす
    }
    break;
  case 'right':
    if ( valid( 1 ) ) {
      ++currentX;  // 右に一つずらす
    }
    break;
  case 'down':
    if ( valid( 0, 1 ) ) {
      ++currentY;  // 下に一つずらす
    }
    break;
  case 'rotate':
    // 操作ブロックを回す
    var rotated = rotate( current );
    if ( valid( 0, 0, rotated ) ) {
      current = rotated;  // 回せる場合は回したあとの状態に操作ブロックをセットする
    }
    break;
  }
}

rotate関数の作成

操作ブロックを回転する処理です。

newCurrent[ y ][ x ] = current[ 3 - x ][ y ];として回転させています。面白いですね。

03.png

// 操作ブロックを回す処理
function rotate( current ) {
  var newCurrent = [];
  for ( var y = 0; y < 4; ++y ) {
    newCurrent[ y ] = [];
    for ( var x = 0; x < 4; ++x ) {
      newCurrent[ y ][ x ] = current[ 3 - x ][ y ];
    }
  }
  return newCurrent;
}

以上で操作ブロックを回転させる処理は完成しました!

おわりに

以上でJavaScriptによるテトリスは完成です。

metal_slug_fiolina_germi_salto.gif

まだまだ改良面もあると思うのでカスタマイズしてみてください!

おつかれさまでした。

このレシピを書いたシェフ

おっ立ち野郎

@ottatiyarou

コードレシピの管理人です。
「for文とか関数とかの基礎はやったけど、実際アプリとかどうやって作るのか分からない」を解決するようなサービスにしていきたいです。


このシェフが書いたレシピ