• Examples (iOS)
• Examples (Android)
• Examples (C++)
Server API Documentation

 

» Tris (Tic-Tac-Toe)

» Overview

The Tris example shows how to create a 3D multiplayer version of the famous "Tic-Tac-Toe" game using Unity and SmartFoxServer 2X. It is fully compatible with the Tris clients from the other APIs, which means users can play together even if they are on different devices.

The following game flow is implemented:

  1. users login and join a Room called "The Lobby" by default; it's a sort of lobby where users can meet and chat before they start or join a game
  2. a user creates a Room where he can play the tic-tac-toe game against another user; as soon as the game is created, he will enter the new Room and wait for the second player
  3. when the second player joins the game (by selecting it in the list), the game starts
  4. SmartFoxServer alternates the players' turns and after every move it checks if there's a winner
  5. the game ends when no more moves are available or one of the player makes a row of three; at the end of the game the user can either start a new game or go back in the lobby

This example shows how to create a SmartFoxServer Room of type "game". Game Rooms are specifically aimed at representing games (or matches) as they can contain two different types of users: players and spectators. A Game Room also provides player indexes: each user joining the room is automatically assigned a unique player index which facilitates the tasks of starting and stopping the game (spectators are not indexed instead).
In this example spectators are not supported, to focus on the core game logic and keep the code simpler.

The Tris example also features a server-side Extension which is in charge of handling the game logic, as opposed to keeping the logic on the client-side only. In general, using a server-side Extension is a more flexible and secure option. 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. Also keeping sensitive game data on the server-side allows overall better security from hacking attempts, cheating, etc.

The server-side Extension that handles the game is dynamically attached to the Game Room representing the match started by one of the players. It sends the game events to the Unity interface. This way we can split the game logic from the game view, which is handled by the Unity client.

>> DOWNLOAD the source files <<

» Setup & run

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

  1. make sure your SmartFoxServer 2X installation contains the BasicExamples Zone definition;
  2. copy the /server/Extension_Java/deploy/Tris folder to the server's /SFS2X/extensions folder;
  3. start SmartFoxServer 2X (v2.13 or later is highly recommended);
  4. start Unity (v5.6 or later required);
  5. in the Projects panel click on the Open icon and browse to the /client/Tris folder, then click the Open button;
  6. wait for the project setup completion (Unity needs to regenerate some libraries);
  7. go to the Project panel, click on the Assets/TrisAssets/Scenes folder and double click on one of the scene files to open it (i.e. Login scene);
  8. if SmartFoxServer and Unity are not running on the same machine, change the IP address of the server in the inspector of the Controller game object in the Login scene;
  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.

All relevant client assets are contained in the Assets/TrisAssets folder; in particular the C# code is in the Scripts subfolder and the SmartFoxServer 2X client API DLLs are in the Plugins subfolder. Read the introduction to understand why multiple DLLs are used.

» Extension

The game features a server-side Extension, as described in the overview. The Extension is available in two versions: Java and JavaScript.

Step 2 above shows how to deploy the Java Extension, which is the default one for the example. In order to access its code, 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 /server/Extension_Java/source/src folder to your Java project's source folder.

In order to use the JavaScript Extension, in step 2 copy the /server/Extension_JavaScript/deploy/Tris-JS folder to the server's /SFS2X/extensions folder. Then, after importing the example in Unity, switch the EXTENSION_ID and EXTENSION_CLASS constants at the top of the LobbyController script in the Assets/TrisAssets/Scripts folder.

» Client code highlights

The example is split into three Unity scenes, each dedicated to a specific task: connection and login, lobby management and and the actual tic-tac-toe game logic. In each scene, the script containing the specific code is attached to a game object called Controller.

» Connection and login

From the UI perspective, the Login scene is similar to the Lobby example, except the login panel (here contained in a screen-space overlay canvas) doesn't feature a Zone name input field. The value set in the Inspector on the controller is used.
The code contains the expected event handlers for connection and login, with a couple of differences with respect to the Lobby example.

As the game features multiple scenes, it is mandatory to have a static (and as such, global) reference to the SmartFox instance, so that it can be accessed from anywhere in the game. For this purpose the singleton SmartFoxConnection class is used, and the SmartFox instance is passed to it when a successful connection is established with the server, before attempting the login.

private void OnConnection(BaseEvent evt) {
    if ((bool)evt.Params["success"])
    {
        // Save reference to SmartFox instance; it will be used in the other scenes
        SmartFoxConnection.Connection = sfs;

        // Login
        sfs.Send(new Sfs2X.Requests.LoginRequest(nameInput.text));
    }
    else
    ...
}

Upon login, it is time to load the next scene, the Lobby.

private void OnLogin(BaseEvent evt) {
    // Remove SFS2X listeners and re-enable interface
    reset();

    // Load lobby scene
    SceneManager.LoadScene("Lobby");
}

NOTE
Before loading a new scene, it is very important to remove all the SFS2X event listeners added by the current scene. Otherwise events generated in the next scene could trigger the handlers in this scene, with possible unwanted side effects or memory leaks. This can easily be done using the SmartFox.RemoveAllEventListeners() method.

» Lobby

When the Lobby scene is ready and the MonoBehaviour.Awake() method is called, the reference to the SmartFox instance is retrieved and the event handlers required by this scene are added. Also, the initial Room (called "The Lobby", predefined in the BasicExamples Zone) is joined.

void Awake() {
    Application.runInBackground = true;
    
    if (SmartFoxConnection.IsInitialized) {
        sfs = SmartFoxConnection.Connection;
    } else {
        SceneManager.LoadScene("Login");
        return;
    }

    loggedInText.text = "Logged in as " + sfs.MySelf.Name;
    
    // Register event listeners
    sfs.AddEventListener(SFSEvent.CONNECTION_LOST, OnConnectionLost);
    sfs.AddEventListener(SFSEvent.PUBLIC_MESSAGE, OnPublicMessage);
    sfs.AddEventListener(SFSEvent.ROOM_JOIN, OnRoomJoin);
    sfs.AddEventListener(SFSEvent.ROOM_JOIN_ERROR, OnRoomJoinError);
    sfs.AddEventListener(SFSEvent.USER_ENTER_ROOM, OnUserEnterRoom);
    sfs.AddEventListener(SFSEvent.USER_EXIT_ROOM, OnUserExitRoom);
    sfs.AddEventListener(SFSEvent.ROOM_ADD, OnRoomAdded);
    sfs.AddEventListener(SFSEvent.ROOM_REMOVE, OnRoomRemoved);
    
    // Populate list of available games
    populateGamesList();

    // Disable chat controls until the lobby Room is joined successfully
    chatControls.interactable = false;

    // Join the lobby Room (must exist in the Zone!)
    sfs.Send(new JoinRoomRequest("The Lobby"));
}

Again, the UI is structured similarly to the Lobby example (except the users list panel which was removed given its limited benefit). Other than the panel for the public chat, there is now a list of games instead of a list of chat rooms that a client can join. Additionally there are two buttons, to start a match and to disconnect and return to the Login scene. The "Start new game" button requests SmartFoxServer to create a Game Room attaching the Extension to it.

public void OnStartNewGameButtonClick() {
    // Configure Game Room
    RoomSettings settings = new RoomSettings(sfs.MySelf.Name + "'s game");
    settings.GroupId = "games";
    settings.IsGame = true;
    settings.MaxUsers = 2;
    settings.MaxSpectators = 0;
    settings.Extension = new RoomExtension(EXTENSION_ID, EXTENSION_CLASS);

    // Request Game Room creation to server
    sfs.Send(new CreateRoomRequest(settings, true, sfs.LastJoinedRoom));
}

If either we joined an existing Room (clicking on a list item in the UI) or created a new game, the client will receive a ROOM JOIN event, causing the OnRoomJoin() handler to unregister lobby callbacks from the SmartFoxServer API and load the Game scene to actually play the tic-tac-toe game. The else clause takes care of the initial join of "The Lobby" Room.

private void OnRoomJoin(BaseEvent evt) {
    Room room = (Room) evt.Params["room"];

    // If we joined a Game Room, then we either created it (and auto joined) or manually selected a game to join
    if (room.IsGame) {
        // Remove SFS2X listeners
        reset ();

        // Load game scene
        SceneManager.LoadScene("Game");
    } else {
        // Show system message
        printSystemMessage("\nYou joined a Room: " + room.Name);

        // Enable chat controls
        chatControls.interactable = true;
    }
}

» Initializing the game

The Game scene features the 3D game board, a sliding panel to chat with the opponent and, in the lower left corner, a small panel showing the game state and buttons to leave the match or restart it. Again, check the Lobby example to learn how to implement the chat feature.

The GameController script, attached to the Controller game object in the scene, is in charge of updating the panels UI based on the game state. The actual game logic and communication with the server side game Extension is demanded to the TrisGame class instead.
Similarly to the Lobby scene before, the MonoBehaviour.Awake() method in GameController retrieves the reference to the SmartFox instance and adds the handlers required for the chat and the events notifying if the opponent entered or left the game.

void Awake() {
    Application.runInBackground = true;
    
    if (SmartFoxConnection.IsInitialized) {
        sfs = SmartFoxConnection.Connection;
    } else {
        SceneManager.LoadScene("Login");
        return;
    }

    sfs.AddEventListener(SFSEvent.CONNECTION_LOST, OnConnectionLost);
    sfs.AddEventListener(SFSEvent.PUBLIC_MESSAGE, OnPublicMessage);
    sfs.AddEventListener(SFSEvent.USER_ENTER_ROOM, OnUserEnterRoom);
    sfs.AddEventListener(SFSEvent.USER_EXIT_ROOM, OnUserExitRoom);
    
    setCurrentGameState(GameState.WAITING_FOR_PLAYERS);

    // Create game logic controller instance
    trisGame = new TrisGame();
    trisGame.InitGame(sfs);
}

Once the client has joined the Room, loaded the Game scene and is ready to play, this is communicated to the Extension by the TrisGame instance in its InitGame() method.

Starting with the "ready" one, all commands sent by the game to the Extension are made of the command identifier string, a SFSObject containing custom parameters (but it can also be empty, like in the following snippet) and the target Room (which of course is always the last one we joined).

public void InitGame(SmartFox smartFox) {
    // Register to SmartFox events
    sfs = smartFox;
    sfs.AddEventListener(SFSEvent.EXTENSION_RESPONSE, OnExtensionResponse);

    // Setup my properties
    myPlayerID = sfs.MySelf.PlayerId;

    // Reset game board
    ResetGameBoard();

    // Tell extension I'm ready to play
    sfs.Send(new ExtensionRequest("ready", new SFSObject(), sfs.LastJoinedRoom));
}

The server Extension communicates with the clients by means of the EXTENSION_RESPONSE event, which triggers the OnExtensionResponse() callback. This updates the clients about the following game states:

These Extension responses are handled in the following client code.

public void OnExtensionResponse(BaseEvent evt) {
    string cmd = (string)evt.Params["cmd"];
    SFSObject dataObject = (SFSObject)evt.Params["params"];
    
    switch ( cmd ) {
        case "start":
            StartGame(dataObject.GetInt("t"),
                dataObject.GetInt("p1i"),
                dataObject.GetInt("p2i"),
                dataObject.GetUtfString("p1n"),
                dataObject.GetUtfString("p2n")
                );
            break;

        case "stop":
            UserLeft();
            break;

        case "move":
            MoveReceived(dataObject.GetInt("t"), dataObject.GetInt("x"), dataObject.GetInt("y"));
            break;

        case "win":
            ShowWinner(cmd, (int)dataObject.GetInt("w"));
            break;
                
        case "tie":
            ShowWinner(cmd, -1);
            break;
    }
}

» Handling moves

When the active player clicks on a non-occupied tile on the board, the following code sends the tile coordinates to the server Extension. Note that the "move" command identifier is used here.
The server will update the game state and check victory conditions.

public void PlayerMoveMade(int tileX, int tileY) {
    EnableBoard(false);

    SFSObject obj = new SFSObject();
    obj.PutInt("x", tileX);
    obj.PutInt("y", tileY);
    
    sfs.Send(new ExtensionRequest("move", obj, sfs.LastJoinedRoom));
}

As shown before, the client will receive the opponent's move via the OnExtensionResponse() callback: this updates the client board to match the server state.

private void MoveReceived(int movingPlayer, int x, int y) {
    whoseTurn = ( movingPlayer == 1 ) ? 2 : 1;

    if ( movingPlayer != myPlayerID ) {
        GameObject tile = GameObject.Find("tile" + x + y);
        TileController ctrl = (TileController)tile.GetComponent("TileController");
        ctrl.SetEnemyMove();
    }

    // Update interface
    SetTurnMessage();

    // Enable interface
    EnableBoard(true);
}

» Server code highlights

In this section we will briefly discuss the structure of the server side Extension attached to the game Room to provide the actual 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 win or tie condition is met, etc.

Specifically we will get the code snippets from the Java version of the Extension, which is enabled by default. The same logic (in a different language) is coded in the JavaScript Extension; you can refer to the Tris tutorial for HTML5 to check the JavaScript code snippets.

The Extension is made of its main class (TrisExtension), a number of handler classes (all ending with the Handler suffix) and a few utility classes.

As stated in the overview, this example doesn't support game spectators. Nonetheless in the Extension code — and in the description below — you will find references to that kind of user.
The reason is that the Extension is shared among different platforms (Unity, Flash, Android, etc) and in some cases the client support spectators. Just ignore those references.

» Extension's initialization

As this is a Room Extension, it is loaded by the server as soon as the Room is created. The default init() method is then always called: this is where we setup all the listeners required by our game.

@Override
public void init()
{
	trace("Tris game Extension for SFS2X started, rel. " + version);
	
	moveCount = 0;
	gameBoard = new TrisGameBoard();
	
	addRequestHandler("ready", ReadyHandler.class);
	addRequestHandler("move", MoveHandler.class);
	addRequestHandler("restart", RestartHandler.class);
	
	addEventHandler(SFSEventType.USER_DISCONNECT, OnUserGoneHandler.class);
	addEventHandler(SFSEventType.USER_LEAVE_ROOM, OnUserGoneHandler.class);
	addEventHandler(SFSEventType.SPECTATOR_TO_PLAYER, OnSpectatorToPlayerHandler.class);
}

The first batch of handlers takes care of the requests sent by the users in the Room by means of the ExtensionRequest class, respectively with the "ready", "move" and "restart" command (some of which we mentioned in the previous section).
The last three handlers listen for server events that might occur during the game and that we need to manage appropriately.

» Request handlers

The ReadyHandler class checks if the user who sent the "ready" command is a player: if yes, and if two players are now in the game Room, it sends the "start" signal to all the users in the Room (so including spectators); if no, it updates the spectator sending him the current board and turn state in the "specStatus" response.

To check if two players are now in the Room, we use the getSize() method of the current Room object (which, from the Extension point of view, is its parent Room): the method returns a RoomSize object with three more methods to get the total number of users in the Room, the number of spectators only or the number of players only. We use the latter:

if (gameExt.getGameRoom().getSize().getUserCount() == 2)
	gameExt.startGame();

Sending a response to clients is straightforward; for example:

send("start", resObj, getParentRoom().getUserList());

The first parameter is the response identifier, the second one is a SFSObject containing custom parameters and the third one is the list of the response recipients.

The MoveHandler class receives the board coordinates of the piece placed by a player. The player move is first validated (this is always a good practice!) to avoid cheating: the request must contain the expected parameters, the game must be already started, the sender of the request must be the active player (the player expected to make a move) and the target tile of the board must still be empty.

If all the conditions are met, the game board is updated, the move is sent to all users in the Room through the "move" response (so they can update the UI), the active player is switched and the victory or tie condition is checked. In case the game just ended, either the "win" or "tie" response is sent to all the users.

The RestartHandler class receives a request from one of the players to restart a match after it ended. It just checks if two players are currently in the Room and in case it sends the "start" signal again (see above).

» Event handlers

The OnUserGoneHandler event handler is called either when a disconnection from the server occurs or a user just leaves the game (clicking the Leave game button in the UI).
In both cases the purpose of the method is to get the identifier of the user who left the game and, if he was a player, end the game and send the "stop" notification to all the other users (player and possible spectators).

The OnSpectatorToPlayerHandler class makes the Extension realize that a spectator requested to be turned into a player. We just have to check if two players are again available in the Room and send the "start" signal to everybody.


To see an other advanced uses of SmartFoxServer, including a complete realtime game, you can now move onwards to the next examples.

NOTE
You should also read the comments to methods and properties in the example source code for additional informations.

» More resources

You can learn more about the SmartFoxServer basics by consulting the following resources: