Appearance
Connection Management
This guide gives an overview of how the connection is formed and maintained within the Metaplay SDK and how to handle the various error states in your game.
Appearance
This guide gives an overview of how the connection is formed and maintained within the Metaplay SDK and how to handle the various error states in your game.
MetaplayClient ​The SDK-provided MetaplayClient handles most of the details of game connection management, leaving just the high-level session lifecycle management for the game to implement.
The primary interactions between the game and MetaplayClient are:
MetaplayClient.Initialize() to initialize the client SDK.MetaplayClient.ConnectAsync() to connect to the backend and retrieves a MetaplaySession.MetaplaySession.WaitForSessionEndAsync() to detect session ending.Using callbacks instead of async methods
MetaplaySDK also supports callback-based lifecycle management. By passing IMetaplayLifecycleDelegate to MetaplayClient.Initialize(), the delegate gets notified of session lifecycle events (such as session start and end). This way, your game "loop" can be merely calling MetaplayClient.Connect(), and then reacting to events via IMetaplayLifecycleDelegate.
In a typical always-online game, the session lifecycle works roughly like this:
MetaplayClient, and then tells it to start a connection. The game is initially in a loading screen or a similar starting scene. MetaplayClient.Initialize, you can set MetaplayClientOptions.ConnectionOptions.ConnectionConfig to configure various time limits and other connection options.MetaplayClient informs the game about session lifecycle events, and the game reacts appropriately: MetaplayClient to start a new connection.This is how the Metaplay sample projects like Idler and HelloWorld behave. This behavior is implemented in their ApplicationStateManager.cs files, which can be used as a reference.
The MetaplaySDK informs the game of any connection failures by emitting a ConnectionLostEvent:
Connection failures can happen three different ways: Failing to connect, failing to start the session and finally losing connection during a session.
MetaplayClient.ConnectAsync() throws FailedToStartSessionException which contains ConnectionLostEvent.MetaplaySession.SessionStartFailed() returns a ConnectionLostEvent.MetaplaySession.WaitForSessionEndAsync() returns a ConnectionLostEvent.async Task SessionLoop()
{
while (true)
{
// Show loading screen...
// Connect to the server
MetaplaySession session;
try
{
session = await MetaplayClient.ConnectAsync();
}
catch (FailedToStartSessionException ex)
{
// Failed to establish a session with the server.
ConnectionLostEvent lostEvent = ex.Failure;
if (lostEvent.Reason == ConnectionLostReason.ClientVersionTooOld)
await ShowUpdateNeeded();
else
await ShowConnectionErrorPopup(lostEvent);
continue;
}
// Session established
try
{
// Setup game
// await SceneManager.LoadSceneAsync("Game Scene");
session.SessionStartComplete();
}
catch (Exception ex)
{
// Game failed to load resources
ConnectionLostEvent sessionStartFailed = session.SessionStartFailed(ex);
await ShowConnectionErrorPopup(sessionStartFailed);
continue;
}
// Wait until session ends
ConnectionLostEvent connectionLost = await session.WaitForSessionEndAsync();
if (connectionLost.AutoReconnectRecommended)
{
// Auto-reconnect
}
else
{
await ShowConnectionErrorPopup(connectionLost);
}
}The IMetaplayLifecycleDelegate methods OnSessionLost and OnFailedToStartSession are called on connection failures. They have a ConnectionLostEvent parameter which describes the connection error.
void IMetaplayLifecycleDelegate.OnSessionLost(ConnectionLostEvent connectionLost)
{
// Ongoing session was lost
if (connectionLost.AutoReconnectRecommended)
SwitchToState(ApplicationState.Initializing);
else
ShowConnectionErrorPopup(connectionLost);
}
void IMetaplayLifecycleDelegate.OnFailedToStartSession(ConnectionLostEvent connectionLost)
{
// Failed to establish a session with the server.
if (connectionLost.Reason == ConnectionLostReason.ClientVersionTooOld)
ShowUpdateNeeded();
else
ShowConnectionErrorPopup(connectionLost);
}The game code can use this parameter to:
AutoReconnectRecommended), or whether to direct the player to download a client update (on ConnectionLostReason.ClientVersionTooOld).Reason member is an enum containing the error reason on a granularity appropriate for the player. This should be localized to a simple description - to get started, EnglishLocalizedReason contains an English description. TechnicalErrorCode to the player - although it's not directly useful to the player, the player can give the code to customer support and it can be helpful for diagnosing the problem using the List of Connection Error Codes.MetaplayClient also routes the error information to the IMetaplayClientAnalyticsDelegate. See Handling Events in the Client for how to integrate client-side analytics.
During a game session, consider also checking MetaplayClient.ConnectionHealth and showing an "unhealthy connection" indicator to the player if it returns ConnectionHealth.Unhealthy. Unhealthy means the connection is of degraded quality but the SDK will still try to recover it for a short time. The indicator provides an early warning to the player that the connection might be lost soon and the current game actions might not reach the server.
Metaplay handles the Unity application being paused and put to the background automatically. If the game spends too long in a paused state, the connection is terminated. By default, this time limit is ConnectionConfig.MaxSessionRetainingPauseDuration.
However, there might be special cases where we expect the application to be paused temporarily, but longer than the Config.MaxSessionRetainingPauseDuration. Such cases can be, for example, when the application shows an Ad, opens a third-party customer service window, or displays Terms-of-Service or Privacy Policy using the device browser. In these cases, the developer should call MetaplaySDK.OnApplicationAboutToBePaused(reason, TimeSpan) just before the expected pause begins. This hints the MetaplaySDK to extend MaxSessionRetainingPauseDuration temporarily and helps the game keep the session alive during the pause. Note that session continuation cannot be guaranteed as background applications may be terminated on mobile devices at any time.
In the case that the same player account connects from a second device while they already had an active session on another device, the old session is forcefully terminated with the code 1500 (force_terminated_received_another_connection) and the new connection replaces the old one. The client should handle the terminated session with an indication to the player why they were disconnected. Optionally, a reconnect button can be shown to allow easily switching back to the original device.
This table describes the meanings of the possible values of ConnectionLostEvent.TechnicalErrorCode.
Fine-grained errors
In the C# code, if you need fine-grained information about the type of error, please examine ConnectionLostEvent's TechnicalError and ConnectionStats instead of relying on TechnicalErrorCode. TechnicalErrorCode (and TechnicalErrorString) is meant for providing a succinct and opaque code to the player for customer support purposes. Its values are based on SDK implementation details and might change in SDK updates. Therefore it is safer to use the properly-typed TechnicalError instead.
| TechnicalErrorCode | TechnicalErrorString | Player-Facing Reason | Description |
|---|---|---|---|
| 1000 | closed_session_init_done | ConnectionLost | Connection was lost (during session). |
| 1001 | closed_handshake_done | CouldNotConnect | Connection was lost (before session had been started). |
| 1002 | closed_handshake_not_done | CouldNotConnect | Connection was lost (before any packets had been received from the server). |
| 1003 | closed_probe_failed | NoInternetConnection | Connection was lost (no internet connection). |
| 1004 | closed_probe_missing | CouldNotConnect | Connection was lost (internet connectivity not known). |
| 1100 | timeout_connect_session_init_done | CouldNotConnect | Reconnection attempt timed out (during session). |
| 1101 | timeout_connect_handshake_done | CouldNotConnect | Reconnection attempt timed out (during session). |
| 1102 | timeout_connect_handshake_not_done | CouldNotConnect | Connection attempt timed out. |
| 1103 | timeout_connect_probe_failed | NoInternetConnection | Connection attempt timed out (no internet connection). |
| 1104 | timeout_connect_probe_missing | CouldNotConnect | Connection attempt timed out (internet connectivity not known). |
| 1112 | timeout_resource_fetch | CouldNotConnect | Resource download timed out. |
| 1113 | timeout_resource_fetch_probe_failed | NoInternetConnection | Resource download timed out (no internet connection). |
| 1114 | timeout_resource_fetch_probe_missing | CouldNotConnect | Resource download timed out (internet connectivity not known). |
| 1120 | timeout_stream_session_init_done | ConnectionLost | Connection timed out (during session). |
| 1121 | timeout_stream_handshake_done | ConnectionLost | Connection timed out (before session had been started). |
| 1122 | timeout_stream_handshake_not_done | ConnectionLost | Connection timed out (before any packets had been received from the server). |
| 1190 | timeout_unhandled | CouldNotConnect | Unknown kind of connection timeout. |
| 1200 | cluster_starting | ServerMaintenance | Backend is starting up and does not yet accept connections. |
| 1201 | cluster_shutting_down | ServerMaintenance | Backend is shutting down and does not accept connections anymore. |
| 1209 | cluster_not_ready_unhandled | ServerMaintenance | Backend is refusing connections for an unknown reason. |
| 1210 | cluster_node_lost | ConnectionLost | Backend server for the session is temporarily not available. |
| 1302 | resource_fetch_failed | CouldNotConnect | Failed to download resources. |
| 1303 | resource_fetch_failed_probe_failed | NoInternetConnection | Failed to download resources (no internet connection). |
| 1304 | resource_fetch_failed_probe_missing | CouldNotConnect | Failed to download resources (internet connectivity not known). |
| 1310 | resource_activation_failed | InternalError | Failed to apply resources. Likely a game config deserialization failure. |
| 1390 | resource_load_failed_unhandled | InternalError | Unknown kind of resource loading failure. |
| 1400 | failed_to_resume_session | ConnectionLost | Failed to resume a game session after a disconnect. Likely the session has already expired, or another device connected to the same player account. |
| 1500 | force_terminated_received_another_connection | ConnectionLost | Another client connected to the same player account. |
| 1501 | force_terminated_kicked_by_admin_action | ConnectionLost | A game admin performed an action that requires the client to reconnect. |
| 1502 | force_terminated_internal_server_error | InternalError | An internal server error occurred. |
| 1503 | force_terminated_unknown | InternalError | An unknown server error occurred. |
| 1504 | force_terminated_client_time_too_far_behind | ConnectionLost | The client-controlled PlayerModel timeline fell too far behind the real time. |
| 1505 | force_terminated_client_time_too_far_ahead | InternalError | The client-controlled PlayerModel timeline advanced too far beyond the real time. |
| 1506 | force_terminated_session_too_long | ConnectionLost | Maximum game session duration was exceeded (configured in server option Session:MaximumSessionLength). |
| 1507 | force_terminated_player_banned | PlayerIsBanned | The player got banned during the game session. |
| 1508 | force_terminated_maintenance_mode_started | ServerMaintenance | Backend maintenance mode started during the game session. |
| 1509 | force_terminated_pause_deadline_exceeded | ConnectionLost | The client was paused for too long. |
| 1510 | force_terminated_game_config_updated | ServerMaintenance | The game config has been updated. |
| 1511 | force_terminated_logic_version_updated | ClientVersionTooOld | The logic version of this player has been updated. |
| 1550 | force_terminated_unhandled | InternalError | The server terminated the session for an unknown reason. |
| 1599 | force_terminated_unhandled_invalid | InternalError | The server terminated the session for an unknown reason. |
| 1600 | protocol_error_unexpected_login_message | InternalError | The client detected a connection protocol violation by the server. |
| 1610 | protocol_error_missing_server_hello | InternalError | The client detected a connection protocol violation by the server. |
| 1620 | protocol_error_session_start_failed | InternalError | The server failed to initialize the session. Internal server error. |
| 1630 | protocol_error_session_protocol_error | InternalError | The client detected a connection protocol violation by the server. |
| 1690 | protocol_error_unhandled | InternalError | The client detected a connection protocol violation by the server. |
| 1700 | session_lost_in_background | ConnectionLost | The connection was lost while the client was paused. |
| 1800 | transport_watchdog_exceeded_session_init_done | InternalError | A connection watchdog timer was triggered. Internal client error. |
| 1801 | transport_watchdog_exceeded_handshake_done | InternalError | A connection watchdog timer was triggered. Internal client error. |
| 1802 | transport_watchdog_exceeded_handshake_not_done | InternalError | A connection watchdog timer was triggered. Internal client error. |
| 1803 | transport_watchdog_exceeded_probe_failed | InternalError | A connection watchdog timer was triggered. Internal client error. |
| 1804 | transport_watchdog_exceeded_probe_missing | InternalError | A connection watchdog timer was triggered. Internal client error. |
| 1810 | resetup_watchdog_exceeded | InternalError | A connection watchdog timer was triggered. Internal client error. |
| 1890 | connection_watchdog_exceeded_unhandled | InternalError | A connection watchdog timer was triggered. Internal client error. |
| 2000 | tls_error_unknown | CouldNotConnect | TLS error. |
| 2001 | tls_error_not_authenticated | CouldNotConnect | TLS error. |
| 2002 | tls_error_failure_while_authenticating | CouldNotConnect | TLS error. |
| 2003 | tls_error_not_encrypted | CouldNotConnect | TLS error. |
| 2009 | tls_error_unhandled | CouldNotConnect | TLS error. |
| 2100 | wire_protocol_version_client_too_old | ClientVersionTooOld | The client is too old for the server. |
| 2101 | wire_protocol_version_client_too_new | ServerMaintenance | The client is too new for the server. |
| 2109 | wire_protocol_version_invalid_mismatch | ServerMaintenance | Unexpected client-server version mismatch. |
| 2200 | invalid_game_magic | InternalError | The server reported a mismatching "game magic" code. The client likely connected to something that isn't a Metaplay game server, or belongs to a different game. |
| 2201 | project_id_mismatch | InternalError | The server reported a mismatching "project ID". The client likely connected to a server that belongs to a different game. |
| 2300 | in_maintenance | ServerMaintenance | Backend is in maintenance. |
| 2400 | logic_version_client_too_old | ClientVersionTooOld | The client is too old for the server. |
| 2401 | logic_version_client_too_new | ServerMaintenance | The client is too new for the server. |
| 2402 | logic_version_invalid_mismatch | ServerMaintenance | Unexpected client-server version mismatch. |
| 2500 | commit_id_mismatch | InternalError | The client and the server have mismatching commit ids. This is only reported if the game has been configured to check for matching commit ids. |
| 2600 | wire_format_error_session_init_done | InternalError | The client failed to parse a packet that was sent by the server. |
| 2601 | wire_format_error_handshake_done | InternalError | The client failed to parse a packet that was sent by the server. |
| 2602 | wire_format_error_handshake_not_done | InternalError | The client failed to parse a packet that was sent by the server. |
| 2700 | no_network_connectivity | NoInternetConnection | No internet connection. |
| 2800 | device_storage_write_error | DeviceLocalStorageError | Could not write account credentials to the client device's storage. |
| 2801 | client_side_connection_error | InternalError | Unexpected client-side connection management error. |
| 2802 | credentials_expired_error | CredentialsExpiredOrInvalid | Provided login credentials were expired or otherwise invalid, used should re-login. |
| 2900 | player_is_banned | PlayerIsBanned | The player is banned. |
| 3000 | player_deserialization_failure | InternalError | The server-side persisted player state failed to be deserialized. |
| 3100 | login_protocol_version_client_too_old | ClientVersionTooOld | The client is too old for the server. |
| 3101 | login_protocol_version_client_too_new | ServerMaintenance | The client is too new for the server. |
| 3102 | login_protocol_version_invalid_mismatch | ServerMaintenance | Unexpected client-server version mismatch. |
| 3200 | direct_connection_lost_on_client | ConnectionLost | The direct connection was lost. |
| 3201 | direct_connection_lost_on_server | ConnectionLost | The server reported the direct connection was lost. |
| 3200 | client_patch_version_too_old | ClientVersionTooOld | The client is too old for the server. |
| 3300 | login_rejected | CredentialsExpiredOrInvalid | Provided login credentials were not accepted. |
| 8000 | social_authentication_force_reconnect | ConnectionLost | The client chose to switch to another player account via an external authentication mechanism (e.g. Facebook Login), and needs to reconnect to access the new player account. |
| 8100 | player_checksum_mismatch | InternalError | A player state mismatch was detected between the client and the server. |
| 8200 | client_terminated_connection | ConnectionLost | The client terminated the connection due to the usage of a debug/development utility which requires closing the ongoing session. |
| 8201 | client_closed_connection | ConnectionLost | The client terminated the connection by calling Connection.Close() |
| 9000 | unknown | InternalError | Unknown error. |
| 9100 | unhandled | InternalError | Unknown error. |
MetaplayConnection Directly ​INFO
You likely won't need to manage MetaplayConnection directly if your game uses the SDK-provided MetaplayClient class, as described in Managing the Session Lifecycle with MetaplayClient. New games are intended to use MetaplayClient, and this section is here mostly to support legacy integrations.
The network connection between the client and the Game Backend is managed by the MetaplayConnection class. The aforementioned class handles credential management, connection lifecycle, reconnection attempts, session config management, timeouts, connection health assessment, and other networking-related tasks.
MetaplayConnection exposes its state as follows:
|
| Close()
V
+---------------+
| Not Connected | (initial state)
+-------+-------+
|
| Connect()
|
V
+----------------+
| Connecting |
+-------+----+---+
| |
| '--------.
V |
+---------------+ |
| Connected | |
+-------+-------+ |
| |
<DisconnectedFromServer> | .---------'
| |
V V
+---------------+
| Error |
+---------------+💡 Pro tip
State transitions might not be observed in graph order as more than one transition might occur in a single update. However, MetaplayConnection.State does not change during a single frame, unless explicitly changed. In a frame, state transitions are performed before any Unity Update() method is called.
To integrate connection state management, the game will need to:
Connect(). In always-online games; this happens very early in the app lifecycle, and is normally done in MetaplayClient.INFO
In the Example Idler Project, this is handled by setting the calling GameObject's (ApplicationStateManager) Execution Order to be the last in the project. In this project, the state changes, such as a loss of connection, cause a switch of the Unity Scene. As the scene changes happen after the end of the frame, it is natural to apply game state changes also at the end of the frame.
Connected and Game Logic Models to become available before showing the game view. The Model states are delivered in the SessionProtocol.SessionStartSuccess message which the game must process. Normally this waiting happens during a loading screen.Connected State if Connected.IsHealthy is false.Error states.MetaplayConnection's Connection Errors ​A connection might fail or be interrupted for a wide variety of reasons. A user might walk out of WiFi range, or the backend might be running an incompatible version. In the case of lost WiFi, simply reconnecting in a moment will most likely succeed. In the case of an incompatible backend version, recovery is unlikely. We aim to make handling these various cases in your game as simple as possible.
💡 Pro tip
You can check Connected.IsHealthy for an early warning that connection quality has been degraded and might be lost soon.
To make reacting to these errors easier, Metaplay networking errors are divided into transient and terminal errors:
Hence, a simple implementation would be:
Update()
{
...
if (connectionState is ConnectionStates.TransientError)
{
// Restart the game
ClearGameModels();
Connection.Reconnect();
GoToLoadingScreen();
}
else if (connectionState is ConnectionStates.TerminalError)
{
// Tell user something went wrong
ShowErrorMessage( ... );
}
}Additionally, a DisconnectedFromServer pseudo-message is emitted just after leaving the Connected state. It can be used to inspect the error before it is handled. For the duration of the message handler, the MetaplayConnection.State is set to the error state.
💡 Pro tip
You can fine-tune timeout durations and other settings by modifying ConnectionConfig.Config in your app start scene.
MetaplayConnection's Errors ​| Error type | Cause | Resolution |
|---|---|---|
| TransientError.​Closed | Connection was closed unexpectedly. | Reconnect. |
| TransientError.​Timeout | Connection timed out. | Reconnect. |
| TransientError.​ClusterNotReady | The backend is currently in a temporary state where it does not accept connections. | Reconnect. |
| TransientError.​ConfigFetchFailed | Config fetch task has thrown an exception. | Inspect exception. Reconnect. |
| TransientError.​FailedToResumeSession | Connection was lost unexpectedly and the backend rejected our session resume request. | Reconnect. |
| TransientError.​SessionForceTerminated | Session was terminated by the backend. Commonly caused by the same user connecting from another device. | Inform user. |
| TransientError.​GameConfigUpdateRequired | Session was terminated since a new game config was published with Immediate (Force Reconnect) setting. | Inform user. We don't recomment automatic reconnect to avoid load spike on server. |
| TransientError.​ProtocolError | Session signaling was malformed or illegal. | Reconnect. |
| TransientError.​SessionLostInBackground | Session was lost while the game was in the background and the game has just been resumed to the foreground. | Reconnect. |
| TransientError.​TemporaryAuthenticationError | Session login using a social account failed due to social service being temporarily unavailable. | Inform user. |
| TransientError.​InternalWatchdogDeadlineExceeded | Internal worker hanged and was killed. | Inform user. |
| TransientError.​TlsError | Internal transient error in the TLS layer. | Reconnect. |
| TransientError.​SocialAuthenticateForceReconnectConnectionError | Account conflict was resolved by switching account and reconnecting. | Reconnect. |
| TransientError.​PlayerChecksumMismatchConnectionError | Session was lost due to desync. | Inform user. |
| TransientError.​ClientTerminatedConnectionConnectionError | Client terminated the connection. | Reconnect. |
| TerminalError.​WireFormatError | Packet framing or payload is corrupt or cannot be deserialized. | Cannot recover. Often caused by an incompatible backend version. Prompt user to update the game. |
| TerminalError.​InvalidGameMagic | The connected peer is not a game backend. | Cannot recover. Backend address is not correct. |
| TerminalError.​ProjectIdMismatchError | The connected peer is not a game backend. | Cannot recover. Backend address is not correct. |
| TerminalError.​WireProtocolVersionMismatch | The low-level communication protocol with the backend is incompatible with this client. | Prompt user to update the game. |
| TerminalError.​InMaintenance | The server is down for maintenance. | Inform user. |
| TerminalError.​LogicVersionMismatch | Server and Client Logic versions are not compatible. | Prompt user to update the game. |
| TerminalError.​CommitIdMismatch | Commit Id Check is enabled and server and client are not built from the same commit. | Prompt user to update the game. |
| TerminalError.​NoNetworkConnectivity | Could not connect to the backend or to a test location on the CDN. | Inform user. |
| TerminalError.​PlayerIsBanned | Player has been banned. | Inform user. |
| TerminalError.​LogicVersionDowngrade | Player attempted to connect with a lower game config version than previously, possibly from another device. | Prompt user to update the game. |
| TerminalError.​LoginProtocolVersionMismatch | The low-level communication protocols are not compatible. | Prompt user to update the game. |
| TerminalError.​ClientPatchVersionTooOld | The patch version of the client is too old and no longer supported by the server. | Prompt user to update the game. |
| TerminalError.​LoginRejected | Session login using a social account failed due to social service rejecting the client's credentials. | Inform user. |
| TerminalError.​ServerPlayerDeserializationFailure | On the server side, player state failed to be deserialized. | Inform user. |
| TerminalError.​ClientSideConnectionError | Exception in client-side session start hook, or injected error. | Inform user. |
| TerminalError.​Unknown | Any other terminal error. | Inform user. |
To protect both the server and the client, we impose limits on the amount of data that can be sent to the client in a single message. By default the limit is set to 1MB, this can be changed by updating ClientConnection.MaxCompressedPacketPayloadSize runtime option. As the name implies this applies to the compressed size of the message rather than the uncompressed size. Similarly a limit is imposed on the data received from the client, by default this is set to 5MB, this can be changed by updating ClientConnection.MaxDecompressedPacketPayloadSize. Often, hitting the limit is caused by having a player model that has grown too large, therefore it is important to be conscious of the player model size. The database entity inspection utility can be used to investigate what parts of the player model are large in size. You can find the database entity inspection utility on the Player's detail page by clicking on the Save file size or by browsing to http://{DASHBOARD_URI}/entities/Player:{PLAYER_ID}/dbinfo.