Building a cluster client
In this document we will lay the foundation of a client for a game running in a cluster environment, learning how to deal with the connection states and the additional cluster-related events fired by the SmartFoxServer API. Please note that the client is entirely developed using the same API used to connect to a standalone SmartFoxServer instance. The APIs for all supported platforms are available on the SmartFoxServer website.
As discussed under Cluster basic concepts, when developing a game running in a cluster environment we need to deal with multiple connections. In this introductory example our client will handle two connections, one to the Lobby and one to a single Game Node, as this is the most common scenario: users join the Lobby and then, by means of the match-making system, they are sent to a Game Node to play the game in a dedicated Room.
This is the usual approach in all realtime games and all turn-based games with fast turns. In games with potentially slow turns (chess, for example), you may need to make your client join multiple Rooms to allow users play multiple games at the same time: in such case multiple connections to more than one Game Node could be required. We will briefly discuss this scenario at the end of this document.
Join the Lobby
As soon as our client is loaded, an initial screen or scene is displayed; we may call it login view. We have now to establish a connection to the Lobby, which is the entry point to a multiplayer game in a cluster environment. Technically two steps are needed: establish a connection and perform the login after requesting the user credentials (to be validated in a custom server-side Extension).
On the client side our favored approach is to implement the initial screen or scene where users can input their username and password, then execute the connection and login steps back-to-back. Additionally, the client could save the credentials locally to skip the input step; in any case a connect method should be called at some point.
private SmartFox sfsLobby; private SmartFox sfsGame; private void Connect() { // Set connection parameters ConfigData cfg = new ConfigData(); cfg.Host = "127.0.0.1"; cfg.Port = 9933; cfg.Zone = "Cluster"; // Predefined Zone name in cluster's Lobby // Initialize SmartFox client used to connect to the cluster's Lobby sfsLobby = new SmartFox(); // Add base event listeners sfsLobby.AddEventListener(SFSEvent.CONNECTION, OnLobbyConnection); sfsLobby.AddEventListener(SFSEvent.CONNECTION_LOST, OnLobbyConnectionLost); sfsLobby.AddEventListener(SFSEvent.LOGIN, OnLobbyLogin); sfsLobby.AddEventListener(SFSEvent.LOGIN_ERROR, OnLobbyLoginError); sfsLobby.AddEventListener(SFSClusterEvent.CONNECTION_REQUIRED, OnGameNodeConnectionRequired); sfsLobby.AddEventListener(SFSClusterEvent.LOAD_BALANCER_ERROR, OnLoadBalancerError); // Add other event listeners ... // Connect to the Lobby sfsLobby.Connect(cfg); }
let sfsLobby; let sfsGame; function connect() { // Reset user interface usernameIn.disabled = true; enterLobbyBt.disabled = true; // Set SmartFox client configuration let config = {}; config.host = "127.0.0.1"; config.port = 8080; config.useSSL = false; config.zone = "Cluster"; // Predefined Zone name in cluster's Lobby // Initialize SmartFox client used to connect to the cluster lobby node sfsLobby = new SFS2X.SmartFox(config); // Add base event listeners sfsLobby.addEventListener(SFS2X.SFSEvent.CONNECTION, onLobbyConnection, this); sfsLobby.addEventListener(SFS2X.SFSEvent.CONNECTION_LOST, onLobbyConnectionLost, this); sfsLobby.addEventListener(SFS2X.SFSEvent.LOGIN, onLobbyLogin, this); sfsLobby.addEventListener(SFS2X.SFSEvent.LOGIN_ERROR, onLobbyLoginError, this); sfsLobby.addEventListener(SFS2X.SFSClusterEvent.CONNECTION_REQUIRED, onGameNodeConnectionRequired, this); sfsLobby.addEventListener(SFS2X.SFSClusterEvent.LOAD_BALANCER_ERROR, onLoadBalancerError, this); // Add other event listeners ... // Connect to the Lobby sfsLobby.connect(); }
private SmartFox sfsLobby; private SmartFox sfsGame; private void connect() { // Set connection parameters ConfigData cfg = new ConfigData(); cfg.setHost("127.0.0.1"); cfg.setPort(9933); cfg.setZone("Cluster"); // Predefined Zone name in cluster's Lobby // Initialize SmartFox client used to connect to the cluster's Lobby sfsLobby = new SmartFox(); // Add base event listeners sfsLobby.addEventListener(SFSEvent.CONNECTION, this::onLobbyConnection); sfsLobby.addEventListener(SFSEvent.CONNECTION_LOST, this::onLobbyConnectionLost); sfsLobby.addEventListener(SFSEvent.LOGIN, this::onLobbyLogin); sfsLobby.addEventListener(SFSEvent.LOGIN_ERROR, this::onLobbyLoginError); sfsLobby.addEventListener(SFSClusterEvent.CONNECTION_REQUIRED, this::onGameNodeConnectionRequired); sfsLobby.addEventListener(SFSClusterEvent.LOAD_BALANCER_ERROR, this::onLoadBalancerError); // Add other event listeners ... // Connect to the Lobby sfsLobby.connect(cfg); }
The method above takes care of setting up the connection to the Lobby following these steps:
- Create a configuration object, passing the required parameters: please note that port can vary based on the connection type (regular socket or WebSocket), while the Zone must always be set to the predefined "Cluster" value, because in a cluster environment SmartFoxServer supports a single Zone per server only.
- Initialize the
SmartFox
instance for the Lobby: a reference to this instance is saved globally, to be able to interact with the Lobby at any given time. - Add all the listeners needed to manage the various connection states, in particular the cluster-related events CONNECTION_REQUIRED and LOAD_BALANCER_ERROR. Note that all the basic listeners are specific to the Lobby; we will need similar listeners to manage the Game Node connection, but they will perform other actions: for this reason we can't reuse the same methods.
Also, in a real-case scenario other event listeners may be needed for the Lobby: for example the listeners for all the buddy-related events (the Lobby is in charge of the whole Buddy List management), or the EXTENSION_RESPONSE event listener for profile management, etc. - Finally, connect to the Lobby.
If the connection is established successfully, we can proceed with the login step passing the user credentials to the LoginRequest
class instance, as shown in the CONNECTION event listener.
private void OnLobbyConnection(BaseEvent evt) { // Check if the connection was established or not if ((bool)evt.Params["success"]) { // Login sfsLobby.Send(new LoginRequest(nameInput.text, passwordInput.text)); } else { // Reset RemoveSmartFoxLobbyListeners(); sfsLobby = null; // Show error message ... } }
function onLobbyConnection(evtParams) { // Check if the connection was established or not if (evtParams.success) { // Login sfsLobby.send(new SFS2X.LoginRequest(nameInput.value)); } else { // Reset removeSmartFoxLobbyListeners(); sfsLobby = null; // Show error message ... } }
private void onLobbyConnection(BaseEvent evt) { // Check if the connection was established or not boolean success = evt.getArguments().get("success"); if (success) { // Login sfsLobby.send(new LoginRequest(nameInput.text, passwordInput.text)); } else { // Reset removeSmartFoxLobbyListeners(); sfsLobby = null; // Show error message ... } }
At this stage a connection issue should never occur (the only reason could be the unavailability of the Lobby server), and in case it is unlikely that a new attempt a few seconds later would lead to a success. In any case it is good practice to clean the client state by removing all the listeners added previously and setting the SmartFox
instance for the Lobby to null
. Also, a feedback should be provided to the user.
A soon as the successful login is notified, the client should move to a new screen or scene we may call lobby view, where a number of options is offered to the user (for example managing their profile, searching for or interacting with buddies, etc), in particular a way to start a new game or join an existing game, as described in the following section.
private void OnLobbyLogin(BaseEvent evt) { // Switch to lobby view ... }
function onLobbyLogin(evtParams) { // Switch to lobby view ... }
private void onLobbyLogin(BaseEvent evt) { // Switch to lobby view ... }
In case of login error, a manual disconnection should be executed, which in turn resets the client (see Manage the disconnections below).
private void OnLobbyLoginError(BaseEvent evt) { // Disconnect sfsLobby.Disconnect(); // Show error message ... }
function onLobbyLoginError(evtParams) { // Disconnect sfsLobby.disconnect(); // Show error message ... }
private void onLobbyLoginError(BaseEvent evt) { // Disconnect sfsLobby.disconnect(); // Show error message ... }
Find a Game Room
In a cluster environment there can be hundreds of thousands of Game Rooms distributed among many servers. Of course it would be impossible to ask the player to choose what Game Node and Room to join, so this is done automatically by means of the filters provided by SmartFoxServer's Match Expressions.
The following method could be called by a Start game button in the user interface; as the name entails, the method allows to find a Game Room to male the user join or, if none could be found, create a new one (and let the user wait for other players to join it).
public void OnStartGameButtonClick() { // Set the common Room settings to create a new Game Room if needed ClusterRoomSettings settings = new ClusterRoomSettings("Room_" + new System.Random().Next()); settings.GroupId = "games"; settings.IsPublic = true; settings.IsGame = true; settings.MaxUsers = 10; settings.MaxSpectators = 0; settings.MinPlayersToStartGame = 2; settings.NotifyGameStarted = false; settings.Extension = new RoomExtension("MyGame", "my.game.MyGameExtension"); // Set requirements to allow users find the Room (see match expression below) in Room Variables List<RoomVariable> roomVars = new List<RoomVariable>(); roomVars.Add(new SFSRoomVariable("min-rank", sfsLobby.MySelf.GetVariable("rank").GetIntValue())); settings.Variables = roomVars; // Set the match expression to search for an existing Game Room MatchExpression exp = new MatchExpression("min-rank", NumberMatch.LESS_OR_EQUAL_THAN, sfsLobby.MySelf.GetVariable("rank").GetIntValue()); // Send request sfsLobby.Send(new ClusterJoinOrCreateRequest(exp, new List<String>() { "games" }, settings)); }
function onStartGameButtonClick() { // Set the common Room settings to create a new Game Room if needed let settings = new SFS2X.ClusterRoomSettings("Room_" + Date.now()); settings.groupId = "games"; settings.isPublic = true; settings.isGame = true; settings.maxUsers = 10; settings.maxSpectators = 0; settings.minPlayersToStartGame = 2; settings.notifyGameStarted = false; settings.extension = new SFS2X.RoomExtension("MyGame", "my.game.MyGameExtension"); // Set requirements to allow users find the Room (see match expression below) in Room Variables let roomVars = []; roomVars.push(new SFS2X.SFSRoomVariable("min-rank", sfsLobby.mySelf.getVariable("rank").value)); settings.variables = roomVars; // Set the match expression to search for an existing Game Room let exp = new SFS2X.MatchExpression("min-rank", SFS2X.NumberMatch.LESS_OR_EQUAL_THAN, sfsLobby.mySelf.getVariable("rank").value); // Send request sfsLobby.send(new SFS2X.ClusterJoinOrCreateRequest(exp, ["games"], settings)); }
public void onStartGameButtonClick() { // Set the common Room settings to create a new Game Room if needed ClusterRoomSettings settings = new ClusterRoomSettings("Room_" + new Random().nextInt(Integer.MAX_VALUE)); settings.setGroupId("games"); settings.setPublic(true); settings.setGame(true); settings.setMaxUsers(10); settings.setMaxSpectators(0); settings.setMinPlayersToStartGame(2); settings.setNotifyGameStarted(false); settings.setExtension(new RoomExtension("MyGame", "my.game.MyGameExtension")); // Set requirements to allow users find the Room (see match expression below) in Room Variables List<RoomVariable> roomVars = new ArrayList<RoomVariable>(); roomVars.add(new SFSRoomVariable("min-rank", sfsLobby.getMyself().getVariable("rank").getIntValue())); settings.setVariables(roomVars); // Set the match expression to search for an existing Game Room MatchExpression exp = new MatchExpression("min-rank", NumberMatch.LESS_OR_EQUAL_THAN, sfsLobby.getMySelf().getVariable("rank").getIntValue()); // Send request sfsLobby.send(new ClusterJoinOrCreateRequest(exp, Arrays.asList("games"), settings)); }
The method above takes care of executing the following steps:
- Create a
ClusterRoomSettings
object containing the configuration of the Game Room, in case a new one should be launched. In particular a server-side Extension is assigned to the Room to handle the game logic: the Extension must to be deployed on the Game Node. - Assign one or more custom properties to the Room through Room Variables, which are used in conjunction with the Match Expression to look for an available Game Room where to join the user. In this example we set a "minimum ranking" equal to the current ranking of the player (retrieved from User Variables, where it could be saved by the server-side Zone Extension after the login).
- Setup the Match Expression used by the Lobby to search for an existing Game Room. In this example, consistently with the Room settings, the match is based on an hypothetical "ranking": the value set for the Room must be equal to or less than the ranking of the user requesting to join it.
- Send the appropriate request to the Lobby, passing the Match Expression, a list of Room Groups where to look for existing Game Rooms and the settings in case a Room is not found and must be created.
Choosing the proper criteria to make the Room "findable" is the critical step, because we want the user to be sent to the right Room. A few other examples of search criteria could be: the game type or variant, the game level, the maximum (or minimum) number of players supported by the game, a flag indicating the game state (to prevent users from joining a game already started), etc. The list could be endless: it is up to the developer to identify the right criteria for their game.
After sending the request, it is then up to the Lobby to locate the target Game Node through the Load Balancing logic, execute the Match Making logic and, if a Room was found or created, notify the client.
In case, for any reason, a Game Node couldn't be located by the Load Balancing system, the LOAD_BALANCER_ERROR event is fired by the sfsLobby
instance. This error should rarely occur and it could be due to the cluster state. For example when all Game Nodes have reached their maximum capacity and new Nodes should be launched, there could be a few seconds in which they are not available yet, and the error is thrown. The event can be useful to provide a feedback to the user and maybe reset the UI.
private void OnLoadBalancerError(BaseEvent evt) { // Show error message ... }
function onLoadBalancerError(evtParams) { // Show error message ... }
private void onLoadBalancerError(BaseEvent evt) { // Show error message ... }
Instead of showing an error message to the user, another option is to show a "waiting" message (maybe in a dedicated view) and schedule a new attempt to join or create a Room a few seconds later. Moreover, with this approach the user should keep waiting until the game actually starts, which means all the steps we are describing in this document have been completed successfully: connect and login to the Game Node, find a matching Game Room or create a new one, join it, wait for the minimum number of players to start the game to be reached. In case an error occurs at any one of these steps, the process should be restarted, unless the user gives up for example clicking a Cancel button in the UI.
Join the Game Node
The Lobby notifies the client that a Game Node must be joined by means of the CONNECTION_REQUIRED event. When received, an additional connection must be established.
private void OnGameNodeConnectionRequired(BaseEvent evt) { // Retrieve connection settings ConfigData cfg = (ConfigData)evt.Params["configData"]; // Retrieve and save login details gameUsername = (string)evt.Params["userName"]; gamePassword = (string)evt.Params["password"]; // Initialize SmartFox client used to connect to the cluster's Game Node sfsGame = new SmartFox(); // Add base event listeners sfsGame.AddEventListener(SFSEvent.CONNECTION, OnGameNodeConnection); sfsGame.AddEventListener(SFSEvent.CONNECTION_LOST, OnGameNodeConnectionLost); sfsGame.AddEventListener(SFSEvent.LOGIN, OnGameNodeLogin); sfsGame.AddEventListener(SFSEvent.LOGIN_ERROR, OnGameNodeLoginError); sfsGame.AddEventListener(SFSEvent.ROOM_CREATION_ERROR, OnGameRoomCreationError); sfsGame.AddEventListener(SFSEvent.ROOM_JOIN, OnGameRoomJoin); sfsGame.AddEventListener(SFSEvent.ROOM_JOIN_ERROR, OnGameRoomJoinError); // Add other event listeners ... // Establish a connection to the Game Node; a Game Room will be joined automatically after login sfsGame.Connect(cfg); }
function onGameNodeConnectionRequired(evtParams) { // Retrieve connection settings let config = evtParams.configObj; // Retrieve and save login details gameUsername = evtParams.userName; gamePassword = evtParams.password; // Initialize SmartFox client used to connect to the cluster's Game Node sfsGame = new SFS2X.SmartFox(config); // Add event listeners sfsGame.addEventListener(SFS2X.SFSEvent.CONNECTION, onGameNodeConnection, this); sfsGame.addEventListener(SFS2X.SFSEvent.CONNECTION_LOST, onGameNodeConnectionLost, this); sfsGame.addEventListener(SFS2X.SFSEvent.LOGIN, onGameNodeLogin, this); sfsGame.addEventListener(SFS2X.SFSEvent.LOGIN_ERROR, onGameNodeLoginError, this); sfsGame.addEventListener(SFS2X.SFSEvent.ROOM_CREATION_ERROR, onGameRoomCreationError, this); sfsGame.addEventListener(SFS2X.SFSEvent.ROOM_JOIN, onGameRoomJoin, this); sfsGame.addEventListener(SFS2X.SFSEvent.ROOM_JOIN_ERROR, onGameRoomJoinError, this); // Add other event listeners ... // Establish a connection to the Game Node; a Game Room will be joined automatically after login sfsGame.connect(); }
private void onGameNodeConnectionRequired(BaseEvent evt) { // Retrieve connection settings ConfigData cfg = (ConfigData) evt.getArguments().get("configData"); // Retrieve and save login details gameUsername = (String) evt.getArguments().get("userName"); gamePassword = (String) evt.getArguments().get("password"); // Initialize SmartFox client used to connect to the cluster's Game Node sfsGame = new SmartFox(); // Add base event listeners sfsGame.addEventListener(SFSEvent.CONNECTION, this::onGameNodeConnection); sfsGame.addEventListener(SFSEvent.CONNECTION_LOST, this::onGameNodeConnectionLost); sfsGame.addEventListener(SFSEvent.LOGIN, this::onGameNodeLogin); sfsGame.addEventListener(SFSEvent.LOGIN_ERROR, this::onGameNodeLoginError); sfsGame.addEventListener(SFSEvent.ROOM_CREATION_ERROR, this::onGameRoomCreationError); sfsGame.addEventListener(SFSEvent.ROOM_JOIN, this::onGameRoomJoin); sfsGame.addEventListener(SFSEvent.ROOM_JOIN_ERROR, this::onGameRoomJoinError); // Add other event listeners ... // Establish a connection to the Game Node; a Game Room will be joined automatically after login sfsGame.connect(cfg); }
The method above takes care of setting up the connection to the Game Node following these steps:
- Retrieve the configuration object from the event parameters. It contains all the predefined settings needed to connect to the target Game Node.
- Retrieve and save globally the username and password from the event parameters, to perform the login step later on. Please note that the password is not the same password of the user account used to log into the Lobby. It is a one-time password specifically required to give the user access to the target Game Node. The password can expire if the connection and login process takes too much time.
- Initialize the
SmartFox
instance for the Game Node: a reference to this instance is saved globally, to be able to interact with the Game Node at any given time. - Add all the listeners needed to manage the various connection states, in particular the ROOM_JOIN event signaling that the target Game Room was joined and the game logic can kick-in. Note that all the listeners are specific to the Game Node and not shared with the Lobby.
Also, in a real-case scenario other event listeners would be needed: for example the EXTENSION_RESPONSE and USER_VARIABLES_UPDATE event listeners for the game logic, etc. - Finally, connect to the Game Node.
If a connection is established successfully, we can proceed with the login step passing the user credentials returned by the CONNECTION_REQUIRED event to the LoginRequest
class instance, as shown in the CONNECTION event listener for the Game Node.
private void OnGameNodeConnection(BaseEvent evt) { // Check if the connection was established or not if ((bool)evt.Params["success"]) { // Enable lag monitor for the Lobby sfsLobby.EnableLagMonitor(true); // Login sfsGame.Send(new LoginRequest(gameUsername, gamePassword)); } else { // Reset RemoveSmartFoxGameListeners(); sfsGame = null; // Show error message ... } }}
function onGameNodeConnection(evtParams) { // Check if the connection was established or not if (evtParams.success) { // Enable lag monitor for the Lobby sfsLobby.enableLagMonitor(true); // Login sfsGame.send(new SFS2X.LoginRequest(gameUsername, gamePassword)); } else { // Reset removeSmartFoxGameListeners(); sfsGame = null; // Show error message ... } }
private void onGameNodeConnection(BaseEvent evt) { // Check if the connection was established or not boolean success = (boolean) evt.getArguments().get("success"); if (success) { // Enable lag monitor for the Lobby sfsLobby.enableLagMonitor(true); // Login sfsGame.send(new LoginRequest(gameUsername, gamePassword)); } else { // Reset removeSmartFoxGameListeners(); sfsGame = null; // Show error message ... } }}
Note that at this stage it is good practice to enable the lag monitor on the Lobby connection. In conjunction with the "Enable keepalive" setting on the Zone (set to true
by default in the Overcast Cluster), this feature helps avoiding the connection to the Lobby being closed because the user appears to be idle while playing, when most of client-server communication occurs over the Game Node connection.
Just like for the Lobby before, a connection issue should never occur at this stage (an issue with the Game Nodes would probably trigger a LOAD_BALANCER_ERROR before). In any case it is good practice to clean the client state for the Game Node connection by removing all the listeners added previously and setting the SmartFox
instance for the Game Node to null
. Also, a feedback should be provided to the user, who can then try to launch a new game.
The LOGIN event is not particularly meaningful here, because the client should move to the actual game screen or scene after the Game Room is joined successfully. Instead, in the unlikely event of a login error (for example because the one-time password expired), a manual disconnection from the Game Node should be executed, which in turn resets the client state (see Manage the disconnections below) making it ready for a new attempt to join or create a game. As always in case of issues, some warning message should be displayed.
private void OnGameNodeLogin(BaseEvent evt) { // Nothing to do; a Room-autojoin is triggered by the server } private void OnGameNodeLoginError(BaseEvent evt) { // Disconnect sfsGame.Disconnect(); // Show error message ... }
function onGameNodeLogin(evtParams) { // Nothing to do; a Room-autojoin is triggered by the server } function onGameNodeLoginError(evtParams) { // Disconnect sfsGame.disconnect(); // Show error message ... }
private void onGameNodeLogin(BaseEvent evt) { // Nothing to do; a Room-autojoin is triggered by the server } private void OnGameNodeLoginError(BaseEvent evt) { // Disconnect sfsGame.disconnect(); // Show error message ... }
Join the Game Room
If the Match Making system could not find a suitable Room to make the user join, the creation of a new Room is attempted. In case an error occurs (for example a name conflict, the maximum number of Rooms for the Zone reached, etc), SmartFoxServer's common ROOM_CREATION_ERROR event is fired. Again, a manual disconnection from the Game Node should be executed to reset the client state.
private void OnGameRoomCreationError(BaseEvent evt) { // Disconnect sfsGame.Disconnect(); // Show error message ... }
function onGameRoomCreationError(evtParams) { // Disconnect sfsGame.disconnect(); // Show error message ... }
private void onGameRoomCreationError(BaseEvent evt) { // Disconnect sfsGame.disconnect(); // Show error message ... }
Whether an existing Room was found, or a new Room was created, the join is then automatically attempted on the server side. If an error occurs, the ROOM_JOIN_ERROR event is fired. Once more we need to manually disconnect from the Game Node to reset the client state and show an error message.
private void OnGameRoomJoinError(BaseEvent evt) { // Disconnect from game node sfsGame.Disconnect(); // Show error message ... }
function onGameRoomJoinError(evtParams) { // Disconnect from game node sfsGame.disconnect(); // Show error message ... }
private void OnGameRoomJoinError(BaseEvent evt) { // Disconnect from game node sfsGame.disconnect(); // Show error message ... }
If instead the Room was joined successfully, the ROOM_JOIN event is dispatched. It is now time to switch to the actual game screen or scene (the game view) and transfer control to the game logic.
private void OnGameRoomJoin(BaseEvent evt) { // Switch to game view ... }
function onGameRoomJoin(evtParams) { // Switch to game view ... }
private void onGameRoomJoin(BaseEvent evt) { // Switch to game view ... }
Leave the game
When the game is over (or the user wants to leave it), the connection to the Game Node should be ditched by means of a manual disconnection. This is important to free client resources, so that a new game can be launched from the lobby view.
public void LeaveGame() { // Disconnect from game node sfsGame.Disconnect(); }
function leaveGame() { // Disconnect from game node sfsGame.disconnect(); }
public void leaveGame() { // Disconnect from game node sfsGame.disconnect(); }
Manage the disconnections
As described above, we have two connection active most of the time: the connection to the Lobby and the one to the Game Node. Both connection can be interrupted on purpose, by means of a manual disconnection, or abruptly, due to reasons independent from the user actions. Let's discuss the possible causes and how to deal with disconnections with respect to the game state.
Lobby
The connection to the Lobby could be ended manually because the user wants to go back to the login view. This is usually done from the lobby view, when no game is active. An unwanted disconnection could be caused by a network error, or a kick/ban action executed by a moderator or administrator. In any case, when the disconnection occurs, the CONNECTION_LOST event listener for the Lobby is called.
private void OnLobbyConnectionLost(BaseEvent evt) { // Get disconnection reason string connLostReason = (string)evt.Params["reason"]; // On Lobby non-manual disconnection, if a game is in progress (which means a connection to the game // node is still active), we can ignore this event and let the user keep playing the game; otherwise // go back to login view if (sfsGame == null) { // Switch to login view ... if (connLostReason != ClientDisconnectionReason.MANUAL) { // Show error message ... } } // Reset RemoveSmartFoxLobbyListeners(); sfsLobby = null; }
function onLobbyConnectionLost(evtParams) { // Get disconnection reason let connLostReason = evtParams.reason; // On Lobby non-manual disconnection, if a game is in progress (which means a connection to the game // node is still active), we can ignore this event and let the user keep playing the game; otherwise // go back to login view if (sfsGame == null) { // Switch to login view ... if (connLostReason != SFS2X.ClientDisconnectionReason.MANUAL) { // Show error message ... } } // Reset removeSmartFoxLobbyListeners(); sfsLobby = null; }
private void onLobbyConnectionLost(BaseEvent evt) { // Get disconnection reason String connLostReason = (String) evt.getArguments.get("reason"); // On Lobby non-manual disconnection, if a game is in progress (which means a connection to the game // node is still active), we can ignore this event and let the user keep playing the game; otherwise // go back to login view if (sfsGame == null) { // Switch to login view ... if (connLostReason != ClientDisconnectionReason.MANUAL) { // Show error message ... } } // Reset removeSmartFoxLobbyListeners(); sfsLobby = null; }
This is how the listener processes the disconnection:
- The disconnection reason is extracted from the event's parameters.
- When the disconnection occurs, if it wasn't requested by the user, a game could still be in progress. In this case we can mostly ignore this event: the client keeps showing the game view and the game can continue, as it relies on its own connection to the Game Node. We will have to deal with the Lobby being disconnected once the game is over and the user wants to go back to the lobby view (see below).
If a game is not in progress, the client should go back to the login view and show an error message (except in case of manual disconnection). - The now closed connection is cleaned by removing all the listeners and setting the
SmartFox
instance for the Lobby tonull
.
NOTE: we said that the event can be mostly ignored when a game is currently in progress because losing the Lobby connection could still have an impact on it, for example due to the lost interaction with the Buddy List system.
Game Node
The connection to the Game Node could be ended manually, because the game is over or the user wants to go back to the lobby view anyway. Another reason for a manual disconnection is to reset the client state if an error occurs during the connect → login → join process on the Game Node, as we have seen multiple times above. An unwanted disconnection could be caused by a network error, or a kick/ban action executed by a moderator or administrator. In any case, when the disconnection occurs, the CONNECTION_LOST event listener for the Game Node is called.
private void OnGameNodeConnectionLost(BaseEvent evt) { string connLostReason = (string)evt.Params["reason"]; // If Lobby connection is available, go to lobby view, otherwise go to login view if (sfsLobby != null && sfsLobby.IsConnected) { // Disable lag monitor for the Lobby sfsLobby.EnableLagMonitor(false); // Switch to lobby view ... } else { // Switch to login view ... // Show warning for missing lobby connection ... } if (connLostReason != ClientDisconnectionReason.MANUAL) { // Show error message ... } // Reset RemoveSmartFoxGameListeners(); sfsGame = null; }
function onGameNodeConnectionLost(evtParams) { let connLostReason = evtParams.reason; // If Lobby connection is available, go to lobby view, otherwise go to login view if (sfsLobby != null && sfsLobby.isConnected) { // Disable lag monitor for the Lobby sfsLobby.enableLagMonitor(false); // Switch to lobby view ... } else { // Switch to login view ... // Show warning for missing lobby connection ... } if (connLostReason != SFS2X.ClientDisconnectionReason.MANUAL) { // Show error message ... } // Reset removeSmartFoxGameListeners(); sfsGame = null; }
private void onGameNodeConnectionLost(BaseEvent evt) { String connLostReason = (String) evt.getArguments().get("reason"); // If Lobby connection is available, go to lobby view, otherwise go to login view if (sfsLobby != null && sfsLobby.isConnected()) { // Disable lag monitor for the Lobby sfsLobby.enableLagMonitor(false); // Switch to lobby view ... } else { // Switch to login view ... // Show warning for missing lobby connection ... } if (connLostReason != ClientDisconnectionReason.MANUAL) { // Show error message ... } // Reset removeSmartFoxGameListeners(); sfsGame = null; }
This is how the listener processes the disconnection:
- The disconnection reason is extracted from the event's parameters.
- When the disconnection occurs, usually the connection to the Lobby is still available. In this case we have to disable the lag monitor (which, remember, was used to keep the connection to the Lobby alive while playing) and move to the lobby view where a new game can be launched.
If instead the connection to the Lobby is not available anymore, the client should go back to the login view and be notified of the issue. - Except in case of manual disconnection, an error message should be displayed. Remember that in case of manual disconnection due to an error during the Game Room join process, an error message was already shown before.
- The now closed connection is cleaned by removing all the listeners and setting the
SmartFox
instance for the Game Node tonull
.
Multi-game client
As mentioned at the beginning of this document, a much more complex scenario involves the possibility to play multiple games concurrently, inside the same client. For example we could play multiple chess games, switching from one to another by means of tabs in the UI, with badges notifying when it's our turn to make our move.
The added complexity is due to the fact that separate connections (so SmartFox
class instances) to multiple Game Nodes are required. Additionally, the Load Balancing logic could select a Game Node to which our client is already connected, because another game is already in progress on that node. In this case the whole connection required → connection → login process is skipped, and the Room join attempted immediately. Therefore we have to be aware that two or more games can be running on the same connection.
When developing a multi-game client you should keep in mind the following tips:
- Use a key-value data structure to keep a list of active games running in the client. For example use the Room object as key, and your game controller class instance as value: this way, whenever a game-related event is fired (i.e. EXTENSION_RESPONSE, PUBLIC_MESSAGE, ROOM_VARIABLES_UPDATE, etc), you can extract the target Room from the event parameters and pass the other event parameters to the appropriate game controller through the mentioned data structure.
- Keep a reference to the related
SmartFox
class instance inside your game controller, so that every game can send its requests to the appropriate Game Node. - When a game is over, check if other games using the same connection are still in progress (in other words other Rooms joined on the same Game Node). If not, the connection can be closed as shown before.
- The Lobby keep-alive shouldn't be stopped as soon as a game ends (like shown before), but only if no other game is still active.
- User Variables should not be used to keep track of game-related data, because they are specific to a Game Node, but not to a Room. In case you have multiple games running on the same Game Node, they would all share the same values.