Unity で簡単な PvP ゲームを作りたいとき,Firebase の Cloud Firestore を使えばバックエンドのコーディングをしなくても動かしてみることができる. Firestore への読み書きはドキュメントに書かれた方法でも OK だが,Attribute を使うともっと簡単にできる.

概要

Cloud Firestore への読み書きを Attribute と独自クラスを使って簡単にする.

  • SDK で定義された FirestoreDataFirestoreProperty を使う
  • DocumentSnapshot から独自クラスに変換できるようにして,読み込みを簡単にする
  • 独自クラスからディクショナリ型に変換できるようにして,書き込みを簡単にする

環境

  • Unity 2020.1.3f1
  • Cloud Firestore for Firebase 6.13.0

Attribute を使った読み込み(基礎編)

Cloud Firestore 読み書きの初歩的なドキュメントでは,取得した DocumentSnapshotToDictionary() でディクショナリ型に変換して値を読み書きする方法が紹介されている. ドキュメントの目的が Getting Started だと考えればこれはこれで正しい方法なのだが,次第にコードが増えていくと object のキャストも増えていくのが気になってくる.

実は Firestore SDK には FirestoreDataFirestoreProperty という Attribute がある. これらを使いつつ独自クラスを定義してあげると,DocumentSnapshot 型から独自クラスへの変換が ConvertTo<T>() メソッドを呼び出すだけで可能になる.

例えば Room を表すクラスを以下のように定義する.

[FirestoreData]
public class RoomEntity {
    [FirestoreProperty]
    public List<string> PlayerIds { get; set; }

    [FirestoreProperty]
    public int PlayerCount { get; set; }
}

この RoomEntity を使うと DocumentSnapshot から ConvertTo<RoomEntity>()RoomEntity にキャストされたオブジェクトを得ることができる.

var db = FirebaseFirestore.DefaultInstance;

var roomDocument = await db.Collection("rooms").Document("room1").GetSnapshotAsync();

var roomEntity = roomDocument.ConvertTo<RoomEntity>();

独自クラスの定義さえ間違えていなければ良いので,都度 object をキャストするよりも簡単で安全と言える.

Attribute を使った読み込み(応用編)

場合によっては,もう少し凝ったクラスを定義したいこともある.

命名規則が違う場合

Firestore のフィールド名と C# コードのプロパティ名の命名規則が違う場合でも, FirestorePropertyName プロパティで紐づければ変換できるようになる.

例えば RoomEntity に対応する Room ドキュメントが Snake Case で定義されている場合.

{
    "player_ids": ["player1", "player2", "player3"],
    "player_count": 3
}

以下のように FirestorePropertyName を指定すればよい.

[FirestoreData]
public class RoomEntity {
    [FirestoreProperty(Name = "player_ids")]
    public List<string> PlayerIds { get; set; }

    [FirestoreProperty(Name = "player_count")]
    public int PlayerCount { get; set; }
}

プロパティを Readonly にしたい場合

独自クラスによっては,プロパティの書き込みを制限したい場合がある. 例えば RoomEntity でいうと,PlayerCountPlayerIds の要素数と整合性をとりたいので,Readonly にしたい.

結果から示してしまうと,以下のように Setter を private や空実装にすればよい.

[FirestoreData]
public class RoomEntity {
    [FirestoreProperty(Name = "player_ids")]
    public List<string> PlayerIds { get; set; }

    [FirestoreProperty(Name = "player_count")]
    public int PlayerCount {
        get => PlayerIds.Count;
        private set {}
    }
}

これは FirestoreProperty の実装に依存すると思われるが,Setter を private にしても問題なく変換することができた. ただし,Setter は private や空実装でも残しておく必要がある. Setter を定義しない場合でもコンパイルや実行は可能だが,実行時に警告が出てしまう.

独自クラスを使った書き込み

Firestore に書き込む場合には SetAdd といったメソッドを使うが,これらはディクショナリ型の引数を取る.

Attribute を使った読み込みができるようになったが,その反対に独自クラスをディクショナリ型に変換する方法は SDK では(おそらく)提供されていないので,簡単な Extension を実装する.

public static class FirestoreExtension
{
    public static Dictionary<string, object> ToFirestoreDictionary<T>(this T entity)
    {
        return typeof(T).GetProperties()
            .Where(prop => Attribute.IsDefined(prop, typeof(FirestorePropertyAttribute)))
            .ToDictionary(
                prop =>
                {
                    var attr = prop.GetCustomAttribute<FirestorePropertyAttribute>();
                    return attr.Name ?? prop.Name;
                },
                prop => prop.GetValue(entity, null)
            );
    }
}

この Extension を使うと Room は以下のように更新できる.

var db = FirebaseFirestore.DefaultInstance;

var roomDocument = await db.Collection("rooms").Document("room1").GetSnapshotAsync();

var roomEntity = roomDocument.ConvertTo<RoomEntity>();
roomEntity.PlayerIds.Add("player4");

await roomDocument.Reference.UpdateAsync(entity.ToFirestoreDictionary());

なお,実際にはクライアントサイドからドキュメントを直接更新することは少ないかもしれない. 特に複数人で共有するリソースの場合,権限判断を含めてサーバーサイドで書き込む方が普通だろう.

まとめ & 感想

Cloud Firestore への読み書きを簡単にする方法をまとめた.

SDK で定義された FirestoreDataFirestoreProperty を使うとドキュメントへの読み書きを簡単にすることができた. クラスの定義を少し工夫すると,フィールド名の差を吸収したり,プロパティを Readonly にして書き込みミスを減らすこともできた.

クライアントサイドから読み書きできるから,プロトタイピングとの相性が良さそう. 本番実装の際に書き込みのロジックはサーバーサイドに移行するとしても,リアルタイム読み込みのロジックは残せるところが多いかも.

参考