【Firebase】FirestoreとCloud Functionsを使ってイータイルズにレーティングシステムを実装した話

イータイルズのパズルゲームとしての面白さには確信がある。しかしそれだけでは世界中で爆発的にヒット出来るとは思わない。プレイヤーの向上心を喚起する仕組みが必要である。

リーダーボードはある。Google Play Gamesのリーダーボードだ。今やってるプレイヤーはこれを励みにしている。しかしこれでは足りない。これでは、そのタイムがどれだけ優れているか劣っているかわからないからだ。ベストタイムの全データから平均値と標準偏差を計算し、偏差値を提供するという方法もあるが、そもそも問題として、ベストタイムだけで実力を評価するのが正しいかという疑問もあるし、全データから計算するのは、スケールしない。一応、今回紹介するレーティングと同様の仕組みでFirestoreにタイムのログやベストタイムを保存しているので、実装的には出来ると思うが、優先度は低い。

もっと直接的な方法としては、ユーザをネットワーク越しに戦わせて、その勝敗に応じてレーティングを変化させることだ。これは、囲碁などのネット対戦では行われていることだし、オーバーウォッチなどの対戦ゲームでもレーティングを上げることがプレイヤーのモチベーションを高めてる。レーティングジャンキーを連鎖的に刺激することによって、イータイルズははじめて上昇気流に乗れると考えている。

レーティングを計算する方法としてもっとも有名なものにELOレーティングというものがある。これはELO HELLという言葉でも使われていて、ELOがすなわちレーティングの意味なのかと勘違いする人がいるかも知れないが、実際には考案者の数学者アルパト・イロに由来する。その根拠(推移則)や計算方法については、ウィキを見てほしい。

イロレーティング - Wikipedia

FirestoreとCloud Functionsを使ってこのELOレーティングを実装するというのが今回のお話である。セキュリティのリスクを冒したくないため、具体的なデータベース名とか関数のコードを出す気はない。完全な語りとなる。

Firestoreを使うに当たって私がもっともはじめに考えたことはお金の問題である。Firestoreでは、無料枠として、

  • read 5万回/日
  • write 2万回/日
  • storage課金 1G/月

が与えられている。この範囲内で出来るだけがんばりたいし、もし従量課金に行くとしても、悪質なユーザによって無限readされて無限課金させられたり(レートリミットも直接的には実装されていないようである)、ストレージが肥大したことによってイータイルズ自体が負債化することは避けたい。特に、後者は、仮にイータイルズが超爆発的に人気が出たあとに突然人気がなくなった時のことを考えた時にストレージが100Gあって、それをずっと保持しなければいけないとなると毎月結構な額を支払い続けることになるから、サービス停止という決断になってしまう。これを避けたい。囲碁は何千年も歴史がある。イータイルズも、そういうものにしたい。

こうするためには、ストレージ量は過去の人気ではなく現在の人気によって決まることが望ましい。よって例えば、毎月クリーニングをかけられるような性質があると良い。そうでないデータがあるのは仕方がないと思うが、そういうデータはせいぜい累積ユーザ数オーダであれば、1Gを使い切るのはなかなか難しいという言い方が出来る。例えば、100万ユーザがいても、一人あたり1K使わないといけない。

毎月クリーニングをかけられる性質のものといえばログである。というわけで、私は、

  • 勝敗のログ (誰が勝った、誰が負けた)
  • レートのログ
  • 現在のレート

を作ることにした。

ユーザから送信するデータは少なくして、出来るだけCloud Functionsの中で操作するという方針とした。これはセキュリティの点(ユーザからアクセスする口を減らせる)でも望ましい。今回であれば、

  1. ユーザからは勝敗のログをFirestoreに送る
  2. ログが作られたことをトリガにしてCloud Functionsが走る
  3. トランザクションの中で、勝者敗者の現在のレートを取得して、新しいELOレーティングを計算して、レートのログと現在のレートに書き戻す

というようにした。初版なので簡素な画面だが、出来上がったものがこんなものである。グラフの描画にはreact-native-svg-chartsを使った。

https://d33wubrfki0l68.cloudfront.net/5a55f5875b9eb6c999c529d4742103f4dba25151/9bc17/images/1560567023.jpg

このグラフはレートのログから生成している。今の規模ではまだ心配することはないが、将来的にデータベースが肥大した場合は、タイムスタンプ的に古いデータをログから削除すればよい。イータイルズのロジックに拠るが、もっとも大事なのは現在のレートや現在の実力なのであって過去のデータというのはそこまで重要なものではないので、一ヶ月以上前のものを捨ててしまってもそこまで不満は起きないはずである。

勝敗のログは、完全にテンポラリなものであるから、使ったらすぐ削除してもよいだろう。(実際にはそうしていない。データが残ってないとデバグ出来ないから)このログは、すべて自前でサーバを作るのであれば永続化されることはなく、メモリの中に存在するまま用がなくなったら消える類のものであるが、少なくとも私が調査した限りにおいてはFirestoreにおいては、データを一度永続化しなければCloud Functionsを走らせることは出来ないようだったため、このようにテンポラリのログを作るハメになった。ただCloud Functionsをキックするためのデータベースがあると、Firestoreはなお便利になると思った。すでにあるならトゥイラー(@GodAimAkira)で教えてほしい。(追記:アナリティックスのイベントを使うというのはあるかもしれないですね)

ちなみに無限リード対策については、レートリミットではなくイータイルズのロジックに頼ることにした。すなわち、レートやログはプレイしなければ更新されることは絶対にないのだから、プレイした時にリードするチケットを一枚与えて、チケットがある場合はサーバから読み、そうでない場合はキャッシュから読むということで対処することにした。こうすれば、プレイする動機にもなるし、レートやログを更新するためにリワード広告を見る動機にもなる。ネットワークアクセスの制御は、react-native-firebaseであればenableNetworkdisableNetworkを使うことが出来る。これは、AndroidやiOSではサポートされている機能であり、AsyncStorageなどと同様に、アプリをアンインストールするとキャッシュも削除される仕組みのようである。キャッシュ機能が今後もずっとサポートされるかどうかであるが、スマホではネットワークが突然切断されることも想定しなければいけないし、そういう時のためにもキャッシュ機能は残ると思うが、一方でAPIとしてキャッシュのON/OFFを制御する機能は必須とまでは言えないと思うので、今後どうなるかはわからないが、もし廃止されそうになったらその時には、超有名パズルゲームのオーナーとして一言物申しに行けばいいだけの話だ。

語りはここまでとなる。今回は、イータイルズにレーティングシステムを実装する際に、FirestoreとCloud Functionsを使ったという話をした。私は、Firestoreに対してユーザから何かのログを送って、それを元にしてCloud Functionsをキックして巨大な関数を動かすというのはFirestoreを使うに当たってのパターンなのではないかと思った。Firestoreがこの2月にベータを抜けて正式リリースされたことで、これから使ってみようという人や企業も増えるのではないかと思うから、事例とともに自分の考えを広めておくことに価値があるのではないかと信じている。


頭脳自慢の挑戦を待っている。

https://play.google.com/store/apps/details?id=jp.gr.java_conf.etiles.android


このエントリーをはてなブックマークに追加

See also