JSで簡単なゲーム作ってみた

今まで授業で習ってきた内容を活用して、JavaScriptで簡単なゲームを作ってみました。
#ゲームについて
###ダウンロード
こちらからダウンロードできます。
https://drive.google.com/file/d/1zp9tvFvftziMUGV-gTdD2AS15D4vkZM9/view?usp=sharing
「Game.zip」を解凍し、その中にある「index.html」を開くことで、ゲームをプレイすることができます。
※「CodePen」を利用してこちらの記事にソースコードを埋め込むこともできたのですが、ゲームが上手く動作しませんでした・・・。
###概要
このゲームは、画面上に一定時間の間隔で移動する1つのボタンを、単純にクリックしていくという、とてもシンプルなゲームです。クリックしていく度に、その間隔は短くなっていきます。FPSゲームのエイム練習にはもってこいのゲームなのではないかと思います。
####起動画面
![game1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869441/2b630dd8-116d-39ed-f519-4a4e2aa476b5.png)
・「START」ボタンを押すと、ゲームが始まります。
・「-LEVEL-」では、自分で難易度を選択することができます。(Hard：難しい、Normal：普通、Easy：易しい)
####ゲーム画面
![game2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869441/fab47ca6-6433-0b79-f2bf-aaa6ee3dda3d.png)
・数字が書かれているボタンをクリックしていきます。画像では「01」となっていますが、クリックすると数字が1増えます。「20」をクリックしたら、ゲーム終了となります（ソースコード「main.js」の変数LOOPで簡単に変更可）。
・「0:05」は、タイムを表しています。
・「Give up」ボタンを押すことで、ゲームを強制終了させることができます。
・ボタンの移動する間隔は、難易度によって異なります。仕様は以下の通りです（「01」→「20」まで、一定の間隔で速くなっていきます）。
<dl>
  <dt>Hard：難しい</dt>
  <dd>最初（「01」）：0.7秒</dd><dd>最後（「20」）：0.4秒</dd>
  <dt>Normal：普通</dt>
  <dd>最初（「01」）：0.85秒</dd><dd>最後（「20」）：0.6秒</dd>
  <dt>Easy：易しい</dt>
  <dd>最初（「01」）：1秒</dd><dd>最後（「20」）：0.8秒</dd>
</dl>
####終了画面
「20」までクリックできた（ゲームクリア）場合
![game3-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869441/6f5dcc40-aafa-8a35-0238-c4d0cc6bbdac.png)
途中で「Give up」ボタンを押した（ゲームオーバー）場合
![game3-2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869441/5e7f34f8-9acf-d226-b05f-000f47de8909.png)
・（前者）「0:40」は、「20」をクリックできた時点でのタイムを表しています。
・（後者）「13 /20」は、自分がクリックできなかったボタンの数字を表しています（画像では「13」）。
・「LEVEL:」は、自分が選択した難易度を表しています。
・「Return」ボタンを押すと、「起動画面」に戻ります（ページリロード）。
#ソースコード
###JavaScript

```JavaScript:main.js
const LOOP = 20;  //数字ボタンの出現回数

let passSec = 0;
let count;
let num;
let cdiv;
let selectLevel;

//背景生成
function chgImg(){
    const pdiv = document.getElementById("parent-div");

    //canvasの描画
    const canvas = document.createElement("canvas");
    canvas.width = 460;
    canvas.height = 345;
    pdiv.appendChild(canvas);
    const context = canvas.getContext("2d");
    context.fillStyle = "#000000";
    context.rect(0,0,canvas.width,canvas.height);
    context.fill();

    //canvasからimgに変換
    const jpg = canvas.toDataURL();
    const newImg = document.createElement("img");
    newImg.src = jpg;
    pdiv.replaceChild(newImg, canvas);
}

//画面内消去
function Clear(){
    while(cdiv.firstChild){
        cdiv.removeChild(cdiv.firstChild);
    }
}

//起動画面
function Start(){
    //タイトル
    const titleText = document.createElement("h1");
    const titleContent = document.createTextNode("Simple Game");
    titleText.appendChild(titleContent);
    titleText.id = "title";
    cdiv.appendChild(titleText);

    //スタートボタン
    const startButton = document.createElement("button");
    const startContent = document.createTextNode("START");
    startButton.appendChild(startContent);
    startButton.id = "start";
    cdiv.appendChild(startButton);

    //難易度選択(ラジオボタン)
    const levelText = document.createElement("p");
    const levelContent = document.createTextNode("-LEVEL-");
    levelText.appendChild(levelContent);
    levelText.id = "level";
    cdiv.appendChild(levelText);
    const levelDiv = document.createElement("div");
    levelDiv.id = "level-div";
    cdiv.appendChild(levelDiv);

      //難易度：難しい
    const hardDiv = document.createElement("div");
    levelDiv.appendChild(hardDiv);
    const hardRadio = document.createElement("input");
    const hardAtt = {
        type: "radio",
        name: "select",
        value: "hard",
        id: "hard"
    }
    for(let i in hardAtt){
        hardRadio[i] = hardAtt[i];
    }
    hardDiv.appendChild(hardRadio);
    const hardLabel = document.createElement("label");
    const hardText = document.createTextNode("Hard");
    hardLabel.appendChild(hardText);
    hardLabel.htmlFor = "hard";
    hardDiv.appendChild(hardLabel);

      //難易度：普通
    const normalDiv = document.createElement("div");
    levelDiv.appendChild(normalDiv);
    const normalRadio = document.createElement("input");
    const normalAtt = {
        type: "radio",
        name: "select",
        value: "normal",
        id: "normal"
    }
    for(let i in normalAtt){
        normalRadio[i] = normalAtt[i];
    }
    normalDiv.appendChild(normalRadio);
    const normalLabel = document.createElement("label");
    const normalText = document.createTextNode("Normal");
    normalLabel.appendChild(normalText);
    normalLabel.htmlFor = "normal";
    normalDiv.appendChild(normalLabel);

      //難易度：易しい
    const easyDiv = document.createElement("div");
    levelDiv.appendChild(easyDiv);
    const easyRadio = document.createElement("input");
    const easyAtt = {
        type: "radio",
        name: "select",
        value: "easy",
        id: "easy",
        checked: "checked"
    }
    for(let i in easyAtt){
        easyRadio[i] = easyAtt[i];
    }
    easyDiv.appendChild(easyRadio);
    const easyLabel = document.createElement("label");
    const easyText = document.createTextNode("Easy");
    easyLabel.appendChild(easyText);
    easyLabel.htmlFor = "easy";
    easyDiv.appendChild(easyLabel);
}

//タイマー処理
function showPassage(){
    const watch = document.getElementById("wat-style");
    passSec++;

    let sec = passSec % 60;  //秒
    let min = parseInt(passSec / 60);  //分

    if(sec <= 9){
        sec = "0" + sec;
    }
    if(min >= 10){
        const watLayout = window.getComputedStyle(watch);
        watch.style.left = watLayout.left;
        watch.style.left = 395 + "px";
    }

    count = min + "：" + sec;
    watch.innerHTML = count;
}

//数字ボタンの位置決定
function Move(){
    let x, y;
    
    do{
        x = Math.random();  //imgの左上隅画素の中心を原点(0,0)とした時の、右方向へ正のX座標を決める乱数
        y = Math.random();  //imgの左上隅画素の中心を原点(0,0)とした時の、下方向へ正のY座標を決める乱数
    }while((x * 429 > 352 && y * 324 > 287) || (x * 429 > 356 && y * 324 < 30));  //終了ボタンやタイマーと重なったらやり直し

    const numberBut = document.getElementById("num-but");
    const numStyle = window.getComputedStyle(numberBut);
    numberBut.style.left = numStyle.left;
    numberBut.style.top = numStyle.top;
    numberBut.style.left = x * 429 + "px";
    numberBut.style.top = y * 324 + "px";
}

//終了画面
function End(issue){
    const name = document.createElement("h1");
    name.id = "nametag";
    const result = document.createElement("h2");
    result.id = "resulttag";

    //1:ゲームクリア, 2:ゲームオーバー
    switch(issue){
        case 1: const winContent = document.createTextNode("-CLEAR TIME-");
                name.appendChild(winContent);
                cdiv.appendChild(name);
                const timeResult = document.createTextNode(count);
                result.appendChild(timeResult);
                cdiv.appendChild(result);
                break;
        case 2: const loseContent = document.createTextNode("-SCORE-");
                name.appendChild(loseContent);
                cdiv.appendChild(name);
                const scoreResult = document.createTextNode(num);
                result.appendChild(scoreResult);
                cdiv.appendChild(result);
                const max = document.createElement("h3");
                max.id = "maxtag";
                const maxContent = document.createTextNode("/" + LOOP);
                max.appendChild(maxContent);
                cdiv.appendChild(max);
    }

    //リターンボタン(ページリロード)
    const returnButton = document.createElement("button");
    const returnContent = document.createTextNode("Return");
    returnButton.appendChild(returnContent);
    returnButton.id = "return";
    cdiv.appendChild(returnButton);
    returnButton.onclick = function(){
        window.location.reload();
    }

    //設定難易度の表示
    const selectText = document.createElement("p");
    const myLevel = selectLevel.charAt(0).toUpperCase() + selectLevel.slice(1);
    selectText.innerHTML = "LEVEL: <span id=\"select-" + selectLevel + "\">" + myLevel + "</span>"
    selectText.id = "select-text";
    cdiv.appendChild(selectText);
}

window.onload = function(){
    cdiv = document.getElementById("child-div");
    chgImg();
    Start();

    document.getElementById("start").addEventListener("click", function(){
        let first, next;

        const select = document.getElementsByName("select");

        //ラジオボタンで難易度設定
        for(let i = 0; i < select.length; i++){
            if(select[i].checked){
                selectLevel = select[i].value;
                break;
            }
        }

          //hard:難しい, normal:普通, easy:易しい
        switch(selectLevel){
            case "hard":   first = 700;
                           next = 300 / (LOOP - 1);
                           break;
            case "normal": first = 850;
                           next = 250 / (LOOP - 1);
                           break;
            case "easy":   first = 1000;
                           next = 200 / (LOOP - 1);
        }

        Clear();

        //タイマー生成
        const watch = document.createElement("p");
        watch.id = "wat-style";
        cdiv.appendChild(watch);
        watch.innerHTML = "0：00";
        passSecID = setInterval("showPassage()", 1000);
            
        //数字ボタンの処理
        const numButton = document.createElement("input");
        numButton.type = "button";
        numButton.value = "01";
        num = 1;
        numButton.id = "num-but";
        cdiv.appendChild(numButton);
        Move();
        timer = setInterval("Move()", first);
        numButton.onclick = function(){
            clearInterval(timer);
            if(num < 9){
                numButton.value = "0" + ++num;
            }else{
                numButton.value = ++num;
            }
            Move();
            timer = setInterval("Move()", first - next * (num - 1));
            if(numButton.value > LOOP){
                clearInterval(timer);
                clearInterval(passSecID);
                Clear();
                End(1);
            }
        }

        //終了ボタン
        const endButton = document.createElement("button");
        const endContent = document.createTextNode("Give up");
        endButton.appendChild(endContent);
        endButton.id = "give-up";
        cdiv.appendChild(endButton);
        endButton.onclick = function(){
            clearInterval(timer);
            clearInterval(passSecID);
            Clear();
            End(2);
        }
    });
}
```

###HTML

```HTML:index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <div id="parent-div">
            <div id="child-div"></div>
        </div>
        <script src="main.js"></script>
    </body>
</html>
```

###CSS

```CSS:style.css
img{
    user-select: none;
    -webkit-user-drag: none;
    -khtml-user-drag: none;
    -moz-user-drag: none;
    -o-user-drag: none;
}

#parent-div{
    position: relative;
    width: 460px;
    height: 345px;
}

#title{
    color: #00bfff;
    font-family: sans-serif;
    font-size: 45px;
    text-align: center;
    position: absolute;
    top: 70px;
    left: 0;
    right: 0;
    margin: auto;
    user-select: none;
}

#start{
    position: absolute;
    top: 230px;
    left: 0;
    right: 0;
    margin: auto;
}

#give-up{
    position: absolute;
    top: 315px;
    left: 390px;
}

#num-but{
    position: absolute;
    left: 0;
    top: 0;
}

#wat-style{
    position: absolute;
    left: 405px;
    top: 5px;
    color: yellow;
    margin: 0;
    user-select: none;
}

#nametag{
    color: red;
    font-family: sans-serif;
    font-size: 50px;
    text-align: center;
    position: absolute;
    top: 70px;
    left: 0;
    right: 0;
    margin: auto;
    user-select: none;
}

#resulttag{
    color: yellow;
    font-family: sans-serif;
    font-size: 40px;
    text-align: center;
    position: absolute;
    top: 180px;
    left: 0;
    right: 0;
    margin: auto;
    user-select: none;
}

#maxtag{
    color: white;
    font-family: sans-serif;
    font-size: 25px;
    position: absolute;
    top: 180px;
    left: 280px;
    user-select: none;
}

#return{
    position: absolute;
    top: 315px;
    left: 396px;
}

#level{
    font-family: sans-serif;
    position: absolute;
    top: 2px;
    left: 5px;
    color: white;
    margin: 0;
    user-select: none;
    font-size: 90%;
}

label[for="hard"]{
    font-family: sans-serif;
    color: #ff6347;
    font-size: 85%;
    font-weight: bold;
    user-select: none;
    padding: 0;
}

label[for="normal"]{
    font-family: sans-serif;
    color: yellow;
    font-size: 85%;
    font-weight: bold;
    user-select: none;
}

label[for="easy"]{
    font-family: sans-serif;
    color: #1e90ff;
    font-size: 85%;
    font-weight: bold;
    user-select: none;
}

#level-div{
    position: absolute;
    top: 18px;
    left: 0;
    line-height: 10px;
}

#select-hard{
    color: #ff6347;
    font-weight: bold;
}

#select-normal{
    color: yellow;
    font-weight: bold;
}

#select-easy{
    color: #1e90ff;
    font-weight: bold;
}

#select-text{
    position: absolute;
    top: 2px;
    left: 5px;
    color: white;
    margin: 0;
    user-select: none;
}
```

###varとletとconstの使い分けについて
再宣言も再代入もできるのがvar、再宣言はできないが再代入ができるのがlet、再宣言も再代入もできないのがconstですが、再宣言も再代入もできるvarが一番便利だから、「変数は基本varでいいんじゃないの？」と思って、このゲームをプログラミングした当初は基本varを使っていました。
しかし、変数が意図せず上書きされてしまったり、思わぬバグが発生したりするケースがあるようです。
なので変数は基本constで宣言し、for文等で使用する再代入が必要な変数はletで宣言するようにしてみました。
上記のJavaScriptのソースコードから分かるように、varは一度も使用していません。
これからも何かJavaScriptでプログラミングする際には、そのように意識してみたいと思います。
#感想
今回、なるべくHTMLはコーディングせず、基本JavaScriptによるDOM操作でプログラミングしてみました。結果として、DOM操作に慣れることができたので良かったと思いました。ただ、非常にシンプルなゲームであっても、イメージを形にすることに時間がかかり、改めて難しいと感じました。まだまだJavaScriptの知識は足りないので、これからも学習を続けていきたいと思います。
#参考サイト
[JavaScript - ページを再読み込みする](https://murashun.jp/blog/20191110-38.html)
[文字列を整数に変換！JavaScriptのparseInt( )の使い方【初心者向け】](https://techacademy.jp/magazine/15335)
[canvasを画像に変換する](https://www.pazru.net/html5/Canvas/150.html)
[JavaScriptで書く「var,let,const」の違いと使い分け方法](https://techacademy.jp/magazine/14872)
[window.getComputedStyle() で要素のスタイルを取得する](https://qiita.com/amamamaou/items/bb79bec002a6ff033810)