ReactNativeとCloud Firestore(Firebase)を連携したゲームを作った

作ったゲーム
-----
- Battle Saga History
  - [🍎iPhone](https://itunes.apple.com/jp/app/battle-saga-history/id1441396163?mt=8&ign-mpt=uo%3D4)
  - [🚩Android](https://play.google.com/store/apps/details?id=com.firebaseapp.horieapp.saga)
- ランキング表示、対人戦（非リアルタイム）ができるように必要なデータをFirestoreに読み/書き
- データを保存するときのバリデーションチェックはすべてFirestoreのルールにまかせる
  - 名前10文字以内など画面に表示する必要があるものだけプログラム内で判定

感想
----
- 簡単に組み込むことができました。
- 「ルール」を使うことでプログラム側を修正する必要がなく、バリデーションチェックの管理が非常に簡単にできます。
- FirestoreとRealtime Databaseは、ルールや使い方が似ているため、どちらか迷ったら料金が安く安定しているFirestoreがオススメです。

Cloud FirestoreがFirebase Realtime Databaseより優れている点
--------------
[参考] https://stackoverflow.com/questions/46549766/whats-the-difference-between-cloud-firestore-and-the-firebase-realtime-database/48397207

- 自動的にインデックスを作成して管理してくれる
- スケーラビリティが優れている
- データが増えても速くて安い
  - Firebase Realtime DatabaseはJSONツリー、Cloud FirestoreはJSONと非常に似ているドキュメントにデータを保存

### Firebase Realtime Databaseの方がいいかもしれないケース
- リアルタイムゲームのように、複数の更新を複数の人に送信する場合は、Firebase Realtime Databaseの方が料金面が安くなる可能性がある

Firestoreの設定
----

[[参考]Firestore の Timestamp の仕様変更による警告と、その対処](https://qiita.com/teramotodaiki/items/b3592326579166003102)

```jsx:firebaseDB.js
import firebase from 'firebase/app'
import 'firebase/firestore'

firebase.initializeApp({
  /* firebase config */
})

const firestore = firebase.firestore()
firestore.settings({ timestampsInSnapshots: true })

export default firestore
```

データの構造
----
users -> ユーザーID -> ユーザーデータ
<img width="400" alt="screen1.png" src="https://qiita-image-store.s3.amazonaws.com/0/69627/a4f1d35d-3f27-7508-de34-1fec1713348c.png">


保存したいオブジェクトデータ
--------
```json
{
	"fname": "フリーファイト",
	"leader": 1,
	"rank": 1,
	"score": 50,
	"updated": "Timestamp",
	"m1": {
		"cmd": "11111",
		"id": "11",
		"name": "シャムシール",
		"pos": 4,
		"sk1": 1,
		"sk1Name": "一文字斬り",
		"sk2": 7,
		"sk2Name": "二段斬り",
		"type": 5
	},
	"m2": {
		"cmd": "22221",
		"id": "21",
		"name": "フロド",
		"pos": 2,
		"sk1": 10,
		"sk1Name": "ローリングスィープ",
		"sk2": 4,
		"sk2Name": "ライジングクラッシュ",
		"type": 7
	},
	"m3": {
		"cmd": "22221",
		"id": "51",
		"name": "メインクーン",
		"pos": 1,
		"sk1": 3,
		"sk1Name": "ナイアガラバスター",
		"sk2": 7,
		"sk2Name": "ツインスライディング",
		"type": 5
	},
	"m4": {
		"cmd": "22221",
		"id": "75",
		"name": "キゥイ",
		"pos": 0,
		"sk1": 3,
		"sk1Name": "デビルズウイング",
		"sk2": 2,
		"sk2Name": "エアロブラスト",
		"type": 6
	},
	"m5": {
		"cmd": "22211",
		"id": "91",
		"name": "イチゴ",
		"pos": 5,
		"sk1": 10,
		"sk1Name": "スーパーノヴァ",
		"sk2": 5,
		"sk2Name": "クロスファイア",
		"type": 3
	}
} 
```

作成したルール
----
```js
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow get
      allow list: if request.query.limit <= 100
      allow write: if isValid()

      function isValid() {
        return isValidData(request.resource.data)
            && isValidMember(request.resource.data.m1)
            && isValidMember(request.resource.data.m2)
            && isValidMember(request.resource.data.m3)
            && isValidMember(request.resource.data.m4)
            && isValidMember(request.resource.data.m5)
      }

      function isValidData(data) {
        return data.keys().hasAll(['updated', 'fname', 'leader', 'rank', 'score', 'm1', 'm2', 'm3', 'm4', 'm5'])
                && data.updated is timestamp
                && isValidStringInput(data.fname, 10)
                && isValidNumber(data.leader, 1, 5)
                && isValidNumber(data.rank, 1, 5)
                && isValidNumber(data.score, 0, 99999)
      }

      function isValidMember(data) {
        return data.keys().hasAll(['id','name','pos','type','sk1','sk2','sk1Name','sk2Name', 'cmd'])
                && isValidNumber(data.pos, 0, 8)
                && isValidNumber(data.type, 1, 10)
                && isValidNumber(data.sk1, 1, 10)
                && isValidNumber(data.sk2, 1, 10)
                && isValidStringInput(data.sk1Name, 10)
                && isValidStringInput(data.sk2Name, 10)
                && isValidStringInput(data.name, 10)
                && data.id.matches('^[1-9][1-9]$')
                && data.cmd.matches('^[12]*$')
      }

      function isValidStringInput(input, maxSize) {
        return input is string
            && input.size() > 0
            && input.size() <= maxSize
      }

      function isValidNumber(input, min, max) {
        return input is int
            && input >= min
            && input <= max
      }
    }
  }
}
```

データの書き込み
------

```js:新規
import DB from './firebaseDB'

DB.collection('users')
  .add(objectData)
  .then(docRef => {
    console.log(`added data. id: ${docRef.id}`)
  })
  .catch(err => {
    console.log(err)
  })
```

```js:更新
import DB from './firebaseDB'

DB.collection('users')
  .doc(userId)
  .update(objectData)
  .then(() => {
    console.log('updated data')
  })
  .catch(err => {
    console.log(err)
  })
```

データの読み込み
------
```js
import DB from './firebaseDB'

DB.collection('users')
  .orderBy('updated', 'desc')
  .limit(100)
  .get()
  .then(docs => {
    docs.forEach(doc => {
      console.log(doc.data())
    })
  })
  .catch(err => {
    console.log(err)
  })
```