SFS2X Docs / ExamplesUnity / lobby-buddies
» Lobby: Buddies
» Overview
This tutorial is the second in a series of three in which we lay the foundations for a lobby application to be used as a template in multiplayer game development. A lobby is a staging area which players access before joining 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.
This example expands the one described in the Lobby: Basics tutorial by adding a friends list to the Lobby scene, with icons to represent the state of friends, controls to add, remove and block them and a panel to exchange private messages. Additionally users can access a profile panel to set their state and other details that will be visibile to their friends.
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 connection to SmartFoxServer among those scenes.
>> DOWNLOAD the source files <<
» Setup & run
In order to setup and run the example, follow these steps:
- unzip the examples package;
- launch the Unity Hub, click on the Open button and navigate to the LobbyBuddies folder;
- if prompted, select the Unity Editor version to use (v2021.3 or later is recommended);
- click on the SmartFoxServer → Demo Project Setup menu item in the Unity UI, and follow the additional instructions provided in the appeared Editor window.
The client's C# code is in the Unity project's /Assets/Scripts folder, while the SmartFoxServer 2X client API DLLs are in the /Assets/Plugins folder. Read the introduction to understand why multiple DLLs are used.
» Introduction to code
The base code for this example is the same of the previous one, expanded to implement the new features.
The LobbySceneController and GameSceneController classes have been updated to add the logic related to the Buddy List management and interaction. Also, in the Managers subfolder, the new BuddyChatHistoryManager static class provides helper methods and cross-scene data sharing for the buddy chat, as discussed below.
» Buddy list initialization
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) in their contact list and communicate with them regardless of the SmartFoxServer Room they are connected to. In fact, in order to optimize the bandwidth usage in SmartFoxServer, messaging and user presence notifications are by default limited to the users in the same Room. Using the Buddy List system, this limit can be overridden in order to offer an improved user experience.
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 listeners to the buddy-related events (which are declared in the dedicated SFSBuddyEvent class) and send the specific InitBuddyList request to the server.
private void Start() { ... // Add event listeners AddSmartFoxListeners(); ... // Initialize Buddy List system if (!sfs.BuddyManager.Inited) sfs.Send(new InitBuddyListRequest()); else InitializeBuddyClient(); } private void AddSmartFoxListeners() { ... sfs.AddEventListener(SFSBuddyEvent.BUDDY_LIST_INIT, OnBuddyListInit); sfs.AddEventListener(SFSBuddyEvent.BUDDY_ERROR, OnBuddyError); sfs.AddEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_UPDATE, OnBuddyOnlineStateUpdate); sfs.AddEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, OnBuddyVariablesUpdate); sfs.AddEventListener(SFSBuddyEvent.BUDDY_BLOCK, OnBuddyBlock); sfs.AddEventListener(SFSBuddyEvent.BUDDY_REMOVE, OnBuddyRemove); sfs.AddEventListener(SFSBuddyEvent.BUDDY_ADD, OnBuddyAdd); sfs.AddEventListener(SFSBuddyEvent.BUDDY_MESSAGE, OnBuddyMessage); }
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 sending the initialization request if the Buddy List system is already initialized. The BuddyManager is an internal manager class of 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.
private void OnBuddyListInit(BaseEvent evt) { // Initialize buddy-related entities InitializeBuddyClient(); }
If an error occurs during initialization, or during any other interaction with the Buddy List system by means of its specific requests, the BUDDY_ERROR event is fired. In this example, in case of errors we show a warning with its details.
The InitializeBuddyClient() method updates the user interface to match the state of their buddies and their own state in the Buddy List system.
private void InitializeBuddyClient() { // Init buddy-related data structures buddyListItems = new Dictionary<string, BuddyListItem>(); // For the buddy list we use a scrollable area containing a separate prefab for each buddy // The prefab contains clickable buttons to chat with the buddy and block or remove them List<Buddy> buddies = sfs.BuddyManager.BuddyList; // Display buddy list items // All blocked buddies are displayed at the bottom of the list foreach (Buddy buddy in buddies) AddBuddyListItem(buddy, !buddy.IsBlocked); // Set current user details in buddy system userProfilePanel.InitBuddyProfile(sfs.BuddyManager); // Display user state in buddy list system DisplayUserStateAsBuddy(); // Show/hide buddy list buddyListPanel.SetActive(sfs.BuddyManager.MyOnlineState); }
First of all, the buddy list of the user is accessed by means of the BuddyManager instance mentioned before. In this example each Buddy in the list is represented by an instance of the Buddy List Item prefab contained in the /Assets/Prefabs folder, with its own script attached. Other than the buddy name (or nickname, if set), the list item shows the buddy state in the system by means of an icon, the age and mood custom properties and a number of buttons to interact with the relative buddy. We will further discuss the list item and how the buddy state and properties are represented in the next section.
The other task of the InitializeBuddyClient() method is to show the state and properties of the current user in the Buddy List system. This is achieved by updating the profile panel, by showing the actual state in the top, right corner of the scene and by showing or hiding the buddy list entirely.
» The user profile
The profile panel is where the user sets its own properties and state in the Buddy List system by means of Buddy Variables: similarly to User Variables and Room Variables, Buddy Variables are stored on the server and broadcasted to clients. In particular, when set or updated, a Buddy Variable is broadcasted to all the users referenced in the buddy list of its 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 SmartFoxServer 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 panel is initialized, 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 and BuddyManager.GetMyVariable() properties and method return the value of the respective Buddy Variables set for the current user.
public void InitBuddyProfile(IBuddyManager buddyManager) { // User online/offline state onlineToggle.isOn = buddyManager.MyOnlineState; // User nickname nickInput.text = (buddyManager.MyNickName != null ? buddyManager.MyNickName : ""); // Available states and current user state stateDropdown.AddOptions(buddyManager.BuddyStates); stateDropdown.SetValueWithoutNotify(buddyManager.BuddyStates.IndexOf(buddyManager.MyState)); // Buddy variable: user birth year BuddyVariable year = buddyManager.GetMyVariable(LobbySceneController.BUDDYVAR_YEAR); yearInput.text = ((year != null && !year.IsNull()) ? year.GetIntValue().ToString() : ""); // Buddy variable: user mood BuddyVariable mood = buddyManager.GetMyVariable(LobbySceneController.BUDDYVAR_MOOD); moodInput.text = ((mood != null && !mood.IsNull()) ? mood.GetStringValue() : ""); }
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 SmartFoxServer. For the online/offline flag, we have to use the GoOnline request; for all other properties, we can use the SetBuddyVariable request.
public void OnOnlineToggleChange(bool isChecked) { // Send request to to toggle online/offline state sfs.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 sfs.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 OnBuddyVariablesUpdate event handler.
public void OnBuddyVariablesUpdate(BaseEvent evt) { Buddy buddy = (Buddy)evt.Params["buddy"]; bool isItMe = (bool)evt.Params["isItMe"]; if (!isItMe) ... else ... }
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 (see below), disable the inputs in the profile panel, etc.
If the update refers to one of the buddies of the current user, we have to update the respective buddy list item accordingly. In our example, the event's buddy parameter is passed to the SetState() method of the script assigned to the list item. The parameter represents an instance of the Buddy class and it gives access to all the profile settings of the respective user: nickname, state, birth year and mood custom values (through Buddy Variables) and various flags (IsOnline, IsBlocked, etc).
public void SetState(Buddy buddy) { // Nickname mainLabel.text = "<b>" + ((buddy.NickName != null && buddy.NickName != "") ? buddy.NickName : buddy.Name) + "</b>"; // Age DateTime now = DateTime.Now; BuddyVariable year = buddy.GetVariable(LobbySceneController.BUDDYVAR_YEAR); mainLabel.text += (year != null && !year.IsNull()) ? " <size=12>(" + (now.Year - year.GetIntValue()) + " yo)</size>" : ""; // Mood BuddyVariable mood = buddy.GetVariable(LobbySceneController.BUDDYVAR_MOOD); if (mood != null && !mood.IsNull() && mood.GetStringValue() != "") { moodLabel.text = mood.GetStringValue(); moodLabel.gameObject.SetActive(true); } else moodLabel.gameObject.SetActive(false); // Save blocked state // (see LobbySceneController.UpdateBuddyListItem method) isBlocked = buddy.IsBlocked; // If buddy is not blocked and is temporary, show add button and hide remove button bool showAddButton = !buddy.IsBlocked && buddy.IsTemp; addButton.gameObject.SetActive(showAddButton); removeButton.gameObject.SetActive(!showAddButton); // Status icon and buttons if (buddy.IsBlocked) { statusIcon.sprite = statusIconBlocked; blockButton.transform.GetComponentInChildren<Image>().sprite = buttonIconUnblock; EnableInteraction(false); SetChatMsgCounter(0); } else { blockButton.transform.GetComponentInChildren<Image>().sprite = buttonIconBlock; if (!buddy.IsOnline) { statusIcon.sprite = statusIconOffline; EnableInteraction(false); SetChatMsgCounter(0); } else { string state = buddy.State; if (state == "Available") statusIcon.sprite = statusIconAvailable; else if (state == "Away") statusIcon.sprite = statusIconAway; else if (state == "Occupied") statusIcon.sprite = statusIconOccupied; EnableInteraction(true); } } }
One last note on the user state in the Buddy List system, set by means of the BV_STATE reserved Buddy Variable. When the user joins a game and the Lobby scene is replaced by the Game scene, their state is automatically set to "Away" (one of the predefined states in SmartFoxServer configuration) to let their buddies know they are busy playing the game.
private void OnRoomJoin(BaseEvent evt) { // Set user as "Away" in Buddy List system if (sfs.BuddyManager.MyOnlineState) sfs.Send(new SetBuddyVariablesRequest(new List<BuddyVariable> { new SFSBuddyVariable(ReservedBuddyVariables.BV_STATE, "Away") })); // Load game scene SceneManager.LoadScene("Game"); }
The "Available" state is then restored in the Game scene controller when the user leaves the scene to go back to the Lobby scene.
public void OnLeaveButtonClick() { // Leave current game room sfs.Send(new LeaveRoomRequest()); // Set user as "Available" in Buddy List system if (sfs.BuddyManager.MyOnlineState) sfs.Send(new SetBuddyVariablesRequest(new List<BuddyVariable> { new SFSBuddyVariable(ReservedBuddyVariables.BV_STATE, "Available") })); // Return to lobby scene SceneManager.LoadScene("Lobby"); }
» 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: AddBuddy and RemoveBuddy.
In this example, a buddy can be added by typing their username in the input field and clicking on the Add buddy button. Of course this approach is for learning purposes only: in a real-case scenario the lobby could feature a user search based on some criteria, or users could just meet in a Game Room when playing and dedicated controls in the Game scene would let them add each other as buddies. Whichever method is used to let users find new friends, they can be added to the buddy list with the AddBuddy request.
public void OnAddBuddyButtonClick() { if (buddyNameInput.text != "") { // Request buddy adding to buddy list sfs.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 adding 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 BuddyMessage request (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 SmartFoxServer 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 sending the RemoveBuddy request. In our example we add a click listener to the Remove button when the item is initialized. When triggered, the request is sent to SmartFoxServer.
public void OnRemoveBuddyButtonClick(string buddyName) { // Request buddy removal from buddy list sfs.Send(new RemoveBuddyRequest(buddyName)); }
If the action is executed successfully, the BUDDY_REMOVE event is dispatched to the requester's client. In our example 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 BlockBuddy request to the server.
public void OnBlockBuddyButtonClick(string buddyName) { bool isBlocked = sfs.BuddyManager.GetBuddyByName(buddyName).IsBlocked; // Request buddy block/unblock sfs.Send(new BlockBuddyRequest(buddyName, !isBlocked)); }
If the action is successful, the BUDDY_BLOCK event is notified to the requester's client. In our example this event 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
As mentioned at the beginning of this tutorial, one of the main goals of the Buddy List system is to allow users to interact when they are outside Rooms (like in this example) or in different Rooms, specifically by exchanging Buddy Messages.
When the user clicks on the Chat button available on the buddy list item, its listener causes a dedicated panel to appear in the Lobby scene. In our example we want to keep track of the messages exchanged with buddies during the current session (in other words only the messages sent and received since the last login — a full-fledged messages history would require a server-side Extension and a database and it's outside the scope of this tutorial). To achieve this we implemented the BuddyChatHistoryManager static class. The reason why we use a static class will be clear in a minute. When the chat panel is displayed, we retrieve the previous messages from the BuddyChatHistoryManager class and show them in the chat.
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 BuddyMessage request is sent by the Lobby scene controller to SmartFoxServer. 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 = sfs.BuddyManager.GetBuddyByName(buddyName); // Send message to buddy sfs.Send(new BuddyMessageRequest(message, buddy, _params)); }
SmartFoxServer receives the request and delivers the message to the user's buddy and to the user 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 class to be memorized: 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.
public void OnBuddyMessage(BaseEvent evt) { // Add message to queue BuddyChatMessage chatMsg = BuddyChatHistoryManager.AddMessage(evt.Params); // Show message or increase message counter if (chatPanel.BuddyName == chatMsg.buddyName) { // Display message in chat panel chatPanel.PrintChatMessage(chatMsg); } else { // Increase unread messages count int unreadMsgCnt = BuddyChatHistoryManager.IncreaseUnreadMessagesCount(chatMsg.buddyName); // Update buddy list item BuddyListItem buddyListItem = buddyListItems[chatMsg.buddyName]; buddyListItem.gameObject.transform.SetAsFirstSibling(); buddyListItem.SetChatMsgCounter(unreadMsgCnt); } }
We already 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, so how can we deal with this occurrence?
An option would be to implement the same buddy chat panel in the Game scene, so that users can keep chatting with their buddies. The problem is that, potentially, a user could receive messages from multiple buddies; also, the Game scene already features the in-game chat... in other words the UI could easily become very messy.
In our example we decided to keep things simple and just implement the handler for the BUDDY_MESSAGE event (added by its controller script when the Game scene is loaded). The handler's 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, the buddy list notifies all unread messages which the user can then check.
public 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); }
You can now proceed to the next example in this Unity series to learn new features of SmartFoxServer.
» More resources
You can learn more about the SmartFoxServer concepts discussed in this example by consulting the following resources: