Realtime Intro

Photon Realtime is our base layer for multiplayer games and higher-level network solutions. It solves problems like matchmaking and fast communication with a scalable approach. It is used by games and our more specific multiplayer solutions PUN and Quantum.

The term Photon Realtime also wraps up our comprehensive framework of APIs, software tools and services and defines how the clients and servers interact with one another.

These pages are primarily the manual for the client-side Realtime APIs but will also provide overview of the structures that are involved.

Clients And Servers

The Photon Realtime API is the client-side software which will be directly used by games and apps. With it your apps Connect, JoinRandomRoom and RaiseEvent for example.

It is available in various languages for most of the popular platforms and engines (SDK Download Page) and pretty much all clients can interact with each other, no matter if iOS, Android, web, console or standalone.


All Photon Realtime clients connect to a sequence of dedicated servers, split by three distinct tasks: Authentication and regional distribution (Name Server), matchmaking (Master Server) and gameplay (Game Server). As those servers are handled by the Realtime API, you don't have to worry about them but it's good to have some background.

The Photon Cloud is a fully managed service, which provides worldwide hosting for Photon Realtime clients. A free Photon Account gives you access to it without obligation and we recommend to use the Photon Cloud while you get used to Photon Realtime.

Code Samples

Below are a few code samples to give you an idea of how the Realtime API is being used. Consider this an overiew but not a complete, working guide.

Use the Photon Cloud and skip setting up servers. You will need to set an AppId and region code (e.g. "eu" or "us") in the code. Here is the list of available regions.

Get your AppId from the Realtime Dashboard after free signup.

The C# snippets on this page are not meant for Unity. For everything related to Unity we recommend using PUN 2 the first-class Unity product. Besides, PUN 2 is built on top of the Photon Realtime Unity API. The PUN 2 package contains Photon Realtime Unity SDK.

Connect to Photon Cloud

The following code is a class that will connect to the Photon Cloud, if you fill in your appid.

C#

using System;
using System.Collections.Generic;
using System.Threading;
using Photon.Realtime;


class GameClass : IConnectionCallbacks
{
    private readonly LoadBalancingClient client = new LoadBalancingClient();
    private bool quit;

    ~GameClass()
    {
        this.client.Disconnect();
        this.client.RemoveCallbackTarget(this);
    }

    public void StartClient()
    {
        this.client.AddCallbackTarget(this);
        this.client.StateChanged += this.OnStateChange;

        this.client.ConnectUsingSettings(new AppSettings() { AppIdRealtime = "<your appid>", FixedRegion = "eu" });

        Thread t = new Thread(this.Loop);
        t.Start();

        Console.WriteLine("Running until key pressed.");
        Console.ReadKey();
        this.quit = true;
    }

    private void Loop(object state)
    {
        while (!this.quit)
        {
            this.client.Service();
            Thread.Sleep(33);
        }
    }

    private void OnStateChange(ClientState arg1, ClientState arg2)
    {
        Console.WriteLine(arg1 + " -> " + arg2);
    }

    #region IConnectionCallbacks

    public void OnConnectedToMaster()
    {
        Console.WriteLine("OnConnectedToMaster Server: " + this.client.LoadBalancingPeer.ServerIpAddress);
    }

    public void OnConnected()
    {
    }

    public void OnDisconnected(DisconnectCause cause)
    {
    }

    public void OnRegionListReceived(RegionHandler regionHandler)
    {
    }

    public void OnCustomAuthenticationResponse(Dictionary<string, object> data)
    {
    }

    public void OnCustomAuthenticationFailed(string debugMessage)
    {
    }

    #endregion
}

C++

class SampleNetworkLogic
{
public:
    SampleNetworkLogic(const ExitGames::Common::JString& appID, const ExitGames::Common::JString& appVersion);
    void connect(void);
    void disconnect(void);
    void run(void);
private:
    ExitGames::LoadBalancing::Client mLoadBalancingClient;
    Listener mListener; // your implementation of the ExitGames::LoadBalancing::Listener interface
    ExitGames::Common::Logger mLogger; // accessed by EGLOG()
};

SampleNetworkLogic::SampleNetworkLogic(const ExitGames::Common::JString& appID, const ExitGames::Common::JString& appVersion)
    : mLoadBalancingClient(mListener, appID, appVersion)
{
}

void SampleNetworkLogic::connect(void)
{
    // connect() is asynchronous - the actual result arrives in the Listener::connectReturn() or the Listener::connectionErrorReturn() callback
    if(!mLoadBalancingClient.connect())
        EGLOG(ExitGames::Common::DebugLevel::ERRORS, L"Could not connect.");
}

int main(void)
{
    static const ExitGames::Common::JString appID = L"<no-app-id>"; // set your app id here
    static const ExitGames::Common::JString appVersion = L"1.0";

    SampleNetworkLogic networkLogic(appID, appVersion);

    networkLogic.connect();

Connect to self-hosted Photon Server

Connecting to your self-hosted Photon Server does not require AppId or region. All you need is the server address and port to use with the appropriate connect method of your client.

You can read about the differences between Photon Cloud and Photon Server here".

Call Service

The LoadBalancing API is built to integrate well with your game logic. You can fine-control when you want to handle incoming messages and how often you send anything. Internally, messages in both directions are buffered until your game calls LoadBalancingClient.Service().

Games often use a game loop that calculates updates and then refreshes the screen. Call Service() 20 to 50 times per second depending on your game and network usage. If you don't call it, no "network progress" gets done at all.

Service covers two tasks:

  • Received events and data gets executed. This is done when you can handle the updates. As a sequence order is always kept intact, everything that the client receives is queued and ordered. Service internally calls DispatchIncomingCommands to do this task.
  • Outgoing data of your client is sent to the server. This includes acknowledgments (created in the background) which are important to keep the connection to the server. Service internally calls SendOutgoingCommands to do this task. Controlling the frequency of SendOutgoingCommands calls controls the number of packages you use to send a client's produced data.

C#

void GameLoop()
{
    while (!shouldExit)
    {
        this.loadBalancingClient.Service();
        Thread.Sleep(50); // wait for a few frames/milliseconds
    }
}

C++

void SampleNetworkLogic::run(void)
{
    mLoadBalancingClient.service(); // needs to be called regularly!
}

int main(void)
{
    static const ExitGames::Common::JString appID = L"<no-app-id>"; // set your app id here
    static const ExitGames::Common::JString appVersion = L"1.0";

    SampleNetworkLogic networkLogic(appID, appVersion);

    networkLogic.connect();

    while(!shouldExit)
    {
        networkLogic.run();
        SLEEP(100);
    }

Disconnect

When the application is quitting or when the user logs out do not forget to disconnect.

C#

using System.Collections.Generic;
using Photon.Realtime;

public class MyClient : IConnectionCallbacks
{
    private LoadBalancingClient loadBalancingClient;

    public MyClient()
    {
        this.loadBalancingClient = new LoadBalancingClient();
        this.SubscribeToCallbacks();
    }

    ~MyClient()
    {
        this.Disconnect();
        this.UnsubscribeFromCallbacks();
    }

    private void SubscribeToCallbacks()
    {
        this.loadBalancingClient.AddCallbackTarget(this);
    }

    private void UnsubscribeFromCallbacks()
    {
        this.loadBalancingClient.RemoveCallbackTarget(this);
    }

    void Disconnect()
    {
        if (this.loadBalancingClient.IsConnected)
        {
            this.loadBalancingClient.Disconnect();
        }
    }

    void IConnectionCallbacks.OnDisconnected(DisconnectCause cause)
    {
        switch (cause)
        {
            // ...

C++

void SampleNetworkLogic::disconnect(void)
{
    mLoadBalancingClient.disconnect(); // disconnect() is asynchronous - the actual result arrives in the Listener::disconnectReturn() callback
}

int main(void)
{
    static const ExitGames::Common::JString appID = L"<no-app-id>"; // set your app id here
    static const ExitGames::Common::JString appVersion = L"1.0";

    SampleNetworkLogic networkLogic(appID, appVersion);

    networkLogic.connect();

    while(!shouldExit)
    {
        networkLogic.run();
        SLEEP(100);
    }

    networkLogic.disconnect();
}

Matchmaking

Create a Game

To create a new room, aka game, invoke "Create Room" operation on your connected LoadBalancing client.

C#

using System.Collections.Generic;
using Photon.Realtime;

// we add IMatchmakingCallbacks interface implementation
public class MyClient : IConnectionCallbacks, IMatchmakingCallbacks
{
    private LoadBalancingClient loadBalancingClient;

    public MyClient()
    {
        this.loadBalancingClient = new LoadBalancingClient();
        this.SubscribeToCallbacks();
    }

    ~MyClient()
    {
        this.UnsubscribeFromCallbacks();
    }

    private void SubscribeToCallbacks()
    {
        this.loadBalancingClient.AddCallbackTarget(this);
    }

    private void UnsubscribeFromCallbacks()
    {
        this.loadBalancingClient.RemoveCallbackTarget(this);
    }
    
    void MyCreateRoom(string roomName, byte maxPlayers)
    {
        EnterRoomParams enterRoomParams = new EnterRoomParams();
        enterRoomParams.RoomName = roomName;
        enterRoomParams.RoomOptions = new RoomOptions();
        enterRoomParams.RoomOptions.MaxPlayers = maxPlayers;
        this.loadBalancingClient.OpCreateRoom(enterRoomParams);
    }

    void IMatchmakingCallbacks.OnJoinedRoom()
    {

    }

    // ...

C++

void createRoom(const ExitGames::Common::JString& roomName, nByte maxPlayers)
{
    mLoadBalancingClient.opCreateRoom(roomName, ExitGames::LoadBalancing::RoomOptions().setMaxPlayers(maxPlayers));
}

This sets the room name and amount of players that are allowed in the room. The client will enter the new room automatically. When using the "Create Room" operation the room will be created before joining, if it doesn't exist. Rooms exist until the last player leaves.

You can define "Custom Room Properties" to set shared values for the room when you create it. The custom room properties can be used (e.g.) to store the map name, level or the round duration. The keys of Custom Properties have to be strings. Of course, those values can be set and modified in the room as well.

You can select any of your custom properties to be shown also in the lobby, by setting the optional array with their names as "Custom Room Properties For Lobby". Properties showing in the lobby can be used for matchmaking and as a filter for random matches.

Read more about matchmaking in our guide here.

Find a Game

Clients join games by name or ask Photon to find a perfect match.

Find rooms ...

  • Random: Matches players randomly. Optionally fills rooms or distributes players evenly.
  • Parameterized: Customize random matching by defining lobby properties as filters.
  • Private: Hide rooms and players can only join if they know the room name.
  • Listing: The lobby lists visible rooms to let players pick and join manually.

C#

using System.Collections.Generic;
using ExitGames.Client.Photon;
using Photon.Realtime;

// we add IMatchmakingCallbacks interface implementation
public class MyClient : IConnectionCallbacks, IMatchmakingCallbacks
{
    private LoadBalancingClient loadBalancingClient;

    public MyClient()
    {
        this.loadBalancingClient = new LoadBalancingClient();
        this.SubscribeToCallbacks();
    }

    ~MyClient()
    {
        this.UnsubscribeFromCallbacks();
    }

    private void SubscribeToCallbacks()
    {
        this.loadBalancingClient.AddCallbackTarget(this);
    }

    private void UnsubscribeFromCallbacks()
    {
        this.loadBalancingClient.RemoveCallbackTarget(this);
    }

    void JoinRandomRoomByMap(byte maxPlayers, int mapIndex)
    {
        // join random rooms easily, filtering for specific room properties, if needed
        Hashtable expectedCustomRoomProperties = new Hashtable();

        // custom props can have any name but the key must be string
        expectedCustomRoomProperties["map"] = mapIndex;

        // joining a random room with the map we selected before
        this.loadBalancingClient.OpJoinRandomRoom(
                new OpJoinRandomRoomParams
                {
                    ExpectedCustomRoomProperties = expectedCustomRoomProperties,
                    ExpectedMaxPlayers = maxPlayers
                }
            );
    }

    void IMatchmakingCallbacks.OnJoinRoomFailed(short returnCode, string message)
    {
        if (returnCode == ErrorCode.NoRandomMatchFound)
        {
            // no match found, try another filter or create a room
        }
        // ...

C++

// join random rooms easily, filtering for specific room properties, if needed
ExitGames::Common::Hashtable expectedCustomRoomProperties;

// custom props can have any name but the key must be string
expectedCustomRoomProperties.put(L"map", 1);

// joining a random room with the map we selected before
mLoadBalancingClient.opJoinRandomRoom(expectedCustomRoomProperties);

Persist Games

With Photon Realtime, rooms' data can be saved and loaded easily. You need to set up webhooks to hook up Photon Cloud with an external web server.

Once set up, room states will be saved automatically for you. To rejoin rooms:

C#

this.loadBalancingClient.OpReJoinRoom(savedRoomName);

C++

mLoadBalancingClient.opJoinRoom(savedRoomName, true);

This feature makes asynchronous matchmaking and gameplay possible.

Read more about how to do this in our Persistence Guide.

Gameplay

Sending Events

Whatever happens on one client can be sent as an event to update everyone in the same room.

Update your players with stats, positions or your current turn. Photon will send it as fast as possible (with optional reliability).

  • Send messages/events: Send any type of data to other players.
  • Player/Room properties: Photon updates and syncs these, even to players who join later.

C#

byte eventCode = 1; // make up event codes at will
Hashtable evData = new Hashtable(); // put your data into a key-value hashtable
this.loadBalancingClient.OpRaiseEvent(eventCode, evData, RaiseEventOptions.Default, SendOptions.SendReliable);

C++

nByte eventCode = 1; // use distinct event codes to distinguish between different types of events (for example 'move', 'shoot', etc.)
ExitGames::Common::Hashtable evData; // organize your payload data in any way you like as long as it is supported by Photons serialization
bool sendReliable = false; // send something reliable if it has to arrive everywhere
mLoadBalancingClient.opRaiseEvent(sendReliable, evData, eventCode);

Your event codes should stay below 200. Each code should define the type of event and the content it carries.

The event data in the example above is a Hashtable. It can be a byte[] or any data type supported by Photon's serialization (a string, float[], etc.). See Serialization in Photon for more information.

Receiving Events

Whenever an event is dispatched a handler is called. An example is shown below.

C#

using System.Collections.Generic;
using ExitGames.Client.Photon;
using Photon.Realtime;

// we add IOnEventCallback interface implementation
public class MyClient : IConnectionCallbacks, IMatchmakingCallabacks, IOnEventCallback
{
    private LoadBalancingClient loadBalancingClient;

    public MyClient()
    {
        this.loadBalancingClient = new LoadBalancingClient();
        this.SubscribeToCallbacks();
    }

    ~MyClient()
    {
        this.UnsubscribeFromCallbacks();
    }

    private void SubscribeToCallbacks()
    {
        this.loadBalancingClient.AddCallbackTarget(this);
    }

    private void UnsubscribeFromCallbacks()
    {
        this.loadBalancingClient.RemoveCallbackTarget(this);
    }

    void IOnEventCallback.OnEvent(EventData photonEvent)
    {
        // we have defined two event codes, let's determine what to do
        switch (photonEvent.Code)
        {
            case 1:
                // do something
                break;
            case 2:
                // do something else
                break;
        }
    }

    // ...

C++

void NetworkLogic::customEventAction(int playerNr, nByte eventCode, const ExitGames::Common::Object& eventContent)
{
    // logging the string representation of the eventContent can be really useful for debugging, but use with care: for big events this might get expensive
    EGLOG(ExitGames::Common::DebugLevel::ALL, L"an event of type %d from player Nr %d with the following content has just arrived: %ls", eventCode, playerNr, eventContent.toString(true).cstr());

    switch(eventCode)
    {
    case 1:
        {
            // you can access the content as a copy (might be a bit expensive for really big data constructs)
            ExitGames::Common::Hashtable content = ExitGames::Common::ValueObject<ExitGames::Common::Hashtable>(eventContent).getDataCopy();
            // or you access it by address (it will become invalid as soon as this function returns, so (any part of the) data that you need to continue having access to later on needs to be copied)
            ExitGames::Common::Hashtable* pContent = ExitGames::Common::ValueObject<ExitGames::Common::Hashtable>(eventContent).getDataAddress();
        }
        break;
    case 2:
        {
            // of course, the payload does not need to be a Hashtable - how about just sending around for example a plain 64bit integer?
            long long content = ExitGames::Common::ValueObject<long long>(eventContent).getDataCopy();
        }
        break;
    case 3:
        {
            // or an array of floats?
            float* pContent = ExitGames::Common::ValueObject<float*>(eventContent).getDataCopy();
            float** ppContent = ExitGames::Common::ValueObject<float*>(eventContent).getDataAddress();
            short contentElementCount = *ExitGames::Common::ValueObject<float*>(eventContent).getSizes();
            // when calling getDataCopy() on Objects that hold an array as payload, then you must deallocate the copy of the array yourself using deallocateArray()!
            ExitGames::Common::MemoryManagement::deallocateArray(pContent);
        }
        break;
    default:
        {
            // have a look at demo_typeSupport inside the C++ client SDKs for example code on how to send and receive more fancy data types
        }
        break;
    }
}

Each event carries its code and data in the way your client sent them. Your application knows which content to expect by the code passed (see above).

For an up-to-date list of default event codes look for the event codes constants in your SDK, e.g. within ExitGames.Client.Photon.LoadBalancing.EventCode for C#.

Custom or Authoritative Server Logic

As is, without authoritative logic, Photon Cloud products already allow for a broad range of game types.

  • First Person Shooters
  • Racing Games
  • Minecraft type of games
  • Casual real-time games
  • Asynchronous and synchronous games
  • ...

Use Photon Server or Photon Plugins to implement your own custom logic.

Back to top