走る！音が出る！HTMLでミニゲームをつくりました

<img width="640" alt="FIRE RUNNER SAN" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/846293/c66878d7-7cb7-d665-027b-d48f3aba6686.png">
会社非公式な感じの、シンプルな**ランゲーム**を作りました。

https://hasegawa-campfire.github.io/fire-runner-san/
こちらから遊べます。
PC推奨です。IEは死にました。すこし音も出ます。

実際に開くと、なんだか下のほうがゴチャゴチャしていますね。
ここ数日、色々と後付けをしてこうなりました。
ランキングとか。リプレイとか。SNSシェアとか。効果音とか。
ああ、付けてみたいなと思ってしまったのですから、これは仕方のないことです。

その辺り、思うところや紆余曲折などイロイロありますが。
モロモロは note にしたためて、こちらは技術的な記録を残します。

**※note**
[会社でランゲームをつくりました](https://note.com/8kg/n/n81ad48ffa927)

## 技術概要

HTML5 でゲーム作りと言えば、やはり WebGL でしょう。
描画が高速で、表現力も高い。ライブラリやフレームワークも充実しています。

しかし今回は WebGL どころか canvas ですらなく、本当に **HTML** で構成されています。
ライブラリも特に使っていません。（Firebase を除く）

```html:概略
<div class="app">
  <div class="world" style="transform: translate(32px, 256px);">
    <svg class="player" style="transform: translate(0px, 0px);">...</svg>
    <svg class="item" style="left: 200px; top: 0px;">...</svg>
    ...
  </div>
  <div class="status show">...</div>
  ...
</div>
```

`div` を連ねて、 `positoin` で配置して、 `border` `background` などで彩る。
`addEventListener` を仕掛けて、`transform` などで動かす。

Webフロントとしては、いつもの見慣れた **HTML** + **CSS** + **JS** です。

### つよいところ

- とにかく書き慣れていること
- 複雑な仕組みが要らないこと
- 簡単なアニメーションが、本当に簡単なこと

あと今回に関して言えば、Webフロントエンジニアの発表物として、
HTML と CSS を活用したほうが紹介しやすい部分も多いだろう、
という目論見もありました。
当初は。

### つらいところ

- とにかく遅いこと
- ゲームとしての表現力は弱いこと
- 表示の見え方についても動作環境に振り回されること

あと今回に関して言えば、スクリーンショットが取れないこと。
SNSシェアに使いたかったんです。
しかし自前で実現しようと思ったら、ものすごく頑張る必要があります。
[html2canvas](https://html2canvas.hertzen.com/) でもダメだったので、諦めました。

## ゲーム表示対応

実は、見慣れた内容になりすぎて、改めて紹介できそうな部分がとても少なくなりました。
とはいえ、
さすがに普段作っているWebページでは使わないようなものも、いくつかあります。

### 描画速度の改善

HTMLの要素をゴリゴリ動かすので描画が遅くなりがちです。特にスマホ。
完成してから、初めて手元のスマホから見て頭を抱えたりもしましたが、もう遅い。

というわけで描画改善ですが、ただおもむろに `will-change` をつけるだけです。
簡単ですね。
これは予めどう動くのか宣言しておくことで、
その要素についてGPUを確保するなど最適化をしてくれるそうです。

ただし、乱用するとGPUやメモリを圧迫して逆に遅くなるとも言います。
あと意図しない表示バグを引き起こしたりもします。

```css
.hoge {
  will-change: transform, opacity;
}
```

とはいえ効果は絶大。ガタガタしていたゲームが、突然ヌルヌルになるほど。

スマホのスペックの目安がわからなくて、どの程度まで動けばいいのか判断に困るのですが、
とりあえず手元の **Rakuten Mini** で動けばだいたい動くのでは？
という調整で作っています。

※ただしゲーム的にはPC推奨。スマホでのクリアは相当困難です。

### GPUで困った時のおまじない

`will-change` や `transform` を使っていると、時々、想定外の事態に出くわします。
表示が欠けるとか、ボケるとか。重なりが `z-index` を無視するとか。

そんな時は `backface-visibility: hidden` を書くと解決する、こともあります。

```css
.hoge {
  backface-visibility: hidden;
}
```

まるで魔法のようですが、私には解決する理由がわからないので、だいたい魔法です。
意味合いとしては裏面を表示しないようにする、つまりカリングだと思うんですが。
どうしてそれで直るんでしょうね。

### 右クリックメニューや文字選択の禁止

Webコンテンツを守る不毛な対策。ではありません。
スマホでの長押し操作は、文字の選択やメニューの表示でキャンセルされてしまうため、
それを回避します。

```js
addEventListener('contextmenu', (e) => {
  e.preventDefault()
})
```
```css
html {
  user-select: none;
}
```

### スマホのビューポート

だいたい定型文で済まされる `viewport` の定義。
しかしスマホやゲームなら、ちょっと立ち止まってみる価値があるかもしれません。

`width` に数値を入れると、画面はその幅で表示してくれます。可能な限り。
もうスマホの画面に合わせようと、スクリプトで頑張らなくてもいいんですね。
画面が合わせます。

ダブルタップや、ピンチアウトでの画面の拡大。
それを禁止するのは良くないことだと言われますが、ゲームでは知ったことではありません。

```html
<meta name="viewport" content="width=640,user-scalable=no">
```

### Pull to Refreshの抑制

画面を下に引っ張って、ページを読み直す。
これもゲームでは操作の間違いになりますから、動かないようにします。

```css
body {
  overscroll-behavior-y: none;
}
```

### キーボードでのスクロールの抑制

当然ですが、スペースキーや矢印キーを押すと、ページがスクロールします。
これもゲームでは嬉しくないのでストップ。
なのですが、テキストの入力も出来なくなってしまいますので、
実際にはそういったものを除外するような記述が必要になります。

```js
addEventListener('keydown', (e) => {
  if (e.key === ' ') {
    e.preventDefault()
  }
})
```

ところで `keydown` ってキャンセルしても `keyup` が動くんですね。
`keypress` は動かなくなりますけど。

## 音周り

JavaScriptにおいて、音の再生はとても簡単です。
`<audio>` を作って `play()` するだけ。ありがたい。

```js
const audio = new Audio('hoge.mp3')
audio.play()
```

しかしこれではスマホで遅延が起きます。
読み込みので話ではありません。1度再生をした音でも毎回再生で遅延します。

とはいえ、遅延くらい別にいいのでは。PC推奨ですしね。と思っていたのですが。

### Safari対応

実はこのままだとSafariでほとんど音が鳴りません。

Chromeでも何か操作をするまで再生できないというセキュリティ仕様がありますが、
Safariは更に厳格で、毎回、ユーザー操作のタイミングでしか再生できないようです。

当然のように回避策はあって、初回のユーザー操作時に再生して、すぐに一時停止して、
あとは好きなタイミングで再開すれば良いとかなんとか。なるほど。

しかし1度再生し終えたら、また再生するには同じことが必要になりそうなので、
効果音として使うのは難しそうでした。
（動きを見ないで次に進んだため、試していません。違っていたらすみません）

### Web Audio API

そこで **Web Audio API** 。
こちらにも似たセキュリティ仕様はありますが、大本の `AudioContext` をなんとかすれば、
あとはそれぞれの音を好きなタイミングで鳴らせます。スマホでの遅延もなくなります。
素敵ですね！

今回はこんな感じのものを書いて `AudioContext` を取りました。

```js
const audioContextPromise = new Promise(resolve => {
  for (const type of ['touchend', 'mouseup', 'keyup']) {
    addEventListener(type, resolve)
  }
}).then(async () => {
  const ctx = new AudioContext()
  await ctx.resume()
  return ctx
})
```


**※参考**
[Web Audio APIの闇](https://qiita.com/zprodev/items/7fcd8335d7e8e613a01f)

## Firebase

今回、気まぐれにランキングやアカウントの紐付けを実装したくなったので、
初めて **Firebase** を利用してみました。

これは、なんと言いますか、
とても簡単に使えすぎて知見としてはあまり残せそうにないのが難ですね。
ありがたいことです。

しいて言うと、
**Firebase Authentication** がユーザー名を持ってくれるので便利だとか、
ランキングは **Firestore** から1度読んだら、あとは放置しても自動更新されて楽だとか、
そんな感じです。

**※参考**
[0から始める Firestore + Firebase Authentication](https://qiita.com/karayok/items/0996c8f0ea219c284dbd)
[Firestore Security Rules の書き方と守るべき原則](https://qiita.com/KosukeSaigusa/items/18217958c581eac9b245)

## おわりに

Webフロントはたのしいですね！
Webフロントはたのしいですよ！

**※ FIRE RUNNER SAN リポジトリ**
https://github.com/hasegawa-campfire/fire-runner-san