⌘K
↑↓ Navigate ↵ Open Esc Close
Tier 3Event17 / 29

Player-to-Player Events: a persisted mailbox that can also push live

Lobby Signals for the live, in-match equivalent of this. Events are the tool for when the recipient might not even be online right now.

brainCloud's Event service delivers a message to a specific player, and persists it if they aren't connected to receive it right away. SendEvent(toProfileId, eventType, jsonData) queues a small JSON payload for that player. If the recipient is offline, it waits in their mailbox until they call GetEvents, whether that's ten seconds or ten hours later. But if the recipient is already connected with RTT and has registered an event callback, the same SendEvent call also arrives live, no poll required. BombersRTT (repositories/unity-examples/BombersRTT) demonstrates both paths at once: its "invite a friend to my lobby" flow lands instantly on a friend who's already in the app, and still waits correctly in their mailbox if they log in later.

How it works

  1. Send is fire-and-forget, addressed by profile id. EventService.SendEvent(toProfileId, eventType, jsonData) does the work. eventType is an app-defined string ("OFFER_JOIN_LOBBY", "CONFIRM_JOIN_LOBBY", "REFUSED_JOIN_LOBBY" in this reference app), and jsonData is whatever payload the recipient needs to act on it.
  2. A connected recipient gets it live, no poll needed. Registering RTTService.RegisterRTTEventCallback(callback) once (alongside RTT chat/presence) means every event sent to this player while they're connected arrives immediately through that callback, tagged operation: "GET_EVENTS" even though nothing was polled.
  3. GetEvents() is the catch-up path, not the only path. Called once at login, it returns everything that queued up while the player wasn't connected to receive the live push. Same events, same data, different delivery timing depending on whether the recipient was online when they were sent.
  4. Every event needs to be explicitly deleted after handling, on both paths. DeleteIncomingEvent(evId) acknowledges and removes it. Events aren't self-expiring, so a client that receives one, live or polled, but never deletes it will see it again on the next GetEvents call.
  5. A three-message handshake is just three SendEvent calls with different eventType strings, each triggering a different UI response on the receiving end. There's no dedicated "invite" API here; the whole pattern is built out of the generic event primitive.

Code

Offer: sent to a specific friend by profile idGFriendsManager.cs:133-146
public void OnInviteToLobby(string in_ProfileId, string in_originalUserName)
{
    // send event to other person
    Dictionary<string, object> jsonData = new Dictionary<string, object>();
    jsonData[BrainCloudConsts.JSON_LAST_CONNECTION_ID] = GCore.Wrapper.Client.RTTConnectionID;
    jsonData[BrainCloudConsts.JSON_PROFILE_ID] = GCore.Wrapper.Client.ProfileId;
    jsonData[BrainCloudConsts.JSON_USER_NAME] = GPlayerMgr.Instance.PlayerData.PlayerName;
    OriginalUserName = in_originalUserName;

    GCore.Wrapper.Client.EventService.SendEvent(in_ProfileId, "OFFER_JOIN_LOBBY",
        JsonWriter.Serialize(jsonData));

    GEventManager.TriggerEvent(GEventManager.ON_INVITED_FRIEND);
}
Registering for the live push, once, alongside chatBombersNetworkManager.cs:1218-1225
public void ConnectToGlobalChat()
{
    GCore.Wrapper.RTTService.RegisterRTTChatCallback(chatCallback);
    GCore.Wrapper.RTTService.RegisterRTTEventCallback(eventCallback);

    // do a get channel call instead of manually appending these, this is for demo purposes
    GCore.Wrapper.ChatService.ChannelConnect(GCore.Wrapper.Client.AppId + ":gl:main", 25, onChannelConnected);
}
The live handler: same three eventType strings, no poll involved (BombersNetworkManager.cs:1297-1360, abridged)
private void eventCallback(string in_message)
{
    Dictionary<string, object> jsonMessage = (Dictionary<string, object>)JsonReader.Deserialize(in_message);
    switch (jsonMessage["operation"] as string)
    {
        case "GET_EVENTS":
            Dictionary<string, object> jsonData = (Dictionary<string, object>)jsonMessage[BrainCloudConsts.JSON_DATA];
            GCore.Wrapper.Client.EventService.DeleteIncomingEvent(jsonData["evId"] as string);

            switch (jsonData["eventType"] as string)
            {
                case "OFFER_JOIN_LOBBY":
                    // bring up the offer to join display on the requested client
                    break;
                case "CONFIRM_JOIN_LOBBY":
                    // they confirmed to join the lobby! create/join the match
                    break;
                case "REFUSED_JOIN_LOBBY":
                    GEventManager.TriggerEvent(GEventManager.ON_REFUSED_INVITE_FRIEND);
                    break;
            }
            break;
    }
}
Confirm or refuse: the recipient answers with their own eventConfirmJoinLobbySubState.cs:31-60
public void ConfirmJoinGameWithOther()
{
    // send event to confirm
    Dictionary<string, object> jsonData = new Dictionary<string, object>();
    jsonData[BrainCloudConsts.JSON_LAST_CONNECTION_ID] = GCore.Wrapper.Client.RTTConnectionID;
    jsonData[BrainCloudConsts.JSON_PROFILE_ID] = GCore.Wrapper.Client.ProfileId;
    jsonData[BrainCloudConsts.JSON_USER_NAME] = GPlayerMgr.Instance.PlayerData.PlayerName;

    // send event to other person
    GCore.Wrapper.Client.EventService.SendEvent(m_offeredByProfileId, "CONFIRM_JOIN_LOBBY",
        BrainCloud.JsonFx.Json.JsonWriter.Serialize(jsonData));

    BombersNetworkManager.WaitOnLobbyJoin();
    ExitSubState();
}

public void RefusedJoinGameWithOther()
{
    // send event to confirm
    Dictionary<string, object> jsonData = new Dictionary<string, object>();
    jsonData[BrainCloudConsts.JSON_LAST_CONNECTION_ID] = GCore.Wrapper.Client.RTTConnectionID;
    jsonData[BrainCloudConsts.JSON_PROFILE_ID] = GCore.Wrapper.Client.ProfileId;
    jsonData[BrainCloudConsts.JSON_USER_NAME] = GPlayerMgr.Instance.PlayerData.PlayerName;

    // send event to other person
    GCore.Wrapper.Client.EventService.SendEvent(m_offeredByProfileId, "REFUSED_JOIN_LOBBY",
        BrainCloud.JsonFx.Json.JsonWriter.Serialize(jsonData));

    ExitSubState();
}
The catch-up path: fetch everything waiting, once at loginGPlayerMgr.cs:160
GCore.Wrapper.Client.EventService.GetEvents(onGetBCEventsSuccess, onGetBCEventsFailed, null);
Parse the queue into local objects (GPlayerMgr.cs:989-1029, abridged)
private void onGetBCEventsSuccess(string in_jsonString, object cb_object)
{
    var jsonData = /* deserialize .data */;
    var incoming_events = (object[])jsonData[BrainCloudConsts.JSON_EVENT_INCOMING_EVENTS];
    foreach (Dictionary<string, object> incomingEvent in incoming_events)
    {
        var eventData = new BaseBrainCloudEventData
        {
            fromPlayerId = (string)incomingEvent[BrainCloudConsts.JSON_EVENT_FROM_PLAYER_ID],
            eventType    = (string)incomingEvent[BrainCloudConsts.JSON_EVENT_EVENT_TYPE],
            evId         = (string)incomingEvent[BrainCloudConsts.JSON_EVENT_EV_ID],
            createdAt    = (long)incomingEvent[BrainCloudConsts.JSON_EVENT_CREATED_AT],
        };
        m_baseBrainCloudEventDataList.Add(eventData);
    }
    GEventManager.TriggerEvent(GEventManager.ON_PROCESS_BC_EVENTS);
}
Acknowledge and remove an event once handled, on the catch-up pathGPlayerMgr.cs:256-266
public void DeleteBCEvent(BaseBrainCloudEventData in_baseBCEventData, SuccessCallback in_success = null, FailureCallback in_failure = null)
{
    if (in_baseBCEventData != null && in_baseBCEventData.evId != null)
    {
        GCore.Wrapper.Client.EventService.DeleteIncomingEvent(
        in_baseBCEventData.evId,
        onDeleteBCEventSuccess + in_success,
        onDeleteBCEventFailed + in_failure,
        null);
    }
}

Gotchas worth knowing up front

  • The live push and the poll deliver the same event, tagged the same way. A live-pushed event arrives through RegisterRTTEventCallback's callback with operation: "GET_EVENTS", the identical operation name GetEvents() returns on the catch-up path. Branching only on operation without checking which callback fired conflates the two; this reference app keeps them apart by handling them in separate functions entirely.
  • DeleteIncomingEvent is required on the live path too, not just the poll, and nothing expires an event automatically. A live-pushed event is still sitting in the recipient's persisted mailbox; BombersRTT's eventCallback deletes it immediately on receipt. Skipping that on either path means the same event gets re-delivered on the next GetEvents catch-up call.
  • The whole invite/accept/refuse flow is three app-defined eventType strings. The SDK doesn't provide three dedicated methods for this. Nothing enforces that "OFFER_JOIN_LOBBY" gets answered with "CONFIRM_JOIN_LOBBY" or "REFUSED_JOIN_LOBBY"; that contract lives entirely in the client code on both ends.
  • Events also carry system-generated types the app never sent, like SYSTEM_TOURNAMENT_COMPLETE in this reference app (triggering an automatic reward claim). An event-processing loop should branch on eventType defensively rather than assume every event came from another player.