Quick Start Guide

Create an App Id

Sign in to the Photon Dashboard and create a new Realtime application. Copy its App Id; the client uses it to identify your application on the Photon Cloud.

Create the Client

A client is created from a set of options. The App Id identifies your application on the Photon Cloud. The App Version separates clients during matchmaking: clients with different versions do not see each other. In C++ only the App Id is required and setting the App Version is recommended, the C and JavaScript APIs require both.

All strings in the API are UTF-8. C++ writes literals with the PHOTON_STR("...") macro, C passes plain UTF-8 const char* strings and JavaScript uses ordinary strings. The Strings page covers conversions to and from other string types.

The following is all it takes to create a client:

C++
C
JavaScript

C++

using namespace RealtimeCore::Matchmaking;

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

RealtimeClient client(options);

C

/* The event queue is created first: realtime_create subscribes the new client
   to it, and every result and callback arrives through it from then on. */
RealtimeEventQueueHandle queue  = realtime_event_queue_create();
RealtimeHandle           client = realtime_create("your-app-id", "1.0", queue);

if (client == NULL)
{
    /* appId, appVersion or the queue handle was NULL */
}

/* Over the C API the strings are plain UTF-8 const char*, so there is no
   PHOTON_STR equivalent, and the App Id and version are the only two
   construction options available. See the C API page for the handles, the
   event queue and the rest of the ABI. */

JavaScript

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

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

createRealtimeClient() is asynchronous because it loads the WebAssembly module on first use. See Install on the JavaScript/TypeScript API page for installing the package.

Drive the Client

The client does its work only while it is serviced. Each service call sends and receives network messages, runs subscribed callbacks and advances pending asynchronous operations. In C++ and C, call the service function on the client often, ideally every frame. In JavaScript, the client services itself on a timer, every 16 ms by default, so pending operations progress without a loop of your own.

The following waits for a connect to complete while the client is serviced:

C++
C
JavaScript

C++

Task<Result<void, ConnectError>> Startup(RealtimeClient& client)
{
    Result<void, ConnectError> connected = co_await client.Connect(); // suspends until the server responds
    co_return connected;
}

// in the game loop:
Task<Result<void, ConnectError>> startup = Startup(client);

while (!startup.IsReady())
{
    client.Service(); // pumps the network and resumes Startup() when its await completes
    std::this_thread::sleep_for(std::chrono::milliseconds(16));
}

C

int connected = 0;
int failed    = 0;

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

while (!connected && !failed)
{
    RealtimeEvent events[64];
    int32_t       count;

    realtime_service(client); /* pumps the network and drains completed operations */

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

    realtime_event_queue_flush(queue);
    SleepMs(16);
}

/* There are no coroutines over the C API. realtime_connect returns
   immediately and the outcome arrives as a ConnectResult event on the
   queue, so the frame loop pumps, polls the queue and flushes it - the
   pattern every C sample on the following pages uses. */

JavaScript

// createRealtimeClient starts a timer that calls client.service() every 16 ms,
// so an operation only needs to be awaited.
await client.connect();

To call client.service() from your own game loop instead, create the client with autoPump: false, see Client and Service Loop.

Connect

Connecting starts the connection handshake and completes asynchronously. In C++ the call returns a task that holds a result, in C the result arrives as an event on the queue and in JavaScript the call returns a promise. Once the operation completes, check it for success or for the error that occurred. The Asynchronous Operations page covers waiting for operations and reading their results.

A successful connect also places the client in the default lobby, so matchmaking works immediately afterwards.

The following connects and reports the error when the connection fails:

C++
C
JavaScript

C++

Task<Result<void, ConnectError>> ConnectToCloud(RealtimeClient& client)
{
    Result<void, ConnectError> result = co_await client.Connect();

    if (result.IsErr())
    {
        std::printf("connect failed (%d): %s\n",
                    static_cast<int>(result.GetError().Code),
                    reinterpret_cast<const char*>(result.GetError().Message.c_str()));
        co_return result;
    }

    // client.IsConnected() is true from here on
    co_return result;
}

C

/* Inside the polled batch, with `blob` from realtime_event_queue_blob. */
if (e->type == RealtimeEventType_ConnectResult)
{
    if (e->mode != 0)
    {
        /* The error message is unterminated UTF-8, so print it with a precision. */
        printf("connect failed (%d): %.*s\n",
               e->mode, (int)e->blobLength,
               (e->blobOffset >= 0) ? (const char*)(blob + e->blobOffset) : "");
    }
    else
    {
        /* realtime_is_connected(client) is non-zero from here on */
    }
}

/* mode carries the ErrorCode on every *Result event: 0 is success, and
   anything else means the blob holds the error message as UTF-8 text. */

JavaScript

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

try {
    await client.connect();
    // client.isConnected is true from here on
} catch (error) {
    if (!isRealtimeError(error)) {
        throw error;
    }
    console.log(`connect failed (${error.code}): ${error.message}`);
}

A failed operation rejects its promise with a RealtimeError, see Errors and Disconnects.

Join a Room

Joining a random room or creating one is the one-call path into a shared room: it joins a random matching room, or creates a new one when none exists. It takes room creation options and matchmaking options, and both have defaults, so the call without arguments is enough for a first test.

The following connects and enters a room in one sequence, then reports the joined room:

C++
C
JavaScript

C++

Task<Result<Room>> joinTask =
    client.Connect()
        .AndThen([&client] { return client.JoinRandomOrCreateRoom(); });

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

Result<Room> joined = joinTask.Get();
if (joined.IsOk())
{
    std::printf("joined room %s with %d players\n",
                reinterpret_cast<const char*>(joined.GetValue().GetName().c_str()),
                joined.GetValue().GetPlayerCount());
}

C

/* Chaining is done by firing the next operation from the previous result. */
switch (e->type)
{
    case RealtimeEventType_ConnectResult:
        /* maxPlayers 0 (unlimited), visible, open, no TTLs, no plugins, no properties */
        realtime_join_random_or_create_room(client, 0, 1, 1, 0, 0, NULL, NULL, 0);
        break;

    case RealtimeEventType_JoinRandomOrCreateRoomResult:
    {
        char name[64] = {0};
        realtime_room_name(client, name, (int32_t)sizeof(name));
        printf("joined room %s with %d players\n",
               name, realtime_room_player_count(client));
        break;
    }

    default:
        break;
}

/* The C API has no equivalent of AndThen: an operation is started from the
   handler of the previous one's result. Room operations report success
   with an empty blob rather than with a serialized room, so read the
   joined room through the realtime_room_* queries once the result arrives. */

JavaScript

await client.connect();
const room = await client.joinRandomOrCreateRoom();
console.log(`joined room ${room.name} with ${room.playerCount} players`);

Send and Receive Events

Register a handler to receive the events the other players in the room send. Sending takes an event code and a payload, and receivers identify the payload by the event code. In C++ the payload can be any trivially copyable struct and in C it is raw bytes. Receivers copy the bytes back out. In JavaScript the payload is any supported value, such as the Float32Array below, and receivers get it back as a value, see Custom Events.

The following registers a receiver and sends a player position, a send that a game triggers once per second from its loop:

C++
C
JavaScript

C++

struct PlayerState
{
    float PositionX;
    float PositionY;
};

constexpr uint8_t PlayerStateEventCode = 1;

RealtimeCore::Common::ScopedSubscription stateSub = client.SubscribeEvent(
    [](uint8_t eventCode, int senderId, std::span<const uint8_t> data) {
        if (eventCode == PlayerStateEventCode && data.size() == sizeof(PlayerState))
        {
            PlayerState state;
            std::memcpy(&state, data.data(), sizeof(PlayerState));
            std::printf("player %d is at %.1f / %.1f\n", senderId, state.PositionX, state.PositionY);
        }
    });

// once per second, from the game loop:
PlayerState state{12.0F, 34.0F};
client.SendEvent(PlayerStateEventCode, state);

C

typedef struct
{
    float positionX;
    float positionY;
} PlayerState;

#define PLAYER_STATE_EVENT_CODE 1

/* Receiving: there is nothing to subscribe to, every event code lands on the
   queue as an Event (108) with the sender in `origin` and the code in `counter`. */
if (e->type == RealtimeEventType_Event &&
    e->counter == PLAYER_STATE_EVENT_CODE &&
    e->blobOffset >= 0 &&
    e->blobLength == (int32_t)sizeof(PlayerState))
{
    PlayerState state;
    memcpy(&state, blob + e->blobOffset, sizeof(state));
    printf("player %u is at %.1f / %.1f\n", e->origin, state.positionX, state.positionY);
}

/* Sending, once per second from the game loop: */
PlayerState state = {12.0F, 34.0F};
realtime_send_event(client, PLAYER_STATE_EVENT_CODE,
                    (const uint8_t*)&state, (int32_t)sizeof(state),
                    1,       /* reliable              */
                    0,       /* channel               */
                    0,       /* receiverGroup: Others */
                    NULL, 0, /* no explicit targets   */
                    0,       /* interestGroup         */
                    0,       /* caching: DoNotCache   */
                    0);      /* encrypt               */

/* The C API sends and receives raw bytes, so the struct is copied in and
   out with memcpy exactly as the C++ SendEvent<T> overload does. Always
   check blobLength against sizeof before copying: the sender decides the
   payload size, not the receiver. */

JavaScript

const PLAYER_STATE_EVENT = 1;

client.onEvent(PLAYER_STATE_EVENT, ({ senderNumber, data }) => {
    if (data instanceof Float32Array && data.length === 2) {
        console.log(`player ${senderNumber} is at ${data[0]} / ${data[1]}`);
    }
});

// once per second, from the game loop:
client.sendEvent(PLAYER_STATE_EVENT, new Float32Array([12, 34]));

Full Example

The complete program below runs one minimal session: it creates a client, connects, joins or creates a room, then sends a ping event every second until it receives one back, and disconnects. Run the program twice: the first instance sends into the room until the second one joins and answers, and each instance exits once the other's ping arrives.

C++
C
JavaScript

C++

#include "RealtimeCore/Matchmaking/RealtimeClient.h"
#include "RealtimeCore/Common/ScopedSubscription.h"

#include <chrono>
#include <cstdio>
#include <cstring>
#include <thread>

using namespace RealtimeCore::Matchmaking;

struct PingMessage
{
    int Counter;
};

constexpr uint8_t PingEventCode = 1;

Task<Result<Room>> EnterRoom(RealtimeClient& client)
{
    Result<void, ConnectError> connected = co_await client.Connect();
    if (connected.IsErr())
    {
        co_return Result<Room>::Err(connected.GetError());
    }

    co_return co_await client.JoinRandomOrCreateRoom();
}

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

    RealtimeClient client(options);

    bool received = false;

    RealtimeCore::Common::ScopedSubscription pingSub = client.SubscribeEvent(
        [&received](uint8_t eventCode, int senderId, std::span<const uint8_t> data) {
            if (eventCode == PingEventCode && data.size() == sizeof(PingMessage))
            {
                PingMessage message;
                std::memcpy(&message, data.data(), sizeof(PingMessage));
                std::printf("ping %d from player %d\n", message.Counter, senderId);
                received = true;
            }
        });

    Task<Result<Room>> entered = EnterRoom(client);

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

    Result<Room> room = entered.Get();
    if (room.IsErr())
    {
        std::printf("failed to enter a room (%d): %s\n",
                    static_cast<int>(room.GetError().Code),
                    reinterpret_cast<const char*>(room.GetError().Message.c_str()));
        return 1;
    }

    std::printf("joined room %s, sending a ping every second\n",
                reinterpret_cast<const char*>(room.GetValue().GetName().c_str()));

    PingMessage ping{0};
    auto        lastSend = std::chrono::steady_clock::now();

    while (!received)
    {
        auto now = std::chrono::steady_clock::now();
        if (now - lastSend >= std::chrono::seconds(1))
        {
            ++ping.Counter;
            client.SendEvent(PingEventCode, ping);
            lastSend = now;
        }

        client.Service();
        std::this_thread::sleep_for(std::chrono::milliseconds(16));
    }

    Task<Result<void, DisconnectError>> disconnect = client.Disconnect();

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

    std::puts("ping received, session complete");
    return 0;
}

C

#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include "RealtimeCore/CAPI/RealtimeCAPI.h"

/* Platform sleep and millisecond clock, whatever your project already uses. */
void     SleepMs(int32_t ms);
uint64_t NowMs(void);

typedef struct { int32_t counter; } PingMessage;

#define PING_EVENT_CODE 1

int main(void)
{
    RealtimeEventQueueHandle queue  = realtime_event_queue_create();
    RealtimeHandle           client = realtime_create("your-app-id", "1.0", queue);

    if (client == NULL)
    {
        realtime_event_queue_destroy(queue);
        return 1;
    }

    int         inRoom   = 0;
    int         received = 0;
    int         failed   = 0;
    int         leaving  = 0;
    int         done     = 0;
    uint64_t    lastSend = 0;
    PingMessage ping     = {0};

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

    while (!done)
    {
        RealtimeEvent events[64];
        int32_t       count;

        realtime_service(client);

        while ((count = realtime_event_queue_poll(queue, events, 64)) > 0)
        {
            int32_t        blobLength = 0;
            const uint8_t* blob       = realtime_event_queue_blob(queue, &blobLength);

            for (int32_t i = 0; i < count; ++i)
            {
                const RealtimeEvent* e = &events[i];

                /* On a failed *Result the blob is unterminated UTF-8 message text. */
                if (e->type <= 15 && e->mode != 0)
                {
                    printf("operation %d failed (%d): %.*s\n",
                           e->type, e->mode, (int)e->blobLength,
                           (e->blobOffset >= 0) ? (const char*)(blob + e->blobOffset) : "");
                    failed = 1;
                    continue;
                }

                switch (e->type)
                {
                    case RealtimeEventType_ConnectResult:
                        realtime_join_random_or_create_room(client, 0, 1, 1, 0, 0, NULL, NULL, 0);
                        break;

                    case RealtimeEventType_JoinRandomOrCreateRoomResult:
                    {
                        char name[64] = {0};
                        realtime_room_name(client, name, (int32_t)sizeof(name));
                        printf("joined room %s, sending a ping every second\n", name);
                        inRoom = 1;
                        break;
                    }

                    case RealtimeEventType_Event:
                        if (e->counter == PING_EVENT_CODE &&
                            e->blobOffset >= 0 &&
                            e->blobLength == (int32_t)sizeof(PingMessage))
                        {
                            PingMessage message;
                            memcpy(&message, blob + e->blobOffset, sizeof(message));
                            printf("ping %d from player %u\n", message.counter, e->origin);
                            received = 1;
                        }
                        break;

                    case RealtimeEventType_DisconnectResult:
                        done = 1;
                        break;

                    default:
                        break;
                }
            }
        }

        realtime_event_queue_flush(queue);

        if (failed)
        {
            break;
        }

        /* Acting on the batch after the loop keeps the pump out of the handlers. */
        if (inRoom && !leaving)
        {
            uint64_t now = NowMs();
            if (now - lastSend >= 1000)
            {
                ++ping.counter;
                realtime_send_event(client, PING_EVENT_CODE,
                                    (const uint8_t*)&ping, (int32_t)sizeof(ping),
                                    1, 0, 0, NULL, 0, 0, 0, 0);
                lastSend = now;
            }
        }

        if (received && !leaving)
        {
            puts("ping received, session complete");
            realtime_disconnect(client);
            leaving = 1;
        }

        SleepMs(16);
    }

    realtime_destroy(client);
    realtime_event_queue_destroy(queue);
    return failed ? 1 : 0;
}

JavaScript

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

const PING_EVENT = 1;

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

const pingReceived = new Promise((resolve) => {
    client.onEvent(PING_EVENT, ({ senderNumber, data }) => {
        if (typeof data === 'number') {
            console.log(`ping ${data} from player ${senderNumber}`);
            resolve(data);
        }
    });
});

try {
    await client.connect();
    const room = await client.joinRandomOrCreateRoom();
    console.log(`joined room ${room.name}, sending a ping every second`);

    let counter = 0;
    const sendPing = () => client.sendEvent(PING_EVENT, ++counter);
    const timer = setInterval(sendPing, 1000);
    await pingReceived;
    clearInterval(timer);

    // A last ping answers the other instance in case it has not received one yet.
    // Leaving the room first delivers it, a disconnect drops anything not sent yet.
    sendPing();
    await client.leaveRoom();
    await client.disconnect();
    console.log('ping received, session complete');
} catch (error) {
    if (!isRealtimeError(error)) {
        throw error;
    }
    console.error(`session failed (${error.code}): ${error.message}`);
} finally {
    client.dispose();
}

Save the program as an ES module, for example quick-start.mjs, install the packages next to it with npm install @photonengine/realtime-core ws and run it twice with node quick-start.mjs (Node.js 22 or later).

Next Steps

Continue with Client and Service Loop, Asynchronous Operations and Callbacks and Subscriptions to understand the mechanics behind everything this guide used. From there, pick topics by need: Rooms and Players, Custom Properties and Custom Events.

Last updated on

Back to top