6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ブラウザゲーム制作:赤と青のブロックを選べ!

Last updated at Posted at 2023-07-20

目次

記事の内容

HTML、CSS、JavaScript(以降JS)を用いたゲームを作成しながら、JavaScriptを学んでいくための記事です。以下のgifがゲーム画面のイメージです。(動きが重くて申し訳ありませんが、実際のゲーム画面ではもっとスムーズです。)
_2023-07-18 10.59.27.gif

このゲームはこちらから遊ぶことができます。また、本記事で作成しているコードの例はこちらで公開しています。
赤と青のブロックが動き回っており、時間内に数の多い色を選ぶ単純なゲームです。0秒の時に、より多いブロックの色を選べていればクリアです。
HTML、CSS、JSそれぞれの基礎知識については触れていないところがありますので、プログラミング完全初心者向けではないかもしれません。また、学習用だとしたら必要のない機能も混ざっており、混乱する部分があるかもしれません。
ご了承ください。

なぜ記事を書こうと思ったのか

最近Web技術に関する勉強がインプットに偏りすぎていたので、アウトプットとして何か作品を作ろうと思いました。とはいえ特段何かアイデアがあったわけではなく、GithubPagesにあげられる簡単なブラウザゲームを作ろうと思いました。特にクオリティは高くないですが、作ってそのまま放置するのは勿体無いので活動記録として記事にしようと思いました。

開発環境について

特に必要なライブラリなどがあるわけではないので、ブラウザとエディタさえあれば誰でも作れると思います。個人的なおすすめとしては、VSCodeのLive Serverを使って開発をするのがおすすめです。ファイルに変更があったときに、随時ブラウザで状態を確認できるため開発がとてもスムーズです。ブラウザはchromeを使用しています。

本編

基本的にHTMLとCSSで見た目の部分を作成してから、JSで要素を動かしていくという流れで作成していく。
最初に開発用のディレトクリに移動して、次のようにファイルを作成しておく。

$ tree           
.
├── css
│   └── style.css
├── game.html
├── index.html
└── js
    ├── main.js
    └── start.js

スタート画面の見た目

最初に訪れるページであるスタート画面の作成を行う。本記事の最後の方で、途中のレベルから遊ぶか初めから遊ぶか選ぶ機能を追加しており、その選択をスタート画面にて行っている。
※この部分を作らずゲーム画面のみ作成する形でも全然問題ない。
それではindex.htmlを以下のように記述する。

<!DOCTYPE html>
<html>
    <head>
        <title>red_or_blue</title>
        <meta charset="utf-8">
        <link rel="stylesheet" type="text/css" href="css/style.css">
        <script src="js/start.js" defer></script>
    </head>
    <body>
        <div class="main">
            <h1 class="title">
                Red or Blue
            </h1>
            <p class="detail">
                Please choose the color of the block that has a greater quantity: red or blue.
            </p>
            <button class="start-button new">
                Start
            </button>
        </div>
    </body>
</html>

HTMLをある程度書ける人であれば、あまり難しい記述はないだろう。buttonの部分はaにしても良いのだが、今後作成する機能のためにあえてこのように記述している。一点だけ注意点があるとすれば、以下の記述にあるdeferを忘れてはいけない。

<script src="js/start.js" defer></script>

これを忘れてしまうと、HTMLのDOMが生成される前にJSが呼び出されてしまい、HTMLの要素を動かそうとしても「要素がありません」と言われてしまう。別の解決策としては、以下のように記述しても良い。

<!DOCTYPE html>
~省略~
            <button class="start-button new">
                Start
            </button>
        </div>
            <script src="js/start.js"></script>
    </body>
</html>

index.htmlの中身をブラウザで開いた時、以下の画像のようになっていれば良い(ブラウザによって見た目が多少違う可能性がある)。
スクリーンショット 2023-07-17 17.00.01.png
見た目に関してはこれだけでもいいのだが、CSSを用いて少しだけ見やすくしていく。
style.cssを以下のように記述する。

html, body {
    margin: 0;
    padding: 0;
    color: #222222;
}

.main {
    padding: 100px 15% 0px 15%;
    text-align: center;
}

.title {
    font-size: 104px;
    text-align: center;
	color: transparent;
	background: repeating-linear-gradient( 90deg, #f94949 0 35%, #45add9 65% 100%);
	-webkit-background-clip: text;
    margin-bottom: 200px;
}
.detail {
    font-size: 24px;
}

.start-button {
    align-items: center;
    text-decoration: none;
    color: #222222;
    background-color: #f3ff4a;
    height: 100px;
    padding: 20px 30px;
    font-size: 52px;
    border: 3px solid #222222;
    border-radius: 15px;
}
.start-button:hover {
    cursor: pointer;
}

これについてもCSSを書いたことがある人ならば、ほとんど理解できると思う。以下の部分は多少テクニックを用いて色をグラデーションにしているが、勉強する上ではこの部分を抜き去ってしまって問題ない(「Red or Blue」の部分が黒になる)。

color: transparent;
background: repeating-linear-gradient( 90deg, #f94949 0 35%, #45add9 65% 100%);
-webkit-background-clip: text;

また、ブラウザにはもともと設定されているCSSが存在している。それらをリセットしてくれるいわゆるリセットCSSというものがあるが、今回は特に利用せずhtml、bodyにpadding、marginが0になるように記述した。そして真っ黒は目が疲れてしまうらしいため、デフォルトの色を#222222にした。
index.htmlの画面表示が以下のようになっていれば良い。
スクリーンショット 2023-07-17 17.16.05.png

ゲーム画面の見た目

次にゲーム画面を作成していく。
game.htmlを以下のように記述する。

<!DOCTYPE html>
<html>
    <head>
        <title>red_or_blue</title>
        <meta charset="utf-8">
        <link rel="stylesheet" type="text/css" href="css/style.css">
        <script src="js/main.js" defer></script>
    </head>
    <body>
        <div class="game-main">
            <div class="game-display">
                <p class="clear result">
                    CLEAR
                </p>
                <p class="gameover result">
                    GAME OVER
                </p>
                <div class="popup">
                    <p class="now-level">
                    </p>
                    <button class="go">
                        Go
                    </button>
                </div>
            </div>
            <div class="user-display">
                <p class="time-display">
                </p>
                <div class="select">
                    <button class="red-button select-button">
                        Red
                    </button>
                    <button class="blue-button select-button">
                        Blue
                    </button>
                </div>
            </div>
        </div>
    </body>
</html>

index.htmlよりも多少複雑になっているが、特にこちらも難しいところはないだろう。
game.htmlをブラウザで表示するとわかるが、今のままではゲームにならないため早速CSSを追記していく。
style.cssに、以下の記述を追加する(上記で書いたCSSの記述は消さないように注意)。

.game-main {
    padding: 0px 3% 0px 3%;
}

.game-display {
    border: 4px solid #222222;
    margin-top: 50px;
    height: 800px;
    position: relative;
    text-align: center;
}
.popup {
    display: none;
}

.now-level {
    font-size: 100px;
}
.go {
    width: 150px;
    height: 150px;
    font-size: 56px;
    background-color: #f3ff4a;
    border: 4px solid #222222;
    border-radius: 75px;
}
.go:hover {
    cursor: pointer;
}

.user-display {
    display: flex;
    align-items: center;
    height: 100px;
    padding: 10px 30% 0px 15%;
    justify-content: space-between;
}
.user-display .time-display {
    margin: 0;
    line-height: 0px;
    font-size: 120px;
}
.select-button {
    width: 100px;
    height: 100px;
    margin-left: 50px;
    border: 4px solid #222222;
    font-size: 30px;
    
}
.select-button:hover {
    cursor: pointer;
}

.result {
    font-size: 200px;
    margin-top: 250px;
}
.gameover {
    display: none;
    color: #8300e1;
}
.clear {
    display: none;
    color: #f4ee44;
}


/*Block*/

.red-block {
    background-color: rgb(255, 60, 60);
    height: 50px;
    width: 50px;
    position: absolute;
}
.blue-block {
    background-color: rgb(60, 60, 255);
    height: 50px;
    width: 50px;
    position: absolute;
}

少し長くなってしまったが、JSの記述によって現れる要素のCSSも含まれているためである。この後JSを記述していく際、見た目の部分を気にすることが無いようにここで一気に記述してしまう。flexを用いている箇所があるがここでは詳しく説明しないため、この記述方法について詳しく知りたい方は別のサイトを参照してほしい。
game.htmlの画面表示が以下のようになっていれば良い。
スクリーンショット 2023-07-17 17.51.16.png

記述量の割に内容が少ないように感じるかもしれないが問題ない。今後JSにより表示される要素が増えていく。
また、index.htmlで「START」ボタンがあったと思うが、押しても何も変化が起こらないと思う。流石にこのままだとしっくりこないため、start.jsに以下を記述する。

let startButtonNew = document.querySelector(".new");
startButtonNew.addEventListener("click", () => {
    window.location.href = 'game.html';
})

これで、「START」ボタンを押すとgame.htmlに移動できるだろう。

動くブロックの作り方

それでは早速ゲームの中身を作っていこうと言いたくなるが、まずは指定された範囲の中で動くブロックを1つだけ作っていく。
main.jsを以下のように記述する。

let gameDisplay = document.querySelector(".game-display");

// blockを生成する。
let blockDiv = document.createElement("div");
blockDiv.className = "red-block block";
gameDisplay.appendChild(blockDiv);

これは1行目でclass名が「game-dispaly」のdiv要素を取得し、その中に以下のような要素を追加するものである。

<div class="red-block block"></div>

これでindex.htmlをブラウザで確認すると、以下のように赤いブロックが追加された。
スクリーンショット 2023-07-17 19.20.52.png
次はこれを動かしていこう。
style.cssを読んでもらえればわかるが、この画面の大きな黒い枠線は、以下の記述のボーダーを表している。

<div class="game-display">
    ...
</div>

ブロックはここからはみ出して動いて欲しくないので、これらの高さと幅を取得しておこう。main.jsに以下の内容を追記する。

let gameDisplay = document.querySelector(".game-display");

// blockを生成する。
let blockDiv = document.createElement("div");
blockDiv.className = "red-block block";
gameDisplay.appendChild(blockDiv);

const gameDisplayWidth = gameDisplay.clientWidth;// 追記
const gameDisplayHeight = gameDisplay.clientHeight;// 追記

そしてこれらの値を用いて動かしていく。やり方をざっくり説明すると、requestAnimationFrameを使って、用意したアニメーションを1フレームごとに呼び出す。requestAnimationFrameはブラウザの描画のタイミングに合わせて指定したコールバック関数を呼び出すインターフェイスである。アニメーションではブロックの縦方向と横方向の位置を把握し、それらが黒い枠を超えないように更新していくプログラムを記述する。
main.jsを以下のように記述する。

let gameDisplay = document.querySelector(".game-display");

// blockを生成する。
let blockDiv = document.createElement("div");
blockDiv.className = "red-block block";
gameDisplay.appendChild(blockDiv);

const gameDisplayWidth = gameDisplay.clientWidth;
const gameDisplayHeight = gameDisplay.clientHeight;

// 以下を追記

let block = document.querySelector(".block"); 
const size = block.clientWidth; // ブロックの横の長さ
let rx = 0; // 現在のx軸方向の位置
let ry = 0; // 現在のy軸方向の位置
let vrx = 2; // x軸方向に動く距離
let vry = 3; // y軸方向に動く距離

// アニメーションの実行
function animate() {
    rx += vrx;
    ry += vry;
    
    if (rx + vrx <= 0 || rx + vrx >= gameDisplayWidth - size) {
        vrx *= -1;
    }
    if (ry + vry <= 0 || ry + vry >= gameDisplayHeight - size) {
        vry *= -1;
    }
    
    block.style.left = rx + "px";
    block.style.top = ry + "px";
    
    requestAnimationFrame(animate);
}

animate();

requestAnimationFrameで次のアニメーションを1フレームごとに呼び出している。基本は1秒間に60フレームだが、ブラウザによっては違うためブロックの動く速度が違う可能性があり注意が必要。また、animate関数の中の以下の記述は、ブロックが枠を超えないようにすると同時にブロックの移動方向を逆向きにしている。ブロックが壁にぶつかった時、反射しているような動きなっただろう。
以下の画像のような感じだ。
スクリーンショット 2023-07-18 12.12.57.png

とりあえずブロックは動いたが、このままでは常に同じ場所からブロックがスタートしてしまう。これだと複数のブロックがあった場合、全く同じ方向に動くブロック同士は重なってしまう。ブロックの初期位置をランダムに設定しよう。また、ついでに移動方向や速度についてもランダムに設定する。
main.jsに以下の記述を追記する。

...
let vrx = 0; // x軸方向に動く距離
let vry = 0; // y軸方向に動く距離

// ここから追記

const minSpeed = 3; // 最小速度

// 正方形の初期位置と方向を設定
rx = Math.random() * (gameDisplayWidth - size);
ry = Math.random() * (gameDisplayHeight - size);
vrx = Math.trunc(Math.random() * 5 + minSpeed);
vry = Math.trunc(Math.random() * 5 + minSpeed);
vrx *= Math.random() > 0.5 ? 1 : -1;
vry *= Math.random() > 0.5 ? 1 : -1;

block.style.left = rx + "px";
block.style.top = ry + "px";

// 追記はここまで

// アニメーションの実行
function animate() {
    rx += vrx;
...

Math.random()を使うことで、0~1(1は含まない)のランダムな小数が生成される。これを用いることで、黒い枠内のどこかにブロックが生成され、ランダムな速度、方向に動き始める。途中にMath.truncがあるが、これは小数以下を切り捨てて整数に変換する。Math.Floorと多少動作が異なるが、詳しくは別サイトを参照してほしい。また、以下は三項演算子を用いてMath.random()が0.5より大きかったらプラス方向、そうでなければマイナス方向に動くという記述をしている。三項演算子についても詳しい説明は別サイトを参照してほしい。

vrx *= Math.random() > 0.5 ? 1 : -1;
vry *= Math.random() > 0.5 ? 1 : -1;

これで動くブロックは完成した。このプログラムを必要な個数だけ増やしていけば、いくらでも動くブロックを生成できる。ところが一点問題がある。ランダムで位置などを決めているため、理論上はブロックが重なってしまう可能性がある。厳密にはこれは気にしなければならないポイントだと思うが、仮に黒い枠が縦800px、横1000pxだった場合を考える。ブロックがぴったり重なる確率としては、800000分の1である(ブロックの大きさを考えない場合)。近い場所に生成されることもなるべく避けたいが、移動速度や方向なども揃う可能性となると、かなり低い確率になるはずだ。本記事は勉強という名目であるため、ここについては目を瞑ることにする。

複数の動くブロックの作り方

それでは上記で作成したブロックを増やしていく。厳密には増やせるような機構を作る。やり方としてはブロック要素をリスト形式として複数取得し、それらにfor文を用いて先ほど作ったanimate関数をそれぞれのブロックに適用すれば終わりだ。しかし変更点が多いことにより、追記という形にするととても見づらくなってしまう。そのため、ここではmain.jsの中身を全て見せる。
main.jsを以下のように記述する。

let gameDisplay = document.querySelector(".game-display");

let redNum= 5; // redBlockの数
let blueNum= 4; // blueBlockの数

// redBlockを生成する。
const makeRedBlock = (n) => {
    for(let i=0; i<n; i++) {
        let redBlockDiv = document.createElement("div");
        redBlockDiv.className = "red-block block";
        gameDisplay.appendChild(redBlockDiv);
    }
}
// blueBlockを生成する。
const makeBlueBlock = (n) => {
    for(let i=0; i<n; i++) {
        let blueBlockDiv = document.createElement("div");
        blueBlockDiv.className = "blue-block block";
        gameDisplay.appendChild(blueBlockDiv);
    }
}

// blockの数を決める。
makeRedBlock(redNum)
makeBlueBlock(blueNum)

const gameDisplayWidth = gameDisplay.clientWidth;
const gameDisplayHeight = gameDisplay.clientHeight;

let block = document.querySelectorAll(".block"); 
const size = block[0].clientWidth; // ブロックの横の長さ
let rx = []; // 現在のx軸方向の位置
let ry = []; // 現在のy軸方向の位置
let vrx = []; // x軸方向に動く距離
let vry = []; // y軸方向に動く距離

// 以下を追記

const minSpeed = 3; // 最小速度

// 正方形の初期位置と方向を設定
for (let i = 0; i < block.length; i++) {
    rx[i] = Math.random() * (gameDisplayWidth - size);
    ry[i] = Math.random() * (gameDisplayHeight - size);
    vrx[i] = Math.trunc(Math.random() * 5 + minSpeed);
    vry[i] = Math.trunc(Math.random() * 5 + minSpeed);
    vrx[i] *= Math.random() > 0.5 ? 1 : -1;
    vry[i] *= Math.random() > 0.5 ? 1 : -1;
    
    block[i].style.left = rx[i] + "px";
    block[i].style.top = ry[i] + "px";
}

// アニメーションの実行
function animate() {
    for (let i = 0; i < block.length; i++) {
        rx[i] += vrx[i];
        ry[i] += vry[i];
        
        if (rx[i] + vrx[i] <= 0 || rx[i] + vrx[i] >= gameDisplayWidth - size) {
            vrx[i] *= -1;
        }
        if (ry[i] + vry[i] <= 0 || ry[i] + vry[i] >= gameDisplayHeight - size) {
            vry[i] *= -1;
        }
        
        block[i].style.left = rx[i] + "px";
        block[i].style.top = ry[i] + "px";
    }
    
    requestAnimationFrame(animate);
}

animate();

1つブロックを動かせてしまえば、特に難しい記述はないと思う。class名が「.block」のdivを取得する部分は、以下のように複数取得している。

let block = document.querySelectorAll(".block"); 

game.htmlを確認してみると、以下の画像のように赤いブロック5個、青いブロック4個がそれぞれ動いているのが確認できるだろう。
スクリーンショット 2023-07-18 17.15.25.png
ここまで来ればほぼ完成と言っても過言ではない。あとは然るべきタイミングで動作が行われるようにしたり、時間制限、効果音などをつければ完成である。

選択機能と時間表示を追加

次に、赤か青かを選ぶことができる選択機能をつけていく。具体的には、黒い枠の下にあったRedとBlueのボタンを押して、押されたボタンに色が付くようにする。
main.jsに以下の記述を追記する

...
    requestAnimationFrame(animate);
}

animate();

// 以下を追記

const RED = 0;
const BLUE = 1;

// 赤と青のどちらを選んだか。Defaultは赤
let select = RED;
let redButton = document.querySelector(".red-button");
redButton.style.backgroundColor = "rgba(255, 60, 60, 0.7)"
let blueButton = document.querySelector(".blue-button");

redButton.addEventListener('click', () => {
    select = RED;
    redButton.style.backgroundColor = "rgba(255, 60, 60, 0.7)"
    blueButton.style.backgroundColor = ""
});
blueButton.addEventListener('click', () => {
    select = BLUE;
    redButton.style.backgroundColor = ""
    blueButton.style.backgroundColor = "rgba(60, 60, 255, 0.7)"
});

内容としては、ボタンの要素を取得して、押された時に色が付くようにしている。さらに、何度同じボタンを押しても色が変わらないようになっているのと、押されたボタンと違う方のボタンの色が戻るようになっていることもポイントである。
これで、ボタンが押されたら色が付き、押されてない方の色が元に戻る動きになる。
以下の画像のようになれば良い。
スクリーンショット 2023-07-18 18.13.09.png
スクリーンショット 2023-07-18 18.13.23.png

次は時間表示を作成していく。具体的には10から1ずつ減っていく表示ができればよい。
main.jsに以下の内容を追記する。

...
blueButton.addEventListener('click', () => {
    select = BLUE;
    redButton.style.backgroundColor = ""
    blueButton.style.backgroundColor = "rgba(60, 60, 255, 0.7)"
});

// 以下を追記

// 時間制限
let time = 10;

let timeDisplay = document.querySelector(".time-display");
timeDisplay.innerText = time;

const timeInterval = setInterval(() => {
    time -= 1;
    timeDisplay.innerText = time;
    if (time == 0) {
        clearInterval(timeInterval);
    }
},1000)

setIntervalを使って制限時間を作成している。詳しい使い方については別ページを参考にしてほしいが、ここでは1000ミリ秒に一度動作するようになっており、その度に時間表示を更新する。そして0秒になったらsetIntervalを削除することで、0という表示で止まるようになっている。これで時間表示も完成である。

開始画面やクリア表示などを追加

今のままでは時間表示が0になっても、正解の色を選べても何も起こらない。というわけで次は開始画面と、正解の時にクリア表示、間違っていた時にゲームオーバー表示を行っていく。しかし、main.jsを変更する前に説明しておくことがある。実はゲーム画面をHTMLで作成した時に、すでに以下の記述で開始画面を作ってある。CSSで**display: none;**という記述をして、見えない状態にしていた。つまり、この部分を見えるようにすれば開始画面が出来上がる。

<div class="popup">
    <p class="now-level">
    </p>
    <button class="go">
        Go
    </button>
</div>

それではmain.jsに以下のように記述をする。

const startGame = () => {
    // ここに今まで作成したプログラムを入れる
}

let popup = document.querySelector(".popup");
popup.style.display = "block";
let go = document.querySelector(".go");
go.addEventListener("click", startGame);

これでGoというボタンがゲーム画面に現れ、押すと今までのプログラムが動き出す。
以下のような画像のようになっており、Goというボタンを押すと今までのプログラムが動けば良い。
スクリーンショット 2023-07-18 19.00.27.png

次にクリア表示を行う。これについても開始画面と同様に、HTMLで要素は作成してあるがCSSで見えないようにしていた。つまりこれも、然るべきタイミングで見えるようにすれば良いわけだ。
まずはmain.jsに以下の記述を追記する。

const timeInterval = setInterval(() => {
        time -= 1;
        timeDisplay.innerText = time;
        if (time == 0) {
            clearInterval(timeInterval);
            judge(); // ここを追記
        }
    }, 1000);

時間表示が0と表示された時、それ以上時間表示が変わらないようにすると同時に、正解かどうかを判定するjudge関数を呼ぶ。
それではjudge関数をmain.jsに追記する。

const judge = () => {
        const ANS = redNum > blueNum ? RED : BLUE;
        redButton = undefined;
        blueButton = undefined;
        if(ANS == RED) {
            let blueBlock = document.querySelectorAll(".blue-block");
            for(let i=0; i<blueBlock.length; i++) {
                blueBlock[i].style.display = "none";
            }
        }
        else {
            let redBlock = document.querySelectorAll(".red-block");
            for(let i=0; i<redBlock.length; i++) {
                redBlock[i].style.display = "none";
            }
        }
        
        if(select == ANS) {
            let clearDisplay = document.querySelector(".clear");
            clearDisplay.style.display = "block";
            setInterval(() => {
                window.location.reload();
            }, 3000)
        } else {
            let gameoverDisplay = document.querySelector(".gameover");
            gameoverDisplay.style.display = "block";
            setInterval(() => {
                window.location.href = 'index.html';
            }, 3000)
        }
    }

一見複雑に見えるかもしれないが、やっていることはとても単純である。箇条書きにして表すとこんな感じだ。

  • 答えが赤と青どちらなのかを求め、保持する。
  • ボタンの要素を未定義にすることでボタンを操作できなくする。
  • 答えの方のブロックのみを残す。(これはなくても良い)
  • 正解なら「CLEAR」と表示し、3秒後にもう一度開始画面を表示
  • 不正解なら「GAME OVER」と表示し、3秒後にindex.htmlへ移動する。

赤のブロックが5個、青のブロックが4個であるため、赤を選んでおけばCLEARが表示され、青を選んでおくとGAMEOVERが表示されるだろう。
だいぶゲームとして遊べるものになったのではないだろうか。

レベルを追加

まだまだ改善点はある。このままではずっと同じブロックの個数で遊ぶことになるため、ゲームとして面白くない。ここではレベルを作って、レベルごとにブロックの数が変わるようにしよう。
まずはmain.jsに以下の記述を追記する。


// 以下を追記

// レベル
if(localStorage.getItem("level") == undefined) {
    localStorage.setItem("level", 1);
}
let level = Number(localStorage.getItem("level"));
let nowLevel = document.querySelector(".now-level");
nowLevel.innerHTML = "LEVEL: " + level;

// 追記はここまで

let popup = document.querySelector(".popup");
popup.style.display = "block";
let go = document.querySelector(".go");
go.addEventListener("click", startGame);

前のセクションでゲームをクリアした時に、ページをリロードすることで再度ゲームを始められるようにしていた。そうすると、変数の内容なども初期化されてしまう。かといってレベル別にページを作るのも面倒だ。3つならともかく、100ともなると不可能に近い。そのため、ここではlocalStorageというものを使って、保存しておいたレベルの値を取り出すことで解決している。localStorageは、ドメインごとに変数を保存しておけるものだ。Cookieとは違って外部に送られることはない。最初のif文ではlevelというKeyが存在しない場合、levelというKeyで1を保存して予期せぬエラーを防いでいる。また、localStorageの値を取り出す際にNumberを使っているが、これはlocalStorageに保存される値は文字列であるため、数値データに変換している。
index.htmlでスタートボタンを押すと、以下のように表示されているページが確認できるだろう。
スクリーンショット 2023-07-18 22.29.46.png

また、わざわざ不正解しないとレベル1からできないというのも不便なので、index.htmlのスタートボタンを押した時は、必ずレベル1から始めるようにしたい。
start.jsに以下の記述を追記する。

let startButtonNew = document.querySelector(".new");
startButtonNew.addEventListener("click", () => {
    localStorage.setItem("level", 1); // ここを追記
    window.location.href = 'game.html';
})

これでスタートボタンを押すと、必ずレベル1から始めることができる。ちなみにこのスタートボタンを押さずに直接game.htmlに飛んでしまえば、途中のレベルから行うことも可能になってしまう。これもまた目を瞑ろう。
次に、正解した時にレベルを増やし、不正解の時にはレベルのデータを削除するプログラムを作成する。
main.jsに以下の記述を追記する。

if(select == ANS) {
    let clearDisplay = document.querySelector(".clear")
    clearDisplay.style.display = "block";
    localStorage.setItem("level", level+1); // ここを追記
    setInterval(() => {
        window.location.reload();
    }, 3000)
} else {
    let gameoverDisplay = document.querySelector(".gameover")
    gameoverDisplay.style.display = "block";
    localStorage.removeItem("level"); // ここを追記
    setInterval(() => {
        window.location.href = 'index.html';
    }, 3000)
}

これで正解すればするほどレベルの値は高くなり、不正解なら1に戻るようになっているはずだ。
これでレベルが自動で変更される機構は作ったので、あとはそのレベルに応じてブロックの数を増やしたりブロックの速度をあげたりして、好きなようにアレンジしてほしい。ここでは例として以下のように記述した。
main.jsに以下の記述を追記する。

const startGame = () => {
    popup.style.display = "none"; // 開始画面を消す
    let gameDisplay = document.querySelector(".game-display");
    // 以下を追記
    let redNum= 5 + level + Math.trunc(Math.random() * 2); // redBlockの数
    let blueNum= Math.random() > 0.5 ? redNum + 1 : redNum - 1; // blueBlockの数
    // 追記はここまで
.
.
.

    let block = document.querySelectorAll(".block");
    const size = block[0].clientWidth; // ブロックの横の長さ
    // 以下を追記
    const speedRange = 3 + level // 速度の範囲
    const minSpeed = 3 + level // 最小速度
    // 追記はここまで
    let rx = []; // 現在のx軸方向の位置
    let ry = []; // 現在のy軸方向の位置
    let vrx = []; // x軸方向に動く距離
    let vry = []; // y軸方向に動く距離

.
.
.
// 正方形の初期位置と方向を設定
    for (let i = 0; i < block.length; i++) {
        rx[i] = Math.random() * (gameDisplayWidth - size);
        ry[i] = Math.random() * (gameDisplayHeight - size);
        vrx[i] = Math.trunc(Math.random() * speedRange + minSpeed); // ここを変更
        vry[i] = Math.trunc(Math.random() * speedRange + minSpeed); // ここを変更
        vrx[i] *= Math.random() > 0.5 ? 1 : -1;
        vry[i] *= Math.random() > 0.5 ? 1 : -1;

        block[i].style.left = rx[i] + "px";
        block[i].style.top = ry[i] + "px";
    }

上記でも記述したが、ここの追記と変更についてはこの通りにする必要はない。個人のお好みに合わせてレベルごとの難易度を調整してほしい。この例の通りにすると、レベル10くらいで人間にはできないレベルになってしまう。

おまけ機能

おめでとう!!とりあえずこれでゲームは完成である。
ここからはなくても良いが、あった方が便利で楽しい機能を追加していく。以下の内容についてはやらなくても全く問題ないが、もし余裕があれば是非追加してみてほしい。

効果音を追加

それではBGMや効果音を追加していく。しかしWebページで音楽を流す時注意しなければならないことがある。それは、音楽を流す場合は必ずユーザーが何らかのアクションを取らなければならないことだ。逆に言えば、音楽が流れているサイトでリロードをした時、特に対策がされていなければ音楽が流れなくなることがある。そのため、音楽が流れるサイトは最初にユーザーにクリックなどの動作を求めることが多いと思う。そのためゲームを始める前に「Go」というボタンを押させることで、途中でリロードされたりしても音楽が流れないことがないように対策をしていた。
game.htmlに以下の記述を追記する。

<body>
    <!-- ここから追記 -->
    <audio id="clearSound" src="musics/clear.mp3" type="audio/mp3"></audio>
    <audio id="tickSound" src="musics/tick.mp3" type="audio/mp3"></audio>
    <audio id="gameoverSound" src="musics/gameover.mp3" type="audio/mp3"></audio>
    <audio id="mainSound" src="musics/main.mp3" type="audio/mp3"></audio>
    <!-- ここまで追記 -->
    <div class="game-main">
        <div class="game-display">
            <p class="clear result">
                CLEAR

この例では以下のようなディレクトリ構造にして、musicsディレクトリの中にそれぞれ必要な音楽ファイルを用意した。用意したのは、正解時の効果音(clear.mp3)、不正解時の効果音(gameover.mp3)、ゲーム中のBGM(main.mp3)、制限時間が減る音(tick.mp3)の4つだ。

...
├── index.html
├── js
│   ├── main.js
│   └── start.js
└── musics
    ├── clear.mp3
    ├── gameover.mp3
    ├── main.mp3
    └── tick.mp3

音楽ファイルについては無料効果音で遊ぼう!の効果音を使用した。どんな音楽ファイルを用意しても構わないが、直リンクを使うのはやめよう(再配布になるため)。必ずダウンロードしてから使うように。
これで音楽ファイルをHTMLに埋め込むことができたので、次はそれらをJSで操作していく。
まずはmain.jsに以下の記述を追記する。

clearSound = document.querySelector("#clearSound"); // これを追記
tickSound = document.querySelector("#tickSound"); // これを追記
gameoverSound = document.querySelector("#gameoverSound"); // これを追記
mainSound = document.querySelector("#mainSound"); // これを追記

const startGame = () => {
    popup.style.display = "none"; // 開始画面を消す
    mainSound.play(); // これを追記
...

まず最初にHTMLの音楽タグを取得して、変数に格納しておく。ついでにゲームがスタートした時にBGMが流れるようにもしておこう。playメソッドを使うことで、任意のタイミングで再生することができる。次に制限時間が減る音を流すようにする。
main.jsに以下の記述を追記する(timeIntervalを以下のように変更する)。

...
const timeInterval = setInterval(() => {
        time -= 1;
        timeDisplay.innerText = time;
        if(time == 5) {
            tickSound.volume = 0.0;
            tickSound.play();
        } else if (time < 5 && 0 < time) {
            tickSound.volume += 0.24;
        } else if (time == 0) {
            tickSound.volume = 0.0;
            mainSound.volume = 0.0;
            judge();
            clearInterval(timeInterval);
        }
    },1000)
...

本来は1秒ごとにtickSoundのplayメソッドを発動させて、時間が0秒になったらゲームのBGMと制限時間の効果音どちらの音量も0にすればよい。ただここでは少しだけ工夫を施してしまった。時間が5秒になってから制限時間の効果音を発動させて、そこから1秒ごとに少しずつ音量を高めることで緊張感を出した。この辺もお好みで設定してもらいたい。注意点としてはvolumeは0から1の間で設定する必要があり、それを超えるとエラーになってしまう。
最後に正解時と不正解時の効果音を追加する。
main.jsに以下の記述を追記する。

...
}
    }
    
    if(select == ANS) {
        let clearDisplay = document.querySelector(".clear");
        clearDisplay.style.display = "block";
        clearSound.play(); // ここを追記
        localStorage.setItem("level", level+1);
        setInterval(() => {
            window.location.reload();
        }, 3000)
    } else {
        let gameoverDisplay = document.querySelector(".gameover");
        gameoverDisplay.style.display = "block";
        localStorage.removeItem("level");
        gameoverSound.play(); // ここを追記
        setInterval(() => {
            window.location.href = 'index.html';
        }, 3000)
    }
...

以上でゲームにBGMと効果音をつけることができた。音を追加するだけでかなりゲームっぽくなった。

途中から始められる機能を追加

これで追加する最後の機能だ。ゲームをプレイしてかなり高いレベルまで辿り着いた時、ハイスコアとして記録を残すことができたら嬉しい。また、localStorageのレベルが保存されていれば、途中のレベルからスタートさせるということも可能だ。ここでは両方ともつけることにする。まずは途中のレベルからスタートできる機能を追加する。
main.jsに以下の記述を追記する。

// ここから追記
let main = document.querySelector(".main");

if(localStorage.getItem("level")) {
    let level = Number(localStorage.getItem("level"));
    if (level != 1){
        let startButtonRestart = document.createElement("button");
        startButtonRestart.className = "start-button restart";
        startButtonRestart.innerHTML = "Start from Level " + level;
        main.appendChild(startButtonRestart);
        startButtonRestart.addEventListener("click", () => {
            window.location.href = 'game.html';
        })
    }
}

// ここまで追記

let startButtonNew = document.querySelector(".new");
startButtonNew.addEventListener("click", () => {
    localStorage.setItem("level", 1); 
    window.location.href = 'game.html';
})

実は少し今までとは違う形でHTMLを追加した。今までは元々HTMLに要素を記述し、CSSでdisplayをnoneにしておいて、JSでそれらを表示する形で要素を出現させていた。ここでは要素から全てJSで追加した。記述量は多くなるが、こういった形で要素を追加させることで、for文を使えば無限に要素を増やすことができる。この機能はおまけということで、このような方法で記述した。実は赤と青のブロックを増やすときにこの方法を使っている。ただし、CSSもJSで記述すると少し見にくくなってしまうので、cssは別途記述しよう。また、この後ハイスコア機能も作るので、その表示のCSSもここで追記してしまおう。
style.cssに以下の記述を追記する(場所はどこでも良い)。

.restart {
    margin-left: 30px;
    background-color: #e88eff;
}
.hiscore {
    font-size: 36px;
}

これで、レベル1以外でgame.htmlのページにいる時に、途中でindex.htmlに移動したりすると、以下の画像のように途中から遊ぶことができるボタンが追加される。
スクリーンショット 2023-07-19 23.54.16.png
ただ、GAME OVER表示されてしまうと、localStorageのlevelの値が消されてしまうため注意。優しい設定にするために、不正解しても途中のレベルから遊べるような機能にしても良いかもしれない。ぜひお好みで設定してほしい。
それではハイスコアを表示できるようにしよう。
start.jsに以下の記述を追記する。

let startButtonNew = document.querySelector(".new");
startButtonNew.addEventListener("click", () => {
    localStorage.setItem("level", 1); 
    window.location.href = 'game.html';
})

// 以下を追記

if(localStorage.getItem("hiscore")) {
    let hiscore = document.createElement("p");
    hiscore.className = "hiscore";
    hiscore.innerHTML = "HI-SCORE: " + localStorage.getItem("hiscore");
    main.appendChild(hiscore);
}

これで表示はできるようになったので、hiscoreというKeyの値を保存する機能を追加していく。
main.js以下の記述を追記する。

...
if(select == ANS) {
    let clearDisplay = document.querySelector(".clear");
    clearDisplay.style.display = "block";
    clearSound.play();
    localStorage.setItem("level", level+1);
    setInterval(() => {
        // ここから追記
        let hiscore = 1;
        if(localStorage.getItem("hiscore")) {
            hiscore = localStorage.getItem("hiscore");
        }
        hiscore = Math.max(hiscore, level+1);
        localStorage.setItem("hiscore", hiscore);
        // ここまで追記
        window.location.reload();
        }, 3000)
    } else {
        let gameoverDisplay = document.querySelector(".gameover");
        gameoverDisplay.style.display = "block";
        localStorage.removeItem("level");
        gameoverSound.play();
        setInterval(() => {
...

hiscoreが定義されていなければ、hiscoreに初期値として1を入れておく。次のレベルとhiscoreの値を比べて大きい方をhiscoreに値を入れて、localStorageで保存する。こうすることで、ゲームで一回正解すれば、index.htmlに以下の画像のようなハイスコアが表示される。
スクリーンショット 2023-07-20 0.09.24.png

終わりに

これで全ての機能が追加されました。ここまで読んでくださった方がいましたら、心から感謝いたします。所々省略している部分や分かりにくい部分があったかと思いますが、少しでもためになる情報があれば幸いです。作ってみるとわかりますが、このゲームにはまだまだ課題がありますし、もっとゲームが面白くなるように拡張することができると思います。それは是非皆さんで拡張していって、さらに面白いゲームになることを期待しております。
これからもアウトプットをするたびにこのような記事を書いていきたいと思いますので、またどこかで見かけましたらその際はぜひご覧ください。ありがとうございました。

※追記
@mogamoga1337さんからのコメントを参考にさせていただき、block要素をClassとして記述したものをmain2.jsとしてGithubに保存しました。もしよろしければそちらも参考にしてください。

6
2
3

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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?