前回はボードゲーム、ナショナルエコノミーオンライン対戦ツールにおけるゲームメカニクス部のStateモナドを用いた実装手法について解説しました。
今回はCloud Firestoreとの通信、およびReactを用いたUIの実装を行います。
はじめに
通信とUIの実装はメカニクス部と異なり実際に動かしながら動作を確認する必要があります。webpack-dev-serverを用いるなり、create-react-appを用いるなりしてローカルに開発環境を構築し、ブラウザからアクセスして見た目を確認しつつ行うのが手っ取り早いでしょう。
身内にのみ限定して共有する予定であったことから、本対戦ツールでは認証などの仕組みを実装しませんでした。したがって本記事で解説する実装は、あまり「安全」な構成にはなっていないことをご留意ください。
Cloud Firestoreとの通信
接続
Firebase SDKのinitializeApp
関数を用いてCloud Firestoreとの接続を確立します。
import firebase from "firebase/app";
const db = firebase.initializeApp({
apiKey: "*******-*******************************",
authDomain: "****.firebaseapp.com",
databaseURL: "https://****.firebaseio.com",
projectId: "****",
storageBucket: "****.appspot.com",
messagingSenderId: "************",
appId: "*******"
}).firestore();
スクリプトの冒頭で作成したこのfirebase.Firestore
型を全体で用いることで送受信を行います。
Cloud Firestoreのデータ構造は、最小単位を「ドキュメント」として、それを保有する「コレクション」によって成り立っています。ドキュメントはKVS型のレコードであり、実質的にはJSONと同じです。したがって、メンバすべてが数値、文字列、あるいはその配列やオブジェクトであるようなGame
型をそのままJSONとして送受信することができます。
実際には、ゲーム内各種操作のログも保持・共有できるように、合わせた型として次のGameAndLog
を定義して、それを送受信します。
type GameAndLog = {
game: Game,
log: string[];
}
受信
ゲーム状態の取得はいちどきりではなく、書き込みが行われるたびにリアルタイムに更新される必要があります。これはコールバックの登録によっても購読できますが、今回はrxfireを用いてObservable
の形でゲーム状態を購読することにします。
import * as firebase from "firebase";
import { Observable } from "rxjs";
import GameAndLog from "entity/gameandlog";
import { docData } from "rxfire/firestore";
export function fetchGame(db: firebase.firestore.Firestore, id: string): Observable<GameAndLog> {
return docData(db.collection("games").doc(id));
}
事前準備としてCloud Firestoreのコンソール上で、「games」コレクションを作成しておきました。「games」コレクション内の適当な「ゲームID」ドキュメントをGameAndLog
型として購読します。
あとはこれを必要な個所でsubscribe
すれば、リアルタイムに更新されるGameAndLog
が得られます。
送信
ゲーム状態の送信は、適切な操作がなされた場合にそれに対応するStateモナドと旧GameAndLog
から新たなGameAndLog
が作成された際に行われます。
export function updateGame(db: firebase.firestore.Firestore, id: string, game: GameAndLog) {
db.collection("games").doc(id).update(game);
}
新たなゲームの開始
送受信メソッドに含まれる「ゲームID」は、Cloud Firestoreに対し新たなドキュメントを作成させる形で生成することができます。適当な手段で決定したリーダークライアントがこのメソッドを呼ぶことにより新規ゲームを開始します。
function createGame(db: firebase.firestore.Firestore, id: string, game: Game) {
db.collection("games").add({game: game, log: []})
.then(doc => db.collection("rooms").doc(`room${id}`).update({game_id: doc.id}));
}
ゲーム開始前のロビーの状態を保持するコレクションとして「rooms」コレクションを定義しており、これにadd
メソッドで生成されたゲームIDを書き込むことで「部屋が進行中のゲームで埋まっている」ことを表現しています。
UI
特別変わったことをするわけではなく、Game
型で定義される現在のゲーム状態を用いて各種コンポーネントを順に作成していきます。
ゲーム状態の購読
ゲーム状態がObservable
で得られるため、次のような購読用カスタムフックを作成するとよいでしょう。
import * as React from "react";
import { Subscription, Observable } from "rxjs";
export default function useObservable<T>(observable: Observable<T>): [T | null, any | null, () => void] {
const [subscription, setSubscription] = React.useState<Subscription | null>(null);
const [value, setValue] = React.useState<T | null>(null);
const [error, setError] = React.useState<any | null>(null);
const ref = React.useRef(subscription);
React.useEffect(() => {
setSubscription(observable.subscribe(v => {
setValue(v)
}, setError));
return () => ref.current?.unsubscribe();
}, []);
return [value, error, () => subscription?.unsubscribe()];
}
Reactコンポーネントの作成
ナショナルエコノミーはすべてのコンポーネントがカードで構成されたゲームであり、「手札」や「所有不動産」など、カードを並べる似た形式のコンポーネントを多く用いることになります。
コンポーネントの作成手段についてはいくらでも記述が見つかると思われるので、ここでは一例としてcards
コンポーネントだけを取り上げます。
import * as React from "react";
import Card from "./atoms/card";
import * as style from "./cards.styl";
import { CardName } from "model/protocol/game/card";
import { Building } from "model/protocol/game/building";
import Workers from "./atoms/workers";
type Props = {
title: string,
tooltip?: string,
cards?: CardName[],
buildings?: Building[]
};
const hand: React.FC<Props> = props => {
const cards = props.cards ? props.cards.map((c, i) => <Card card={c} key={`${i}-${c}`} />) : null;
const buildings = props.buildings ? props.buildings.map((b, i) => (
<Card card={b.card} key={`${i}-${b.card}`}>
<Workers owners={b.workersOwner} />
</Card>
)) : null;
return (
<section className={style.cards}>
<h2 title={props.tooltip}>{props.title}</h2>
{cards ? <ul>{cards}</ul> : null}
{buildings ? <ul>{buildings}</ul> : buildings}
</section>
);
};
export default hand;
まとめ
今回の記事でCloud Firestoreとの通信、およびUIの構成について説明したため、これでゲーム作成に必要な要素を構築できるようになりました。
Firebase Authenticationを利用して、ゲームプレイにユーザ登録を要求するなど、触れていない部分も多くありますが、とりあえずStateモナドとCloud Firestoreの組み合わせ方については解説できたのではないかと思われます。