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++

using namespace RealtimeCore::Matchmaking;

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

RealtimeClient client(options);

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++

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));
}

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++

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;
}

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++

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());
}

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++

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);

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++

#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;
}

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