見出し画像

JavaScriptでテトリスゲームをプログラミングするプロセスを理解する

本記事では、JavaScriptを使ったテトリスゲームのプログラミングに必要なプロセスを紹介します。

具体的には、JavaScriptでテトリスを実装するためのプログラミングの流れ、プレイヤーがテトリスのブロックを動かす方法、ブロックが落下する動作の実装方法などを詳しく解説します。
また、このゲームを実装するためのTipsも紹介しますので、JavaScriptを使ったテトリスのプログラミングの流れを理解したい方は、ぜひ本記事をご覧ください。

1. JavaScriptによるテトリス・プログラミング入門

テトリスゲームのプログラミングを始める前に、ゲームの基礎とゲーム制作に必要なものを理解しておくことが重要です。
テトリスは、形や大きさの異なる幾何学的なピース(「テトリミノ」と呼ばれる)を回転させながら組み合わせ、パズルを完成させるパズルゲームです。
このゲームでは、プレイヤーはピースを移動・回転させ、横長のプレイエリア(「プレイフィールド」)を操作して、列を埋め、盤面をクリアしていく必要があります。
ゲームのコードは、ブロックの動き、ブロックの回転、ゲームのスコアキーピング(ゲームの実装方法によって、単純なスコアからより複雑な3次元のスコアまで様々)を考慮する必要があります。
ゲームをプログラミングするためには、ゲームで使われるJavaScriptの基本的な関数や変数を理解する必要があります。
JavaScript は、ウェブベースのゲームのプログラミングに最もよく使われる言語のひとつで、比較的言語習得しやすい言語だと思われます。
JavaScriptのコードを表示します。
ファイルはhtmlです。


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tetris-like Game</title>
    <style>
        canvas {
            display: block;
            margin: 0 auto;
            background-color: #eee;
        }
    </style>
</head>
<body>
    <canvas id="gameCanvas" width="320" height="640"></canvas>
    <script>
        window.onload = function () {
            const canvas = document.getElementById('gameCanvas');
            const context = canvas.getContext('2d');
            const blockSize = 32;
            const boardWidth = 10;
            const boardHeight = 20;
            const board = createEmptyBoard();
            let currentBlock = createBlock();
            let nextBlock = createBlock();
            let gameInterval = null;
            let isPaused = false;
            let score = 0;

            function createEmptyBoard() {
                const board = [];
                for (let y = 0; y < boardHeight; y++) {
                    board[y] = [];
                    for (let x = 0; x < boardWidth; x++) {
                        board[y][x] = 0;
                    }
                }
                return board;
            }

            function createBlock() {
                const blocks = [
                    // I
                    {
                        shape: [
                            [1, 1, 1, 1]
                        ],
                        color: 'cyan'
                    },
                    // J
                    {
                        shape: [
                            [1, 0, 0],
                            [1, 1, 1]
                        ],
                        color: 'blue'
                    },
                    // L
                    {
                        shape: [
                            [0, 0, 1],
                            [1, 1, 1]
                        ],
                        color: 'orange'
                    },
                    // O
                    {
                        shape: [
                            [1, 1],
                            [1, 1]
                        ],
                        color: 'yellow'
                    },
                    // S
                    {
                        shape: [
                            [0, 1, 1],
                            [1, 1, 0]
                        ],
                        color: 'green'
                    },
                    // T
                    {
                        shape: [
                            [0, 1, 0],
                            [1, 1, 1]
                        ],
                        color: 'purple'
                    },
                    // Z
                    {
                        shape: [
                            [1, 1, 0],
                            [0, 1, 1]
                        ],
                        color: 'red'
                    }
                ];

                const block = blocks[Math.floor(Math.random() * blocks.length)];
                return {
                    x: Math.floor(boardWidth / 2) - Math.ceil(block.shape[0].length / 2),
                    y: 0,
                    shape: block.shape,
                    color: block.color
                };
            }

            function drawBlock(block, offsetX = 0, offsetY = 0) {
                context.fillStyle = block.color;
                for (let y = 0; y < block.shape.length; y++) {
                    for (let x = 0; x < block.shape[y].length; x++) {
                          if (block.shape[y][x]) {
                            context.fillRect((block.x + x) * blockSize + offsetX, (block.y + y) * blockSize + offsetY, blockSize, blockSize);
                            context.strokeStyle = 'white';
                            context.strokeRect((block.x + x) * blockSize + offsetX, (block.y + y) * blockSize + offsetY, blockSize, blockSize);
                        }
                    }
                }
            }

            function drawBoard() {
                for (let y = 0; y < boardHeight; y++) {
                    for (let x = 0; x < boardWidth; x++) {
                        if (board[y][x]) {
                            context.fillStyle = board[y][x];
                            context.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);
                            context.strokeStyle = 'white';
                            context.strokeRect(x * blockSize, y * blockSize, blockSize, blockSize);
                        }
                    }
                }
            }

            function drawScore() {
                context.font = '20px Arial';
                context.fillStyle = 'black';
                context.fillText('Score: ' + score, 8, 20);
            }

            function mergeBlock() {
                for (let y = 0; y < currentBlock.shape.length; y++) {
                    for (let x = 0; x < currentBlock.shape[y].length; x++) {
                        if (currentBlock.shape[y][x]) {
                            board[currentBlock.y + y][currentBlock.x + x] = currentBlock.color;
                        }
                    }
                }
            }

            function rotate() {
                const prevShape = currentBlock.shape;
                currentBlock.shape = currentBlock.shape[0].map((_, i) => currentBlock.shape.map(row => row[i])).reverse();
                if (checkCollision(currentBlock, 0, 0)) {
                    currentBlock.shape = prevShape;
                }
            }

            function move(dir) {
                if (!checkCollision(currentBlock, dir, 0)) {
                    currentBlock.x += dir;
                }
            }

            function checkCollision(block, dx, dy) {
                for (let y = 0; y < block.shape.length; y++) {
                    for (let x = 0; x < block.shape[y].length; x++) {
                        if (block.shape[y][x] && (board[block.y + y + dy] && board[block.y + y + dy][block.x + x + dx]) !== 0) {
                            return true;
                        }
                    }
                }
                return false;
            }

            function clearLines() {
                outer: for (let y = boardHeight - 1; y >= 0; y--) {
                    for (let x = 0; x < boardWidth; x++) {
                        if (!board[y][x]) {
                            continue outer;
                        }
                    }

                    board.splice(y, 1);
                    board.unshift(Array(boardWidth).fill(0));
                    score += 100;
                }
            }

            function updateGame() {
                if (!checkCollision(currentBlock, 0, 1)) {
                    currentBlock.y++;
                } else {
                    mergeBlock();
                    clearLines();
                    currentBlock = nextBlock;
                    nextBlock = createBlock();
                    if (checkCollision(currentBlock, 0, 0)) {
                        // Game over
                        clearInterval(gameInterval);
                        context.font = '30px Arial';
                        context.fillStyle = 'red';
                        context.fillText('Game Over', canvas.width / 4, canvas.height / 2);
                        return;
                    }
                }
            }


            document.onkeydown = function (e) {
                if (isPaused) return;
                switch (e.key) {
                    case 'ArrowUp':
                        rotate();
                        break;
                    case 'ArrowRight':
                        move(1);
                        break;
                    case 'ArrowLeft':
                        move(-1);
                        break;
                    case 'ArrowDown':
                        if (!checkCollision(currentBlock, 0, 1)) {
                            currentBlock.y++;
                            score += 10;
                        }
                        break;
                    case ' ':
                        while (!checkCollision(currentBlock, 0, 1)) {
                            currentBlock.y++;
                            score += 10;
                        }
                        break;
                    default:
                        break;
                }
            };

            function drawGame() {
                if (!isPaused) {
                    context.clearRect(0, 0, canvas.width, canvas.height);
                    drawBoard();
                    drawBlock(currentBlock);
                    drawBlock(nextBlock, canvas.width - blockSize * 4, 0);
                    drawScore();
                }
                requestAnimationFrame(drawGame);
            }

            function startGame() {
                if (gameInterval) return;
                gameInterval = setInterval(updateGame, 1000 / 2);
            }

            function pauseGame() {
                clearInterval(gameInterval);
                gameInterval = null;
            }

            function restartGame() {
                board.length = 0;
                Object.assign(board, createEmptyBoard());
                currentBlock = createBlock();
                nextBlock = createBlock();
                score = 0;
                startGame();
            }

            document.addEventListener('keydown', (event) => {
                if (event.key === 'Enter') {
                    if (!gameInterval) {
                        startGame();
                    } else {
                        pauseGame();
                    }
                } else if (event.key === 'r' || event.key === 'R') {
                    restartGame();
                }
            });

            drawGame();
        }
    </script>
</body>
</html>

簡単にコードの説明をします。

プログラムの起動は、まず上記ファイルがブラウザによって読み込まれるところからはじまります。
次に、HTMLファイル全体が読み込まれたときに通知されるwindow.onloadと言うイベントが呼ばれます。
そうすると function() {内のコードが実行されます。
その間には変数の宣言などがあり、関数の定義などもあります。
最後にdrawGame()がありますが、
ここでdrawGame()関数が実行されます。

window.onload = function () {
            const canvas = document.getElementById('gameCanvas');
            const context = canvas.getContext('2d');
            const blockSize = 32;
            const boardWidth = 10;
            const boardHeight = 20;
            const board = createEmptyBoard();
            let currentBlock = createBlock();
            let nextBlock = createBlock();
            let gameInterval = null;
            let isPaused = false;
            let score = 0;
~省略~

            drawGame();
        }

2. ゲーム中のブロックの動きを理解する

ゲーム中のブロックの移動の仕組みは、矢印キーによるものです。
各キーは、プレイヤーがブロックを動かすことができる方向(左、右、上、下)と関連付けられています。
プレイヤーが矢印キーを押すと、document.onKeyDownイベントの通知が来て、そこに設定されている関数を実行します。
以下キー入力部分のコードです。

            document.onkeydown = function (e) {
                if (isPaused) return;
                switch (e.key) {
                    case 'ArrowUp':
                        rotate();
                        break;
                    case 'ArrowRight':
                        move(1);
                        break;
                    case 'ArrowLeft':
                        move(-1);
                        break;
                    case 'ArrowDown':
                        if (!checkCollision(currentBlock, 0, 1)) {
                            currentBlock.y++;
                            score += 10;
                        }
                        break;
                    case ' ':
                        while (!checkCollision(currentBlock, 0, 1)) {
                            currentBlock.y++;
                            score += 10;
                        }
                        break;
                    default:
                        break;
                }
            };

また、盤面の左側にスペースがないのに左方向に移動しようとする場合など、盤面から外れた位置にブロックを移動させようとする場合も考慮します。
ブロックを回転させる機能を考慮することも重要です。
ゲームではよくあることですが、ブロックが盤面の境界にあり、簡単に回転させることができない場合を検出するようにコードを書かなければなりません。
また、ブロックがボードの底に到達したことを検知し、ブロックの落下を止める条件文も含める必要があります。
以下接触チェックを行うコードを表示します。

            function checkCollision(block, dx, dy) {
                for (let y = 0; y < block.shape.length; y++) {
                    for (let x = 0; x < block.shape[y].length; x++) {
                        if (block.shape[y][x] && (board[block.y + y + dy] && board[block.y + y + dy][block.x + x + dx]) !== 0) {
                            return true;
                        }
                    }
                }
                return false;
            }

3. テトリスブロックの落下動作の実装

テトリスというゲームでは、ブロックは常に盤の上から下へ落ちていく必要があります。
この落下運動は、タイマーを使って制御されます。
タイマーは、所定の間隔でイベントが発生するように設定されており、これによりボード上のブロックの位置を更新します。
コードを表示します。

            function drawGame() {
                if (!isPaused) {
                    context.clearRect(0, 0, canvas.width, canvas.height);
                    drawBoard();
                    drawBlock(currentBlock);
                    drawBlock(nextBlock, canvas.width - blockSize * 4, 0);
                    drawScore();
                }
                requestAnimationFrame(drawGame);
            }

requestAnimationFrame(drawGame)は、一回の描画毎にdrawGameを呼び出しています。

さらに、ボード上の1列がブロックで完全に埋まったことを検知し、その列の上にあるブロックを1列分下に移動させるようにします。
コードを表示します。

            function clearLines() {
                outer: for (let y = boardHeight - 1; y >= 0; y--) {
                    for (let x = 0; x < boardWidth; x++) {
                        if (!board[y][x]) {
                            continue outer;
                        }
                    }

                    board.splice(y, 1);
                    board.unshift(Array(boardWidth).fill(0));
                    score += 100;
                }
            }

4. JavaScriptでテトリスゲームを実装するコツとポイント

テトリスゲームをプログラミングする場合、いくつかの便利なヒントやトリックを覚えておくことが重要です。
これらのヒントやトリックは、より良いゲームを作り、コーディング作業をより簡単にするのに役立ちます。
まず、コードを整理して構造化するために、複数の関数を使用することが重要です。
ブロックの移動、ブロックの回転、列が埋まったことの検出、得点の更新など、別々の関数を使うことで、より効率的にゲームをコーディングできます。
さらに、ゲームのロジックと別のものを分離することも重要です。
例えば、ブロックの衝突判定を扱う場合、衝突を処理するコードは別の関数にしておくとよいでしょう。
そうすることで、何か問題が発生したときに、ゲームのデバッグがしやすくなります。
最後に、グローバル変数の使用は控えめにすることが重要です。
グローバル変数は、複雑なゲームでは簡単に上書きされてしまい、ゲームの動作に予期せぬ影響を与える可能性があります。
コードの保守性を保つために、可能な限りローカル変数を使用することが最善です。

結論

今回は、JavaScriptを使ってテトリスゲームをプログラミングする流れについて説明しました。
具体的には、JavaScriptでテトリスを実装するためのプログラミングの流れ、プレイヤーがテトリスのブロックを動かす方法、ブロックが落ちる動作を実装する方法について説明しました。
さらに、このゲームを効率よく実装するためのコツやポイントについても解説しました。
JavaScriptを使ったテトリスゲームのプログラミングの流れを理解したいと思っていた方は、ぜひこの記事を参考にしてください。
また、このコードをまずは一つ一つどんな意味があるのかを調べてみることです。
コードを完全に理解したときかなりの力がついているはずです。
そして、このコードを改造して自分オリジナルのゲームを作り上げてください。


この記事が気に入ったらサポートをしてみませんか?