Client and Service Loop

Creating a Client

The client is the single entry point to the SDK: a RealtimeClient in C++ (in the RealtimeCore::Matchmaking namespace) and in JavaScript, and a RealtimeHandle in C. One instance represents one connection to the Photon Cloud and owns everything that belongs to it: the connection state, the current room, the callbacks and all pending operations. Every feature described in this manual is a function or a callback of this one client.

A client is created from a set of construction options. The App Id is required: it identifies your application on the Photon Cloud and comes from the Photon Dashboard. The App Version separates clients during matchmaking, because clients with different App Versions do not see each other. Setting it is recommended in C++, and the C and JavaScript APIs require it.

The following creates a client that is ready to connect:

C++
C
JavaScript

C++

using namespace RealtimeCore::Matchmaking;

ClientConstructOptions options;
options.AppId      = PHOTON_STR("your-app-id");
options.AppVersion = PHOTON_STR("1.0");

auto client = std::make_unique<RealtimeClient>(options);

C

/* The queue must outlive the client, so create it first and destroy it last. */
RealtimeEventQueueHandle queue  = realtime_event_queue_create();
RealtimeHandle           client = realtime_create("your-app-id", "1.0", queue);

/* ... run the session ... */

realtime_destroy(client);
realtime_event_queue_destroy(queue);

/* The C API handle plays the same role as the C++ instance: one handle is
   one connection, it is not copied or moved and it must be destroyed
   exactly once. Only AppId and AppVersion are settable at creation; the
   rest of ClientConstructOptions is not part of the ABI, so those settings
   keep their defaults. The runtime tunables further down the Configuration
   page are available through realtime_set_* functions. */

JavaScript

import { createRealtimeClient } from '@photonengine/realtime-core';

const client = await createRealtimeClient({ appId: 'your-app-id', appVersion: '1.0' });

createRealtimeClient() loads the WebAssembly module on first use and resolves to a client whose service timer is already running. Call client.dispose() when the client is no longer needed; see Lifetime.

In C++, the class is final, non-copyable and non-movable. Decide where the client lives up front, as a member of a long-lived game system or in a std::unique_ptr, and hand out references to it.

Construction Options

The construction options are baked into the client when it is created and stay fixed for its whole lifetime. In C++ they are the fields of ClientConstructOptions, and in JavaScript the properties of the object passed to createRealtimeClient(). The C API takes only the App Id and the App Version, as arguments of realtime_create(). Settings that can be adjusted at runtime are covered on the Configuration page.

C++ JavaScript Type Default Description
AppId appId string empty The application id from the Photon Dashboard. Required.
AppVersion appVersion string empty Version string that separates clients during matchmaking. Required in JavaScript.
Protocol protocol ConnectionProtocol Default, 'wss' Transport protocol. In C++, Default resolves to UDP, or to WebSocket on WASM builds. JavaScript offers 'ws' and 'wss' only.
UseAlternativePorts useAlternativePorts boolean false Use the alternative Photon Cloud port range.
RegionSelection regionSelection RegionSelectionMode Default, 'default' How the connect region is chosen. See Connection and Regions.
AutoLobbyStats autoLobbyStats boolean false Have the server push lobby statistics automatically, delivered through OnLobbyStats in C++ and the lobbyStats event in JavaScript.
DisconnectTimeoutMs disconnectTimeoutMs integer 10000 Time in milliseconds without acknowledgements before the connection counts as lost.
PingIntervalMs pingIntervalMs integer 2000 Interval in milliseconds between keep-alive pings.
EnableCrc enableCrc boolean false CRC checksums on packets. Must be decided before connecting.
SentCountAllowance sentCountAllowance integer 5 Resend attempts for a reliable command before the connection counts as lost.
QuickResendAttempts quickResendAttempts integer 0 Additional early resends for reliable commands. Clamped to 4.
LimitOfUnreliableCommands limitOfUnreliableCommands integer 0 Cap on buffered incoming unreliable commands. 0 means no limit.
— autoPump boolean or { intervalMs } true How the client is serviced: true runs a timer every 16 ms, { intervalMs } sets another interval and false leaves the calls to client.service() to you. See The Service Loop.
— wasm WasmLocation — Where the .wasm file is loaded from. Only the first client's setting is used. See Loading the Module.

Most of the network settings can also be changed on a live client; see Configuration for the full list and for which of them can change while connected.

The Service Loop

The service call is the engine of the whole SDK: Service() in C++, realtime_service() in C and client.service() in JavaScript. One call performs the pending network I/O, dispatches incoming data to your subscribed callbacks and completes the operations whose results have arrived. Nothing progresses without it: no callback fires and no operation completes between two service calls.

Call it regularly from your game loop, typically once per frame. It is cheap when there is nothing to do, and it is safe to call while disconnected. Long gaps between calls delay acknowledgements and event delivery and can eventually time out the connection.

In JavaScript, a timer started by createRealtimeClient() makes the service call every 16 ms, so most code never calls it. client.pump changes the interval through intervalMs and stops and restarts the timer with stop() and start(). Create the client with autoPump: false to make the service call from your own game loop instead. The timer uses setInterval rather than requestAnimationFrame, so it keeps running in a background tab, where browsers throttle it to once per second and after five minutes to once per minute. When a game must stay connected through that, raise the disconnect timeout or run the client in a Web Worker. client.service() must not be called from inside an event handler.

The following services the client from a loop until a pending connect completes:

C++
C
JavaScript

C++

Task<Result<void, ConnectError>> connectTask = client->Connect();

while (!connectTask.IsReady())
{
    client->Service();
    std::this_thread::sleep_for(std::chrono::milliseconds(16));
}

Result<void, ConnectError> connected = connectTask.Get();

C

int32_t connectError = -1; /* -1 = still pending */

realtime_connect(client, NULL, NULL, 255, NULL, NULL);

while (connectError < 0)
{
    RealtimeEvent events[64];
    int32_t       count;

    realtime_service(client); /* pumps, dispatches and drains onto the queue */

    while ((count = realtime_event_queue_poll(queue, events, 64)) > 0)
    {
        for (int32_t i = 0; i < count; ++i)
        {
            if (events[i].type == RealtimeEventType_ConnectResult)
            {
                connectError = events[i].mode;
            }
        }
    }

    realtime_event_queue_flush(queue);
    SleepMs(16);
}

/* realtime_service is the exact C counterpart of Service() and carries the
   same obligation: nothing progresses between two calls. It does one thing
   more than the C++ version, which is to drain every completed operation
   onto the event queue, so the poll always follows the pump. */

JavaScript

import { createRealtimeClient } from '@photonengine/realtime-core';

const client = await createRealtimeClient({ appId: 'your-app-id', appVersion: '1.0', autoPump: false });

const gameLoop = setInterval(() => client.service(), 16); // about 60 times a second
await client.connect();

Granular Pump Control

The granular pump functions exist in C++ and C. In JavaScript, client.service() always runs the full pump, dispatch included; control when it runs through client.pump or autoPump: false, as described in The Service Loop.

The full service call bundles three steps: exchanging data with the network layer, dispatching queued incoming commands and sending queued outgoing commands. The granular functions below give advanced control over when each step happens, for example to keep the connection alive while a level loads, or to batch sends at a lower frequency than the frame rate. If you bypass the full service call, you must call the basic service function regularly yourself.

C++ C JavaScript What It Does When to Use It
Service(dispatchIncomingCommands = true) realtime_service() client.service() The full pump: network I/O, dispatch of the queued incoming commands and sending of the queued outgoing commands. The default; call once per frame. In C++, pass false to queue incoming commands without dispatching them, for example while loading a level.
ServiceBasic() realtime_service_basic() — Exchanges data with the system's network layer only; nothing is dispatched. Call regularly whenever you replace the full service call with the granular functions.
SendOutgoingCommands() realtime_send_outgoing_commands() — Transmits the queued outgoing commands, aggregating them into as few packets as possible. Returns true while more queued commands remain to be sent. Call at your own send frequency; fewer calls mean fewer packets but higher latency.
SendAcksOnly() realtime_send_acks_only() — Sends only acknowledgements (UDP) or a ping (TCP and WebSocket). Keeps the connection alive while deliberately pausing real data sends.
DispatchIncomingCommand() realtime_dispatch_incoming_command() — Dispatches a single queued incoming command and returns true if one was dispatched. Call in a loop until it returns false, or budget a fixed number of dispatches per frame.

Threading Model

The client is not thread-safe. Every call, including the service call, must happen on the same thread. If other threads need to interact with the client, funnel those interactions through a queue that the servicing thread drains.

Callbacks, and in C++ coroutine resumptions, run inside the service call, on the thread that calls it. Handlers can therefore touch game state without locks, but they must not block: a stalled handler stalls the whole network pump.

In C++, ConnectOptions.UseBackgroundSendReceiveThread (true by default) moves only the low-level socket send and receive onto a background thread. It does not change the single-thread rule: all API calls, callbacks and coroutine resumptions stay on the thread that calls Service(). The flag takes effect on the Nintendo Switch only. On every other platform its value is ignored and the client behaves as if it were false.

In JavaScript, everything runs on the single-threaded event loop of the page, the Web Worker or the Node.js process, and the web build has no background thread. Event handlers run inside the service call, and promise continuations run after it has returned. A long-running handler, or any other long task on the same thread, delays the next service call.

Client State Queries

The client reports its current connection state: disconnected, connecting, connected, joining a room, in a room, leaving a room or disconnecting. Convenience checks answer the common questions: whether the client is connected, in a room or in a lobby.

Query C++ C JavaScript
Connection state GetState(), a ConnectionState realtime_state(), a RealtimeConnectionState client.state, a ConnectionState
Connected IsConnected() realtime_is_connected() client.isConnected
In a room IsInRoom() realtime_is_in_room() client.isInRoom
In a lobby IsInLobby() realtime_is_in_lobby() client.isInLobby

State changes are also announced as they happen, for example when the client joins a room, leaves a room or disconnects: through OnRoomJoined, OnRoomLeft and OnDisconnected in C++, as RealtimeEventType_RoomJoined, RealtimeEventType_RoomLeft and RealtimeEventType_Disconnected events in C and as the roomJoined, roomLeft and disconnected events in JavaScript. See Connection and Regions for the connection lifecycle and Callbacks and Subscriptions for how to subscribe.

Last updated on

Back to top