ortの灰ログ

人狼のことや技術のことや日々雑感

ワードウルフオンラインの技術スタック

ワードウルフオンラインの技術スタック

技術よりのお話です。
やっと個人アプリでJava以外の言語使いました。
(業務だと色々使ってるのですが..)

インフラ

Netlifyで動かしてます。
解説サイトがいくらでもあるので割愛しますが、今は静的サイトのホスティングサイトがいくつもあるし無料だしで本当に便利になりましたね。 独自ドメインSSLも大体いけますし。

  • GitHub Pages
  • Firebase Hosting
  • GitLab Pages
  • Netlify

このあたりで迷いましたが、ビルドできるのと無料枠が広いのでNetlifyにしました。
(他のでも別に問題があったわけではないです)

バックエンド

Firebase以外は使っていません。サーバーレスアプリにしました。

項目 採用したもの
DB Firebase Realtime Database / Firebase Cloud Firestore併用
認証 Firebase Authentication
バッチ処理 Cloud Functions (Typescript)

バッチ処理は終了した村を過去ログに移す程度の簡単なやつです。
DB併用のところは後で書きます。

フロントエンド

項目 採用したもの
言語 Vue.js
フレームワーク Nuxt.js
状態管理 Vuexfire
CSS Bulma, Font awesome

Angular2とかも業務で触っていたりするのですが、Vue.jsは触ったことがなかったのですよね。
使ってみた所感としては、簡単な個人アプリとかならVue.jsはかなり良さげでした。
ちゃんと型を定義して業務用のものを作るとかだとReactとかAngularとかのほうが良いかも?という感じ。
Nuxt.jsはなんとなくで使ってみたのですが、このくらいの規模なら不要だったかもしれないです。
規約ベースで混乱せずに開発できるので、チームで開発するとかだと良いかもしれないですね。

苦戦したところ

SPAも初、サーバーレスも初、Firebaseも初、、で手探りで始めてみましたが、Vue.jsもFirebaseもドキュメントが豊富でやりやすかったです。日本語ドキュメントもかなり豊富。
挙げるとしたら以下の点でしょうか。

Firebaseの無料枠に収める

ワードウルフオンラインはほぼほぼチャットアプリなので、DBアクセスの容量やAPI呼び出し回数以外は問題にならないだろうなとは思っていました。
参考: Firebase 料金
変わっていくかもしれませんが、この記事の執筆当時で

  • Realtime Databaseの1ヶ月のダウンロード容量は1GBまで
  • Cloud FunctionsのDocument Reads/dayが50kまで

になっています。
この表を見て「1日に5万回もAPIコールできるならFirestoreだな!」と思い、意気揚々とFirestoreにチャットメッセージを登録/読み取りするよう実装していたのですが、、、

呼び出し回数をモニタリングしながらテストプレイしていると、モリモリ増えていくDocument Reads。
6-7人で1時間プレイ、精々チャットメッセージ数は1000程度なのに1日の30%以上消費。

なんでぇ!?と思いながら調べてみると、どうやらDocument Readsは「API呼び出し回数」でなく「取得したdocument数」だったようで。。(今考えるとそりゃそうだ感ありますが)

いちいち大量のチャットメッセージを取得するガバガバ処理を修正したのでした。
(ついでに、チャットだけRealtime Databaseに、チャット以外をFirestoreに保存することで分散)
(↑ちなみにこの分散はFirestoreのDocument Readsの有料枠が安いので、容量次第ですが逆のほうが良い可能性があります。)

ガバコード

  async [INIT_MESSAGE]({ commit }, { roomKey }) {
    await messagesRef
      .where('roomKey', '==', roomKey)
      .orderBy('createdAt', 'desc')
      .onSnapshot(snapshot => {
        let messages = []
        let cnt = 0
        snapshot.forEach(function(doc) {
          if (cnt >= 100) return // 最新の100個だけ表示
          messages.push(doc.data())
          cnt++
        })
        commit('initMessage', messages)
      })
  },

まさかのlimitすらなし。

修正後

async [INIT_MESSAGE]({ commit }, { roomKey, isComplete }) {
    const messages = []
    
    messagesRef = dbMessagesRef(database, roomKey)
    await messagesRef
      .orderByChild('createdAt')
      .limitToLast(100)
      .once('value')
      .then(function(snapshots) {
        snapshots.forEach(snapshot => {
          messages.unshift(snapshot.val())
        })
      })
    await messagesRef
      .orderByChild('createdAt')
      .limitToLast(5)
      .on('value', function(snapshots) {
        snapshots.forEach(snapshot => {
          const newMessage = snapshot.val()
          const latestMessages = messages.slice(0, 10)
          if (latestMessages.some(l => l.key === newMessage.key)) {
            return
          }
          messages.unshift(newMessage)
        })
      })
    commit('initMessage', messages)
  }

一度に大量発言されると最新発言をうまく読み込めないので、相変わらず「変更があるたびに最後の5発言を取得」みたいにはしてますが。。
これでだいぶ改善されました。

今のところこれで無料枠に余裕で収まっています。
万が一利用者が増えて制限を超えそうになったらまた様子を見て最適化ですね。

こんなところでしょうか。