Cluster Lobby Mockup

Overview

The Cluster Lobby Mockup example lays the foundations for games running in a cluster environment. A lobby is a staging area commonly used in multiplayer games to host players before they join the actual game. In a lobby, users can usually customize their profile, chat with friends, search for a game to join or launch a new game, invite friends to play and more.

In this tutorial you will learn how to set up the basic structure of a multiplayer game, divided into three scenes: Login, Lobby and Game. The Login scene is where the connection to the cluster's Lobby Node is established and login performed. The Lobby scene is the core of the example, where an existing Game Room on a Game Node can be joined, or a new Room can be launched. Finally, the Game scene acts as a placeholder for an actual game, but it also shows a very basic interaction.

In this document we assume that you already went through the Cluster Connector tutorial, where we described how to setup and use the SmartFox client API object to establish a connection to both the Lobby and Game Node, and how to deal with the connection states and the additional cluster-related events fired by the SmartFoxServer API. In this tutorial we will further improve this, creating a class to share the connections to Lobby and Game Node among the project's three scenes.

Download

Click on the following button to download the complete pack of Unity examples for the cluster.

Download examples pack

Setup & run

In order to setup and run the example, follow these steps:

  1. unzip the examples pack;
  2. make sure you have a cluster running in Overcast;
  3. launch the Unity Hub, click on the Open button and navigate to the /Client/Cluster_LobbyMockup folder;
  4. if prompted, select the Unity Editor version to use (v2021.3 or later is recommended);
  5. go to the Project panel, click on the /Assets/Scenes folder and double click on the Login scene to open it;
  6. click on the Controller game object and change the Host setting in the Editor's Inspector panel to the IP address or domain name of your Lobby Node;
  7. click on the Play button to run the example in the Unity Editor, or go to the Build settings panel and build it for your target platform of choice.

The client's C# code is in the /Assets/Scripts folder and the SmartFoxServer 2X client API DLLs are in the /Assets/Plugins folder. Read this introduction to understand why multiple DLLs are used.

Introduction to code

The code for this example is divided into multiple classes contained in the /Assets/Scripts folder. The Controllers subfolder contains three <name>SceneController scripts which are attached to the empty Controller game object in their respective scenes. All controllers extend the BaseSceneController abstract class, which in turn extends MonoBehavior.
All controllers are basic Unity C# scripts implementing the Awake(), Start() and Update() methods as needed. They also contain the listeners for the events fired by UI components (i.e. buttons), some helper methods and the listeners for SmartFoxServer's client API events.

The Managers subfolder contains the GlobalManager singleton class, which holds a reference to the two SmartFox class instances to share the client-server connections to the Lobby and Game Nodes among the project's multiple scenes.

Finally, the Prefabs subfolder contains the scripts attached to a few prefabs used by the project, like modal panels.

The shared connection

Unlike the basic Cluster Connector example, the approach we followed in developing this and the next examples is to separate the logic into three scenes. This largely simplifies the scene hierarchy and gives each scene's controller its own responsibilities, as outlined in the overview above. However each scene can't be totally standalone, because they all need to interact with SmartFoxServer to accomplish their own tasks. Additionally, each scene can potentially deal with two connections at the same time (with the Lobby and the Game Node), and we want the client to establish a single communication channel with each server.

In order to achieve this we use a singleton class we called GlobalManager, which keeps a reference to the two instances of the SmartFox class used by all scenes to communicate with either the Lobby or the Game Node or both (the private sfsLobby and sfsGame fields). This singleton extends the MonoBehavior class, so it can be attached to a game object which is dynamically added to the initial scene by the singleton itself. To prevent the object from being destroyed on scene change, the DontDestroyOnLoad() method from Unity's API is called in its Awake() callback. The method also makes sure the example will run in background too when executed.

			private void Awake()
			{
				// Do not destroy this object on scene change
				DontDestroyOnLoad(this);
			
				// Make sure the application runs in background
				Application.runInBackground = true;
			}

The singleton also takes care of triggering the network events processing in its implementation of the Update() method, as required by the thread safety mechanism implemented by Unity and described in the previous tutorial.

			private void Update()
			{
				if (sfsLobby != null)
					sfsLobby.ProcessEvents();
			
				if (sfsGame != null)
					sfsGame.ProcessEvents();
			}

One more important Unity callback implemented by our manager is OnApplicationQuit(), which makes sure a disconnection from all SmartFoxServer nodes is always executed on quit. This is strongly recommended because an active network socket during the quit process can cause a crash on some platforms. Additionally, when inside the Unity Editor, stopping playmode doesn't interrupt the internal threads launched by the SmartFox API, which may lead to unwanted issues. Forcing a disconnection ensures that all threads are stopped as appropriate.

			private void OnApplicationQuit()
			{
				if (sfsGame != null && sfsGame.IsConnected)
					sfsGame.Disconnect();
			
				if (sfsLobby != null && sfsLobby.IsConnected)
					sfsLobby.Disconnect();
			}

The GlobalManager class provides the methods needed to create the SmartFox instance to connect to the Lobby Node (using TCP socket or WebSocket communication), or access an existing instance for both the Lobby and Game Nodes. The creation of the SmartFox instance to connect to the Game Node doesn't require a dedicated method instead, because it is performed internally by the class as described later on.

The singleton is also responsible of the overall application flow already described in the Cluster Connector tutorial in great detail. In fact it implements the general connect → login → join process for both node types and it takes care of handling disconnections and switch scene (or not) based on the state of both connections (to the Lobby Node and to the Game Node). This is useful to avoid being forced to handle the disconnection event in every scene.

Introduction to scenes

In this example all scene controllers share a basic behavior provided by their parent BaseSceneController parent class. The two fundamental actions inherited from this class are executed in Unity's Awake() and OnDestroy() methods implementation.

The Awake() method, called by Unity when the script instance is loaded, gets a reference to the GlobalManager singleton class, where the connections to cluster nodes is made available to all scenes as described before.

			protected virtual void Awake()
			{
				// Get Global Manager instance
				gm = GlobalManager.Instance;
			
				// Set reference to scene controller in Global Manager
				gm.SetCurrentSceneController(this);
			}

The OnDestroy() method instead is called by Unity when a scene is closed to move to another scene or the game is ended. The method makes sure all the SmartFoxServer client event listeners potentially added by the scene now being closed are removed by calling the abstract RemoveSmartFoxListeners() method, which must be implemented by all scene controllers (whether it's needed or not).

			protected virtual void OnDestroy()
			{
				RemoveSmartFoxListeners();
			}

NOTE
Before loading a new scene, it is very important to remove all the SFS2X event listeners added by the current scene. Otherwise events generated in the next scene could trigger the handlers in the previous scene, with possible unwanted side effects and memory leaks.

Another task common to all scenes loaded while one or more connections are already available (in other words the Lobby and Game scenes) is to retrieve the relevant reference to the SmartFox instance from the GameManager singleton, in order to add their own SmartFoxServer event listeners and interact with the server. This is accomplished in their own Start() callbacks, together with other stuff specific to each scene.

			private void Start()
			{
				// Get reference to static SmartFox client instances for the lobby and game nodes
				sfsLobby = gm.GetLobbyClient();
				sfsGame = gm.GetGameClient();
			
				// Add game-related SmartFox event listeners
				AddSmartFoxGameListeners();
				
				...
			}

The Login scene

This scene is in charge of the connection and login to the Lobby Node.

As soon as the user clicks the Login button, the scene controller's Connect() method is executed. It creates the SmartFox instance to communicate with the Lobby Node by calling the GlobalManager.CreateLobbyClient() method. Here, the singleton adds its own listeners, required to handle the next steps in the flow. The user name must be passed to the method, because the GlobalManager class will take care of the login step automatically.

The returned SmartFox instance is then configured by the controller (i.e. the logging), and the SmartFox.Connect() method is called. The passed configuration object contains some default parameters (Port and Zone must be set to the predefined values for Overcast) and the Host and Debug parameters set to the values set in the Inspector panel for the Controller game object.

			private void Connect()
			{
				// Disable user interface
				EnableUI(false);
		
				// Set connection parameters
				ConfigData cfg = new ConfigData();
				cfg.Host = host;
				cfg.Port = TCP_PORT;
				cfg.Zone = ZONE;
				cfg.Debug = debug;
		
				#if UNITY_WEBGL
				cfg.Port = HTTP_PORT;
				#endif
		
				// Retrieve SmartFox instance from Global Manager
				sfsLobby = gm.CreateLobbyClient(nameInput.text);
		
				// NOTE
				// If this scene needed its own SmartFox event listeners, they should be added now
				// In such case, listeners must be removed in the RemoveSmartFoxListeners method above
		
				// Configure SmartFox internal logger
				sfsLobby.Logger.EnableConsoleTrace = debug;
		
				// Connect to SmartFoxServer
				sfsLobby.Connect(cfg);
			}

Note that if the scene needs its own listeners to SmartFoxServer events, they can be added here before starting the connection process. In our example this is not needed though.

Control is now passed to the GlobalManager singleton, which completes the connection and login process automatically by listening to the CONNECTION and LOGIN events. The first event causes the login request to be sent to the Lobby Node (see OnLobbyNodeConnection() event handler), while the second one makes the application switch to the Lobby scene (see OnLobbyNodeLogin() event handler). The Login scene is destroyed, but not the singleton manager of course.

In case an error occurs (CONNECTION event with success parameter set to false, or LOGIN_ERROR event), the GlobalManager singleton resets the connection and passes an error message to the current scene (see OnLobbyNodeConnection() and OnLobbyNodeLoginError() handlers), which shows it to the user.

The Lobby scene

The Lobby scene is where a Game Room on a Game Node can be joined, whether it's a new Room or an existing one.

Our mockup interface simply features a few buttons, the main one being the Play button. When clicked, its listener displays a modal panel containing a "waiting" message and a Cancel button, then the JoinOrCreateRoom() method is called: this tries to find a Game Room to join the user in by means of the ClusterJoinOrCreateRequest class.

The method behaves like we already described in the Cluster Connector example, and the same caveat on the simplified Match Expression applies here.

			private void JoinOrCreateRoom()
			{
				// Set the match expression to search for an existing Game Room
				MatchExpression exp = new MatchExpression(RoomProperties.NAME, StringMatch.STARTS_WITH, GAME_ROOM_NAME_PREFIX);
				
				// Set the Room settings to create a new Game Room if one is not found
				// The Room is assigned a name allowing the above match expression find it when other users will try to join
				ClusterRoomSettings settings = new ClusterRoomSettings("MockRoom__" + new System.Random().Next());
				settings.GroupId = GAME_ROOMS_GROUP_NAME;
				settings.IsPublic = true;
				settings.IsGame = true;
				settings.MaxUsers = 2;
				settings.MaxSpectators = 0;
				settings.MinPlayersToStartGame = 2;
				settings.NotifyGameStarted = false;
				
				sfsLobby.Send(new ClusterJoinOrCreateRequest(exp, new List() { GAME_ROOMS_GROUP_NAME }, settings));
			}

After the request is sent, it is up to the Lobby Node to locate the target Game Node through the Load Balancing logic, execute the Match Making logic and, if a Room was found, notify the client. If the Room can't be found, a new Room is created on the Game Node.

In case, for any reason, a Game Node couldn't be located by the Load Balancing system, the LOAD_BALANCER_ERROR event is fired. The GlobalManager.OnLoadBalancerError() listener catches the event and notifies it to the scene's controller, which schedules a new attempt after a few seconds, while the "waiting" message is still displayed. The user can click the Cancel button to stop the Room joining process.

Whether an existing Game Room is located on a Game Node (or a new one is created), this is notified to the client by means of the cluster's CONNECTION_REQUIRED event. The OnGameNodeConnectionRequired() handler on the GlobalManager singleton is in charge of establishing the new connection to the Game Node.

The handler retrieves the connection settings and the username and temporary password from the event parameters, creates the instance of the SmartFox class specific for the Game Node and adds all the event listeners required to manage the various steps of the process. Finally the handler attempts to connect to the Game Node indicated by the event.

If the connection is successful, the OnGameNodeConnection() listener enables the lag monitor on the Lobby connection (to avoid it to be closed due to the user being idle) and attempts the login on the Game Node using the credentials provided before by the CONNECTION_REQUIRED event.

Upon successful login, the Game Node attempts to join the user in the target Game Room automatically. Despite the GlobalManager class added a listener for the LOGIN event, this can be ignored as no action is required at this stage. The ROOM_JOIN event notifies that a Game Room was joined successfully, and the OnGameRoomJoin() listener switches the application to the Game scene, where the game logic can kick-in (or would, if this was an actual game).

In case an error occurs at any of the steps above (connection, login, potential Room creation, Room join), the relevant event causes the GlobalManager singleton to disconnect from the Game Node (if needed) and reset its own state in the OnGameNodeConnectionLost() listener. The scene controller's OnGameJoinFailed() method is also called, so that a new attempt to find a Game Room can be scheduled, as mentioned before.

Going back to the Login scene and its controller, the other two buttons in the UI are the Logout button and the Kill Lobby Connection button. The first one triggers the manual disconnection from the Lobby Node. The GlobalManager singleton reacts to the CONNECTION_LOST event through its OnLobbyNodeConnectionLost() listener and reverts the application to the Login scene. The Kill Lobby Connection button simulates an unwanted disconnection from the Lobby Node, which again triggers the same behavior. The only difference is that a disconnection warning is displayed in the Login scene.

The Game scene

The Game scene is where the actual game should take place after a Game Room was joined on the target Game Node to which the user was redirected by the Lobby.

When loaded, the scene controller adds its own event listeners to the SmartFox instance connected to the Game Node, in particular to be notified when a user enters or leaves the Game Room (USER_ENTER_ROOM and USER_EXIT_ROOM events) and when User Variables are updated (USER_VARIABLES_UPDATE event).

			private void AddSmartFoxGameListeners()
			{
				sfsGame.AddEventListener(SFSEvent.USER_ENTER_ROOM, OnUserEnterRoom);
				sfsGame.AddEventListener(SFSEvent.USER_EXIT_ROOM, OnUserExitRoom);
				sfsGame.AddEventListener(SFSEvent.USER_VARIABLES_UPDATE, OnUserVariablesUpdate);
			}

The USER_VARIABLES_UPDATE event listener is needed because in this mockup scene we implemented an Interact button to simulate, well, the interaction of the game client with the server. In fact, when the button is clicked, the scene controller simply increases a numeric value saved in a User Variable.

			public void OnInteractButtonClick()
			{
				UserVariable clickVar = sfsGame.MySelf.GetVariable("click");
			
				if (clickVar == null)
					clickVar = new SFSUserVariable("click", 1);
				else
					clickVar = new SFSUserVariable("click", clickVar.GetIntValue() + 1);
			
				sfsGame.Send(new SetUserVariablesRequest(new List() { clickVar }));
			}

The USER_VARIABLES_UPDATE event is handled by the OnUserVariablesUpdate() listener which, just like the other two listeners, simply prints a message in the scene UI.

The other three buttons in the UI are the Leave Game button, the Kill Lobby Connection button and the Kill Game Connection button. The first one triggers the manual disconnection from the Game Node. The GlobalManager singleton reacts to the CONNECTION_LOST event through its OnGameNodeConnectionLost() listener and reverts the application to the Lobby scene.

The two Kill ... Connection buttons simulate unwanted disconnections from the Game Node and Lobby Node respectively. If the connection to the Lobby Node is lost, nothing happens: the game can continue. If instead the connection to the Game Node is lost, again the GlobalManager singleton's OnGameNodeConnectionLost() listener is triggered. If the connection to the Lobby Node is still available, the application simply moves back to the Lobby scene, and a warning message is displayed. If the connection to the Lobby Node was lost too, the Login scene is displayed instead (again with a warning).


You can now go back to the Unity examples series and check the next tutorial.

More resources

You can learn more about development in the Overcast Cluster environment by consulting the following resources: