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
- 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.
- 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.
- 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.
- 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.
- 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
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);
}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);
}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;
}
}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();
}GCore.Wrapper.Client.EventService.GetEvents(onGetBCEventsSuccess, onGetBCEventsFailed, null);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);
}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.