GS2-Inbox

プレゼントボックス機能

システムや運営からプレイヤーにメッセージやプレゼントを届ける仕組みを実現します。

ゲームでは「お詫び配布」「ログインボーナス」「課金特典」「イベント報酬」など、プレイヤーに非同期で報酬を届けるシチュエーションが頻繁に発生します。 GS2-Inbox はメッセージのキューイング、未読/既読管理、報酬の添付、有効期限による自動削除を提供し、ゲームのライフサイクル全般を通じてプレゼントボックス機能を担います。

sequenceDiagram
  participant Ops as 運営 / サーバー
  participant Inbox as GS2-Inbox
  participant Player as プレイヤー
  Ops->>Inbox: メッセージ送信 (報酬付き)
  Inbox-->>Player: receiveNotification 通知
  Player->>Inbox: メッセージ一覧取得
  Player->>Inbox: メッセージ開封 (Read)
  Inbox->>Player: 添付報酬を付与 (トランザクション)
  Player->>Inbox: 既読メッセージの削除

メッセージ

既読管理

メッセージは既読状態をもちます。 既読状態のメッセージをリストに残すか、リストから削除するかを設定が可能です。

ネームスペース設定の isAutomaticDeletingEnabled を有効にすると、既読化のタイミングでメッセージが自動削除されます。 プレゼントボックスの一覧には未受領のメッセージだけを残す仕様を、サーバー側の追加実装なしで実現できます。

報酬の添付

メッセージには報酬を添付することが可能です。 既読フラグを立てる代わりに、メッセージに添付された報酬を受け取ることができます。

添付できる報酬は GS2 のトランザクション機構を介した入手アクション(readAcquireActions)として表現されるため、GS2-Inventory GS2-Money GS2-Experience など、任意のマイクロサービスのリソースを報酬として配布できます。

有効期限

メッセージには有効期限(expiresAt)を設定できます。 有効期限を迎えたメッセージは、未読状態、開封後の既読状態にかかわらず、自動的に削除されます。

添付された報酬を受け取っていなかったとしても削除されます。

メッセージのライフサイクル

stateDiagram-v2
  [*] --> Unread: メッセージ受信
  Unread --> Read: 開封 (Read)
  Unread --> Expired: 有効期限切れ
  Read --> Deleted: 削除 (Delete)
  Read --> AutoDeleted: isAutomaticDeletingEnabled
  Read --> Expired: 有効期限切れ
  Deleted --> [*]
  AutoDeleted --> [*]
  Expired --> [*]

グローバルメッセージ

特定のプレイヤーではなく、全プレイヤーに対して同一のメッセージを配布したい場合は「グローバルメッセージ」を使用します。 グローバルメッセージはマスターデータとして定義し、各プレイヤーが ReceiveGlobalMessageAsync を呼び出した際に、まだ受け取っていないメッセージをそのプレイヤーの受信ボックスにコピーします。

機能 説明
expiresAt 絶対時刻でのメッセージ有効期限
expiresTimeSpan 受信からの相対期間で有効期限を決定(例: 受信から3日間)
messageReceptionPeriodEventId GS2-Schedule のイベントIDを指定し、特定期間中だけグローバルメッセージを受信可能とする
{
  "version": "2018-04-20",
  "globalMessages": [
    {
      "name": "welcome",
      "metadata": "新規ユーザー向けプレゼント",
      "readAcquireActions": [
        {
          "action": "Gs2Money:DepositByUserId",
          "request": "{\"namespaceName\":\"money-0001\",\"slot\":1,\"userId\":\"#{userId}\",\"price\":0,\"count\":100}"
        }
      ],
      "expiresTimeSpan": { "days": 7 }
    }
  ]
}

マスターデータの種類には以下があります。

  • GlobalMessage: 全プレイヤーに配信するメッセージ定義

マスターデータの登録はマネージメントコンソールから登録する他、GitHubからデータを反映したり、GS2-Deployを使ってCIから登録するようなワークフローを組むことが可能です。

スクリプトトリガー

ネームスペースに receiveMessageScriptreadMessageScriptdeleteMessageScript を設定すると、メッセージ受信、開封、削除時の前後でカスタムスクリプトを実行できます。スクリプトは同期・非同期の実行方式を選択でき、非同期では GS2-Script や Amazon EventBridge を利用した外部連携も可能です。

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

  • receiveMessageScript(完了通知: receiveMessageDone): メッセージ受信の前後
  • readMessageScript(完了通知: readMessageDone): メッセージ開封の前後
  • deleteMessageScript(完了通知: deleteMessageDone): メッセージ削除の前後

プッシュ通知

設定できる主なプッシュ通知と設定名は以下の通りです。

  • receiveNotification: メッセージ受信時に通知

通知端末がオフラインの場合にモバイルプッシュ通知へ転送する設定も指定でき、確実な配信を実現できます。

GS2-Gateway の WebSocket 接続を持つプレイヤーには即時通知を、未接続のプレイヤーには iOS / Android のモバイルプッシュ通知を自動送信するといった配信戦略を、追加実装なしで実現できます。

トランザクションアクション

GS2-Inbox では以下のトランザクションアクションを提供しています。

  • 消費アクション: メッセージの開封、メッセージの削除
  • 入手アクション: メッセージの送信

「メッセージの送信」を入手アクションとして利用することで、ショップでの商品購入時やクエストクリア時の報酬として、プレイヤーのプレゼントボックスに直接メッセージ(アイテム付き)を届けるといった処理をトランザクション内で完結させることが可能になります。これにより、報酬付与のタイミングを柔軟に制御でき、プレイヤーのインベントリが一杯な場合でも、一旦プレゼントボックスに報酬を退避させるといった運用が容易になります。

実装例

メッセージを送信

メッセージの送信はゲームエンジン用の SDK では直接呼び出すことはできません。通常はサーバーサイドのスクリプトや、上記の「トランザクションアクション」を介して実行されます。

運営からの一括配布の場合はマネージメントコンソールや GS2-Deploy 経由で送信できます。 ゲーム内で「メッセージ送信」を報酬や演出のトリガーにする場合は、GS2-Exchange や GS2-Quest の acquireActions に「Gs2Inbox:SendMessageByUserId」を組み込みます。

受信したメッセージ一覧を取得

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

未読のメッセージのみの一覧を取得

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

既読のメッセージのみの一覧を取得

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

メッセージを開封

開封すると、メッセージが Read 状態に遷移すると同時に readAcquireActions に定義された報酬付与処理がトランザクションとして実行されます。 ネームスペース設定で isAutomaticDeletingEnabled を有効にしている場合、開封と同時にメッセージが削除されます。

    var result = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Message(
        messageName: "message-0001"
    ).ReadAsync(
    );
    const auto Future = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Message(
        "message-0001" // messageName
    )->Read(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

メッセージを一括開封

複数のメッセージをまとめて開封し、それぞれの添付報酬を1つのトランザクションとして受け取ります。 報酬付与処理は内部で逐次実行されるため、同一アイテムが大量に添付されているメッセージを並列開封した際に発生しうる更新レート制限の問題を回避できます。

    var result = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).BatchReadAsync(
        messageNames: new [] {
            "message-0001",
            "message-0002",
            "message-0003",
        }
    );
    const auto Future = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->BatchRead(
        []
        {
            const auto v = MakeShared<TArray<FString>>();
            v->Add("message-0001");
            v->Add("message-0002");
            v->Add("message-0003");
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

メッセージを削除

    var result = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Message(
        messageName: "message-0001"
    ).DeleteAsync(
    );
    const auto Future = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Message(
        "message-0001" // messageName
    )->Delete(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

グローバルメッセージを受信

グローバルメッセージの中でまだ受け取っていないメッセージがある場合、 自分の受信ボックスにメッセージをコピーします。 ログイン時やタイトル画面のロード完了時など、タイミングをゲーム側で制御できます。

    var result = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).ReceiveGlobalMessageAsync(
    );
    const auto Future = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->ReceiveGlobalMessage(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

よくある質問

メッセージの一括開封はできますか?

はい、BatchReadAsync を使用することで、複数のメッセージを1つのトランザクションとしてまとめて開封できます。詳しくは メッセージを一括開封 を参照してください。

なお、個別の ReadAsync を並列で呼び出すことも可能ですが、報酬付与処理が各マイクロサービスの定義するリミットに達しないかを十分配慮するべきです。

例えば、GS2-Inventory には同一のアイテムの所持数量の更新は1秒間に3回までと定義されています。 同じアイテムが添付された10のメッセージを並列で開封するとこの制限を超えることがあります。

このリミットを超過しても、原則として結果が失われることはありません。 しかし、開封APIを呼び出した後、非同期で動作する報酬付与処理の完了まで時間を要する可能性があります。 こうした場合でも、BatchReadAsync であれば内部で順次実行されるため、レート制限を踏まずに確実に報酬を受け取ることができます。

メッセージの未読数バッジを表示したい

MessagesAsync(isRead: false) で取得したリストの件数を未読数として表示できます。受信通知(receiveNotification)を購読しておけば、新着メッセージが届いたタイミングでクライアント側で再取得することができます。

詳細なリファレンス