This document is about: QUANTUM 2
SWITCH TO

Reconnecting

The following documentation will shed light into all aspects of reconnecting into a running Quantum session. The basic flow is implemented in the demo menu sample that is shipped with the Quantum SDK.

The reconnection process consists of two main parts: How to get back into the Photon Realtime room and what to do the Quantum simulation?

Detecting Disconnects

  • The socket connection managed by Photon Realtime reports an error IConnectionCallbacks.OnDisconnected(DisconnectCause cause) with a different reason than DisconnectCause.DisconnectByClientLogic.
  • The Quantum server detects an error and disconnects the client, register to QuantumCallback.SubscribeManual<CallbackPluginDisconnect>(OnPluginDisconnect) to catch these cases.
  • Mobile app loses focus. In most cases the online session cannot be resumed and the client should perform a complete reconnection.

Simulation Network Errors To Test Disconnects

  • In the Unity Editor just hit Play to stop and start the application
  • LoadBalancingClient.SimulateConnectionLoss(true) will stop sending and receiving and will result in a DisconnectCause.ClientTimeout disconnection after 10 seconds.
  • LoadBalancingClient.LoadBalancingPeer.StopThread() immediately causes a disconnect DisconnectCause.None.
  • Use an external network tool (for example clumsy) to block the game server ports
Clumsy:
Filter  (udp.DstPort == 5056 or udp.SrcPort == 5056) or (tcp.DstPort == 4531 or tcp.SrcPort == 4531)
Drop    100%

Photon Realtime Fast Reconnect

To return to the room there are two ways:

  • Connecting through the name server (Photon Cloud) and, when arriving at the master server, joining the room as new Photon Actor (this is referred to as the default way)
  • Or reconnecting and rejoining

C#

LoadBalancingClient.ReconnectAndRejoin()

The method will try to directly connect to the game server and rejoin the room using cached authentication data, server address, room name, and so on. This information is cached on the LoadBalancingClient object.

Rejoining a room will assign the same Photon Actor id to the client.

Rejoining a room can also be performed after reconnecting or connecting to the master server:

C#

LoadBalancingClient.ReconnectToMaster()
// ..
public void IConnectionCallbacks.OnConnectedToMaster() {
    _client.OpReJoinRoom(roomName);
}

The rejoin operation only works if the client has not left the room, yet. (see next section PlayerTTL)

Caveat: Fast reconnect in Quantum version 2.0.x will only work when providing a local snapshot (StartParameters.InitialFrame and StartParameters.FrameData).

C#

var frameData = QuantumRunner.Default.Game.Frames.Verified.Serialize(DeterministicFrameSerializeMode.Blit);
var initialFrame = QuantumRunner.Default.Game.Frames.Verified.Number;

Requirements: PlayerTTL

Clients inside a room are generally active. They become inactive..

  • after a timeout of 10 seconds (by default) without answering the server
  • after calling LoadBalancingClient.Disonnect()
  • after calling LoadBalancingClient.OpLeaveRoom(true)

Now, there are two options:

A) the room runs the player-left logic (PlayerTTL is 0, default)

B) the player is marked inactive and kept in that state for PlayerTTL milliseconds before running the leave-room routine. The PlayerTTL value needs to be explicitly set in the RoomOptions when creating the room. Usually 20 seconds (20000 ms) is a good value to start with.

Fast Reconnect will allow clients back into their room when they are still active (before the 10 second timeout which OpJoinRoom() does not allow) and inactive (during the PlayerTTL) timeout.

When the client rejoined successfully IMatchmakingCallbacks.OnJoinedRoom() is called.

The demo menu sample implements two options to check out (UIConnect.OnReconnectClicked()).

  1. When PlayerTTL > 0 do ReconnectAndRejoin()
  2. When PlayerTTL == 0 do ReconnectToMaster() followed by OpJoinRom()

Requirements: RoomTTL (Waiting For Snapshots)

When the room detects that all clients are inactive it will close itself right away. To prevent that set RoomOptions.EmptyRoomTTL. This may be important when your room only has a small number of players and the probability that all of them have connection problems at the same time is given. Because there needs to be someone present to send a snapshot, this will only work reliably with custom server plugin and server-side snapshots.

Consider this case: In a two player online game one player is reconnecting or late-joining and waiting for a snapshot while the other player starts to have connections problems. The snapshot is never send and the player is stuck waiting.

The simple solution is to detect the game start timeout and disconnect the waiting player. This can be done by passing QuantumRunner.StartParameters.StartGameTimeoutInSeconds and checking QuantumRunner.HasGameStartTimedOut and finally implementing custom error handling (like giving player feedback and returning back to the lobby UI). As shown in UIGame.Update().

Requirements: Photon UserId

Photon Realtime: Lobby And Matchmaking | UserIds And Friends

In Photon, a player is identified using a unique UserID. To return to the room using rejoin the UserId has to be the same. It does not matter if the UserId originally has been set explicitly or by Photon.

Once in the room, the UserId does not matter for Quantum as it uses a different id to identify players (see section Quantum CliendId).

The Photon UserId can be..

  1. set by the client when connecting (AuthenticationValues.UserId)
  2. if left empty it is set by Photon
  3. or set by an external authentication service

To complete the background info about Photon ids:

  • Photon Actor Number (also referred to as actor id) identifies the player in his current room and is assigned per room and only valid in that context. Clients leaving and joining back a room will get a new actor id. A successful OpRejoinRoom() or ReconnectAndRejoin() will retain the actor id. Quantum provides a way to backtrace the actor ids and match them to a player Frame.PlayerToActorId(PlayerRef). But keep in mind, that they can change for player leaving and joining back (not rejoining).

  • Photon Nickname is a Photon client property that gets propagated in the rooms to know a bit more about the other clients. Has nothing to do with Quantum.

Possible Error: ReconnectAndRejoin Returns False

The current connection handler LoadBalancingClient is missing relevant data to perform a reconnect. Run your default connection sequence and try to join or rejoin the room in a regular way.

Also see section Reconnecting After App Restart.

Possible Error: PlayerTTL Ran Out

When rejoining past the PlayerTTL timeout ErrorCode.JoinFailedWithRejoinerNotFound is thrown.

This also means that we are connected to the MasterServer and can join the room with OpJoinRoom().

C#

public void (IMatchmakingCallbacks.)OnJoinRoomFailed(short returnCode, string message) {
    switch (returnCode) {
        case ErrorCode.JoinFailedWithRejoinerNotFound:
            // Try to join the room without rejoining
            _client.OpJoinRoom(new EnterRoomParams { RoomName = roomName });
    break;
}

Possible Error: Authentication Token Timeout

The authentication ticket expires after 1 hour. It will be refreshed automatically before running out in the course of the Quantum game session (Photon Realtime: Encryption | Token Refresh). If your general game sessions are long and you want to support reconnecting players after around 20 minutes you need to handle this error. Resolution is to restart the default connection routine and try to join back into the room.

C#

public void OnDisconnected(DisconnectCause cause) {
    switch (cause) {
        case DisconnectCause.AuthenticationTicketExpired:
        case DisconnectCause.InvalidAuthentication:
            // Restart with your default connection sequence
        break;

Possible Error: Connection Still Unavailable

Of course the connection can still be obstructed or other errors can occur. In each case a IConnectionCallbacks.OnDisconnected(DisconnectCause cause) is called.

Reconnecting After App Restart

The LoadBalancingClient object caches data (token) relevant to the rejoining operation and that information can get lost when restarting the application.

In that case the connection has to be restarted from scratch while reusing the same UserId, FixedRegion and AppVersion. When arriving at the master server either Rejoin() or Join() back into the room.

Due to the lost connection cache, rejoining may fail with ErrorCode.JoinFailedFoundActiveJoiner because the server did not register the disconnect, yet (10 sec timeout). In this case retry until rejoining worked or another error occurs.

The demo menu sample class ReconnectInformation demonstrates how relevant minimum viable reconnection data is saved and restored using Unity PlayerPrefs.

The reconnection process is applied inside UIConnect.OnReconnectClicked() and UIReconnecting.OnConnectedToMaster().

Saving the Photon UserId to PlayerPrefs can of course be replaced by custom authentication.

It is also possible to store and load a snapshot inside PlayerPrefs, which may be interesting for games with a very low player count. To store binary data in PlayerPrefs as a string use base64 en- and decoding.

Different Master Server

ReconnectAndRejoin() and ReconnectToMaster() both prevent a fringe case that would let the client end up on a different master server than before when connecting back via the cloud. Reasons are:

  • There are multiple cluster for one app
  • Master server has been replaced (rotated out)
  • Best region ping has a new result

Other Photon Realtime Topics

These features are not important for reconnection but are part of the demo menu sample so we might as well cover them here.

Best Region Summary

The class QuantumLoadBalancingClient wraps around Photon Realtime LoadBalancingClient. This is just for conveniently caching the best region ping results. After successfully connecting to the master server we store them into Unity PlayerPrefs and provide them for the next connection attempt via the AppSettings object to speed up connecting to the best region.

The region ping is forced from time to time but to be certain players are not stuck with a bad ping result it could be smart to implement invalidation so players are not stuck with a bad or wrong result forever (e.g. if ping is above threshold clear BestRegionSummary every other day). Also it could happen that players travel to other parts of the world where a new ping would be required to find the closest region.

AppVersion

The demo menu sample uses random matchmaking to match players. The AppVersion we supply with the AppSettings will group the player bases for the same AppId into separate groups. Players connecting to the same AppId and different AppVersions will not find each other at all.

This is useful when running multiple game versions live as well as during development to prevent other clients (that have a different code base and would instantly desync the game) to join a game running by a developer.

UIConnect.cs covers adding generic AppVersions and having a private key for uninterrupted testing. The private AppVersion string is generated for every workspace once (see PhotonPrivateAppVersion.cs). Every build that has been created from that workspace is able to match players with each other.

Further Readings

Reconnecting Into A Running Quantum Game

Quantum ClientId

The ClientId is a secret between the client and the server. Other clients never know it. It is passed when starting the QuantumRunner.

C#

public static QuantumRunner StartGame(String clientId, StartParameters param)

Independently of having joined as a new Photon room actor or having rejoined, reconnecting clients are identified by their ClientId and will be assigned to the same player index they previously had if the slot was not filled by another players in the meantime. In short: player must use the same ClientId when reconnecting.

Quantum will not let a client start the session while another active player with the same ClientId is inside the room and waits for the disconnect timeout (10 seconds):

DISCONNECTED: Error #5: Duplicate client id

This is why ReconnectAndRejoin() is required to recover from short term connection losses.

Further Readings

Restarting The Quantum Session

After a disconnect the QuantumRunner and QuantumSession are not usable any more and must be destroyed and recreated.

When the client either joined or re-joined back into the room that runs the Quantum game the QuantumRunner needs to be restarted. The simulation will be paused until the snapshot arrives from another client. Then will catch-up and sync to the most recent game time.

Rough outline:

  • detect disconnect, destroy QuantumRunner
  • reconnect and rejoin the room
  • re-start Quantum by calling QuantumRunner.Start()

To stop and destroy the QuantumSession call:

C#

QuantumRunner.ShutdownAll(true);

Only call this method with immediate:true when you are on the Unity main thread and never from inside a Quantum callback. Call with immediate:false or delay the call manually until it gets picked up from a Unity update call.

The demo menu sample demonstrates how starting a new game, late-joining or re-joining a running game require very similar procedures. In UIRoom.OnShowScreen() we detect that that game has already been started by evaluating the room properties and then immediately start the game.

EntityViews And UI

Late-joins and reconnection players put high demands on how flexible your game is constructed. It needs to support starting the game from any point in time and possibly reusing instantiated prefabs and UI as well as stopping and cleaning up the game at any possible moment. Side effects are high loading times, having unwanted VFX and animations in the new scene, being stuck in UI transitions, etc.

If you want to keep the EntityViewUpdater and the EntityViews alive to reuse them, they need to manually be stopped from being updated, re-match them with the new QuantumGame instance, subscribe to the new callbacks, etc.

On the other side the handling of Quantum is extremely simple: shutdown runner, start runner.

Events

The client will not receive previous events that were raised before the player joined or rejoined. The game view should be able to fully initialize/reset itself by polling the current state of the simulation and use future events/polling to keep itself updated.

SetPlayerData

Calling SetPlayerData() for reconnecting players is optional. It depends if your avatar setup logic in the simulation requires this.

StartParameters.QuitBehaviour

When the Quantum shutdown sequence is being executed (QuantumRunner.ShutdownAll) the QuantumNetworkCommunicator class will optionally perform room leave operations or disconnect the LoadBalancing client. Set to QuitBehaviour.None on the QuantumRunner.StartParameters to handle it yourself.

Late-Joining And Buddy-Snapshots

A Quantum game snapshot is a platform independent blob of data that contains the complete state of the game after a verified (all input has been received) tick. The Quantum simulation can be started from a snapshot and seamlessly continue from that state on.

A client can create its own snapshot when the simulation is still running (local snapshot), the snapshot can be requested from other clients (buddy snapshot) or it can be send down from a custom server plugin that runs the simulation.

Starting or restarting from snapshots is very handy and is provided turn-key by Quantum. Otherwise late-joining or reconnecting clients would have to start the game session from the very beginning and fast-forward through the input history send by the server which can render the client app useless until it caught up and also input history stored on the server is limited to ~10 minutes.

The buddy snapshot process is started automatically when any client is starting its QuantumRunner (no matter if the client is starting the session for the first time, late-joining or reconnecting). The session will be put into paused mode DeterministicSession.IsPaused and a snapshot will be requested. Successful late joins will log the following messages:

Waiting for snapshot. Clock paused.
Detected Resync. Verified tick: 6541

Buddy snapshots are requested for clients connecting 5 seconds after the initial start.

The server uses a load balancing mechanism to decide which client it will ask for a buddy snapshot to not overburden individual clients.

Errors during the snapshot process will be forwarded to the client using the Disconnect message (e.g. the snapshot waiting state will time out after 15 seconds):

Name Description
Error #13: Snapshot request failed For a late-joining or rejoining client when requesting a snapshot there is no other client in the room/game that can send a buddy snapshot.
Error #15: Snapshot request timed out For a late-joining or rejoining client a buddy snapshot has been requested, but the other clients did not answer in time (15 sec) or all other clients have left the room/game.

There are a few differences when starting from a snapshot during the game starting routines:

  • Instead of CallbackGameStarted the callback CallbackGameResynced is executed.
  • System.OnInit() is called before the snapshot is received.

Local Snapshots

As an optional reconnection strategy a local snapshot of the last verified tick can be saved and used when starting the new QuantumRunner. This works best when the anticipated time offline is small. Local snapshots are generally more bandwidth friendly and faster.

Guidelines

Quantum enforces tight limitations around the local snapshot acceptance timing, because starting from a snapshot that is too old can degrade the user experience.

By default local snapshots that are older than 10 seconds are not accepted by the server and instead a buddy-snapshot is requested. The process works transparently and from the clients perspective the only difference is the received snapshot age.

For games that have a low user count (e.g. 1 vs 1) the chance that there is no other client online that can provide a buddy snapshot is high. These types of games usually require to work with the EmptyRoomTTL value and Quantum prolongs the local snapshot acceptance time to EmptyRoomTTL but to a maximum of two minutes.

Workflow

  • Detect disconnect
  • Take snapshot
  • Shutdown QuantumRunner
  • Fast Photon Reconnect
  • restart Quantum with snapshot

Read through reconnecting sequence in the demo menu. UIGame.OnDisconnect creates a snapshot when the disconnect reason is other than the client disconnected itself. It uses a timeout of 10 seconds after which the snapshot is not used and a new one is requested from another client/server because the catching up time would become too long.

C#

_frameSnapshot = QuantumRunner.Default.Game.Frames.Verified.Serialize(DeterministicFrameSerializeMode.Blit);
_frameSnapshotNumber = QuantumRunner.Default.Game.Frames.Verified.Number;
_frameSnapshotTimeout = Time.time + 10.0f;

During the reconnecting in UIRoom.StartQuantumGame() the snapshot info is set as StartParameter.

C#

var param = new QuantumRunner.StartParameters {
    FrameData    = IsRejoining ? UIGame.Instance?.FrameSnapshot : null,
    InitialFrame = IsRejoining ? (UIGame.Instance?.FrameSnapshotNumber).Value : 0,
    // ...
}

When successful Quantum will log this with the requested tick number:

Resetting to tick 4316
Detected Resync. Verified tick: 4316
Back to top