このサイトはアフィリエイトリンクを含んでいます

HTMLテトリスにBGMを実装する方法【Sunoで生成したBGMでさらに楽しく!Ver1.6アップデート解説】

ついにテトリスゲームにBGMを実装 作ってみた!

HTMLとJavascriptで作るテトリスゲームに、オリジナルのBGMを追加したいと思いませんか?この記事では、シンプルなテトリスゲームに、音楽生成AI「Suno」で作成したBGMを実装する方法を、Ver1.5.15からVer1.6へのアップデートを例に解説します。初心者の方でも理解しやすいよう、コード解説と合わせてBGM再生のポイントを紹介します。

あんちゃん
あんちゃん

PCブラウザ版での操作方法は今までと同じだよ!
Qキーは左回り、Wキーは右回りでブロックが回るよ。

左右の矢印キーでブロックが左右に動くよ!

下矢印キーはブロックがスス~っと落ちます。

ゲームスタートは「START」ボタンを押してね~。

スマホ版はゲーム下のボタンで操作してください!

テトリスゲーム(ver1.5.15)の基本的な作り方

HTMLでゲーム画面を作成

ver1.5.15のHTMLでは、テトリスゲームの表示や操作に必要な要素を定義しています。

canvas要素でゲーム画面とnextPiece表示エリアを作成

ゲーム画面の描画には<canvas>要素を使用します。id="game"がゲームのメイン画面、id="nextPiece"が次に落下するブロックを表示するエリアです。

<canvas id="game" width="240" height="400"></canvas>
<canvas id="nextPiece" width="60" height="60"></canvas>

score表示エリア、スタートボタンの作成

スコア表示には<div id="score">、ゲーム開始ボタンには<button id="startButton">を使用します。

<div id="score">Score: 0</div>
<button id="startButton" onclick="startGame()">START</button>

操作ボタン(PC以外)の作成

PC以外でプレイする場合の操作ボタンは、<div id="controls">内に配置されます。左右移動、回転、落下ボタンがあります。

<div id="controls">
    <div id="moveControl">
        <button class="control-button connected-button" id="leftButton">&larr;</button>
        <button class="control-button connected-button" id="rightButton">&rarr;</button>
    </div>
    <button class="control-button" id="rotateButton">&#x21BB;</button>
    <button class="control-button" id="dropButton">&#x2B07;</button>
</div>

JavaScriptでテトリスのロジックを実装

JavaScriptでは、テトリスゲームの動作を制御するロジックを記述します。

ブロックの生成、落下、回転、移動

createPiece()関数でブロックの形を定義し、playerDrop()関数でブロックを落下させます。playerMove()関数で左右移動、playerRotate()関数で回転を制御します。

衝突判定、ライン消去

collide()関数でブロックが他のブロックやフィールドの端に衝突したかを判定します。arenaSweep()関数でラインが揃った場合に消去します。

スコア計算、ゲームオーバー判定

updateScore()関数でスコアを更新し表示します。ゲームオーバー時はgameOver変数をtrueにしてdisplayFinalScore()関数を呼び出し、ゲームオーバー画面を表示します。

キーボード操作とタッチ操作の実装

PCでのキーボード操作

document.addEventListener('keydown', event => { ... });でキーボード操作を検知し、対応する関数を呼び出します。例えば、左矢印キーが押されたらplayerMove(-1)を呼び出してブロックを左に移動させます。

スマホでのタッチ操作

操作ボタンにはそれぞれクリックイベントリスナーが設定されており、対応する関数が呼び出されます。例えば、落下ボタンが押されたらdropStart変数をtrueにしてブロックを高速落下させます。

ver1.6アップデートでSuno製のBGMを追加!

音楽生成AI「Suno」でBGMを作成

音楽生成AI「Suno」でBGMを作成

ver1.6では、ゲームプレイ中のBGMとして、Meta社が開発した音楽生成AI「Suno」で生成した音源を使用しています。

  • Sunoの特徴
    Sunoは、テキストやメロディなどの入力に基づいて、高品質な音楽を生成できるAIです。様々なジャンルの音楽に対応しており、作曲の知識がなくても簡単にオリジナルの音楽を作ることができます。
  • 作成したBGM
    今回のテトリスゲームでは、Sunoを使って軽快でゲームに合った雰囲気のBGMを生成しました。生成したBGMは以下から試聴できます。

[BGM試聴リンク:https://tokodomo.xyz/wp-content/uploads/2024/09/Fall-of-the-Blocks.mp3]

HTMLにaudio要素を追加

BGMを再生するために、HTMLに<audio>要素を追加します。

<audio id="bgm" loop>
    <source src="https://tokodomo.xyz/wp-content/uploads/2024/09/Fall-of-the-Blocks.mp3" type="audio/mpeg">
    Your browser does not support the audio element.
</audio>
  • id="bgm":JavaScriptからこの要素を操作するためにIDを指定します。
  • loop:BGMをループ再生するためにloop属性を指定します。
  • <source src="...">:BGM音源のURLを指定します。
  • type="audio/mpeg":音源の形式を指定します。今回はMP3形式を使用しています。

JavaScriptでBGMの再生と停止を制御

JavaScriptでゲームの開始時と終了時にBGMの再生と停止を制御します。

  • ゲームスタート時にBGM再生

startGame()関数内で、document.getElementById('bgm').play();を実行してBGMを再生します。

function startGame() {
    // ... (ゲーム開始処理) ...

    // BGMの再生
    document.getElementById('bgm').play();
}
  • ゲームオーバー時にBGM停止

displayFinalScore()関数内で、document.getElementById('bgm').pause();を実行してBGMを停止します。また、document.getElementById('bgm').currentTime = 0;で再生位置を先頭に戻します。

function displayFinalScore() {
    // ... (ゲームオーバー処理) ...

    // BGMの停止
    document.getElementById('bgm').pause();
    document.getElementById('bgm').currentTime = 0;
}

コード解説:BGM再生部分

  • document.getElementById('bgm').play();:IDが”bgm”の<audio>要素を再生します。
  • document.getElementById('bgm').pause();:IDが”bgm”の<audio>要素を一時停止します。
  • document.getElementById('bgm').currentTime = 0;:IDが”bgm”の<audio>要素の再生位置を先頭(0秒)に戻します。

BGM実装のポイント

HTML5のテトリスゲームへのBGM実装ポイント

HTML5のテトリスゲームにBGMを実装する際には、いくつかのポイントを押さえることで、より効果的にBGMを活用し、プレイヤーにとって快適なゲーム体験を提供することができます。

Sunoで効果的なBGMを作るポイント

音楽生成AI「Suno」を使ってBGMを作成する際には、テトリスゲームの特性を考慮することが重要です。

  • テトリスゲームに合う雰囲気のBGMを作る

テトリスは、ブロックを積み重ねてラインを消していく、シンプルながらも中毒性のあるパズルゲームです。BGMもゲームの雰囲気に合わせて、軽快でリズミカルなものが好ましいでしょう。ただし、プレイヤーの集中力を妨げないように、過度に激しい音楽や複雑なメロディは避けるべきです。

Sunoでは、生成する音楽のジャンルやムードを指定することができます。例えば、「アップビートなエレクトロニックミュージック」や「リラックスできるアンビエントミュージック」といったように、ゲームに合った雰囲気の音楽を生成するように指示することができます。

  • ループ再生を考慮した構成にする

テトリスゲームのBGMは、ゲームプレイ中ずっとループ再生されます。そのため、ループ再生しても違和感のないような音楽構成にすることが重要です。

Sunoでは、音楽の開始部分と終了部分を滑らかに繋げることで、ループ再生しても自然な流れになるようにすることができます。また、音楽の展開をある程度シンプルにすることで、繰り返し聴いても飽きにくいBGMを作成することができます。

音源形式の選択

BGMとして使用する音源の形式は、ブラウザでの再生互換性を考慮して選択する必要があります。

  • ブラウザ対応の形式を選ぶ(MP3など)

HTML5の<audio>要素は、主要なブラウザで広くサポートされていますが、対応している音源形式はブラウザによって異なります。そのため、できるだけ多くのブラウザで再生できるように、広くサポートされているMP3形式を使用するのがおすすめです。

もし、他の音源形式を使用したい場合は、複数の形式の音源を用意し、ブラウザに応じて適切な形式の音源を再生するようにJavaScriptで制御する必要があります。

ループ再生の設定

BGMをループ再生するには、<audio>要素のloop属性を指定します。

  • loop属性でBGMを繰り返し再生

loop属性を指定することで、音源の再生が終了したら自動的に先頭に戻り、繰り返し再生されるようになります。

<audio id="bgm" loop>
    <source src="bgm.mp3" type="audio/mpeg">
</audio>

JavaScriptでループ再生を制御することもできます。endedイベントを監視し、再生が終了したらcurrentTimeプロパティを0に設定して先頭に戻すことで、ループ再生を実現できます。

音量調整

BGMの音量は、プレイヤーがゲームに集中できる程度に調整する必要があります。

  • audio要素の音量プロパティで調整 (必要に応じて)

<audio>要素のvolumeプロパティで音量を調整することができます。volumeプロパティは0から1までの値を取り、0はミュート、1は最大音量を表します。

const bgm = document.getElementById('bgm');
bgm.volume = 0.5; // 音量を50%に設定

ゲーム内で音量調整機能を提供する場合は、プレイヤーが自由に音量を変更できるように、スライダーなどのUI要素と連携してvolumeプロパティを操作できるようにする必要があります。

まとめ

BGMの実装でゲーム体験が爆上がり

この記事では、HTMLとJavascriptでシンプルなテトリスゲームを作成し、さらに音楽生成AI「Suno」で生成したBGMを実装する方法を、Ver1.5.15からVer1.6へのアップデートを例に詳しく解説しました。

BGM実装でゲーム体験を向上

以前のバージョン(ver1.5.15)では、ゲームのビジュアルと操作性に焦点を当てて開発を進めてきましたが、ver1.6では、新たにBGMを実装することで、プレイヤーのゲーム体験をさらに豊かにすることを目指しました。

BGMはゲームの雰囲気を大きく左右する要素の一つです。適切なBGMを選ぶことで、プレイヤーの没入感を高めたり、ゲームの世界観をより深く表現したりすることができます。

Sunoとaudio要素で簡単なBGM実装

今回のアップデートでは、近年注目を集めている音楽生成AI「Suno」を活用しました。Sunoは、テキストやメロディなどの簡単な指示を与えるだけで、高品質な音楽を生成することができるAIです。これにより、作曲の知識がない開発者でも、手軽にオリジナルのBGMをゲームに組み込むことができるようになりました。

また、HTML5の<audio>要素とJavascriptを組み合わせることで、BGMの再生、停止、ループ再生などを簡単に制御することができます。ゲーム開始時にBGMを再生し、ゲームオーバー時に停止するといった基本的な制御はもちろん、プレイヤーがゲーム内の設定で音量を調整できるようにするなど、より高度な制御も可能です。

今後の展望

今回のアップデートでは、Sunoで生成したBGMを実装することで、HTML5テトリスゲームをより魅力的なものにすることができました。しかし、ゲーム開発の可能性は無限に広がっており、今後も様々な要素を追加していくことで、さらに面白いゲームを作ることができるでしょう。

例えば、以下のような要素を追加することで、ゲームをさらに進化させることができます。

  • 効果音の追加: ブロックの落下音やラインが消える音など、効果音を追加することで、ゲームをより臨場感のあるものにすることができます。
  • レベルアップ機能: スコアに応じてゲームスピードを速くしたり、ブロックの種類を増やしたりすることで、ゲームに難易度と奥行きを与えることができます。
  • マルチプレイ機能: 複数のプレイヤーが同時に対戦できるようにすることで、ゲームの競争性を高めることができます。
  • ビジュアルエフェクトの強化: ブロックが消える際のアニメーションなどを追加することで、ゲームをより視覚的に楽しめるものにすることができます。

創造性を活かしたゲーム開発

HTML5とJavascriptは、Webブラウザ上で動作するゲームを開発するための強力なツールです。この記事で紹介したBGM実装を参考に、さらに創造性を活かして、自分だけのオリジナルゲームを開発してみてください。

あんちゃん
あんちゃん

さぁさぁおまたせしました!!

それではテトリスコードの全容を公開しまーす!

ごらんあれ~♬

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>テトリスゲーム Ver1.6</title>
    <style>
        /* 既存のCSSはそのまま */
        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            background-image: url('https://tokodomo.xyz/wp-content/uploads/2024/09/tetris_ver1.6_background.webp');
            background-size: cover;
            background-position: center;
        }
        #gameContainer {
            position: relative;
            width: 100%;
            flex-grow: 1;
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: rgba(0, 0, 0, 0.7);
            z-index: 5;
        }
        #game {
            background-color: #000;
        }
        #scoreContainer {
            font-size: 20px;
            background-color: rgba(255, 255, 255, 0.7);
            padding: 10px;
            position: fixed;
            top: 0;
            width: 100%;
            display: flex;
            justify-content: space-between;
            box-sizing: border-box;
            z-index: 10;
        }
        #nextPieceContainer {
            position: absolute;
            top: 100px;
            right: 80px;
            text-align: center;
        }
        #nextPiece {
            width: 60px;
            height: 60px;
            background-color: rgba(255, 255, 255, 0.7);
            margin-bottom: -5px;
        }
        #nextLabel {
            font-size: 14px;
            color: white;
            letter-spacing: 2px;
            text-transform: uppercase;
            font-weight: bold;
        }
        #finalScore {
            font-size: 30px;
            color: red;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: rgba(255, 255, 255, 0.7);
            padding: 10px;
            display: none;
            width: 100%;
            text-align: center;
        }
        #controls {
            position: fixed;
            bottom: 0;
            width: 100%;
            display: flex;
            flex-direction: row;
            justify-content: space-between;
            align-items: center;
            padding: 10px 20px;
            box-sizing: border-box;
            z-index: 10;
            background-color: rgba(0, 0, 0, 0.5);
        }
        .control-button {
            width: 60px;
            height: 60px;
            background-color: rgba(255, 255, 255, 0.7);
            border: none;
            border-radius: 50%;
            font-size: 20px;
            font-weight: bold;
            color: black;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        #moveControl {
            width: 140px;
            display: flex;
            justify-content: space-between;
        }
        .connected-button {
            border-radius: 0;
            flex-grow: 1;
            margin: 0;
        }
        .connected-button:first-child {
            border-top-left-radius: 50%;
            border-bottom-left-radius: 50%;
        }
        .connected-button:last-child {
            border-top-right-radius: 50%;
            border-bottom-right-radius: 50%;
        }
        @media (min-width: 601px) {
            #controls {
                display: none;
            }
        }
    </style>
</head>
<body>
    <div id="scoreContainer">
        <div id="score">Score: 0</div>
        <button id="startButton" onclick="startGame()">START</button>
    </div>
    <div id="gameContainer">
        <canvas id="game" width="240" height="400"></canvas>
        <div id="finalScore">GAME OVER<br>SCORE: 0</div>
        <div id="nextPieceContainer">
            <canvas id="nextPiece" width="60" height="60"></canvas>
            <div id="nextLabel">Next</div>
        </div>
    </div>
    <div id="controls">
        <div id="moveControl">
            <button class="control-button connected-button" id="leftButton">&larr;</button>
            <button class="control-button connected-button" id="rightButton">&rarr;</button>
        </div>
        <button class="control-button" id="rotateButton">&#x21BB;</button>
        <button class="control-button" id="dropButton">&#x2B07;</button>
    </div>

    <!-- BGM用のオーディオ要素を追加 -->
    <audio id="bgm" loop>
        <source src="https://tokodomo.xyz/wp-content/uploads/2024/09/Fall-of-the-Blocks.mp3" type="audio/mpeg">
        Your browser does not support the audio element.
    </audio>

    <script>
        const canvas = document.getElementById('game');
        const context = canvas.getContext('2d');
        const nextCanvas = document.getElementById('nextPiece');
        const nextContext = nextCanvas.getContext('2d');
        const scale = 20;
        context.scale(scale, scale);
        nextContext.scale(10, 10);

        const arena = createMatrix(12, 20);

        const player = {
            pos: {x: 0, y: 0},
            matrix: null,
            score: 0,
            nextPiece: null,
        };

        const colors = [
            null,
            '#FF0D72',
            '#0DC2FF',
            '#0DFF72',
            '#F538FF',
            '#FF8E0D',
            '#FFE138',
            '#3877FF',
        ];

        let gameOver = false;
        let dropStart = false;
        let dropSpeed = 50;

        function createMatrix(w, h) {
            const matrix = [];
            while (h--) {
                matrix.push(new Array(w).fill(0));
            }
            return matrix;
        }

        function createPiece(type) {
            if (type === 'T') {
                return [
                    [0, 0, 0],
                    [1, 1, 1],
                    [0, 1, 0],
                ];
            } else if (type === 'O') {
                return [
                    [2, 2],
                    [2, 2],
                ];
            } else if (type === 'L') {
                return [
                    [0, 3, 0],
                    [0, 3, 0],
                    [0, 3, 3],
                ];
            } else if (type === 'J') {
                return [
                    [0, 4, 0],
                    [0, 4, 0],
                    [4, 4, 0],
                ];
            } else if (type === 'I') {
                return [
                    [0, 5, 0, 0],
                    [0, 5, 0, 0],
                    [0, 5, 0, 0],
                    [0, 5, 0, 0],
                ];
            } else if (type === 'S') {
                return [
                    [0, 6, 6],
                    [6, 6, 0],
                    [0, 0, 0],
                ];
            } else if (type === 'Z') {
                return [
                    [7, 7, 0],
                    [0, 7, 7],
                    [0, 0, 0],
                ];
            }
        }

        function drawMatrix(matrix, offset, context) {
            matrix.forEach((row, y) => {
                row.forEach((value, x) => {
                    if (value !== 0) {
                        context.fillStyle = colors[value];
                        context.fillRect(x + offset.x, y + offset.y, 1, 1);
                    }
                });
            });
        }

        function draw() {
            context.fillStyle = '#000';
            context.fillRect(0, 0, canvas.width / scale, canvas.height / scale);

            const xOffset = (canvas.width / scale - arena[0].length) / 2;
            drawMatrix(arena, {x: xOffset, y: 0}, context);
            drawMatrix(player.matrix, {x: player.pos.x + xOffset, y: player.pos.y}, context);

            nextContext.fillStyle = '#000';
            nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);

            const nextPieceOffset = calculateCenterOffset(player.nextPiece);
            drawMatrix(player.nextPiece, nextPieceOffset, nextContext);
        }

        function calculateCenterOffset(matrix) {
            const matrixWidth = matrix[0].length;
            const matrixHeight = matrix.length;

            const canvasSize = nextCanvas.width / 10; 
            const offsetX = (canvasSize - matrixWidth) / 2;
            const offsetY = (canvasSize - matrixHeight) / 2;

            return {x: offsetX, y: offsetY};
        }

        function merge(arena, player) {
            player.matrix.forEach((row, y) => {
                row.forEach((value, x) => {
                    if (value !== 0) {
                        arena[y + player.pos.y][x + player.pos.x] = value;
                    }
                });
            });
        }

        function rotate(matrix, dir) {
            for (let y = 0; y < matrix.length; ++y) {
                for (let x = 0; x < y; ++x) {
                    [matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
                }
            }

            if (dir > 0) {
                matrix.forEach(row => row.reverse());
            } else {
                matrix.reverse();
            }
        }

        function playerDrop() {
            player.pos.y++;
            if (collide(arena, player)) {
                player.pos.y--;
                merge(arena, player);
                playerReset();
                arenaSweep();
                updateScore();
                if (collide(arena, player)) {
                    gameOver = true;
                    displayFinalScore();
                }
            }
            dropCounter = 0;
        }

        function playerMove(offset) {
            player.pos.x += offset;
            if (collide(arena, player)) {
                player.pos.x -= offset;
            }
        }

        function playerReset() {
            if (player.nextPiece === null) {
                player.nextPiece = createPiece('TJLOSZI'[Math.random() * 7 | 0]);
            }
            player.matrix = player.nextPiece;
            player.nextPiece = createPiece('TJLOSZI'[Math.random() * 7 | 0]);
            player.pos.y = 0;
            player.pos.x = (arena[0].length / 2 | 0) - (player.matrix[0].length / 2 | 0);
            if (collide(arena, player)) {
                gameOver = true;
                displayFinalScore();
            }
        }

        function playerRotate(dir) {
            const pos = player.pos.x;
            let offset = 1;
            rotate(player.matrix, dir);
            while (collide(arena, player)) {
                player.pos.x += offset;
                offset = -(offset + (offset > 0 ? 1 : -1));
                if (offset > player.matrix[0].length) {
                    rotate(player.matrix, -dir);
                    player.pos.x = pos;
                    return;
                }
            }
        }

        function collide(arena, player) {
            const [m, o] = [player.matrix, player.pos];
            for (let y = 0; y < m.length; ++y) {
                for (let x = 0; x < m[y].length; ++x) {
                    if (m[y][x] !== 0 &&
                        (arena[y + o.y] &&
                        arena[y + o.y][x + o.x]) !== 0) {
                        return true;
                    }
                }
            }
            return false;
        }

        function arenaSweep() {
            outer: for (let y = arena.length - 1; y > 0; --y) {
                for (let x = 0; x < arena[y].length; ++x) {
                    if (arena[y][x] === 0) {
                        continue outer;
                    }
                }

                const row = arena.splice(y, 1)[0].fill(0);
                arena.unshift(row);
                ++y;

                player.score += 10;
            }
        }

        let dropCounter = 0;
        let dropInterval = 1000;

        let lastTime = 0;
        function update(time = 0) {
            if (!gameOver) {
                const deltaTime = time - lastTime;

                dropCounter += deltaTime;
                if (dropCounter > (dropStart ? dropSpeed : dropInterval)) {
                    playerDrop();
                }

                lastTime = time;

                draw();
                requestAnimationFrame(update);
            }
        }

        function updateScore() {
            document.getElementById('score').innerText = `Score: ${player.score}`;
            document.getElementById('finalScore').innerText = `GAME OVER\nSCORE: ${player.score}`;
        }

        document.addEventListener('keydown', event => {
            if (event.keyCode === 37) {
                playerMove(-1);
            } else if (event.keyCode === 39) {
                playerMove(1);
            } else if (event.keyCode === 40) {
                event.preventDefault();
                playerDrop();
            } else if (event.keyCode === 81) {
                playerRotate(-1);
            } else if (event.keyCode === 87) {
                playerRotate(1);
            }
        });

        document.getElementById('leftButton').addEventListener('click', () => playerMove(-1));
        document.getElementById('rightButton').addEventListener('click', () => playerMove(1));
        document.getElementById('rotateButton').addEventListener('click', () => playerRotate(1));

        document.getElementById('dropButton').addEventListener('mousedown', (event) => {
            event.preventDefault();
            dropStart = true;
        });

        document.getElementById('dropButton').addEventListener('mouseup', (event) => {
            event.preventDefault();
            dropStart = false;
        });

        document.getElementById('dropButton').addEventListener('touchstart', (event) => {
            event.preventDefault();
            dropStart = true;
        });

        document.getElementById('dropButton').addEventListener('touchend', (event) => {
            event.preventDefault();
            dropStart = false;
        });

        function startGame() {
            arena.forEach(row => row.fill(0));
            player.score = 0;
            gameOver = false;
            document.getElementById('finalScore').style.display = "none";
            playerReset();
            updateScore();
            update();

            // BGMの再生
            document.getElementById('bgm').play();
        }

        function displayFinalScore() {
            document.getElementById('finalScore').style.display = "block";
            document.getElementById('finalScore').innerText = `GAME OVER\nSCORE: ${player.score}`;

            // BGMの停止
            document.getElementById('bgm').pause();
            document.getElementById('bgm').currentTime = 0;
        }

        playerReset();
        updateScore();
    </script>
</body>
</html>

コメント

タイトルとURLをコピーしました