⌘K
↑↓ Navigate ↵ Open Esc Close
Tier 5Lobby23 / 29

Lobby Signals: one call for any "tell everyone in this match" event

Global Chat for the portal-registered, cross-session channel this is not; Lobbies for the connection this rides on; Player-to-Player Events for reaching someone who isn't even in your lobby.

A lobby signal is a one-call broadcast to everyone currently in a lobby: sendSignal(lobbyId, payload), riding the RTT connection the lobby already holds open. There's no channel to pre-register in the portal, no separate connect step, no history to fetch. Every reference RelayTestApp port builds its "this lobby" chat tab (the tab scoped to just the current match) on this call alone, without touching the Chat service. The primitive isn't limited to chat text. Anything that fits "broadcast this small JSON blob to everyone in my lobby" works: a ready-check, a vote, a draft pick, whatever the game needs.

How it works

  1. Send any JSON payload with sendSignal(lobbyId, payload). There's no schema. The reference apps send {"text": "..."}", but the payload's structure is entirely up to the app.
  2. Receive it back through the same lobby event callback already registered for MEMBER_JOIN/ROOM_READY/etc., as an event whose operation is SIGNAL. The payload comes back under signalData, and the server attaches the authoritative sender identity under from (cxId, name). The sender name doesn't need to be trusted as client-asserted; the server already vouches for it.
  3. Filter the sender's own echo. The server sends a signal back to its own sender along with everyone else's, so every reference app compares from.cxId against its own id and skips rendering (or re-applying) its own signal, since it already rendered it optimistically the moment it sent.

Code

C++: send
void app_sendLobbySignal(const std::string &text)
{
    Json::Value signal;
    signal["text"] = text;
    pBCWrapper->getLobbyService()->sendSignal(state.lobby.lobbyId, Json::FastWriter().write(signal), nullptr);
    state.lobby.chatMessages.push_back({state.user.name, text}); // optimistic local echo
}
C++: receive
else if (operation == "SIGNAL")
{
    const auto &fromCxId = jsonData["from"]["cxId"].asString();
    std::string fromName = jsonData["from"]["name"].asString();
    std::string text     = jsonData["signalData"]["text"].asString();
    if (!text.empty() && fromCxId != state.user.cxId) // skip echo of our own send
        state.lobby.chatMessages.push_back({fromName, text});
}
JavaScript
// send
onSendLobbySignal (text) {
  this.bc.lobby.sendSignal(this.state.lobby.lobbyId, { text }, () => {})
  state.lobby.chatMessages = (state.lobby.chatMessages || []).concat([{ fromName: state.user.name, text }])
}

// receive, inside onLobbyEvent
} else if (result.operation === 'SIGNAL') {
  const fromCxId = result.data.from && result.data.from.cxId
  const text = result.data.signalData && result.data.signalData.text
  if (text && fromCxId !== this.state.user.cxId) {
    const fromName = (result.data.from && result.data.from.name) || 'Player'
    state.lobby.chatMessages = (state.lobby.chatMessages || []).concat([{ fromName, text }])
  }
}
Godot (C#)
_brainCloudWrapper.LobbyService.SendSignal(_lobbyID, signalData, successCb, failureCb);
// received in the "SIGNAL" case of the same lobby-event handler used for MEMBER_JOIN etc.
Godot (GDScript)
func _on_lobby_signal_send_requested(text: String) -> void:
    AppState.bc.lobby_service.send_signal(AppState.lobby_id, {"text": text})
    AppState.lobby_chat_history.append({"from": AppState.username, "text": text, "is_me": true})

func _on_lobby_signal_received(data: Dictionary) -> void:
    var from_cx_id: String = data.get("from", {}).get("cxId", "")
    if from_cx_id == AppState.user_cx_id: return   # skip echo of own signal
    var text: String = data.get("signalData", {}).get("text", "")

Gotchas worth knowing up front

  • This is not the Chat service. There's no message history to fetch, no portal registration, and messages don't survive the lobby ending. Chat that needs to outlive a single match belongs in Global Chat instead (see the earlier article).
  • Every sender receives its own signal back, every time. Every reference app filters it out by comparing from.cxId. Skipping this filter double-renders the sender's own messages.
  • The mechanism isn't chat-specific. It's a generic, low-latency, server-brokered lobby broadcast; the reference apps only ever put {"text": "..."} in the payload, but a ready check, a drawing-tool selection, or a draft pick is exactly as valid.