1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Link-UAdvent Calendar 2020

Day 5

TypeScript+React+Cloud Firestore+Stateモナドで、ブラウザで動くボードゲームの対戦ツールを作った (3/3 UI・通信編)

Last updated at Posted at 2020-12-04

前回はボードゲーム、ナショナルエコノミーオンライン対戦ツールにおけるゲームメカニクス部の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を定義して、それを送受信します。

game-and-log.ts
type GameAndLog = {
    game: Game,
    log: string[];
}

受信

ゲーム状態の取得はいちどきりではなく、書き込みが行われるたびにリアルタイムに更新される必要があります。これはコールバックの登録によっても購読できますが、今回はrxfireを用いてObservableの形でゲーム状態を購読することにします。

fetch.ts
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が作成された際に行われます。

fetch.ts(続き)
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で得られるため、次のような購読用カスタムフックを作成するとよいでしょう。

use-observable.ts
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コンポーネントだけを取り上げます。

cards.tsx
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の組み合わせ方については解説できたのではないかと思われます。

参考文献

Cloud Firestore

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?