「Applibot Advent Calendar 2020」 5日目の記事になります。
前日は @taroshun の 3Dモデルから2D画像を自動生成する方法について という記事でした。
#はじめに
趣味アプリ開発、してますか?
縛られず自由に開発ができるアプリ開発は楽しいものです。
が、趣味である故に以下のように考える事もあるかと思います。
- なるべく手軽に作りたい
- 時間を作るのも大変、興味の鮮度も大事
- ちょっと変わった事がしたい
- 既存 + α で変化をつける、あわよくばバズるかも
……そんなとき 「オンライン機能」 を手軽につけられたらどうでしょうか?
できます。そう、Cloud Firestore ならね。
#この記事の概要
- Cloud Firestore を用いて、なるべく簡単にオンライン機能のあるアプリを作ります
- ここではオンライン機能のわかりやすい例として、対戦ゲームを作ることにします
- また、オンライン化しやすいゲームの種類とその設計について説明します
#Cloud Firestore
Cloud Firestore は Google の提供するクラウドデータベースです。
Firebase という mBaaS の一部となっています。
以下のような特徴があり、オンラインゲームを作るにあたり重宝します。
-
リアルタイムアップデート
- ドキュメントの追加や更新を監視する機能
- → オンライン機能における同期が簡単に実装できる
-
Firebase Authentication との統合
- 認証やデータ検証の機能
- → サーバ側の実装無しに、クライアント完結で作業できる
-
オートスケールと豊富な無料枠
- → ミニマムで始めて、万が一バズっても安心!
つまり、趣味のアプリ開発に最適という事です。
#オンライン化しやすいゲーム
さて、さっそくオンライン対戦ゲームを作っていきたいですね。
が、残念な事に対戦ゲームにおいては、
オンライン化しやすいものとそうでないものがあります。
ここではその説明をします。
先に結論を書くと、確定完全情報ゲーム かつ 低頻度の同期 のものがおすすめです。
確定完全情報ゲーム
これは確定と完全情報2つの部分に分解できて、それぞれ以下のように言い換えられます。
- 確定
- ランダム要素が無いこと
- 完全情報
- 秘密の手番が存在しないこと
これが何故おすすめかといえば、この条件を満たす事でサーバ側の実装を省きクライアント完結で実装できるようになるためです。これらを満たせない場合は、以下のようにロジックに第三者の介入が必要になります。(第三者 = サーバ側の実装)
-
ランダム要素
- サイコロを第三者に振ってもらう必要がある
-
秘密の手番
- 真正性を担保するため、第三者に秘密の手を開示する必要がある
とはいえ、これらの要素を含んでいたとしても、不正を意図して行わない限り問題はありません。趣味アプリはそこまで厳密さを求める必要のない事も多いですし、適度に妥協していい部分ではあります。
また、Clound Functions を使い Cloud Firestore を拡張することで対応もできます。が、この記事では簡単さのため扱いません。
低頻度の同期
Cloud Firestore のベストプラクティスには、以下の制限があります。
- 同一ドキュメントの更新は秒間 1 回までにする
- クライアントに push するドキュメントのレートを 1 ドキュメント/秒未満にする
これらはレートリミットではなく Firestore の仕様的な問題なので、一時的に超えても直ちに問題になる訳ではないですが……。毎秒1回以上の同期を必要とするようなゲームには向いていないと言えるでしょう。
アプリ作成: 1. オフライン版アプリ実装
オンライン化しやすい条件がわかったので、さっそく条件を満たすアプリを作ります。この部分こそがアプリ作りの本質です。自由な発想で、オフラインで動作する独自のアプリを作りましょう。
ただ、この記事のメインではないのでここは省きます。
この記事では、オセロリバーシを作ることにします。
……
……
出来ました。
https://ref3000.github.io/othello/local.html
ソースコード
飾り気のないシンプルな Web アプリです。良いですね。当然オンライン対戦はまだ出来ません。
アプリ作成: 2. Cloud Firestore を使ってみる
この記事では雰囲気を掴む事を目的とし、ハンズオン風に流れをみていきます。
※Cloud Firestore の詳細説明は行わないので、必要に応じて後からドキュメントなどを参照してみて下さい。
Firebase プロジェクトの作成
Google アカウントでログインし Firebase のコンソールからプロジェクトを追加します。
なんやかんやと進み、プロジェクトの概要まで進んだら、アプリを追加します。
今回は Web アプリを選択しています。
適当に進めて、このようにアプリが登録された状態になれば準備完了です。
Cloud Firestore でデータベースを作成
左のメニューから Cloud Firestore を選択し、データベースの作成を行います。
セキュリティルールは後で編集しますが、一旦テストモードにしておきます。
ロケーションはお好みですが、 asia-northeast1
(東京)にしておくのが良いでしょう。
設定が終わると、しばらく初期化処理が走った後、Firestore が使えるようになります。
Cloud Firestore を Web アプリから使う
設定の下の方にある Firebase SDK snippet で CDN を選択すると次のようなコードが表示されます。
ここの firebaseConfig
のオブジェクト内の項目が重要です。
以下の HTML ファイルを作成し、 firebaseConfig
を自分のものに書き換えてブラウザで開いてみましょう。
<!DOCTYPE html>
<html>
<head><title>Firestore てすと</title></head>
<body>
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/8.1.2/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.1.2/firebase-firestore.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.1.2/firebase-auth.js"></script>
<script>
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "AIzaSyBkuRRmyhKj9X3MLI7urRbTS9R4S06CMDk",
authDomain: "hoge-57c9f.firebaseapp.com",
projectId: "hoge-57c9f",
storageBucket: "hoge-57c9f.appspot.com",
messagingSenderId: "702871027936",
appId: "1:702871027936:web:9b9787d3786e35e029d085",
measurementId: "G-6BLESST1CK"
};
// Initialize Firebase
var app = firebase.initializeApp(firebaseConfig);
var db = firebase.firestore(app);
function onClick() {
db.collection("test").add({
hoge: "てすと",
createdAt: firebase.firestore.FieldValue.serverTimestamp()
});
};
</script>
<button onclick="onClick()">書き込みテスト</button>
</body>
</html>
「書き込みテスト」ボタンが表示されているかと思います。これを押した後 Firestore のデータタブを開くと、以下のようにデータが作成されているはずです。
書き込みしている部分を抜粋すると、これだけです。簡単ですね。
db.collection("test").add({
hoge: "てすと",
createdAt: firebase.firestore.FieldValue.serverTimestamp() // このようにするとサーバ側での書き込み時刻を記録できる
});
新しいドキュメントの作成を監視する事も簡単にできます。
// 新しいドキュメントが作成された際に、最新の1件のみを取得するクエリ
db.collection("test").orderBy("createdAt", "desc").limit(1).onSnapshot(function (querySnapshot) {
querySnapshot.forEach(function(queryDocSnapshot) {
var data = queryDocSnapshot.data();
console.log(data);
});
});
script タグの最後などにこのコードを追加して、ブラウザを複数窓で開きながら「書き込みテスト」ボタンを押してみて下さい。コンソールを表示しておくと、ある窓で書き込んだものが他の窓でも確認できるはずです。
この仕組みを利用することで、Firestore でデータを集中管理し、複数クライアント間でゲームの進行状態を同期できるようになる訳です。
#アプリ作成: 3. Cloud Firestore セキュリティルールの設定
さて、最後にデータ設計も行ってしまいましょう。
対戦ゲームの場合、ゲームの進行状態をユーザの「行動」のチェーンとして表現することがおすすめです。簡単のため、ユーザの「行動」は1コレクション内の1ドキュメントとして表現するのが良いでしょう。(ここでは、コレクション名を actions とします。)
そして、ルールを以下のように設定します。
このシンプルなルールだけで、以下のように強力なセキュリティが担保されます。
-
allow create
- create のみ許可し update をさせない事で、改ざんが防止される
-
request.resource.data.uid == request.auth.uid
- 認証された ID を確認する事で、なりすましを防ぐ
-
exists(request.resource.data.prev)
- 前の「行動」の指定を強制させる(「行動」のチェーンを作る)
-
request.resource.data.createdAt == request.time
- 作成日時の改ざん防止
アプリ作成: 4. オフラインアプリのオンライン化
ここまでくれば、オンライン化はほぼ完成していると言って過言ではありません!
リバーシのゲームにおいて、同期しなければいけないデータは以下の3つです。
- 現在の盤面
- 現在黒と白どちらの手番か
- 最終手の座標
アプリ -> Firestore
これをまとめてユーザの**「行動」**として先程のセキュリティルールに必要な情報と一緒にドキュメント化します。これを着手決定時に Firestore に書き込むようにします。
db.collection("actions").add({
x: ${最終手のX座標},
y: ${最終手のY座標},
stone: ${黒と白どちらの手番か},
field: ${現在の盤面},
uid: uid, // ※後述
prev: ${1つ前のドキュメントへの参照},
createdAt: firebase.firestore.FieldValue.serverTimestamp() // サーバ側での作成時刻
});
書き込まれると、Firestore のコンソール上からはこのように見えます。
※ uid について
uid は、Firebase Authentication におけるユーザの ID の事です。Firebase Authentication は Firebase における認証のサービスで、これを使うと簡単にアプリに認証機能を実装することができます。
Firestore のセキュリティルールの良い所は、この Firebase Authentication で認証されたユーザ ID を使う事ができる部分にあります。今回のアプリでは匿名認証を使い、ユーザを一意に識別するために利用する事にします。
var uid = null;
firebase.auth().signInAnonymously();
firebase.auth().onAuthStateChanged(function (user) {
if (user) uid = user.uid;
});
このような数行を記述しておくことで、認証が完了した段階で uid が取得され設定されます。
Firestore -> アプリ
あとは actions コレクションを監視し、更新があった際にそれをゲーム内に反映すれば OK です。
1つ前のドキュメントの参照もここで取得します。
db.collection("actions").orderBy("createdAt", "desc").limit(1).onSnapshot(function (querySnapshot) {
querySnapshot.forEach(function(queryDocSnapshot) {
var data = queryDocSnapshot.data();
ref = queryDocSnapshot.ref;
...
// ここで data の内容をゲームに反映する
});
})
これで、ゲームの進捗が同期され、オンラインで対戦が可能になりました!
https://ref3000.github.io/othello
ソースコード
お疲れさまでした!
いかがだったでしょうか。クライアント側の実装と、簡単な Firestore の設定だけでゲームがオンライン化できるというのは、なかなか面白い体験だったと思います。
ただし、この状態だとゲームを行うにあたっていくつか気になる点もあるかもしれません。
例えば、黒も白も関係なく置けてしまうのでそこの取り決めをどうするかであったり、同時に着手があった場合後勝ちで上書きされてしまったり…。
この辺りは、うまく競合しないような UI を考えるだとか、Clound Functions を使って Cloud Firestore を拡張したりだとか、色々工夫のしどころとも言えるでしょう。是非考えてみて下さい。
#おわりに
Cloud Firestore を使うことで、オンライン対戦ゲームも簡単に作ることができました。
駆け足の説明ではありますが、まだ使ったことのない人に少しでも雰囲気が伝われば幸いです。
ぜひ日々の趣味アプリ開発でも、Cloud Firestore を使って気軽にオンライン機能を実装していきましょう!
「Applibot Advent Calendar 2020」 5日目の記事でした。
明日は @kaz2ngt さんです。