Cluster Tic-Tac-Toe

Overview

The Tic-Tac-Toe example shows how to develop a full multiplayer turn-based game with Unity and SmartFoxServer 2X hosted in the Overcast cluster environment. The example implements the well-known paper-and-pencil game for two players also known as Noughts and Crosses. In this game players take turns marking the spaces in a three-by-three grid with X or O; the player who succeeds in placing three of their marks in a horizontal, vertical or diagonal row is the winner.

The example is built on the foundations laid in the Cluster Lobby Mockup tutorial. Using the same structure we completed the Lobby scene by adding a buddy list and the user profile management, and we implemented the actual game logic substituting the mockup Game scene.

The buddy list features icons to represent the state of friends, controls to add, remove and block them, a button to invite them to play and a panel to exchange private messages. In the profile panel users can set their state and other details that will be visibile to their friends, other than their experience and ranking which helps demonstrating how the match-making system works when a Game Room must be located or created in the cluster.

This example also features a server-side Extension deployed on the Game Nodes which implements the main game logic: it determines if the game should start or stop, validates the player moves, checks if the victory condition is met, etc. Using a server-side Extension — or, in other words, having an authoritative Game Node — is a more flexible and secure option with respect to keeping all the game logic on the client-side only.
The server-side Extension is dynamically attached to the Game Room when created; it updates the game state and sends game-related events back to the Unity client, which in turn updates the Game scene accordingly.

In this document we assume that you already went through the previous tutorial, where we explained the subdivision of the application into three scenes and how to create a GlobalManager class to share the connections to both the Lobby and Game Nodes among 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, possibly with a single Game Node;
  3. connect to the Lobby Node with the AdminTool, go to Zone Configurator module → Cluster Zone → Buddy List tab and check if the Buddy List system is active: if not, activate it and restart the Lobby Node, so that the updated Zone configuration is loaded;
  4. connect to the Game Node with the AdminTool, go to the Extension Manager module and create the TicTacToe folder, then upload the content of folder /Server/Cluster_TicTacToe/Extension-Java/deploy to it (*);
  5. launch the Unity Hub, click on the Open button and navigate to the /Client/Cluster_TicTacToe folder;
  6. if prompted, select the Unity Editor version to use (v2021.3 or later is recommended);
  7. go to the Project panel, click on the /Assets/Scenes folder and double click on the Login scene to open it;
  8. 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;
  9. 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.

Server-side Extension

The server-side Extension is available in two versions: Java and JavaScript.

Step 4 above describes how to deploy the Java Extension, which is the default one for this example. In order to access its source code, create and setup a new project in your Java IDE of choice as described in the Writing the first Java Extension tutorial of the SmartFoxServer 2X documentation. Copy the content of the /Server/TicTacToe/Extension-Java/src folder to your project' source folder.

In order to use the JavaScript Extension, at step 4 above create the TicTacToe-JS folder instead, then upload the content of folder /Server/TicTacToe/Extension-JS to it (*). Then, after importing the example in Unity and before running it, switch the EXTENSION_ID and EXTENSION_CLASS constants at the top of the LobbySceneController script in the /Assets/Scripts/Controllers folder.

(*) If more than one Game Node is running in your cluster, you will need to either execute step 4 above on each node, or follow the procedure described under Cluster Extension deployment.

Introduction to code

Let's briefly discuss how the code of this example is laid out.

Client code

The structure of the client code is the same we already discussed in the Cluster Lobby Mockup tutorial, including the GlobalManager singleton class used to share the connections to the Lobby and Game Nodes among the example scenes. The main differences in the client code are related to the Lobby scene and the Game scene of course.

In particular, the LobbySceneController and GameSceneController classes have been updated to add the logic related to the Buddy List management and interaction, and the logic to send invitations to play a game. Also, the BuddyChatHistoryManager static class contained in the the Managers subfolder provides helper methods and cross-scene data sharing for the buddy chat.

The Game scene instead contains a new, dedicated game object called TicTacToe Game. The client-side game logic is implemented by the TicTacToeGameManager class attached to that game object, while the GameSceneController class still takes care of all non-game-specific SmartFox events, like public chat and buddy messages.

Server code

As mentioned above, the Extension is available in two versions: Java and JavaScript. The Java Extension consists of the main TicTacToeExtension class, a number of handler classes (all ending with the Handler suffix) and a few classes representing the game objects (board, marks). The JavaScript Extension has a main file with its handler methods (TicTacToeExtension.js) and a number of game objects defined in a separate file (GameObjects.js).

The game logic

The flow of the tic-tac-toe game is quite simple and it is controlled by the TicTacToeGameManager class (on the client side) and by the server-side Extension deployed on the Game Node and attached to the Game Room when created.

On the client side, the manager class collects the user input, sends requests to the Game Node, receives game events from the server and updates the TicTacToe Game object in the scene according to the game state.
On the server side, the Room Extension monitors users entering or leaving the Room in order to start or stop the game, receives and validates the user moves, checks the game end conditions and sends the updated game state to clients.

This logic has been described in great detail in the tutorial of the non-clustered Tic-Tac-Toe example available in the SmartFoxServer 2X documentation. The code of the TicTacToeGameManager class is exactly the same from that example, with the exception of a single additional parameter passed to its Init() method. The Room Extension code is the exact copy of the one used in the non-clustered environment. For these reasons we won't describe the actual game flow in this tutorial, concentrating our attention on the peculiarities of the cluster environment, the features of the Lobby scene and the match-making condition when a Game Room must be joined. Refer to the original tutorial for everything else.

Please note that the server-side logic supports spectators in Game Rooms, and the option to turn them to actual players. This is not implemented in this example.

The Lobby scene

As mentioned in the introduction, the Lobby scene of this example features a friends list and the controls to manage it, a chat panel for privately chatting with friends and a user profile panel where the user can set their own details, among which a couple of properties useful in the match-making process.

Buddy List

Using SmartFoxServer's Buddy List API, developers can add an instant-messenger-like interface to any application, allowing users to see the state of their friends (the so-called buddies) and communicate with them regardless of the Room they joined or even, in the cluster environment, the Game Node they are connected to. In SmartFoxServer, in order to optimize the bandwidth usage, messaging and user presence notifications are by default limited to the users in the same Room. Using the Buddy List system in conjunction with the always active connection to the cluster's Lobby Node, this limit can be overridden in order to offer an improved user experience.

Buddy List initialization

The initialization of the Buddy List involves two steps, executed in the Lobby scene controller's Start() method. As soon as the scene is loaded, we add all the buddy-related event listeners (and others) to the sfsLobby instance and send the initialization request to the Lobby Node by means of the InitBuddyListRequest class.

			private void Start()
			{
				// Get reference to static SmartFox client instance for the lobby
				sfsLobby = gm.GetLobbyClient();
			
				// Add lobby event listeners
				AddSmartFoxLobbyListeners();
			
				...
			
				// Initialize Buddy List system
				// We have to check if it was already initialized, in case this scene was reloaded after leaving the Game scene
				if (!sfsLobby.BuddyManager.Inited)
					sfsLobby.Send(new InitBuddyListRequest());
				else
					InitializeBuddyClient();
			
				...
			}

Note that the Buddy List initialization must be executed only once. Given the Lobby scene could be reloaded multiple times (it's first loaded after the login, but then reloaded every time the Game scene is left), we have to check the BuddyManager.Inited flag to avoid re-sending the initialization request if the Buddy List system is already initialized. The BuddyManager class is a manager internal to the SmartFox API; its instance gives developers access to the buddy list, allowing interaction with the Buddy and BuddyVariable objects.

If the Buddy List is already initialized, we can move to the InitializeBuddyClient() method directly. Otherwise we have to wait for the BUDDY_LIST_INIT event fired by the SmartFox API, whose handler calls the same method. This updates the user interface populating the friends list (each item is represented by an instance of the Buddy List Item prefab, with its own script attached) and initializing the user profile panel based on the current user's own state in the Buddy List system.

Add, remove and block buddies

Buddies can be added to and removed from the user's buddy list stored on the server by means of two specific client requests represented by the AddBuddyRequest and RemoveBuddyRequest classes.

In this example, a buddy can be added by typing their username in the input field and clicking on the Add buddy button at the bottom of the list. Of course this approach is for learning purposes only: in a real-case scenario, for example, a dedicated in-game control could let user who met in a Game Room add each other as buddies. Whichever method is used to let users find new friends, they can be added to the buddy list by sending an AddBuddyRequest instance to the Lobby Node.

			public void OnAddBuddyButtonClick()
			{
				if (buddyNameInput.text != "")
				{
					// Request buddy adding to buddy list
					sfsLobby.Send(new AddBuddyRequest(buddyNameInput.text));
					buddyNameInput.text = "";
				}
			}

If the user is added successfully to the requester's buddy list, the BUDDY_ADD event is dispatched to the requester's client. The event handler takes care of creating a new buddy list item and adds it to the UI, just like when the list was first initialized.

			public void OnBuddyAdd(BaseEvent evt)
			{
				Buddy buddy = (Buddy)evt.Params["buddy"];
			
				// Add buddy list item at the top of the list
				AddBuddyListItem(buddy, true);
			}

Please note that when user A adds user B to their buddy list, this action is not mutual: user A is NOT added to B's buddy list automatically. But what if user A then tries to interact with user B as a buddy, for example sending a BuddyMessageRequest instance (see next section)? In this case user A will pop up anyway in B's buddy list as a temporary buddy. This means that A will be buddy of B as long as B is online. If user B disconnects from the Lobby Node and connects again, user A won't be in their buddy list anymore (unless, again, user A sends another message).
In our example we deal with this corner case by checking the Buddy.IsTemp property in the SetState() method of the list item script, substituting the item's Remove button with the Add button, allowing the user to turn a temporary buddy into an actual buddy.

Speaking of the Remove button on the buddy list item, a buddy can always be removed from the current user's buddy list by means of the RemoveBuddyRequest class. In our example we add a click listener to the Remove button when the item is initialized. When triggered, the request is sent to the Lobby Node.

			public void OnRemoveBuddyButtonClick(string buddyName)
			{
				// Request buddy removal from buddy list
				sfsLobby.Send(new RemoveBuddyRequest(buddyName));
			}

If the action is executed successfully, the BUDDY_REMOVE event is received by the Lobby scene controller's related listener. This causes the corresponding buddy list item to be destroyed.

Another available action is to block (or unblock) a buddy: a blocked buddy can't send messages to the current user. When the Block button on the buddy list item is clicked, its listener (which we added during the initialization of the list item) sends the BlockBuddyRequest instance to the Lobby Node.

			public void OnBlockBuddyButtonClick(string buddyName)
			{
				bool isBlocked = sfsLobby.BuddyManager.GetBuddyByName(buddyName).IsBlocked;
			
				// Request buddy block/unblock
				sfsLobby.Send(new BlockBuddyRequest(buddyName, !isBlocked));
			}

If the action is successful, the BUDDY_BLOCK event is notified to the requester's client and again processed by the Lobby scene controller's related listener. This updates the buddy list item by disabling most of its controls and placing it at the bottom of the list. In particular, the Block button is substituted by the Unblock button. In fact a buddy can be unblocked by means of the same request and its corresponding event.

Exchanging messages with buddies

When the user clicks on a Chat button available on the buddy list items, its listener causes a dedicated panel to appear in the Lobby scene. By means of the input field and Send button on the chat panel, and a custom event dispatched by the script attached to the panel, a request is sent by the Lobby scene controller to the Lobby Node by means of the BuddyMessageRequest class.

Note that a custom recipient parameter is added to the request, set to the name of the message recipient. This is not required by the server to dispatch the message (the buddy parameter passed to the request constructor is everything it needs), but it is useful when the message is notified back to the client.

			public void OnBuddyMessageSubmit(string buddyName, string message)
			{
				// Add a custom parameter containing the recipient name
				ISFSObject _params = new SFSObject();
				_params.PutUtfString("recipient", buddyName);
			
				// Retrieve buddy
				Buddy buddy = sfsLobby.BuddyManager.GetBuddyByName(buddyName);
			
				// Send message to buddy
				sfsLobby.Send(new BuddyMessageRequest(message, buddy, _params));
			}

The Lobby Node receives the request and delivers the message to the sender's buddy and to the sender themselves by means of the BUDDY_MESSAGE event, for which we registered an handler when the scene was loaded.
The handler passes the message to the BuddyChatHistoryManager static class to be memorized, as we want to keep track of all messages exchanged with buddies during the current session: this is where the custom recipient parameter comes in handy. In fact, as the sender also receives their own message back, we have to determine to which "conversation" it must be added.

Finally, the handler checks if the chat panel with the target buddy is currently visible. If yes, the message is displayed immediately. Otherwise an unread messages counter is increased and the corresponding buddy list item is updated to display the total number of unread messages.

For additional details on the buddy list implementation and more code snippets, check the Lobby: Buddies tutorial for Unity in the SmartFoxServer 2X documentation.

User profile

The profile panel is divided in two sections. On the left side users can set their own properties and state in the Buddy List system; on the right side users can set a couple of additional properties useful to show how the match-making system works. The panel is implemented by means of a dedicated prefab, with the UserProfilePanel class attached to it.

Buddy profile

User properties in the Buddy List system are set by means of Buddy Variables, which are stored on the Lobby Node and broadcasted, when set or updated, to all users referenced in the buddy list of their owner.

Some Buddy Variables are predefined and reserved to store specific information:

  • The online/offline status of the user; in fact a user can be connected to the Lobby Node but offline in the Buddy List system.
  • The nickname of the user, which can differ from the username used to login.
  • The personal state of the user with respect to its presence in the buddy list system (available, busy, etc); SmartFoxServer comes with a few predefined states (which we use in this example), but the list can be fully customized in the AdminTool's Zone Configurator module.

Buddy Variables can be online or offline. Offline variables can be accessed even if the user is not connected to SmartFoxServer (for example to store buddy details which are independent from the current session), while the online variables require the user presence. In our example the user's birth year is saved as an offline Buddy Variable, while their current mood as an online one. All reserved variables above are stored automatically as offline; custom variables instead can be set as offline by prefixing their name with the symbol defined by the SFSBuddyVariable.OFFLINE_PREFIX constant provided by the SmartFox API.

When the buddy portion of panel is initialized (see UserProfilePanel.InitBuddyProfile() method), che current value of the stored properties can be retrieved using the My-prefixed properties of the BuddyManager object: BuddyManager.MyOnlineState, BuddyManager.MyNickName, BuddyManager.MyState properties and BuddyManager.GetMyVariable() method return the value of the respective Buddy Variables set for the current user.

When the value of a profile field is changed, the panel script fires a custom event which is collected by the main controller, which in turn sends a request to the Lobby Node. For the online/offline flag, we have to use the GoOnlineRequest class; for all other properties, we can use the SetBuddyVariableRequest class.

			public void OnOnlineToggleChange(bool isChecked)
			{
				// Send request to toggle online/offline state
				sfsLobby.Send(new GoOnlineRequest(isChecked));
			}
			
			public void OnBuddyDetailChange(string varName, object value)
			{
				List<BuddyVariable> buddyVars = new List<BuddyVariable>();
				buddyVars.Add(new SFSBuddyVariable(varName, value));
				
				// Set Buddy Variables
				sfsLobby.Send(new SetBuddyVariablesRequest(buddyVars));
			}

The two requests respectively cause the BUDDY_ONLINE_STATE_UPDATE and BUDDY_VARIABLES_UPDATE events to be dispatched to the users who have the sender in their buddy list, and to the sender as well. In the related listeners we have to check who the event refers to, if the current user or one of their buddies. This can be determined by means of the isItMe event parameter. For example, check the following OnBuddyVariablesUpdate() event handler.

			public void OnBuddyVariablesUpdate(BaseEvent evt)
			{
				Buddy buddy = (Buddy)evt.Params["buddy"];
				bool isItMe = (bool)evt.Params["isItMe"];
				
				if (!isItMe)
					UpdateBuddyListItem(buddy);
				else
					DisplayUserStateAsBuddy();
			}

If the update refers to the current user, we have to update the UI accordingly: for example, if the user went offline in the Buddy List system, we have to hide the buddy list and chat panel, disable the inputs for buddy properties in the profile panel, etc. If the update refers to one of the buddies of the current user instead, we have to update the respective buddy list item accordingly.

Player profile

In order to show an example of match-making more significant than what you saw in previous tutorials, we need to define some player properties (which we'll call matching criteria) to filter the Game Rooms that the user is able to join. A common approach in multiplayer games is to let players face opponents with a similar skill level, so we made up the fictitious experience and ranking properties: hypothetically, the more a user plays over a period of time, the higher their experience should be; or the more they win, and the higher their ranking should be.

Of course this should be determined by the game logic automatically, based on the player behavior and performance. In order to keep the server-side logic simple and compatible with the non-cluster version of this example, we instead decided to add the experience and ranking to the user profile panel, making them manually editable to help testing different conditions for learning purposes.

The experience and ranking of a user are set to their default values when the Lobby scene is loaded for the first time during the current session, but in a real-case scenario their values should be retrieved from a database where the user state is stored and updated at the end of each game. As already mentioned, in our example those values are updated manually, and they get lost once the user disconnects from the Lobby Node.
In order to keep track of the current values, and have a direct access to them when executing the match-making, we make use of User Variables, which are initialized in the UserProfilePanel.InitPlayerProfile() method.

Just like for the Buddy Variables described before, changing the experience or ranking in the profile panel causes a custom event to be dispatched by the attached script. The event is handled by the scene controller script, which updates the respective User Variable by means of the SetUserVariableRequest class.

			public void OnPlayerDetailChange(string varName, object value)
			{
				List<UserVariable> userVars = new List<UserVariable>();
				userVars.Add(new SFSUserVariable(varName, value));
			
				// Set User Variables
				sfsLobby.Send(new SetUserVariablesRequest(userVars));
			}

Whether the variables are set for the first time or they are updated later, the USER_VARIABLES_UPDATE event is fired by the SmartFox API. In its handler we have to check which user the event refers to: if the current user, we have to update the profile panel (required in particular if the User Variables have been set for the first time), otherwise we can ignore the event because in our example we are not showing the user experience or ranking anywhere else.

			public void OnUserVariablesUpdate(BaseEvent evt)
			{
				User user = (User)evt.Params["user"];
			
				// Display player details in user profile panel
				if (user.IsItMe)
					userProfilePanel.InitPlayerProfile(user);
			}

Join a game

As already discussed in other tutorials, in a cluster environment there can be hundreds of thousands of Game Rooms distributed among many servers. Choosing the one to make the user join is done automatically by the Overcast Load Balancing system and SmartFoxServer's Match Expressions.

In this example, the criteria to make a Room "findable" are based on the two player properties we discussed before: experience and ranking. A Room can be joined if the user experience is equal to the one set when the Room was created and if their ranking is greater than or equal to the value set upon Room creation.

When the user clicks the Play button in the Lobby scene, a modal panel containing a "waiting" message and a Cancel button is displayed, then the JoinOrCreateGame() method tries to find or create a Game Room to join the user in.

			private void JoinOrCreateGame()
			{
				// Set the common Room settings to create a new game Room if needed
				ClusterRoomSettings settings = new ClusterRoomSettings("TicTacToe_" + new System.Random().Next());
				settings.GroupId = GAME_ROOMS_GROUP_NAME;
				settings.IsGame = true;
				settings.MaxUsers = 2;
				settings.MaxSpectators = 0;
				settings.MinPlayersToStartGame = 2;
				settings.NotifyGameStarted = false;
				settings.Extension = new RoomExtension(EXTENSION_ID, EXTENSION_CLASS);
			
				// Set Room as public or private
				if (invitedBuddy == null)
				{
					// Set Room as public
					settings.IsPublic = true;
			
					// 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(ROOMVAR_USER_EXPERIENCE, sfsLobby.MySelf.GetVariable(USERVAR_EXPERIENCE).GetStringValue()));
					roomVars.Add(new SFSRoomVariable(ROOMVAR_USER_MIN_RANKING, sfsLobby.MySelf.GetVariable(USERVAR_RANKING).GetIntValue()));
					settings.Variables = roomVars;
			
					// Set the match expression to search for an existing game Room (see Room Variable above)
					MatchExpression exp = new MatchExpression(ROOMVAR_USER_EXPERIENCE, StringMatch.EQUALS, sfsLobby.MySelf.GetVariable(USERVAR_EXPERIENCE).GetStringValue())
														 .And(ROOMVAR_USER_MIN_RANKING, NumberMatch.LESS_OR_EQUAL_THAN, sfsLobby.MySelf.GetVariable(USERVAR_RANKING).GetIntValue());
			
					// Send request
					sfsLobby.Send(new ClusterJoinOrCreateRequest(exp, new List<string>() { GAME_ROOMS_GROUP_NAME }, settings));
				}
				else
				{
					...
				}
			}

Most of the method code is dedicated to collecting the settings of the Game Room in case it should be created; this is done through the ClusterRoomSettings class, of which the most noticeable parameters are the following:

  • GroupId is the name of the group in which the Room should be included; all Rooms created by this example are grouped under "games".
  • MinPlayersToStartGame and NotifyGameStarted control the client notification by means of a reserved Room Variable that the game should start (after the Room is joined) because the minimum number of players was reached. This is not particularly useful in a 2-player game, so the notification is turned off.
  • Extension allows attaching the server-side Extension to the Game Room, which will be instantiated and initialized as soon as the Room is created.
  • IsPublic sets the game as public, which makes it joinable by all players who meet the requirements, or private, which makes it joinable upon invitation only. This is discussed in the next section of this tutorial; we can now focus on public games only.
  • Variables contains the list of Room Variables needed to set the requirements which, in conjunction with the Match Expression, allow the Game Node to find the Room. The first Room Variable indicates the required player experience, while the second one indicates the minimum required player ranking. Both are set to the values saved in the corresponding User Variables for the current user.

The JoinOrCreateGame() method then sets up the Match Expression allowing the cluster to actively search for a suitable Game Room. The expression is a logical condition defined in a very natural way, to perform any type of queries on Room objects (and User objects in other contexts). Specifically, when one or more conditions are passed to the ClusterJoinOrCreateRequest class constructor, they are used as "search criteria" that the Game Node uses to check if a Room can be joined by the current user or not.

In our code we concatenate two expressions with the And() method, representing one of the available logic operators (the other one is represented by the Or() method of course). The parameters passed to each MatchExpression instance are: the name of the Room Variable to check, the match operator (which is different for strings, numbers and booleans) and the value to compare, set to the value of the corresponding User Variable of the user looking for a game to join.

Finally the JoinOrCreateGame() method sends the appropriate request to the Lobby Node, passing the Match Expression, a list of Room Groups where to look for existing Game Rooms (which just contains the "games" Group, always assigned to new Rooms) and the settings in case a Room is not found and must be created.

When the Lobby Node receives the request, it locates an available Game Node through the Load Balancing logic, executes the Match Making logic to find a suitable Game Room (or, if not found, creates one) and notifies the client by means of the cluster's CONNECTION_REQUIRED event. The OnGameNodeConnectionRequired() handler on the GlobalManager singleton receives the event and starts the connect → login → join process on the Game Node, as already described in previous tutorials. Upon successful Game Room join, the Game scene is loaded.

Any error occurring during the process is notified to the scene controller, which schedules a new attempt while the user is still waiting.

Invitations

The buddy list items in the UI feature an Invite button represented by a joystick icon. When clicked, the name of the corresponding buddy is saved before the "waiting" message is displayed and the scene controller's JoinOrCreateGame() method is called. Having a buddy name saved in memory causes a different code path to be executed.

			private void JoinOrCreateGame()
			{
				// Set the common Room settings to create a new game Room if needed
				ClusterRoomSettings settings = new ClusterRoomSettings("TicTacToe_" + new System.Random().Next());
				settings.GroupId = GAME_ROOMS_GROUP_NAME;
				settings.IsGame = true;
				settings.MaxUsers = 2;
				settings.MaxSpectators = 0;
				settings.MinPlayersToStartGame = 2;
				settings.NotifyGameStarted = false;
				settings.Extension = new RoomExtension(EXTENSION_ID, EXTENSION_CLASS);
			
				// Set Room as public or private
				if (invitedBuddy == null)
				{
					...
				}
				else
				{
					// Set Room as private
					settings.IsPublic = false;
			
					// Invite a buddy
					settings.InvitedPlayers = new List<object>();
					settings.InvitedPlayers.Add(sfsLobby.BuddyManager.GetBuddyByName(invitedBuddy));
					settings.InvitationExpiryTime = 15;
			
					// Send request
					sfsLobby.Send(new ClusterJoinOrCreateRequest(settings));
				}
			}

In this case the method doesn't generate the Match Expression and doesn't set the Room Variables related to it, because the Game Node won't search for a suitable Game Room: instead a new game will be created anyway, and it will be set as private. This means that it can be joined upon invitation only: in fact SmartFoxServer sets a random password for the Game Room and grants the access only to users which are invited and accept the invitation. In order to send the invitation, we have to pass the Buddy object corresponding to the invited user through the InvitedPlayers setting.

The application flow is now the same we already discussed before: the JoinOrCreateGame() method sends the appropriate request to the Lobby Node, which locates an available Game Node through the Load Balancing logic, creates a Game Room based on the passed settings and notifies the client by means of the cluster's CONNECTION_REQUIRED event, just like you have seen before.

At the same time the Lobby Node sends the invitation to the recipient's client, which receives the INVITATION event. This is handled by the Lobby scene controller's OnInvitation() listener. As a user could receive multiple invitations during the time it takes to accept or refuse one, the method implements a queue where to hold new invitations while previous ones are processed. Invitation is wrapped in a custom class saving the date and time when it was received.

			public void OnInvitation(BaseEvent evt)
			{
				Invitation invitation = (Invitation)evt.Params["invitation"];
			
				// Add invitation wrapper to queue
				if (invitationQueue == null)
					invitationQueue = new Queue<InvitationWrapper>();
			
				invitationQueue.Enqueue(new InvitationWrapper(invitation));
			
				// Trigger invitation processing
				ProcessInvitations();
			}
		
			private void ProcessInvitations()
			{
				// If the invitation panel is visible, then the user is already dealing with an invitation
				// Otherwise we can go on with the processing
				if (!invitationPanel.IsVisible)
				{
					while (invitationQueue.Count > 0)
					{
						// Get next invitation in queue
						InvitationWrapper iw = invitationQueue.Dequeue();
			
						// Evaluate remaining time for replying
						DateTime now = DateTime.Now;
						TimeSpan ts = now - iw.date;
			
						// Update expiration time
						iw.expiresInSeconds -= (int)Math.Floor(ts.TotalSeconds);
			
						// Display invitation only if expiration will occur in 3 seconds or more, otherwise discard it
						if (iw.expiresInSeconds >= 3)
						{
							invitationPanel.Show(iw);
							break;
						}
					}
				}
			}

The ProcessInvitations() method takes care of extracting the first invitation wrapper from the queue, then checks the time passed since the invitation was received and updates the wrapper's internal expiration countdown: if there's still time for the user to reply to the invitation (at least 3 seconds in our example) an invitation panel is displayed in the scene.

Whether the time to reply to the invitation runs out, or the user actively refuses or accepts the invitation by clicking one of the two buttons on the panel, the script attached to it fires a custom event containing the original SFSInvitation object and a boolean indicating if the invitation was accepted or not. The event is caught by the Lobby scene controller which executes the OnInvitationReplyClick method.

Here we have to actually reply to the invitation by means of the InvitationReplyRequest class. If the invitation is accepted, the Lobby Node dispatches the CONNECTION_REQUIRED event to the client of the invited user, triggering the connect → login → join process executed by the GlobalManager singleton, as already mentioned before.

The last portion of the method checks if other invitations were received while the user was deciding whether to accept or refuse the first invitation: if the queue is not empty and the user accepted the invitation, all the subsequent ones are automatically refused; if the first invitation was refused, the next one is then processed by calling the ProcessInvitations() method again.

For additional details on the invitations implementation and more code snippets, check the Lobby: Matchmaking tutorial for Unity in the SmartFoxServer 2X documentation.

The Game scene

The Game scene is where the actual tic-tac-toe game runs after a Game Room was joined on the target Game Node to which the user was redirected by the Lobby Node.

As already mentioned before, in this tutorial we are not discussing the game logic implementation. So in this section we will simply highlight the differences of the Game scene with respect to the Cluster Lobby Mockup example.

The Game scene is loaded by the GlobalManager singleton as soon as the ROOM_JOIN event is received. The GameSceneController.Start() method is responsible of the scene's basic setup.

			private void Start()
			{
				// Get reference to static SmartFox client instances for the lobby and game nodes
				sfsLobby = gm.GetLobbyClient();
				sfsGame = gm.GetGameClient();
			
				// Set user as "Away" in lobby node Buddy List system
				if (sfsLobby.BuddyManager.MyOnlineState)
					sfsLobby.Send(new SetBuddyVariablesRequest(new List<BuddyVariable> { new SFSBuddyVariable(ReservedBuddyVariables.BV_STATE, "Away") }));
			
				// Add SmartFox event listeners
				AddSmartFoxLobbyListeners();
				AddSmartFoxGameListeners();
			
				// Initialize game manager
				gameManager.Init(sfsGame, sfsLobby.BuddyManager);
			
				// If user is the first player in the Room, set a timeout
				if (sfsGame.LastJoinedRoom.PlayerList.Count == 1)
					runTimer = true;
			}

The following steps are executed:

  • A reference to both SmartFox instances is retrieved from the GlobalManager singleton. We need both because this scene not only interacts with the Game Node, but also with the Lobby Node.
  • The state of the user in the Buddy List system is set automatically to "Away" (one of the predefined states in SmartFoxServer configuration) to let the buddies know that the user is busy playing the game.
  • The scene's own SmartFoxServer event listeners are added for both connections, to the Lobby Node and Game Node. The interaction with the Lobby Node includes a couple of buddy-related events, BUDDY_ADD and BUDDY_MESSAGE, as discussed in the next section. As it regards the Game Node instead, the scope of the scene controller is limited to the USER_ENTER_ROOM, USER_EXIT_ROOM and PUBLIC_MESSAGE events. The first two are mainly used to print a message in the UI's public chat panel; the last one will be discussed in the last chapter of this tutorial.
  • The TicTacToeGameManager class instance, attached to the TicTacToe Game object in the scene, is initialized. The parameters passed to its Init() method are: 1) the reference to the Game Node's SmartFox instance, so the game manager can add its own listeners to (mainly) interact with the server-side Room Extension; 2) the reference to the BuddyManager instance for the Lobby Node connection, so the game manager can check if the opponent is buddy of the current user and set the UI accordingly.
  • If the user is still the only player in the Room, a 20 seconds countdown is activated (see the Update() method). When over, a message is displayed suggesting that the player should leave the game. This is useful, for example, if a buddy didn't accept the invitation to play. Note that the countdown is stopped as soon as an opponent enters the Game Room (check the USER_ENTER_ROOM event listener).

Buddy List interaction

As already described when discussing the Lobby scene, all buddy-related requests and events are handled by the Lobby Node. This is true for the Game scene too, which in fact adds its own specific listeners to the sfsLobby instance, as mentioned before.

We also mentioned that when the current user joins a game and the scene is switched from Lobby to Game, their state is automatically set to "Away". This doesn't prevent their buddies from sending chat messages anyway, so we have to deal with this occurrence.

In our example we decided to keep things simple and implemented the handler for the BUDDY_MESSAGE event only. Its task is to add the message to the BuddyChatHistoryManager class (which, being static, its independent from the scene management) and increase the counter of unread messages. When, at the end of the game, the user goes back to the Lobby scene, the buddy list notifies all unread messages which the user can then check.

			private void OnBuddyMessage(BaseEvent evt)
			{
				// Add message to queue in BuddyChatHistoryManager static class
				BuddyChatMessage chatMsg = BuddyChatHistoryManager.AddMessage(evt.Params);
			
				// Increase message counter
				BuddyChatHistoryManager.IncreaseUnreadMessagesCount(chatMsg.buddyName);
			}

The Game scene controller is also in charge of adding the opponent to the current user's buddy list if this is triggered the actual game UI. In fact the TicTacToeGameManager fires a custom event handled by the OnAddBuddyClick() listener, which in turn sends the related request to the Lobby Node by means of the AddBuddyRequest() class. The success of the action is notified by the BUDDY_ADD event, which is handled by the scene controller to update the game UI.

Public chat

A quite common feature in multiplayer games, in particular turn-based ones, is the in-game chat. Our example is no exception and implements a panel to the right of the actual game area.

Tic-tac-toe is a game for two players only: for this reason the chat panel could just be an instance of the buddy chat panel already used in the Lobby scene. Nonetheless we wanted to provide a broader example for use cases in which more than two player are supported, or even spectators.
This is achieved by means of public messages, one of the most basic features of SmartFoxServer which simply requires users to be inside the same Room.

The UI features a scrollable area for the messages, an input field and a Send button. Whether the user clicks on the button or hits the enter key after typing their message, the controller's SendMessage() method is called. Here, an instance of the PublicMessageRequest class is passed to the Send() method of the SmartFox instance connected to the Game Node.

			private void SendMessage()
			{
				if (messageInput.text != "")
				{
					// Send public message to Room
					sfsGame.Send(new PublicMessageRequest(messageInput.text));
			
					// Reset message input
					messageInput.text = "";
					messageInput.ActivateInputField();
					messageInput.Select();
				}
			}

The Game Node receives the request and delivers the message to all users in the Room, including the sender, by means of the PUBLIC_MESSAGE event. The handler extracts the sender name and message from the event parameters and prints the message in the chat panel.


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: