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

Fill a ClientConstructOptions and construct a RealtimeClient from it. AppId is the only required field, and setting AppVersion is recommended because clients with different versions do not see each other during matchmaking.

All strings in the API are UTF-8 u8 strings. Write literals with the PHOTON_STR("...") macro; the Strings page covers conversions to and from other string types.

The following is all it takes to create a client.

C++
C

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. */

Drive the Client

Call Service() on the client often, ideally every frame, to send and receive network messages, fire subscribed callbacks and to advance pending async operations.

A coroutine co_awaits an operation, and the frame loop keeps calling Service() until the coroutine's task completes.

C++
C

C++

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

// in the game loop:
Task<Result<void>> 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 == RT_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. */

Connect

Connect() starts the connection handshake and returns a Task<Result<void>>. Keep driving Service() until the task is ready, then check the Result for success or the error that occurred. More details about Task and Result can be found in the Asynchronous Operations page.

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

Inside a coroutine the same flow reads sequentially, without any polling.

C++
C

C++

Task<Result<void>> ConnectToCloud(RealtimeClient& client)
{
    Result<void> 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 == RT_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. */

Join a Room

JoinRandomOrCreateRoom(createOptions, matchmakingOptions) is the one-call path into a shared room: it joins a random matching room, or creates a new one when none exists. Both option structs have sensible defaults, so the parameterless call is enough for a first test.

Chained with AndThen, connect and room entry become one task that resolves to the joined room.

C++
C

C++

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

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

Result<MutableRoomView> 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 RT_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 RT_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. */

Send and Receive Events

SubscribeEvent(callback) registers a handler for the events the other players in the room send. To send, SendEvent<T>(code, value) transmits any trivially-copyable struct; receivers identify it by the event code and copy the bytes back out.

The snippet registers a receiver and sends a small struct and triggers the send once per second from your game loop.

C++
C

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 == RT_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               */
                    0);      /* cacheSliceIndex       */

/* 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. */

Full Example

The complete program below runs one minimal session: it constructs a client, connects, joins or creates a room, then sends a ping event every second until it receives one back, and disconnects. Run the compiled 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

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<MutableRoomView>> EnterRoom(RealtimeClient& client)
{
    Result<void> connected = co_await client.Connect();
    if (connected.IsErr())
    {
        co_return Result<MutableRoomView>::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<MutableRoomView>> entered = EnterRoom(client);

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

    Result<MutableRoomView> 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>> 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>

/* Declarations of the entry points this program uses. RealtimeCAPI.h declares
   the same signatures, but with extern "C", so it belongs to a C++ translation
   unit; from C, declare what you call. See the C API page. */
typedef void* RealtimeHandle;
typedef void* RealtimeEventQueueHandle;

#pragma pack(push, 4)
typedef struct
{
    int32_t  type;
    uint32_t origin;
    uint32_t counter;
    int32_t  mode;
    int32_t  blobOffset;
    int32_t  blobLength;
    int32_t  _reserved;
} RealtimeEvent;
#pragma pack(pop)

enum
{
    RT_ConnectResult                = 0,
    RT_DisconnectResult             = 1,
    RT_JoinRandomOrCreateRoomResult = 14,
    RT_Event                        = 108,
};

RealtimeEventQueueHandle realtime_event_queue_create(void);
void                     realtime_event_queue_destroy(RealtimeEventQueueHandle);
int32_t                  realtime_event_queue_poll(RealtimeEventQueueHandle, RealtimeEvent*, int32_t);
const uint8_t*           realtime_event_queue_blob(RealtimeEventQueueHandle, int32_t*);
void                     realtime_event_queue_flush(RealtimeEventQueueHandle);

RealtimeHandle realtime_create(const char*, const char*, RealtimeEventQueueHandle);
void           realtime_destroy(RealtimeHandle);
void           realtime_service(RealtimeHandle);
void           realtime_connect(RealtimeHandle, const char*, const char*, int32_t, const char*, const char*);
void           realtime_disconnect(RealtimeHandle);
void           realtime_join_random_or_create_room(RealtimeHandle, uint8_t, int32_t, int32_t,
                                                   int32_t, int32_t, const char*, const uint8_t*, int32_t);
int32_t        realtime_room_name(RealtimeHandle, char*, int32_t);
int32_t        realtime_send_event(RealtimeHandle, uint8_t, const uint8_t*, int32_t, int32_t, uint8_t,
                                   int32_t, const int32_t*, int32_t, uint8_t, int32_t, int32_t, int32_t);

/* 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 RT_ConnectResult:
                        realtime_join_random_or_create_room(client, 0, 1, 1, 0, 0, NULL, NULL, 0);
                        break;

                    case RT_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 RT_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 RT_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, 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;
}

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.

Back to top