LoginSignup
32
31

More than 1 year has passed since last update.

javascriptでブロックゲームを作ってみる

Last updated at Posted at 2021-10-04

はじめに

若手社員で勉強会を行っているのですが、せっかくIT企業に来たのだからゲームを作ってみたいという意見があったので、学習の一環としてjavascriptでよくあるブロックゲームを作ってみました。
作成のプロセスを、どのような流れ、どのような考えで実装したかを重視して整理しました。
似たようなゲームを作成する方の参考になれば幸いです。

tetris_play.gif

ブロックゲームのルール

縦20マス、横10マスのフィールドがあります。
フィールドの上から4つのマスで構成されたブロックが落ちてきます。
プレイヤーはそのブロックを操作してフィールドの最下段、もしくはすでに固定されているブロックの上に配置、固定します。
ブロックを固定した時に、ブロックが横一列に隙間なく並んだ場合、その列を消すことができます。
ブロックを固定すると、次のブロックが落ちてきます。
ブロックが積み上がり、フィールド上に落とせなくなるとゲームオーバーになります。

まとめると、落ちてくるブロックをきれいに配置して、ひたすら消し続けるゲームです。

実装の流れ

ブロックゲームは次の流れで実装します。

  1. フィールドを作る。
  2. フィールド上にブロックを表示する。
  3. フィールドに表示したブロックを落とす。
  4. ブロックを落として積み上げられるようにする。
  5. ブロックが上まで積み上がった時にゲームオーバーにする。
  6. ブロックを移動できるようにする。
  7. ブロックを回転できるようにする。
  8. 横一列が揃った時に消えるようにする。

前提条件

  • htmlとjavascriptのみで作成します。
  • ブラウザはChromeを使用します。
  • Canvas APIを使用します。

1. フィールドを作る

表示するフィールドは縦20マス、横10マスとしますが、ゲームオーバ-の判定のため、縦に1列の余白を作ります。

image.png

まず、htmlを書きます。
ゲームの画面はCanvas APIを用いて表示します。

index.html

<html>
<head>
    <meta charset="utf-8">
    <title>game</title>
</head>
<body>
    <div>
        <canvas id="game"></canvas>
    </div>
    <script type="text/javascript" src="game.js"></script>
</body>
</html>

次に、javascriptでフィールドを描く処理を実装します。
今回は、各関数共通で使用することが多い変数はグローバル変数として定義します。
まず、初期化処理として、Canvas APIで表示する画面の設定を行います。

game.js
   const BLOCK_SIZE = 20;
   const SCREEN_HEIGHT = 21 * BLOCK_SIZE;
   const SCREEN_WIDTH = 10 * BLOCK_SIZE;
   let canvas;
   let c;

   window.onload = () => {
      init();
   };

   const init = () => {
      canvas = document.getElementById("game");
      canvas.width = SCREEN_WIDTH;
      canvas.height = SCREEN_HEIGHT;
      c = canvas.getContext("2d");  
   };

次に、フィールドを作成します。
フィールドは21*10の二次元配列FIELDで定義します。
FIELDの各要素でマスの状態を表すことにします。

  • 0: ブロックがない状態
  • 1: 操作中のブロックがある状態
  • 9: 固定されたブロックがある状態

将来的に、ブロックの形によって状態を変更する可能性を考慮し、2から8は欠番としています。
初期化時は、フィールド上にブロックは存在しないので、全ての要素を0とします。

また、マスの状態によって色を変更するため、色の定義も行います。

game.js
// フィールドを表す配列
let FIELD = new Array(21)
for (let y = 0; y < FIELD.length; y++) {
    FIELD[y] = new Array(10).fill(0);
}

// ブロックがないマスの色
const BACK_FILL_COLOR = "#000000";
const BACK_EDGE_COLOR = "#FFFFFF";
// 操作中のブロックがあるマスの色
const ACTBLOCK_FILL_COLOR = "#C80000";
const ACTBLOCK_EDGE_COLOR = "#323324";
// 固定されたブロックがあるマスの色
const FIXBLOCK_FILL_COLOR = "#888888";
const FIXBLOCK_EDGE_COLOR = "#FFFFFF";


次に、マスを描画する関数drawBlock()を実装します。
drawBlock()は、フィールドの座標と、マスの色(中身の色と外枠の色)を引数に持ち、Canvas APIでマスを作成します。

game.js
const drawBlock = (x, y, fillStyle, strokeStyle) => {
    let px = x * BLOCK_SIZE;
    let py = y * BLOCK_SIZE;
    c.fillStyle = fillStyle;
    c.fillRect(px, py, BLOCK_SIZE, BLOCK_SIZE);
    c.strokeStyle = strokeStyle;
    c.strokeRect(px, py, BLOCK_SIZE, BLOCK_SIZE);
}  

フィールド全体を表示するためにdrawField()を実装します。

drawField()では、FIELDの余白の列以外の各要素を読み取り、フィールドの各マスを描画します。マスの状態によってdrawBlock()の引数を変更し、マスの色を変えています。

game.js
const drawField = () => {
    // 余白以外の列を読み取るため、yの初期値は1とする。
    for (let y = 1; y < FIELD.length; y++) {
        for (let x = 0; x < FIELD[y].length; x++) {
            switch (FIELD[y][x]) {
                 // 0はブロックが存在しない
                case 0:
                    drawBlock(x, y, BACK_FILL_COLOR, BACK_EDGE_COLOR)
                    break;
                // 1は操作中のブロック
                case 1:
                    drawBlock(x, y, ACTBLOCK_FILL_COLOR, ACTBLOCK_EDGE_COLOR);
                    break;
                // 9は固定されたブロック
                case 9:
                    drawBlock(x, y, FIXBLOCK_FILL_COLOR, FIXBLOCK_EDGE_COLOR)
                    break;
            }
        }
    }
}

そして、newGame()という関数からdrawField()を呼び出します。

game.js
window.onload = () => {
    init();
    newGame();
};

const newGame = () => {
    drawField();
};

ここで、index.htmlをブラウザで開くと、次のようなフィールドが表示されます。
フィールドの作成はこれで完了です。

image.png

2. フィールド上にブロックを表示する

落ちてくるブロックは次の7種類とします。

image.png

FIELDと同様に、ブロックは配列で表現します。
ブロックを4*4の配列で表現すると次のようになります。

game.js
const BLOCK = [
    [
        [0, 0, 0, 0],
        [1, 1, 1, 1],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
    ],
    [
        [0, 0, 0, 0],
        [0, 1, 1, 0],
        [0, 1, 1, 0],
        [0, 0, 0, 0],
    ],
    [
        [0, 0, 0, 0],
        [0, 0, 1, 0],
        [1, 1, 1, 0],
        [0, 0, 0, 0],
    ],
    [
        [0, 0, 0, 0],
        [1, 0, 0, 0],
        [1, 1, 1, 0],
        [0, 0, 0, 0],
    ],
    [
        [0, 0, 0, 0],
        [1, 1, 0, 0],
        [0, 1, 1, 0],
        [0, 0, 0, 0],
    ],
    [
        [0, 0, 0, 0],
        [0, 1, 1, 0],
        [1, 1, 0, 0],
        [0, 0, 0, 0],
    ],
    [
        [0, 0, 0, 0],
        [0, 1, 0, 0],
        [1, 1, 1, 0],
        [0, 0, 0, 0],
    ],
];

FIELD上の、操作中のブロックの位置は、POSという構造体で表します。
POS.xPOS.yで、FIELDにおける、ブロックの左上の座標を表します。

image.png

操作中のブロックの初期位置は(3,0)になります。
また、新しいブロックが落ちてくる際に、ブロックの位置を初期化する必要があるので、位置を初期化する関数initPos()を実装します。

game.js
// ブロックの左上の座標
let POS = {
    x: 3,
    y: 0,
};

// ブロックの位置の初期化
const initPos = () => {
    POS.x = 3;
    POS.y = 0;
}

次に、フィールドにブロックを配置する関数putBlock()を実装します。

変数blockには、変数BLOCKに定義されているブロックからランダムに選択したブロックを代入します。
完全にランダムにした場合、出現するブロックの種類が偏ってしまう可能性があるため、7回で7種類のブロック全てが出現するように調整します。
その後、ランダムに選択されたブロックをFIELDに格納します。

game.js

let BLOCK_STOCK = [0, 1, 2, 3, 4, 5, 6];

const putBlock = () => {
    // ブロックをランダムに選択する。ただし、7回で全てのブロックが出現するようにする。
    let idx = Math.floor(Math.random() * BLOCK_STOCK.length);
    let block = BLOCK[BLOCK_STOCK[idx]];
    BLOCK_STOCK = BLOCK_STOCK.filter((block) => block !== BLOCK_STOCK[idx]);
    if (BLOCK_STOCK.length === 0) {
        BLOCK_STOCK = [0, 1, 2, 3, 4, 5, 6];
    }

    initPos();

    // FIELDに新しいブロックを格納する。
    for (let y = 0; y < block.length; y++) {
        for (let x = 0; x < block[y].length; x++) {
            FIELD[POS.y + y][POS.x + x] = block[y][x];
        }
    }
};

ブロックを表示するため、putBlock()newGame()から呼び出します。
putBlock()FIELDを更新した後、drawField()で画面上にフィールドを表示します。

game.js
const newGame = () => {
    putBlock();
    drawField();
};  

ここで、index.htmlをブラウザで開くと、ブロックが表示されていることが確認できます。
ブラウザを更新すると、表示されるブロックが変わります。
これでフィールド上でのブロックの表示は完了です。

image.png

3. フィールドに表示したブロックを落とす

次に、フィールド上に表示したブロックを下に落としてみます。
まずは、フィールド上の動かしているブロックを取得する関数getActBlock()を実装します。
FIELDPOSの位置から縦横4列づつを読み込み、操作中のブロックを取得して返します。

game.js
const getActBlock = () => {
    // 操作中のブロックを格納する配列
    let tmpBlock = new Array(4);
    for (let y = 0; y < tmpBlock.length; y++) {
        tmpBlock[y] = new Array(4).fill(0);
    }

    for (let y = 0; y < 4; y++) {
        for (let x = 0; x < 4; x++) {
            // フィールドの内側かつマスの状態が操作中のブロックだった場合、操作中のブロックとして取得する。
            if (y + POS.y < FIELD.length && x + POS.x < FIELD[0].length && FIELD[y + POS.y][x + POS.x] === 1) {
                tmpBlock[y][x] = FIELD[y + POS.y][x + POS.x];
            }
        }
    }
    return tmpBlock;
};

次に、操作中のブロックを動かす関数moveBlock()を実装します。

game.js
const moveBlock = (px, py) => {
    // 操作中のブロックを取得する。
    let tmpBlock = getActBlock();

    // 動かす前の操作中のブロックがあるマスを、ブロックがない状態にする。
    for (let y = 0; y < 4; y++) {
        for (let x = 0; x < 4; x++) {
            if (y + POS.y < FIELD.length && x + POS.x < FIELD[0].length && FIELD[POS.y + y][POS.x + x] === 1){
                FIELD[POS.y + y][POS.x + x] = 0;
            }
        }
    }
    // ブロックの位置を動かす。
    POS.x += px;
    POS.y += py;

    // 動かした位置にブロックを配置する。
    for (let y = 0; y < 4; y++) {
        for (let x = 0; x < 4; x++) {
            if (y + POS.y < FIELD.length && x + POS.x < FIELD[0].length && tmpBlock[y][x] === 1) {
                FIELD[y + POS.y][x + POS.x] = tmpBlock[y][x];
            }
        }
    }
}  

ブロックを動かす前に、指定した位置にブロックを動かせるか判定する必要があります。
そこで、ブロックを動かせるか確認する関数checkMove()を実装します。

checkMove()では、ブロックの移動先がフィールドの範囲外の場合や、固定されたブロックが存在する場合はfalseを返します。それ以外はtrueを返します。

game.js
const checkMove = (px, py) => {
    let tmpBlock = getActBlock();
    for (let y = 0; y < tmpBlock.length; y++) {
        for (let x = 0; x < tmpBlock[0].length; x++) {
            // ブロックが動いた先がフィールドの範囲外だった場合
            if (tmpBlock[y][x] === 1 &&
                ((POS.y + py + y >= FIELD.length || POS.y + py + y < 0) 
                || (POS.x + px + x >= FIELD[0].length || POS.x + px + x < 0))) {
                return false;
            }
            // ブロックが動いた先に固定されたブロックが存在する場合。
            if (tmpBlock[y][x] === 1 &&
                POS.y + py + y < FIELD.length &&
                POS.x + px + x < FIELD[0].length &&
                FIELD[POS.y + py + y][POS.x + px + x] === 9
                ) {
                return false;
            }
        }
    }
    return true;
}; 

ここで、実際にブロックを動かすためにnewGame()を修正します。
setInterval()でブロックを1秒ごとに1マス落下させる処理を実装します。

game.js
let SPEED = 1000;
let INTERVAL_ID;

const newGame = () => {
  putBlock();
  INTERVAL_ID = setInterval(() => {
    drawField();
    if (checkMove(0, 1)) {
      moveBlock(0, 1);
    } else {
      clearInterval(INTERVAL_ID);
    }
  }, SPEED);
};

新しいブロックが降ってきて、フィールドの最下段に到達すると止まることが確認できます。

image.png

これでフィールドに表示したブロックを落とす処理の実装が完了しました。

4. ブロックを落として積み上げられるようにする

ブロックを積み上げるためには、ブロックが最下段に到達したあと、操作中のブロックを固定されたブロックに変換し、新しいブロックを出現させる必要があります。

まずは、操作中のブロックを固定する関数fixBlock()を実装します。
FIELDのうち、操作中のブロックの状態を固定されたブロックに変更します。

game.js
const fixBlock = () => {
    for (let y = 0; y < 4; y++) {
        for (let x = 0; x < 4; x++) {
            // 操作中のブロックを固定されたブロックに変更する。
            if (POS.y + y < FIELD.length && POS.x + x < FIELD[0].length && FIELD[POS.y + y][POS.x + x] === 1) {
                FIELD[POS.y + y][POS.x + x] = 9;
              }
        }
    }
};

新しいブロックを落とすのは、「2. フィールド上にブロックを表示する」で実装したputBlock()で実現可能です。
fixBlock()putBlock()を組み合わせて、newGame()にブロックを積み上げる処理を実装します。
ブロックの固定は、操作中のブロックが最下段、または固定されたブロックに到達し落下できない状態になってからすぐには行わず、一定の猶予を設けます。今回は、「操作中のブロックが落下できない状態」が5回発生した時に操作中のブロックを固定します。

game.js
// 操作中のブロックが固定されているか
let FIX_BLOCK_FLAG = true;
// 操作中のブロックが固定されるまでのカウンター
let FIX_COUNTER = 0;
// 操作中のブロックを固定するまでのカウント数
const FIX_COUNT_LIMIT = 5;

const newGame = () => {
  INTERVAL_ID = setInterval(() => {
    drawField();
    // 操作中のブロックが固定された場合、新しいブロックを落とす。
    if (FIX_BLOCK_FLAG === true) {
      putBlock();
      drawField();
      FIX_BLOCK_FLAG = false;
    }
    if (checkMove(0, 1)) {
      moveBlock(0, 1);
    } else {
      // 操作中のブロックが落下できなかった回数をカウントする。
      FIX_COUNTER++;
      // カウントの回数が一定数を超えた時、操作中のブロックを固定する。
      if (FIX_COUNTER === FIX_COUNT_LIMIT) {
        FIX_COUNTER = 0;
        fixBlock();
        FIX_BLOCK_FLAG = true;
      }
    }
  }, SPEED);
};

これで、ブロックを積み上げることができるようになりました。(落下スピードを約10倍にしています。)

image.png

5. ブロックが上まで積み上がった時にゲームオーバーにする

ブロックを積み上げられるようになりましたが、このままだとゲームオーバーにならないので、永遠に新しいブロックが生成されてしまいます。
そこで、ゲームオーバーを判定する関数isGameOver()を実装します。
まず、FIELDの一列目(表示されない領域)に固定されたブロックが存在する時、ゲームオーバーとします。

game.js
const isGameover = () => {
    return FIELD[0].includes(9);
};

また、putBlock()で新しいブロックをフィールドの上に配置する際に、新しいブロックの配置位置に、固定されたブロックが存在する場合もゲームオーバーとします。
そこで、putBlock()を実行した際に、新しいブロックの位置に固定されたブロックがないかをbooleanで返します。

game.js
const putBlock = () => {
    // ブロックをランダムに選択する。ただし、7回で全てのブロックが出現するようにする。
    let idx = Math.floor(Math.random() * BLOCK_STOCK.length);
    let block = BLOCK[BLOCK_STOCK[idx]];
    BLOCK_STOCK = BLOCK_STOCK.filter((block) => block !== BLOCK_STOCK[idx]);
    if (BLOCK_STOCK.length === 0) {
        BLOCK_STOCK = [0, 1, 2, 3, 4, 5, 6];
    }

    initPos();

    for (let y = 0; y < block.length; y++) {
        for (let x = 0; x < block[y].length; x++) {
            // 新しいブロックの位置に固定されたブロックが存在する場合、新しいブロックを生成せずにfalseを返す。
            if (block[y][x] === 1 && FIELD[POS.y + y][POS.x + x] === 9) {
                return false;
            } else if (block[y][x] === 1) {
                FIELD[POS.y + y][POS.x + x] = block[y][x];
            }
        }
    }
    return true;
};

次に、putBlock()の戻り値によってゲームオーバーを判定します。
ゲームオーバーの条件は、isGameOver()がtrueになる、もしくは、putBlock()がfalseとなるかのどちらかとします。
ゲームオーバーになった場合、ダイアログを表示しゲームオーバーになったことをプレイヤーに伝えます。

game.js
// ゲームオーバーか
let GAMEOVER_FLAG = false;

const newGame = () => {
    INTERVAL_ID = setInterval(() => {
        drawField();
        // ブロック挿入の判定
        if (GAMEOVER_FLAG === false && FIX_BLOCK_FLAG === true) {
            GAMEOVER_FLAG = !putBlock();
            drawField();
            FIX_BLOCK_FLAG = false;
        }
        // ゲームオーバーの判定
        if (GAMEOVER_FLAG === true) {
            alert("GAMEOVER");
            clearInterval(INTERVAL_ID);
        }
        if (checkMove(0, 1)) {
            moveBlock(0, 1);
        } else {
            FIX_COUNTER++;
            if (FIX_COUNTER === FIX_COUNT_LIMIT) {
                FIX_COUNTER = 0;
                fixBlock();
                FIX_BLOCK_FLAG = true;
            }
        }
        GAMEOVER_FLAG = isGameover();
    }, SPEED);
};

これでゲームオーバーの処理が完成しました。(落下スピードを約30倍にしています。)

image.png

6. ブロックを移動できるようにする

ブロックが積み上がるようになったので、今度はブロックを操作できるようにします。
まずは、縦や横への移動のためのボタンを作ります。ボタンは次の4種類です。()内はボタンに表示する文字列を表します。

  • ハードドロップ(↑)
  • 下に移動(↓)
  • 左に移動(←)
  • 右に移動(→)

ハードドロップとは、操作中のブロックを最下段に向けて即時落下させる移動です。
ボタンは次のように実装します。

index.html
    <input id="up" type="button" value="↑">
    <input id="down" type="button" value="↓">
    <input id="left" type="button" value="←">
    <input id="right" type="button" value="→">  

init()の中で、ボタンのclickイベントに関数を割り当てます。

各関数では、checkMove()moveBlock()を用いてブロックの移動を実現しています。
ハードドロップのみ、落下後に操作中のブロックを固定されたブロックに変更し、新しいブロックを出現させます。

game.js
const init = () => {
    // ボタンを設定
    document.getElementById('up').addEventListener('click', clickUp);
    document.getElementById('down').addEventListener('click', clickDown);
    document.getElementById('right').addEventListener('click', clickRight);
    document.getElementById('left').addEventListener('click', clickLeft);

    canvas = document.getElementById("game");
    canvas.width = SCREEN_WIDTH;
    canvas.height = SCREEN_HEIGHT;
    c = canvas.getContext("2d");
};  

// ハードドロップ
const clickUp = () => {
    while (checkMove(0, 1)) {
        moveBlock(0, 1);
    }
    fixBlock();
    GAMEOVER_FLAG = !putBlock();
    drawField();
}

// 下に移動
const clickDown = () => {
    if (checkMove(0, 1)) {
        moveBlock(0, 1);
    }
    drawField();
}

// 右に移動
const clickRight = () => {
    if (checkMove(1, 0)) {
        moveBlock(1, 0);
    }
    drawField();
}

// 左に移動
const clickLeft = () => {
    if (checkMove(-1, 0)) {
        moveBlock(-1, 0);
    }
    drawField();
}

最下段に到達した後も、操作中のブロックが固定されるまでに一定時間操作の猶予があります。
ハードドロップを行った場合は、すぐに新しいブロックが出現します。

image.png

これで、プレイヤーがブロックを縦横に操作できるようになりました。

7. ブロックを回転できるようにする

まずは、ブロックを回転させるのためのボタンを作ります。
ボタンは次の2種類です。()内はボタンに表示する文字列を表します。

  • 左に回転(↓●↑)
  • 右に移動(↑●↓)
index.html
    <input id="rotateleft" type="button" value="↓●↑">
    <input id="rotateright" type="button" value="↑●↓">

次に、操作中のブロックを回転させる関数rotateBlock()を実装します。
ブロックを回転には、左回転と右回転の2種類があるので引数で制御します。
今回は、配列を左回転させる関数rotateLeft()と右回転させる関数rotateRight()を実装し、rotateBlock()の引数とします。

配列の右回転は、配列を転置してから左右反転することで可能です。
配列の左回転は右回転とは逆に、配列を左右反転してから転置することで可能です。

javascriptにおける配列の転置の実装はこちらの記事を参考にいたしました。

image.png

game.js
// 配列を左回転させる関数
const rotateLeft = array => array[0].map((_, i) => array.map(row => row[i])).reverse();
// 配列を右回転させる関数
const rotateRight = array => array.reverse()[0].map((_, i) => array.map(row => row[i]));

// 操作中のブロックを回転させる関数
const rotateBlock = (rotateFunc) => {
    let rotatedBlock = rotateFunc(getActBlock());

    // 回転させる前の操作中のブロックを削除
    FIELD.forEach(row => {
        row.forEach(i => {
            if (i === 1) {
                i = 0;
            }
        })
    });
    // 回転させたブロックをフィールドに配置
    for (let y = 0; y < rotatedBlock.length; y++) {
        for (let x = 0; x < rotatedBlock[y].length; x++) {
            if (FIELD[y + POS.y][x + POS.x] !== 9 && y + POS.y < FIELD.length && x + POS.x < FIELD[0].length) {
                FIELD[y + POS.y][x + POS.x] = rotatedBlock[y][x];
            }
        }
    }
};

ブロックを移動する処理と同様に、回転したブロックがフィールドに配置できるか確認してから、ブロックを回転させます。
そのため、ブロックを回転できるか判定する関数checkRotate()を実装します。
checkRotate()の引数には、rotateBlock()と同様に配列を回転させる関数を指定します。

game.js
// 回転できるか判定する関数
const checkRotate = (rotateFunc) => {
    let rotatedBlock = rotateFunc(getActBlock());

    for (let y = 0; y < rotatedBlock.length; y++) {
        for (let x = 0; x < rotatedBlock[0].length; x++) {
            // ブロックが動いた先が範囲外だった時
            if (rotatedBlock[y][x] === 1 &&
                ((POS.y + y >= FIELD.length || POS.y + y < 0) || (POS.x + x >= FIELD[0].length || POS.x + x < 0))) {
                return false;
            }
            // ブロックが動いた先に、固定されたブロックが存在する時
            if (FIELD[POS.y + y][POS.x + x] === 9 &&
                rotatedBlock[y][x] === 1) {
                return false;
            }
        }
    }
    return true;
};  

操作中のブロックを回転させる処理と、操作中のブロックを回転できるか確認する処理を実装しました。
次は、ボタンを押下した時にブロックを回転させる処理を実行します。
ボタンを押下した時の処理をそれぞれ、clickLeftRotate()clickRightRotate()として実装し、clickイベントに割り当てます。

game.js
// 左に回転
const clickLeftRotate = () => {
    if (checkRotate(rotateLeft)) {
        rotateBlock(rotateLeft);
    }
    drawField();
}

// 右に回転
const clickRightRotate = () => {
    if (checkRotate(rotateRight)) {
        rotateBlock(rotateRight);
    }
    drawField();
}

const init = () => {
    // ボタンを設定
    document.getElementById('up').addEventListener('click', clickUp);
    document.getElementById('down').addEventListener('click', clickDown);
    document.getElementById('right').addEventListener('click', clickRight);
    document.getElementById('left').addEventListener('click', clickLeft);
    document.getElementById('rotateright').addEventListener('click', clickRightRotate); // 追加
    document.getElementById('rotateleft').addEventListener('click', clickLeftRotate); // 追加

    canvas = document.getElementById("game");
    canvas.width = SCREEN_WIDTH;
    canvas.height = SCREEN_HEIGHT;
    c = canvas.getContext("2d");
};

これで、プレイヤーがブロックを回転できるようになりました。

tetris_rotate.gif

8. 横一列が揃った時に消えるようにする

最後に、横一列が揃った時に揃ったブロックを消す処理を実装します。
横一列が揃っているか検証し、揃っていれば消す処理としてdeleteLine()を実装します。
フィールドの横一列を取得し、全て固定されたブロックだった場合、列を削除して下に詰めます。

game.js
const deleteLine = () => {
    for (let i = FIELD.length - 1; i >= 1; i--) {
        // フィールドの横一列を取得し、全て固定されたブロックの場合は列を削除する。
        if (typeof (FIELD[i].find(item => item !== 9)) === "undefined") {
            for (let j = i; j >= 1; j--) {
                FIELD[j] = FIELD[j - 1].concat();
            }
            i++;
        }
    }
};

横一列が揃っているかの判定は、操作中のブロックが固定されたブロックに変わった時に行います。
つまり、deleteLine()は、fixBlock()の後に実行します。
fixBlock()が呼ばれているnewGame()clickUp()内にdeleteLine()を追加します。

game.js
const newGame = () => {
    INTERVAL_ID = setInterval(() => {
        drawField();
        // ブロック挿入の判定
        if (GAMEOVER_FLAG === false && FIX_BLOCK_FLAG === true) {
            GAMEOVER_FLAG = !putBlock();
            drawField();
            FIX_BLOCK_FLAG = false;
        }
        // ゲームオーバーの判定
        if (GAMEOVER_FLAG === true) {
            alert("GAMEOVER");
            clearInterval(INTERVAL_ID);
        }
        if (checkMove(0, 1)) {
            moveBlock(0, 1);
        } else {
            FIX_COUNTER++;
            if (FIX_COUNTER === FIX_COUNT_LIMIT) {
                FIX_COUNTER = 0;
                fixBlock();
                deleteLine(); // 追加
                FIX_BLOCK_FLAG = true;
            }
        }
        GAMEOVER_FLAG = isGameover();
    }, SPEED);
};
game.js
// ハードドロップ
const clickUp = () => {
    while (checkMove(0, 1)) {
        moveBlock(0, 1);
    }
    fixBlock();
    deleteLine(); // 追加
    GAMEOVER_FLAG = !putBlock();
    drawField();
}

実際に操作して、一列揃えるとブロックが消えることが確認できます。

tetris_delete.gif

これでブロックゲームの実装は完了です。

まとめ

javascriptで初めてゲームを実装してみたのですが、普段業務で開発しているアプリケーションよりも動きが多く、デバッグが難しいと感じました。
また、今回はCanvas APIを用いて実装したのですが、学習のためということであれば、使わずに実装すべきだったと少し反省しています。

次に出てくるブロックの表示等、まだ実装したい機能はありますので、(時間があれば)引き続き学習していきたいと思います。

参考文献

また、

  • 記載の会社名、製品名、サービス名等はそれぞれの会社の商標または登録商標です。
32
31
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
31