• Examples (Godot 4.x)
• Examples (iOS)
• Examples (Java/Android)
• Examples (C++)
Server API Documentation

 

» Tic-Tac-Toe

» Overview

The Tic-Tac-Toe example shows how to develop a full multiplayer turn-based game with Godot 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 Godot 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:

  1. unzip the examples package;
  2. launch Godot, click on the Import button and navigate to the SFS_TicTacToe_GD4 folder;
  3. click the Build button in the top right corner of the Godot editor before running the example.

The client's C# code is in the Godot project's res://scripts folder, while the SmartFoxServer 2X client API DLLs are in the res:// folder.

» Server-side Extension

The server-side Extension is available in two versions: Java and JavaScript, and the game client expects the Java extension to be deployed. At the end of this article we also explain how to use the JavaScript version.

To deploy the Java Extension, copy the TicTacToe/ folder from SFS2X-TicTacToe-Ext/deploy/ to your current SFS2X installation under SFS2X/extensions/.

The source code of the java Extension is provided under the SFS2X-TicTacToe-Ext/Java/src folder. You can 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 SFS2X-TicTacToe-Ext/Java/src folder to your Java project' source 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 node called TicTacToeGame 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 node.
The GameManager 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 Godot scene from Lobby to Game. When the scene is loaded, its GameManager class gets a reference to the SmartFox client instance and adds its own listeners as usual. Additionally, it initializes the TicTacToeGame 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 manager, 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 node and its manager.
The scene manager 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 TicTacToeGame script.

As mentioned above, when the Game scene is loaded, the scene manager initializes the actual game instance by calling the TicTacToeGame.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)
		{
			playerTags = new PlayerTag[3];
			playerTags[1] = playerTag1;
			playerTags[2] = playerTag2;
	
	
			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);
	
	
			// Hide player tags
			playerTags[1].Hide();
			playerTags[2].Hide();
	
			// 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 Godot series to learn new features of SmartFoxServer.

» Using the JavaScript Extension

As mentioned in the introduction we provide a JavaScript version of the server side code. If you want to learn more about writing SFS2X Extensions in JavaScript take a look at this guide.

» Deploy the Extension

From the example package copy the SFS2X-TicTacToe-Ext/Javascript/TicTacToe-JS/ folder to your SmartFox installation, under SFS2X/extensions/.

» Change the client code

Last step is to modify the client code so that game Rooms will load the JavaScript Extension rather than the Java one. From the scripts/ folder in the TicTacToe example open the GameBoard.cs file. At line 39-40 modifiy the following constants:
		private const string EXTENSION_ID = "TicTacToe-JS";
    	private const string EXTENSION_CLASS = "TicTacToeExtension.js";
	

Now you can rebuild the project and test it.

» More resources

You can learn more about the SmartFoxServer concepts discussed in this example by consulting the following resources: