SFS2X Docs / ExamplesUnity / tic-tac-toe
» Tic-Tac-Toe
» Overview
The Tic-Tac-Toe example shows how to develop a full multiplayer turn-based game with Unity and SmartFoxServer 2X by implementing 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 expands the lobby application discussed in previous tutorials, to which we added the actual game assets and client logic updating the existing mock-up Game scene.
This example features a server-side Extension which implements the main game logic: it determines if the game should start or stop, validates the player moves, updates the spectators if they join a game already in progress, checks if the victory condition is met, etc.
Using a server-side Extension — or, in other words, having an authoritative server — is a more flexible and secure option with respect to keeping all the game logic on the client-side only. Even if SmartFoxServer provides powerful tools for developing application logic on the client, this approach can be limiting when your games start getting more complex. Additionally, keeping the game state on the server-side allows overall better security from hacking or cheating attempts.
The server-side Extension is dynamically attached to the SFSGame 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.
This example also shows how to deal with two special features provided by Game Rooms: player indexes and spectators. Each user joining a Game Room is automatically assigned a unique player index which facilitates the tasks of starting and stopping the game, determining whose turn is it, etc. Spectators instead can replace players when they leave, by means of a dedicated client request.
In this document we assume that you already went through the previous tutorials, where we explained the subdivision of the application into three scenes, how to create a GlobalManager class to share the connection to SmartFoxServer among scenes and how to implement the buddy list, the match-making logic and invitations.
>> 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 TicTacToe 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.
» Server-side Extension
The server-side Extension is available in two versions: Java and JavaScript.
The Java Extension source code is contained in the /Assets/SFS2X-TicTacToe.zip file. Create and setup a new project in your Java IDE of choice as described in the Writing the first Java Extension document of the Java Extensions Development section. Copy the content of the /Extension-Java/src folder to your Java project' source folder.
The JavaScript Extension code is contained in the /Assets/SFS2X-TicTacToe.zip as well, under the /Extension-JS folder.
» Introduction to code
» Client code
The client code of this example is mostly the same we implemented for the Lobby template described in previous tutorials. The main differences, described in the next paragraphs, are related to the Room creation and the Game scene of course.
In particular, the Game scene contains a new, dedicated game object called TicTacToe Game which implements the actual game. The client-side game logic is quite simple (it collects the user input, sends requests to SmartFoxServer and receives game events updating the view accordingly) and it's contained in the TicTacToeGameManager class attached to the mentioned game object.
The GameSceneController class instead 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. In the following sections of this tutorial we will mainly refer to the Java version of the Extension, but JavaScript code snippets are also provided.
The Java Extension is made of its main class (TicTacToeExtension), 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 (TicTacToeExtension.js) with its handler methods and a number of game objects defined in a separate file (GameObjects.js).
» Creating the Game Room
In SmartFoxServer Extensions can be attached to a Zone or a Room, depending on the scope of the Extension itself. A Room Extension has a smaller scope, dealing with events, calls and users of that Room, while a Zone Extension can listen for a much larger number of events and controls all Rooms and users at once.
Our Tic-Tac-Toe makes use of a Room Extension, which must be attached to the Room when this is created. With respect to the Lobby: Matchmaking example, we have to modify the OnStartGameConfirm() method adding a the Extension dedicated setting.
public void OnStartGameConfirm(bool isPublic, string buddyName) { // Configure Room string roomName = sfs.MySelf.Name + "'s game"; SFSGameSettings settings = new SFSGameSettings(roomName); settings.GroupId = GAME_ROOMS_GROUP_NAME; settings.MaxUsers = 2; settings.MaxSpectators = 10; settings.MinPlayersToStartGame = 2; settings.IsPublic = isPublic; settings.LeaveLastJoinedRoom = true; settings.NotifyGameStarted = false; settings.Extension = new RoomExtension(EXTENSION_ID, EXTENSION_CLASS); // Additional settings specific to private games ... // Define players match expression to locate the users to invite ... // Request Room creation to server sfs.Send(new CreateSFSGameRequest(settings)); }
The Extension is declared by means of the RoomExtension object. As mentioned in the overview, we make use of two constants declaring the identifier and class of the Extension to be used. By default the constants point to the Java version of the Extension, but they can be switched in the properties declaration section of the LobbySceneController class.
» Extension initialization
On the server side, when the Room is created, its Extension is instantiated and the default init() method called immediately. This is where we initialize some properties and data structures used by the game logic and setup all the listeners required by our game.
@Override public void init() { trace("TicTacToe Game Extension launched"); // Initialize properties moveCount = 0; wins = new int[] {0,0,0}; gameBoard = new Board(); // Add request handlers addRequestHandler("ready", ReadyHandler.class); addRequestHandler("move", MoveHandler.class); addRequestHandler("restart", RestartHandler.class); // Register server event handlers addEventHandler(SFSEventType.USER_DISCONNECT, OnUserGoneHandler.class); addEventHandler(SFSEventType.USER_LEAVE_ROOM, OnUserGoneHandler.class); addEventHandler(SFSEventType.SPECTATOR_TO_PLAYER, OnSpectatorToPlayerHandler.class); }
function init() { trace("TicTacToe Game Extension launched"); // Initialize properties moveCount = 0; wins = [0,0,0]; gameBoard = new Board(); // Add request handlers addRequestHandler("move", onMoveRequest); addRequestHandler("restart", onRestartRequest); addRequestHandler("ready", onReadyRequest); // Register server event handlers addEventHandler(SFSEventType.USER_DISCONNECT, onUserGoneEvent); addEventHandler(SFSEventType.USER_LEAVE_ROOM, onUserGoneEvent); addEventHandler(SFSEventType.SPECTATOR_TO_PLAYER, onSpectatorToPlayerEvent); }
The first batch of handlers takes care of the requests sent by the clients in the Room by means of the ExtensionRequest class, respectively with the "ready", "move" and "restart" commands. The last three handlers listen for internal server events that might occur during the game flow and that we need to manage appropriately.
» Game initialization
On the client side, as soon as the Room is joined, the ROOM_JOIN event handler switches the active Unity scene from Lobby to Game. When the scene is loaded, its GameSceneController class gets a reference to the SmartFox client instance and adds its own listeners as usual. Additionally, it initializes the TicTacToeGameManager instance passing a reference to the SmartFox client, so that it can interact with the server-side Room Extension.
Speaking of the Game scene and its controller, these can be a little simplified with respect to the mock-up view of the previous examples, because we can remove all the player-related parts, which now belong to the TicTacToe Game object and its manager.
The scene controller also adds a new handler for the SPECTATOR_TO_PLAYER SmartFox event.
private void OnSpectatorToPlayer(BaseEvent evt) { User user = (User)evt.Params["user"]; // Display system message if (user == sfs.MySelf) PrintSystemMessage("Game joined as player"); else { PrintSystemMessage("User " + user.Name + " is now a player"); // Stop timeout if (user.IsPlayer) StopTimeout(false); } }
The event is triggered when a spectator requests to become a player through the game manager, as discussed below. The handler just takes care of a few secondary tasks (like interrupting the timeout described in the Lobby: Basics tutorial), while the actual impact on the game state and behavior is left to the serve-side Extension and the client-side TicTacToeGameManager script.
As mentioned above, when the Game scene is loaded, the scene controller initializes the actual game instance by calling the TicTacToeGameManager.Init() method. This is where the game manager adds its own handlers for the events dispatched by the SmartFox API, initializes the "tags" showing the player name and other information and, above all, sets the initial state of the game to WAITING_FOR_PLAYERS. Finally the method sends the first request to the server-side Extension.
Starting with the "ready" one, all requests sent by the game to the Extension include a command identifier string, an SFSObject containing custom parameters when needed and a target Room (which is always the last one we joined, because in this example we are not using the multi-join-Room feature of SmartFoxServer).
public void Init(SmartFox sfs) { this.sfs = sfs; this.buddyMan = sfs.BuddyManager; // Add SmartFoxServer-related event listeners required by this game sfs.AddEventListener(SFSEvent.EXTENSION_RESPONSE, OnExtensionResponse); sfs.AddEventListener(SFSEvent.USER_ENTER_ROOM, OnUserEnterRoom); sfs.AddEventListener(SFSEvent.USER_EXIT_ROOM, OnUserExitRoom); sfs.AddEventListener(SFSEvent.SPECTATOR_TO_PLAYER, OnSpectatorToPlayer); // Add listeners to "Add buddy" buttons in player tags // A custom event is fired so the main controller can send the appropriate request playerTags[1].addBuddyButton.onClick.AddListener(() => onAddBuddyClick.Invoke(playerTags[1].playerName.text)); playerTags[2].addBuddyButton.onClick.AddListener(() => onAddBuddyClick.Invoke(playerTags[2].playerName.text)); // Hide player tags playerTags[1].gameObject.SetActive(false); playerTags[2].gameObject.SetActive(false); // Display player tags UpdatePlayerTags(); // Set initial state state = State.WAITING_FOR_PLAYERS; // Display game state UpdateGameState(); // Tell Room Extension that user is ready sfs.Send(new ExtensionRequest("ready", new SFSObject(), sfs.LastJoinedRoom)); }
The game manager makes use of four SmartFox event handlers: the USER_ENTER_ROOM and USER_EXIT_ROOM event handlers simply update and show/hide the player tags; the SPECTATOR_TO_PLAYER handler also updates the player tag (in order to display the name of the new player) but it also updates the game view based on the current game state. Note that the player tag also contains an Add buddy button which is visible in the opponent's tag if they are not yet a buddy of the current user: this allows the user to add a new friend to their buddy list, as discussed in the previous tutorial.
The EXTENSION_RESPONSE event handler is the core of the client-side logic, as it receives the game updates from the Room Extension, as we'll discuss in the next sections.
private void OnExtensionResponse(BaseEvent evt) { string cmd = (string)evt.Params["cmd"]; ISFSObject data = (SFSObject)evt.Params["params"]; switch (cmd) { case "start": StartGame(data.GetInt("t"), data.GetInt("p1w"), data.GetInt("p2w")); break; case "state": SetSpectatorBoard(data.GetInt("t"), data.GetBool("run"), data.GetSFSArray("board"), data.GetInt("p1w"), data.GetInt("p2w"), data.ContainsKey("w") ? data.GetInt("w") : -1); break; case "stop": StopGame(); break; case "move": ExecuteMove(data.GetInt("t"), data.GetInt("r"), data.GetInt("c")); break; case "over": DeclareWinner(data.GetInt("w")); break; } }
» The game starts
The game begins when two players send the "ready" command to the Extension. The Extension's Ready handler is where the client request is received on the server side. Note that the "ready" notification is sent by all users joining the Game Room, so both players and spectators. Therefore the handler must check the user type and act accordingly.
public class ReadyHandler extends BaseClientRequestHandler { @Override public void handleClientRequest(User user, ISFSObject params) { TicTacToeExtension gameExt = (TicTacToeExtension) getParentExtension(); if (user.isPlayer()) { // Check if two players are available and start game if (gameExt.getParentRoom().getSize().getUserCount() == 2) gameExt.startGame(); } else { // Send game board state to spectator ISFSObject outParams = new SFSObject(); outParams.putInt("t", gameExt.getWhoseTurn() == null ? 0 : gameExt.getWhoseTurn().getPlayerId()); outParams.putBool("run", gameExt.isGameStarted()); outParams.putSFSArray("board", gameExt.getGameBoard().toSFSArray()); outParams.putInt("p1w", gameExt.getPlayerWins(1)); outParams.putInt("p2w", gameExt.getPlayerWins(2)); // If the game ended right before the spectator joined, also send its outcome if (!gameExt.isGameStarted() && gameExt.getParentRoom().getSize().getUserCount() == 2) outParams.putInt("w", gameExt.getLastWinnerId()); send("state", outParams, user); } } }
function onReadyRequest(params, user) { if (user.isPlayer()) { // Check if two players are available and start game if (getParentRoom().getSize().getUserCount() == 2) startGame(); } else { // Send game board state to spectator var outParams = new SFSObject(); outParams.putInt("t", whoseTurn == null ? 0 : whoseTurn.getPlayerId()); outParams.putBool("run", gameStarted); outParams.putSFSArray("board", gameBoard.toSFSArray()); outParams.putInt("p1w", wins[1]); outParams.putInt("p2w", wins[2]); // If the game ended right before the spectator joined, also send its outcome if (!gameStarted && getParentRoom().getSize().getUserCount() == 2) outParams.putInt("w", gameExt.getLastWinnerId()); send("state", outParams, user); } }
If the user who sent the request is a player and the supported number of players is reached, the handler calls the startGame() method on the Extension's main class. In case of a spectator instead, they could join the Room at any time: when the game is not yet started, when it's in progress or even when it's already over. For this reason the whole state of the game must be sent to spectators, so that their clients can be synchronized with all the others.
public void startGame() { if (gameStarted) throw new IllegalStateException("Game is already started"); // Reset state gameStarted = true; gameBoard.reset(); // No turn assigned? let's start with player 1 if (whoseTurn == null) whoseTurn = getParentRoom().getUserByPlayerId(1); // Send START event to clients with: // - the active player // - the total wins of player 1 and 2 ISFSObject outParams = new SFSObject(); outParams.putInt("t", whoseTurn.getPlayerId()); outParams.putInt("p1w", wins[1]); outParams.putInt("p2w", wins[2]); send("start", outParams, getParentRoom().getUserList()); }
function startGame() { if (gameStarted) throw ("Game is already started"); // Reset state gameStarted = true; gameBoard.reset(); // No turn assigned? let's start with player 1 if (whoseTurn == null) whoseTurn = getParentRoom().getUserByPlayerId(1); // Send START event to clients with: // - the active player // - the total wins of player 1 and 2 var outParams = new SFSObject(); outParams.putInt("t", whoseTurn.getPlayerId()); outParams.putInt("p1w", wins[1]); outParams.putInt("p2w", wins[2]); send("start", outParams, getParentRoom().getUserList()); }
The game start is signaled by the Extension through the "start" response, dispatched to both players and spectators (if any) by means of the send() method. On the client side, the EXTENSION_RESPONSE event handler calls the StartGame() method, passing the custom parameters sent by the Extension: the id of the active player and the total wins of both player 1 and player 2 since they joined the Room. The method resets the game board (just in case a previous game was restarted), sets the game state to RUNNING, updating the view accordingly, and enables user interaction with the game board.
private void StartGame(int whoseTurn, int p1Wins, int p2Wins) { this.whoseTurn = whoseTurn; // Reset game board board.Reset(); // Set game state state = State.RUNNING; // Display game state UpdateGameState(); // Display player wins playerTags[1].Wins = p1Wins; playerTags[2].Wins = p2Wins; // Enable game board if (sfs.MySelf.IsPlayer) board.IsEnabled = true; }
Other conditions can trigger the game start: in particular when one of the players leaves the game and a spectator takes their place, or when the Restart button is clicked by one of the two players after the game ended. We will discuss these conditions later on.
» Player turns
Players now alternate in taking turns, during which they click the board slots to place their marks. Every time an empty slot is clicked by a player, a custom event is fired by the board object and received by the game manager's OnBoardSlotClick() listener. This method checks if the user is allowed to interact with the board and sends the "move" command to the server, together with a custom SFSObject containing the coordinates (row and column) of the board slot clicked by the player.
public void OnBoardSlotClick(int row, int col) { // Only player in turn is allowed to interact with the board when the game is running if (state == State.RUNNING && sfs.MySelf.PlayerId == whoseTurn) { ISFSObject move = new SFSObject(); move.PutInt("r", row); move.PutInt("c", col); // Send move to Extension sfs.Send(new ExtensionRequest("move", move, sfs.LastJoinedRoom)); } }
The server's Move handler validates the move and sends it back to all users in the Room (spectators and players, including the active one who made the move) by means of the usual EXTENSION_RESPONSE event, this time with the "move" response identifier. Then it switches the active player and checks if the victory or tie condition is met, and declare the game over in case.
public class MoveHandler extends BaseClientRequestHandler { @Override public void handleClientRequest(User user, ISFSObject params) { // Request must contain the 'r' and 'c' parameters, indicating which board slot was clicked by the player in turn if (!params.containsKey("r") || !params.containsKey("c")) throw new SFSRuntimeException("Invalid request, one or more mandatory params are missing; required 'r' and 'c'"); TicTacToeExtension gameExt = (TicTacToeExtension) getParentExtension(); Board board = gameExt.getGameBoard(); // Extract parameters int row = params.getInt("r"); int col = params.getInt("c"); gameExt.trace(String.format("Handling move from player %s (%s, %s)", user.getPlayerId(), row, col)); // Game must be started if (gameExt.isGameStarted()) { // The player sending the move must be the active one if (gameExt.getWhoseTurn() == user) { // The board slot corresponding to the passed coordinates must be empty if (board.getMarkAt(row, col) == Mark.EMPTY) { // Set game board mark board.setMarkAt(row, col, user.getPlayerId() == 1 ? Mark.CROSS : Mark.RING); // Send move to all users in the Room, including the player who made it ISFSObject outParams = new SFSObject(); outParams.putInt("r", row); outParams.putInt("c", col); outParams.putInt("t", user.getPlayerId()); send("move", outParams, gameExt.getParentRoom().getUserList()); // Update game state gameExt.updateGameState(); } // Wrong move else gameExt.trace(ExtensionLogLevel.WARN, "Wrong move error; slot " + row + "," + col + " is not empty"); } // Wrong turn else gameExt.trace(ExtensionLogLevel.WARN, "Wrong move error; expected turn of player " + gameExt.getWhoseTurn() + ", received move from: " + user); } // Wrong game state else gameExt.trace(ExtensionLogLevel.WARN, "Wrong move error; game not yet started"); } }
function onMoveRequest(params, user) { // Request must contain the 'r' and 'c' parameters, indicating which board slot was clicked by the player in turn if (!params.containsKey("r") || !params.containsKey("c")) throw ("Invalid request, one or more mandatory params are missing; required 'r' and 'c'"); // Extract parameters var row = params.getInt("r"); var col = params.getInt("c"); trace("Handling move from player " + user.getPlayerId() + " (" + row + ", " + col + ")"); // Game must be started if (gameStarted) { // The player sending the move must be the active one if (whoseTurn == user) { // The board slot corresponding to the passed coordinates must be empty if (gameBoard.getMarkAt(row, col) == Mark.EMPTY) { // Set game board mark gameBoard.setMarkAt(row, col, user.getPlayerId() == 1 ? Mark.CROSS : Mark.RING); // Send move to all users in the Room, including the player who made it var outParams = new SFSObject(); outParams.putInt("r", row); outParams.putInt("c", col); outParams.putInt("t", user.getPlayerId()); send("move", outParams, getParentRoom().getUserList()); // Update game state updateGameState(); } // Wrong move else trace("Wrong move error; slot " + row + "," + col + " is not empty"); } // Wrong turn else trace("Wrong move error; expected turn of player " + whoseTurn + ", received move from: " + user); } // Wrong game state else trace("Wrong move error; game not yet started"); }
As the snippet above shows, the validation of a request can be quite extensive, to avoid cheating, hacking attempts and what more. In this example we make sure the request contains the expected parameters, the game is started, the move comes from the expected player and the board coordinates are valid.
On the client side, when a player move is received, the ExecuteMove() method switches the active player, shows the proper mark on the board and in general updates the view according to the new game state.
private void ExecuteMove(int playerId, int row, int col) { // Set new turn whoseTurn = (playerId == 1) ? 2 : 1; // Display game state UpdateGameState(); // Show mark on game board board.SetMark(row, col, playerId); }
» Game over and restart
As mentioned above, whenever a move is received by the Extension and the game state is updated, the victory condition is checked.
public void updateGameState() { // Increase moves counter moveCount++; // Switch turn whoseTurn = getParentRoom().getUserByPlayerId(whoseTurn.getPlayerId() == 1 ? 2 : 1); // Check if game is over checkGameOver(); } private void checkGameOver() { State state = gameBoard.getState(moveCount); if (state != State.RUNNING) { trace("Game ended"); // Stop game stopGame(false); // Get winner id int winnerId = getLastWinnerId(); if (state == State.END_WITH_WINNER) { // Update wins counter wins[winnerId]++; trace("Winner is player ", winnerId); } else if (state == State.END_WITH_TIE) { trace("It's a tie"); } ISFSObject outParams = new SFSObject(); outParams.putInt("w", winnerId); // Send update to clients send("over", outParams, getParentRoom().getUserList()); } }
function updateGameState() { // Increase moves counter moveCount++; // Switch turn whoseTurn = getParentRoom().getUserByPlayerId(whoseTurn.getPlayerId() == 1 ? 2 : 1); // Check if game is over checkGameOver(); } function checkGameOver() { var state = gameBoard.getState(moveCount); if (state != State.RUNNING) { trace("Game ended"); // Stop game stopGame(false); // Get winner id var winnerId = gameBoard.getWinner(); if (state == State.END_WITH_WINNER) { // Update wins counter wins[winnerId]++; trace("Winner is player ", winnerId); } else if (state == State.END_WITH_TIE) { trace("It's a tie"); } var outParams = new SFSObject(); outParams.putInt("w", winnerId); // Send update to clients send("over", outParams, getParentRoom().getUserList()); } }
The state of the game board determines if there's a winner or it's a tie, or if the game should continue. If the game is ended, the id of the winning player (1 for player one and 2 for player two) is sent to all users in the Room with the "over" response identifier. In case of a tie, the id is set to 0.
On the client side, the "over" response causes the DeclareWinner() method to be called. Here the game state is updated to WON, LOST or TIE according to the result sent by the server, and the board is disabled.
On game end, the client interface for the players shows a button to restart it. If one of the players clicks the Restart button, a "restart" request is sent to the server.
public void OnRestartButtonClick() { // Send request to Extension sfs.Send(new ExtensionRequest("restart", new SFSObject(), sfs.LastJoinedRoom)); }
On the server side the Restart handler checks if two players are still in the Room and eventually starts a new game calling the startGame() method already described before.
public class RestartHandler extends BaseClientRequestHandler { @Override public void handleClientRequest(User user, ISFSObject params) { TicTacToeExtension gameExt = (TicTacToeExtension) getParentExtension(); // Check if two players are available and start game if (gameExt.getParentRoom().getSize().getUserCount() == 2) gameExt.startGame(); } }
function onRestartRequest(params, user) { // Check if two players are available and start game if (getParentRoom().getSize().getUserCount() == 2) startGame(); }
Another condition can cause a game to end. In fact at any time during the game a user can leave the game, either by clicking the Leave game button (which makes the client leave the Room and switch back to the Lobby scene) or by closing the game client. In both cases the server Extension, through its OnUserGone handler detects that the user has left and, if player, sends the "stop" command to all users (the remaining player and the spectators, if any).
public class OnUserGoneHandler extends BaseServerEventHandler { @Override public void handleServerEvent(ISFSEvent event) throws SFSException { TicTacToeExtension gameExt = (TicTacToeExtension) getParentExtension(); Room gameRoom = gameExt.getParentRoom(); // Get id of the player who left the game // This can't be extracted from the User object available in event parameters, so we have to rely on other parameters Integer oldPlayerId; // User disconnected if (event.getType() == SFSEventType.USER_DISCONNECT) { @SuppressWarnings("unchecked") Map<Room, Integer> playerIdsByRoom = (Map<Room, Integer>) event.getParameter(SFSEventParam.PLAYER_IDS_BY_ROOM); oldPlayerId = playerIdsByRoom.get(gameRoom); } // User left the room else oldPlayerId = (Integer) event.getParameter(SFSEventParam.PLAYER_ID); if (oldPlayerId != null) { // Check if gone user was a player if (oldPlayerId > 0) { // Stop game resetting turn and players wins gameExt.stopGame(true); // If a player is still inside the game room, notify them that the game is now stopped if (gameRoom.getSize().getUserCount() > 0) gameExt.send("stop", null, gameRoom.getUserList()); } } } }
function onUserGoneEvent(event) { var gameRoom = getParentRoom(); // Get id of the player who left the game // This can't be extracted from the User object available in event parameters, so we have to rely on other parameters var oldPlayerId; // User disconnected if (event.getType() == SFSEventType.USER_DISCONNECT) { var playerIdsByRoom = event.getParameter(SFSEventParam.PLAYER_IDS_BY_ROOM); oldPlayerId = playerIdsByRoom.get(gameRoom); } // User left the room else oldPlayerId = event.getParameter(SFSEventParam.PLAYER_ID); if (oldPlayerId != null) { // Check if gone user was a player if (oldPlayerId > 0) { // Stop game resetting turn and players wins stopGame(true); // If a player is still inside the game room, notify them that the game is now stopped if (gameRoom.getSize().getUserCount() > 0) send("stop", null, gameRoom.getUserList()); } } }
Note that in case of abrupt disconnection (USER_DISCONNECT server event), the user id is not directly available, because the user is not in the system anymore. The id is needed to check if the user was a player, in which case the game must be stopped.
On the client side, the "stop" event causes the game state to change to INTERRUPTED and the game board to be disabled.
You should remember from the previous tutorials that a game can be either public or private. A private game is based on invitations, so it can't be joined by spectators. In our example, when a private game is interrupted because one of the players left, the other player can't do anything but leave the game too. In a public game instead they can wait for another player to join. Also, if one or more spectators are in the public Room when the "stop" event is received, the spectators view shows a button to join the game.
If the button is clicked, a SpectatorToPlayer request is sent to SmartFoxServer.
public void OnJoinButtonClick() { // Send request to server sfs.Send(new SpectatorToPlayerRequest()); }
If the switch is successful (an error could occur if, meanwhile, another user occupied the available player slot), the dedicated OnSpectatorToPlayer event handler on the server side checks if two players are now available and starts a new game.
public class OnSpectatorToPlayerHandler extends BaseServerEventHandler { @Override public void handleServerEvent(ISFSEvent event) throws SFSException { TicTacToeExtension gameExt = (TicTacToeExtension) getParentExtension(); // Check if two players are available and start game if (gameExt.getParentRoom().getSize().getUserCount() == 2) gameExt.startGame(); } }
function onSpectatorToPlayerEvent(event) { // Check if two players are available and start game if (getParentRoom().getSize().getUserCount() == 2) startGame(); }
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: