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 thanDisconnectCause.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 aDisconnectCause.ClientTimeout
disconnection after 10 seconds.LoadBalancingClient.LoadBalancingPeer.StopThread()
immediately causes a disconnectDisconnectCause.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
andStartParameters.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()
).
- When PlayerTTL > 0 do ReconnectAndRejoin()
- 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..
- set by the client when connecting (
AuthenticationValues.UserId
) - if left empty it is set by Photon
- 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 playerFrame.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
- Photon Realtime: Analysing Disconnects | Quick Rejoin (ReconnectAndRejoin)
- Photon Realtime: Known Issues | Mobile Background Apps
- Photon Realtime: .NET Client API | LoadBalancingClient
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 callbackCallbackGameResynced
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
- Detecting Disconnects
- Photon Realtime Fast Reconnect
- Requirements: PlayerTTL
- Requirements: RoomTTL (Waiting For Snapshots)
- Requirements: Photon UserId
- Possible Error: ReconnectAndRejoin Returns False
- Possible Error: PlayerTTL Ran Out
- Possible Error: Authentication Token Timeout
- Possible Error: Connection Still Unavailable
- Reconnecting After App Restart
- Different Master Server
- Other Photon Realtime Topics
- Further Readings
- Reconnecting Into A Running Quantum Game