GS2-Datastore

バイナリデータストレージ機能

GS2-Datastore を利用することで、任意のバイナリデータをサーバーに保存できます。

アップロードしたデータにはアクセス権限を設定でき、public(全員に公開)、protected(指定したユーザーIDにのみ公開:最大100件)、private(自分のみ)から選択します。

GS2-Datastore は UGC やレースゲームのゴーストデータのようなデータをアップロードするのが主たる目的で、プレイヤーのユーザーデータを保存するのには必ずしも適切ではありません。

なぜなら、所持品の所持数量をバイナリデータとして保存していると、セーブデータやアプリ本体の改竄でアイテムを増殖したり、入手量を不正に増加させた改造アプリでのプレイを許してしまうためです。 所持品の所持数量であれば、GS2-Inventory のような専用のマイクロサービスを利用することで、そのような不正行為は行えなくなります。

ユーザーデータの中でも、コンフィグの設定値など、改竄されてもゲームバランスに影響を与えることがないデータについて保存することを否定するわけではありません。

ユースケース

GS2-Datastore が想定する代表的なユースケースは以下の通りです。

  • ユーザーが投稿したスクリーンショットやリプレイなどの UGC コンテンツの保存
  • レースゲームにおけるゴーストデータの共有
  • フォトモードで撮影した画像のクラウド共有
  • ゲーム設定など改竄されてもゲームバランスに影響を与えないユーザーデータの保存
  • ステージのカスタムマップデータの公開・共有

アクセススコープ

データオブジェクトには 3 種類のアクセススコープがあり、用途に応じて使い分けます。

スコープ 説明 主な用途
public 全プレイヤーがダウンロード可能 UGC コンテンツの公開、ゴースト共有
protected allowUserIds に指定したユーザーのみがダウンロード可能(最大100件) フレンドのみへの共有
private アップロードしたユーザー本人のみがダウンロード可能 個人のセーブデータ、設定値

スコープと許可ユーザーは UpdateDataObject で後から変更することも可能です。

アーキテクチャ

GS2-Datastore はバイナリデータの保存場所などを記録したメタデータを管理しており、実際のバイナリデータの保存には外部のクラウドストレージを利用しています。 そのため、アップロード・ダウンロード処理は複数のステップを必要とします。

この処理の流れは、Game Engine 向けの SDK ではラップした高レベルなAPIが用意されているため、気にする必要はありませんが 各種プログラミング言語向けの SDK には高レベルなAPIは用意されていませんので、利用者自身で複数ステップを処理する必要があります。

アップロードプロセス

sequenceDiagram
  actor Player as プレイヤー
  participant Namespace as GS2-Datastore#Namespace
  participant Storage as Cloud Storage
  Player->>Namespace: PrepareUpload
  Namespace-->>Player: Cloud Storage URL
  Player->>Storage: Upload Payload
  Storage-->>Player: OK
  Player->>Namespace: DoneUpload
  Namespace->>Storage: Check exists
  Namespace-->>Player: OK

ダウンロードプロセス

sequenceDiagram
  actor Player as プレイヤー
  participant Namespace as GS2-Datastore#Namespace
  participant Storage as Cloud Storage
  Player->>Namespace: PrepareDownload
  Namespace-->>Player: Cloud Storage URL
  Player->>Storage: Download
  Storage-->>Player: Payload

アップロード処理中のダウンロード

GS2-Datastore はすでにアップロードしたデータを更新することができます。 Prepare ReUpload を呼び出してから Done Upload を呼び出すまでの間は、更新前の古いファイルがダウンロード可能な状態となり、中途半端なデータがダウンロードされることはありません。

アップロードデータの過去バージョンの取得

GS2-Datastore では過去30日分の過去バージョンにアクセスできるようになっています。 データオブジェクトの更新履歴(DataObjectHistory)を取得することで、過去の各世代の世代IDを取得でき、その世代IDを指定してデータをダウンロードすることが可能です。

これは削除済みのデータにも適用されており、削除リクエスト後 30 日後に実際に削除されます。 ただし、法的要件によってデータの削除を行った場合は、この条件に当てはまらないことがあります。

データサイズとステータス

1つのデータオブジェクトの最大サイズは10MBです。アップロード中はUPLOADING、完了するとACTIVE、削除リクエスト後はDELETEDに遷移します。DELETED状態のデータは30日以内であればrestoreDataObjectで復元できます。

stateDiagram-v2
  [*] --> UPLOADING: PrepareUpload
  UPLOADING --> ACTIVE: DoneUpload
  ACTIVE --> UPLOADING: PrepareReUpload
  ACTIVE --> DELETED: DeleteDataObject
  DELETED --> ACTIVE: RestoreDataObject (30日以内)
  DELETED --> [*]: 30 日経過

データオブジェクトの主な属性

属性 説明
dataObjectId データオブジェクトの一意な ID(GRN)
name データオブジェクト名。ユーザーごとに一意
userId アップロードしたユーザーのID
scope アクセススコープ (public / protected / private)
allowUserIds protected スコープで参照を許可するユーザーIDの一覧
platform アップロードに利用されたプラットフォーム情報
status データの状態 (UPLOADING / ACTIVE / DELETED)
generation 現在世代の識別子
previousGeneration 1つ前の世代の識別子

スクリプトトリガー

データオブジェクトのアップロード完了報告の前後で GS2-Script を呼び出すイベントトリガーを設定できます。同期実行で完了報告の拒否を行ったり、非同期実行で Amazon EventBridge を利用した外部連携も可能です。

設定できる主なイベントトリガーとスクリプト設定名は以下の通りです。

  • doneUploadScript(完了通知: doneUploadDone): アップロード完了報告の前後

同期実行のスクリプトでアップロードを拒否することで、画像の中身を別サービスで検査し、不適切なコンテンツであれば登録を拒否するといった運用が可能です。

実装例

データのアップロード

データのアップロードは PrepareUpload → クラウドストレージへの PUT → DoneUpload の 3 ステップで構成されますが、ゲームエンジン向け SDK では UploadAsync 1 つの呼び出しで完結します。

    var result = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).UploadAsync(
        name: "dataObject-0001",
        scope: "public",
        data: data
    );

    var item = await result.ModelAsync();
    var dataObjectId = item.DataObjectId;
    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto Future = Domain->Upload(
        "dataObject-0001", // name
        data, // data
        "public" // scope
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

既存データの再アップロード

同じ名前のデータオブジェクトに対して新しいバイナリを書き込む際は再アップロードを使用します。 再アップロード中も、DoneUpload が呼び出されるまでの間は更新前の古いデータが取得できます。

    var domain = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).ReUploadAsync(
        data: newData
    );
    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObject(
        "dataObject-0001" // dataObjectName
    );
    const auto Future = Domain->ReUpload(
        NewData
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

データのダウンロード(データオブジェクトID指定)

    var binary = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DownloadAsync(
        dataObjectId: dataObjectId
    );
    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto Future = Domain->Download(
        dataObjectId // dataObjectId
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

データのダウンロード(ユーザーIDとデータオブジェクト名を指定)

    var binary = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).User(
        userId: "user-0001"
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).DownloadByUserIdAndDataObjectNameAsync(
    );
    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->User(
        "user-0001" // userId
    )->DataObject(
        "dataObject-0001" // dataObjectName
    );
    const auto Future = Domain->DownloadByUserIdAndDataObjectName(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

自分がアップロードしたデータのダウンロード(データオブジェクト名を指定)

    var binary = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).DownloadOwnAsync(
    );
    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObject(
        "dataObject-0001" // dataObjectName
    );
    const auto Future = Domain->DownloadOwn(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

自分がアップロードしたデータの一覧取得

    var items = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObjectsAsync(
    ).ToListAsync();
    const auto It = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObjects();
    TArray<Gs2::UE5::Datastore::Model::FEzDataObjectPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

データオブジェクトのアクセススコープを変更

UpdateDataObject を利用するとアクセススコープや許可ユーザーリストを後から変更できます。 たとえば、最初は private で作成し、共有準備が整ったタイミングで public に切り替えるといった運用が可能です。

    var domain = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).UpdateDataObjectAsync(
        scope: "protected",
        allowUserIds: new [] { "user-0002", "user-0003" }
    );
    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObject(
        "dataObject-0001" // dataObjectName
    );
    const auto Future = Domain->UpdateDataObject(
        "protected", // scope
        { "user-0002", "user-0003" } // allowUserIds
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

データオブジェクトの削除

削除リクエスト後 30 日以内であれば RestoreDataObject で復元することができます。

    await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).DeleteDataObjectAsync(
    );
    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObject(
        "dataObject-0001" // dataObjectName
    );
    const auto Future = Domain->DeleteDataObject(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

データオブジェクトの更新履歴の取得

過去の世代を取得することで、ロールバックや過去のリプレイ閲覧などを実現できます。

    var items = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).DataObjectHistoriesAsync(
    ).ToListAsync();
    const auto It = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObject(
        "dataObject-0001" // dataObjectName
    )->DataObjectHistories();
    TArray<Gs2::UE5::Datastore::Model::FEzDataObjectHistoryPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

古い世代のデータへのアクセス制限

データをダウンロードする際には世代IDを指定して、具体的なファイルを特定します。世代IDをダウンロードリクエストに付加することで、リストアップした時点でのデータを確実にダウンロードすることを保証できます。

ただし、いつまでも世代の古いデータにアクセスできることが望ましくない場合もあります。そのため、データの所有者以外は、更新後60分以内かつ1つ前の世代に限り古い世代のデータのダウンロードを許可するオプションが存在します。

詳細なリファレンス