Firestoreで無策にMMO的なものを作ろうとしたら料金が凄い事になることに気づいた話

#前置き
半分くらい仮説とか入ってるので話半分で

#経緯
ふとしたことでNuxt.jsとFirestoreでチャットアプリを作ってみたところ
「Firestoreしゅごいー！これ使えばMMOも簡単に作れるんじゃ？！(安易)」
と思い、とりあえず座標だけを共有する簡単な試作品を作る事にした。

そして複数人で動かしてみたところ数分で止まった(爆

#試作品の概要
<img width="283" alt="試作.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106608/60ca55fd-c78e-bec0-f49b-36282b777c61.png">

左下にある変な丸っこいのが、ジョイスティック的なやつ。
ログインしているプレイヤーは(FontAwesomeで適当にチョイスした)Twitterアイコンで表現。
ジョイスティックを動かして移動するだけのシンプルなもの。(もともとスマホでPWAを想定してNuxtを使ったという経緯もあってクリックとタッチどちらにも対応)
**FPS60**で座標の更新を行い、都度Firestoreへ反映。
スクショには写ってないけど下の方にチャット機能もある。

ログイン周りや座標の共有はこのあたりを参考にしながら作った。

[【v2対応】Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築する](https://qiita.com/potato4d/items/cfddeb8732fec63cb29c)

[Cloud Firestore でリアルタイム アップデートを入手する](https://firebase.google.com/docs/firestore/query-data/listen?hl=ja)

onSnapshotがとにかく便利。追加・変更・削除が行われるたび、Firestoreから通知が来るイメージ。

試作品の作りの事情とか入っていてあまり参考にならないソースだが雰囲気だけ。
・ログイン時にユーザ名・位置座標などをuserコレクションに追加
・アプリ側はログイン時にFirestore側で発行されるキーをそのままキーとして持たせたuserListでログインユーザを管理
・snapshot(onSnapshotのコールバックに渡ってくる変更内容のオブジェクト)のdocChangesメソッドで配列形式の変更内容がとれる

```javascript
// コレクションの変更を監視
this.listener.position = this.$firebase
  .firestore()
  .collection("user")
  .onSnapshot(snapshot => {
      // 変更無しなら何もしない
      if (snapshot.docChanges().length !== 1) return;
      snapshot.docChanges().forEach(change => {
        // vueは参照が変わらないと監視できないっぽいから配列・オブジェクトはcloneを作って後で代入してあげる
        let clone = { ...this.userList };
                
        switch (change.type) {
          case "added": // 新規の場合(ログインなのでuserListに追加)
            clone[change.doc.id] = change.doc.data();
            this.userList = clone;
            break;
          case "modified": // 変更の場合(移動なので対象の座標を更新)
            clone[change.doc.id].position.x = change.doc.data().position.x;
            clone[change.doc.id].position.y = change.doc.data().position.y;
            this.userList = clone;
            break;
          case "removed": // 削除の場合(ログアウトなのでuserListから除外)
            delete clone[change.doc.id];
            this.userList = clone;
            break;
          default:
            break;
        }
      });
    },
    error => {
      console.error("Oh my God !!!", error);
    }
  );
```

ログインキューの処理とかチャンネルごとのユーザの管理とかでFirestoreのドキュメント・コレクションに関するネタもあるのだけどそれは別の機会に書く。
あとジョイスティックの実装はvueの方のアドベントカレンダーに書く。(書いた→[Vue.jsでぷ○コンを作ってコンポーネント化した](https://qiita.com/vedokoy/items/743af4ef878009cabff8))

#止まったときの状況
自分1人で2ユーザで座標共有の検証は出来たので、10vs10くらいの対戦形式とか1チャンネル30人くらいでわちゃわちゃとかをこの仕組でやったらどうなのだろうと気になり数名に声をかける。
全然リッチな見た目でもないし(というか皆無)、座標共有くらいで処理落ちしたら話にならないからそのための簡単な確認が出来たらなぁという感じ。

なので問題が起きても、FPSの調整とか座標計算のロジックがいけてなくて重いとかそんな話だろうなってこのときは思っていた。

(画面下に設置したチャットで)チャットしながら、5ユーザ(PC2ユーザ、スマホ3ユーザ)でジョイスティックを動かしていたら数分後に他のユーザが動かなくなった。
PCでコンソールを見るとエラーが…

#止まった原因
無料枠の1日あたりの制限オーバー。
<img width="1317" alt="使用状況.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106608/622ddfbc-b175-5aec-34c6-64e35a0c03ec.png">
画像はFirebaseのダッシュボードで
Database > 使用状況
を見たもの。
※ オーバーした当時のをスクショしてなったのでさっき撮った平凡なもの

当日に確認した際は、この「読み取り」が上限の5万をオーバーしていた。

なお今回調べて知ったのだが、FirestoreというかGCP全般、「割り当て」から何がどれくらい使ってるかを確認する事が出来る。
[Google Cloud Platform](https://console.cloud.google.com/)のダッシュボードから
App Engine > 割り当て

Firebaseで確認した「読み取り」も
<img width="1195" alt="割当.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106608/a630865e-2272-efd7-cd91-ec0e1b7faa77.png">
「Cloud Firestore 読み取りオペレーション数」という項目がそれだと思われる。

#onSnapshotが怪しい
そもそもなんで1日で5万超えたのか。
ダッシュボードで見る限りだと(1時間刻みなので正確には分からないけど)5人で始めてから止まるまでで急激に上昇し数分で4万ほど行ったように見えた。
厳密に検証したわけではないが、この「読み取り」はonSnapshotが絡んでいる気がしてならない。
ただダッシュボードを見た限りユーザ数が増えるとものすごい勢いで増加している。

onSnapshotの動作を考えてみる。

・プレイヤー1人
自分の位置座標をFPS60、つまり1秒に60回更新している。(＝ 1秒で60回書き込み)
そしてonSnapshotで自分の変更分だけ1回受け取る。(1秒で60回読み込み)

・プレイヤー2人
書き込みは2人分になっただけなので1秒で120回書き込み。
読み込みも2人分になっただけ…なんだけどそうじゃない…！
プレイヤーA、プレイヤーBとすると、
1FrameあたりプレイヤーAとプレイヤーBの2回の書き込みを、**プレイヤーAとプレイヤーBがそれぞれ取得する。**
1秒で240回の読み込み。

つまり読み込みをonSnapshotの取得も含むとするなら、
(プレイヤー数)^2 × FPS
の回数発生する事になる。

プレイヤー5人とすると1分で
10(人)^2 × 60(fps) × 60(秒) = 90,000

上のはあくまでも1分間全員が常に動かしていたらの場合。
当時はちょろちょろ動かしていたし検証始めて数分で4万読み取りというのは、これが原因なんじゃないかと感覚値としては説明がついてしまう。

これではFPSよりプレイヤー数の方が圧倒的なネックになる…

#仮に試算
onSnapshotの取得がイコール「読み取り」だった場合

一応有料枠も考える。
[Cloud Firestore の課金について](https://firebase.google.com/docs/firestore/pricing?hl=ja#tokyo)

東京リージョンだと10万読み取りで$0.038
約4円らしい(2019/12/12 現在)

FPS60で10人がCPUのボス1人と10分戦うのを1クエストとして想定すると
10(人)^2 × 60(fps) × 60(秒) × 10(分) = 3,600,000(読み取り)
1クエスト約149円…

上の仕組みで1000人のアクティブユーザ(100チーム)が5クエストすると約74,500円…

この仕組でリ○ージュ作ったら1日で破産する()

#という事で
無策でMMOを作るのは石油王じゃないと無理。
格ゲーみたいなFPS60で1フレームの遅延が…みたいなもので無い限り、同期はFPSよりも遥かに長い(数秒に1回とか)スパンで行い、余計な通信をしないように節約した作りにしないといけないんだろうな…
そもそもNoSQLがセットでついてくるFirestoreじゃなく、別にソケットサーバを立てて、永続化が必要が無いものはより低コストにリアルタイム性を求めるとか。
某イカちゃんみたいにP2Pでサービス提供側のコストを抑えるとかとか。

出来ればFirestoreで完結させたいので、このへんとか使いつつ現実的なものが出来たらまた続編書こうと思う。
[オフラインでデータにアクセスする](https://firebase.google.com/docs/firestore/manage-data/enable-offline?hl=ja)