beatmania IIDXをブラウザで練習できるようにした

## 目次

- [記事の内容](#記事の内容)
- [なぜ記事を書こうと思ったのか](#なぜ記事を書こうと思ったのか)
- [なぜ作ろうと思ったのか](#なぜ作ろうと思ったのか)
- [本編](#本編)
  - [画面の見た目](#画面の見た目)
  - [JS部分の説明](#js部分の説明)
    - [要素取得](#要素取得)
    - [保存データ適用](#保存データ適用)
    - [同時に落ちるノーツ数の決定](#同時に落ちるノーツ数の決定)
    - [重複なしの場合のノーツが落ちる場所を決定](#重複なしの場合のノーツが落ちる場所を決定)
    - [ノーツが落ちる場所を決定](#ノーツが落ちる場所を決定)
    - [1P2Pの変更](#1p2pの変更)
    - [SUDDENとLIFTの高さを更新](#suddenとliftの高さを更新)
    - [皿が落ちる間隔を変更](#皿が落ちる間隔を変更)
    - [開始ボタンが押された時の動作](#開始ボタンが押された時の動作)
    - [その他ボタンの動作](#その他ボタンの動作)
- [終わりに](#終わりに)

## 記事の内容
本記事で紹介しているWebアプリ：https://nightshrine.github.io/iidx_browser/
上記のWebアプリのコード：https://github.com/nightshrine/iidx_browser
詳しい使い方は[こちら](https://github.com/nightshrine/iidx_browser)のページに記載しています。

![画面収録-2023-07-25-14.05.09.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/151f9332-e04e-1fa4-9f41-0e727e3d1de8.gif)
本記事ではアーケード音楽ゲームであるbeatmania IIDX（以降、IIDX）をブラウザでシミュレーションができるWebアプリを紹介するものです。注意ですが、このWebアプリは実際に遊べるものブラウザゲームなどではなく、あくまでシミュレーションによって認識力を高めることを目的としています。ご了承ください。
また、本記事は紹介が主な内容ですので、細かい部分のプログラムの説明を省略している箇所がありますが、詳しくは[こちら](https://github.com/nightshrine/iidx_training)をご覧ください。

## なぜ記事を書こうと思ったのか
本記事は活動記録が大きな目的ですが、Qiitaを利用しているユーザーにIIDXをプレイしている方がいたら、ぜひこのWebアプリを紹介したいと思ったことがきっかけです。それに加え、このWebアプリをより良くするためのアドバイスなどをコメントでいただけたら最高です。

## なぜ作ろうと思ったのか
IIDXでの認識力を高めるためにシミュレーションのようなことができるアプリがないか探していたところ、スマホアプリでは存在していました。こちらも素晴らしいアプリだったのでしばらく利用させて頂きましたが、ブラウザで気軽にできたら嬉しいと思い探してみたところ、見つかりませんでした。そこでJavaScriptの勉強ついでに、このアプリを開発しました。もしかしたらもうすでにこのようなWebアプリがあったのかもしれませんが、自分にもこのようなWebアプリが作れるということがまた1つ自信になり、とても良い経験になりました。

## 本編
それではプログラムを見ていく。全体的な流れとしては、HTML、CSSのプログラムを軽く説明してから、残りはJSのプログラムについて説明をしていく。また、コードの全体像については[こちら](https://github.com/nightshrine/iidx_training)をご覧いただきたい。
プログラムの構成としては以下のようになっている。
```console
.
├── index.html
├── js
│   └── main.js
└── style
    └── style.css
```

### 画面の見た目
まずはindex.htmlについて軽く説明を行う。これは以下のような記述構成になっている。
```html
<!DOCTYPE html>
<html>
    ...
    <body>
        <main>
            <div id="setting">
                <!-- シミュレーション全体の設定 -->
                <div id="setting_title">
                    <h1>設定</h1>
                </div>
                <div id="setting_form">
                    <label>
                        <p>1P / 2P</p>
                        <select id="player" class="setting_input">
                            <option value="1" selected>1P</option>
                            <option value="2">2P</option>
                        </select>
                    </label>
                    <label>
                        <p>SUDDEN</p>
                        <input type="range" id="sudden-slider" min="0" max="60" value="10">
                    </label>
                    <label>
                        <p>LIFT</p>
                        <input type="range" id="lift-slider" min="0" max="80" value="30">
                    </label>
                    ...

                    <!-- n個のノーツが同時に落ちてくる割合 -->
                    <label>
                        <p>scratch: </p>
                        <input type="number" class="setting_input" id="scratch-note-ratio" min="0" max="100" value="0">
                    </label>
                    <label>
                        <p>0notes: </p>
                        <input type="number" class="setting_input" id="zero-note-ratio" value="0" min="0" max="100">
                    </label>
                    <label>
                        <p>1notes: </p>
                        <input type="number" class="setting_input" id="one-note-ratio" value="0" min="0" max="100">
                    </label>
                    <label>
                        <p>2notes: </p>
                        <input type="number" class="setting_input" id="two-note-ratio" value="0" min="0" max="100">
                    </label>
                    ...

                    <!-- 操作ボタンなど -->
                    </label>
                    <button id="start-button">開始</button>
                    <button id="stop-button">停止</button>
                    <button id="reset-button">設定リセット</button>
                    <button id="save-button">設定保存</button>
                    <label>
                        <p>設定の詳細説明：</p>
                        <a id="detail" href="https://github.com/nightshrine/iidx_training">詳細説明↩︎</a>
                    </label>
                </div>
            </div>
        ...
　　　　　　　　　　　　　　　　　　　　　　　　<div id="game-display">
                <!-- SUDDEN部分 -->
                <div id="game-sudden">
                </div>
                <!-- レーン部分 -->
                <div id="game-lane">
                    <div class="scratch-display" id="scratch-display">
                    </div>
                    <div class="note-display" id="one-note-display">
                    </div>
                    <div class="note-display" id="two-note-display">
                    </div> 
                    <div class="note-display" id="three-note-display">
                    </div>  
                    <div class="note-display" id="four-note-display">
                    </div>
                    <div class="note-display" id="five-note-display">
                    </div>
                    <div class="note-display" id="six-note-display">
                    </div>
                    <div class="note-display" id="seven-note-display">
                    </div>
                </div>
                <!-- リフト部分 -->
                <div id="game-lift">
                </div>
            </div>
        </main>

    </body>
</html>
```

基本的には設定やレーンなどの情報が多く羅列されているが、難しい記述は特にないと思う。内容としてはまずbpmやLIFTの高さなど、全体的な見た目や動きに影響する設定項目の要素がある。その下に降ってくるノーツ数を設定できる要素があり、さらにその下に開始ボタンや停止ボタンなどの操作ができる要素を記述している。これで設定部分についての記述は終わりである。あとはシミュレーションを行う部分となる要素を記述している。
今思えばJSの方で要素を追加しても良かったのだが、あまり大きなWebアプリではないと思いHTMLで直書きしてしまった。繰り返しの記述が多いので、本当はJSで記述した方が良いと思う。

CSSについての記述は特に説明することはないと思ったので、具体的な記述については[こちら](https://github.com/nightshrine/iidx_training)をご覧いただきたい。ちなみにIIDXは皿レーンを除いて1、3、5、7レーン目はそれ以外のレーンに比べて多少横幅が大きかったりする。その辺りは実際のゲームの見た目に近くなるようにこだわった。

### JS部分の説明
ここからはJSの説明を行なっていく。HTMLに要素が多いため非常に見にくい部分があると思うが、内容に関して言えば特に難しいアルゴリズムを使っている訳ではないので、理解しやすいと思う。それではプログラムを要素ごとに区切って説明していく。繰り返しになるが、コードの全体像については[こちら](https://github.com/nightshrine/iidx_training)をご覧いただきたい。


#### 要素取得
```javascript
let startButton = document.querySelector('#start-button');
let stopButton = document.querySelector('#stop-button');
let resetButton = document.querySelector('#reset-button');
let saveButton = document.querySelector('#save-button');

let scratchDisplay = document.querySelector('#scratch-display');
let notesDisplay = document.querySelectorAll('.note-display');
let gameSudden = document.querySelector('#game-sudden');
let gameLift = document.querySelector('#game-lift');

let player = document.querySelector('#player');
let suddenSlider = document.querySelector('#sudden-slider');
let liftSlider = document.querySelector('#lift-slider');
let greenNumber = document.querySelector('#green-number');

let bpm = document.querySelector('#bpm');
let keyInterval = document.querySelector('#key-interval');
let scratchInterval = document.querySelector('#scratch-interval');

let isSrand = document.querySelector('#is-srand');
let scratchNoteRatio = document.querySelector('#scratch-note-ratio');
let zeroNoteRatio = document.querySelector('#zero-note-ratio');
let oneNoteRatio = document.querySelector('#one-note-ratio');
let twoNoteRatio = document.querySelector('#two-note-ratio');
let threeNoteRatio = document.querySelector('#three-note-ratio');
let fourNoteRatio = document.querySelector('#four-note-ratio');
let fiveNoteRatio = document.querySelector('#five-note-ratio');
let sixNoteRatio = document.querySelector('#six-note-ratio');
let sevenNoteRatio = document.querySelector('#seven-note-ratio');
```
この部分では、HTML上の要素をJavaScriptで取得している。各要素にはIDやクラスが割り当てられており、JavaScriptのquerySelectorメソッドを使用して対応する要素を取得している。

#### 保存データ適用
```javascript
window.onload = () => {
    // ローカルストレージから保存データを取得
    let iidxSettingSaveData = JSON.parse(localStorage.getItem('iidxSettingSaveData'));
    // 保存データがある場合は各要素に値をセット
    if (iidxSettingSaveData) {
        // ...省略... 各要素に保存されている値を設定
    }
}
```
この部分では、ページが読み込まれた時にローカルストレージから保存されているデータを取得し、各要素にその値を設定している。これにより、ページを閉じて再度開いたときに前回の設定が復元される仕組みを実現している。

#### 同時に落ちるノーツ数の決定
```javascript
// ノーツの個数を選択
const selectNotesNum = (notes_rate_list) => {
    // ...省略... ノーツの個数を決定して返す
}
```
この部分ではノーツの種類ごとの出現確率を配列として受け取り、その確率に基づいてノーツの個数をランダムに選択する。この関数は後のノーツの生成に使用される。

#### 重複なしの場合のノーツが落ちる場所を決定
```javascript
// 重複なしでノーツを選択
let pre_select_notes = [];
const selectNotes = (notes_num) => {
    // ...省略... 重複なしでノーツが落ちる場所を選択して返す
}
```
この部分では、前回の選択結果と重複しないようにノーツが落ちる場所を選択する。前回の選択結果はpre_select_notesという配列に保持しており、それを考慮して新しい場所を選択する。

#### ノーツが落ちる場所を決定
```javascript
// 重複ありでノーツを選択
const sSelectNotes = (notes_num) => {
    // ...省略... ノーツが落ちる場所を重複ありで選択して返す
}
```
この部分では、重複ありの場合にノーツが落ちる場所をランダムに選択する。IIDXで言うところのS乱のような配置になる。

#### 1P2Pの変更
```javascript
// Playerの変更
const playerChange = () => {
    // ...省略... Playerの値に応じてノーツが落ちる場所を変更
}
```
設定によって、皿レーンが1番右か左になるようにHTMLの要素を書き換えている。


#### SUDDENとLIFTの高さを更新
```javascript
const suddenChange = () => {
    // ...省略... SUDDENの高さを変更
}

const liftChange = () => {
    // ...省略... LIFTの高さを変更
}
```
これらの部分では、プレイヤーの選択やSUDDEN/LIFTの設定変更に応じて、ノーツやスクラッチが落ちる位置を変更する処理を行っている。

#### 皿が落ちる間隔を変更
```javascript
const scratchIntervalChange = (scratchIntervalInit = -1) => {
    // ...省略... スクラッチの間隔を変更
}
```
この部分では、scratch Intervalという設定に応じてスクラッチが落ちる頻度を決定している。



#### 開始ボタンが押された時の動作
```javascript
startButton.addEventListener('click', () => {
    // ...省略... ノーツとスクラッチの生成とアニメーション
});
```
この部分では、「開始ボタン」がクリックされた際の動作を定義しており、HTMLでの設定を反映させてさまざまな値を求めている。ノーツとスクラッチの生成とアニメーションを行なっているのだが、かなり複雑になっている部分だと思う。

#### その他ボタンの動作
```javascript
stopButton.addEventListener('click', () => {
    started = false;
});

resetButton.addEventListener('click', () => {
    // ...省略... 各要素の値を初期化
    // ...省略... 各要素の初期設定を反映
});

saveButton.addEventListener('click', () => {
    // ...省略... 各要素の値をローカルストレージに保存
});
```
これらの部分では、それぞれ「停止ボタン」、「リセットボタン」、「保存ボタン」がクリックされた際の動作を定義している。started変数の値を変更することで、ノーツやスクラッチの生成、アニメーションが制御されます。また、リセットボタンでは各要素の値を初期化し、保存ボタンでは各要素の値をローカルストレージに保存している。

## 終わりに
この記事を見ていただいた方の中でIIDXをプレイしている方がいましたら、ぜひこのアプリがより良いものになるようなアドバイスをいただけたら幸いです。また、もしよろしければ実際に使ってみてください。
ちなみに私は現在中伝ですので、皆伝取得に向けて地道に頑張っています。
ここまで見てくださって本当にありがとうございました。