1.3 Tutorials: C#/Unity3d SmartFoxTris PRO

» Introduction

In this article we'll develop a complete 3d turn-based game of multiplayer tic-tac-toe using server side extensions and Unity3D. We will look at the server side extension code and how it communicates with the game clients.

We won't however go into the Unity specific things too much and rather focus on how to use the SmartFox API, for further reference about how to do things in Unity please see the documentation included with the engine. The code for the game will build on the previous tutorial "C#/Unity3d lobby", so we won't explain the topics covered in there, how to set up a lobby, how to handle chat, etc, either.

What are the advantages of using server side logic? In general, using a server side extensions is a more flexibile 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 game

The server side extension that handles the game is going to be dynamically attached to each game room created, and it will send the game events to the interface. This way we can split the game logic, by moving it on the server, from the game view which is handled by the client.

» Server side events

Before we analyze the game code, it's a good idea to take a look at the the events that the extension will handle:

game start   The game begins when 2 players are available in the room
game end   The game ends when all board tiles have been filled. The extension will then calculate if there's a winner or if it's a tie
game stop   The game is interrupted because one of the players has left the room in the middle of a match

These conditions will be detected by the server side extension and will be sent to the clients to update the game status.

» Client side setup code

The most important part of the client code is located in LobbyGUI.cs in the function OnGUI:

if ( GUI.Button(new Rect(screenW-200, 255, 100, 24), "New game") ) {
	Hashtable roomObj = new Hashtable();
	roomObj.Add("name", smartFox.myUserName + "'s game");
	roomObj.Add("isGame", true);
	roomObj.Add("maxUsers", 2);
	roomObj.Add("uCount", true);

	Hashtable ext = new Hashtable();
	ext.Add("name", "tris");
	ext.Add("script", "sfsTris.as");

	roomObj.Add("extension", ext);

	smartFox.CreateRoom(roomObj);
}


This displays a button with the title "New Game", when clicked the code creates the new game room with support for 2 players and it attaches our server side extension to it. The actionscript file is called "sfsTris.as" and it is located (as usual) in the sfsExtensions/ folder. The name that we'll use to invoke the extension is "tris".

When we call smartFox.CreateRoom we will get the OnJoinRoom call back so we handle that and load the "game" scene.

public void OnJoinRoom(Room 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() ) {
		started = false;
		Debug.Log("Joining game room " + room.GetName());
		UnregisterSFSSceneCallbacks();
		Application.LoadLevel("game");
	}
}


In the game scene we do the usual things like registering our callbacks and such, as mentioned in the introduction we won't go into this since it was covered in the previous tutorial. Once it's all set up we wait until we have two players and start the game:

void StartGame() {
	chatWindow.AddSystemMessage("Game started! May the best man win");
	currentGameState = GameState.RUNNING;
}

The TrisGame object is our main game code, located in TrisGame.cs. It has all the basic variables we need set up:

private string extensionName;
private string gameStatus;
private int player1Id;
private int player2Id;
private string player1Name;
private string player2Name;
private int whoseTurn;
private Boolean gameStarted;
private int myPlayerID;
    

At this point the game will more or less stay idle and wait for events coming from the server. When we recieve an event we'll take care of it in TrisGame::OnExtensionResponse().


» The sfsTris.as extension

We will now inspect the code of the server side extension. The first few lines define the main variables to handle the game state:

var whoseTurn			// keep track of the current turn
var board				// a 2D array containing the board game data
var numPlayers			// count the number of players currently inside
var users = []			// an array of users

var gameStarted			// boolean, true if the game has started
var currentRoomId		// the Id of the room where the extension is running
var p1id				// userId of player1
var p2id				// userId of player2
var moveCount			// count the number of moves

var endGameResponse		// save the final result of the game

function init()
{
        numPlayers = 0
        gameStarted = false
}

function destroy()
{
        // Nothing special to do here
}


The init() function sets the gameStarted flag to false and the destroy() function is empty as we're not going to use any particular resources such as setIntervals or database connections.

We can now take a look a the handleInternalEvents() function and see how game events are dispatched:

function handleInternalEvent(evt)
{
        evtName = evt.name
        
        // Handle a user joining the room
        if (evtName == "userJoin")
        {
                // get the id of the current room
                if (currentRoomId == undefined)
                currentRoomId = evt["room"].getId()
                
                // Get the user object

                u = evt["user"]
                
                // add this user to our list of local users in this game room
                // We use the userId number as the key
                users[u.getUserId()] = u
                
                // Handle player entering game
                // Let's check if the player is not a spectator (playerIndex != -1)
                if (u.getPlayerIndex() != -1)
                {
                        numPlayers++
                        
                        if (u.getPlayerIndex() == 1)
                        p1id = u.getUserId()
                        else

                        p2id = u.getUserId()
                        
                        // If we have two players and the game was not started yet
                        // it's time to start it now!
                        if(numPlayers == 2 && !gameStarted)
                        startGame()
                }
                else
                {
                        
                        // If a spectator enters the room
                        // we have to update him sending the current board status

                        updateSpectator(u)
                        
                        if (endGameResponse != null)
                        _server.sendResponse(endGameResponse, currentRoomId, null, [u])
                }
        }
        
        // Handle a user leaving the room or a user disconnection
        else if (evtName == "userExit" || evtName == "userLost")
        {
                // get the user id

                var uId = evt["userId"]
                
                // get the playerId of the user we have lost
                var oldPid = evt["oldPlayerIndex"]
                
                var u = users[uId]
                
                // Let's remove the player from the list

                delete users[uId]
                
                // If the user we have lost was playing
                // we stop the game and tell everyone
                if (oldPid > 0)
                {
                        numPlayers--
                        
                        gameStarted = false
                        
                        if(numPlayers > 0)
                        {
                                var res = {}
                                res._cmd = "stop"
                                res.n = u.getName()
                                _server.sendResponse(res, currentRoomId, null, users)
                        }
                }
        }
        
        // Handle a spectator switching to a player

        else if (evtName == "spectatorSwitched")
        {
                if (!gameStarted && evt["playerIndex"] > 0)
                {
                        numPlayers++
                        
                        // Update the playerId

                        this["p" + evt["playerIndex"] + "id"] = evt["user"].getUserId()
                        
                        // If we now have 2 players the game should be started
                        if(numPlayers == 2)
                        	startGame()
                }
        }
}

Even if the code is made up of many lines we can split it into three main sections and analyze each of them:

1) Handle the "userJoin" event: in this case we're getting notified about the arrival of a new user inside the game room.
There's code here for checking whether the client is a player or spectator, but note that the client side currently doesn't include support for logging into a game as a spectator so we won't talk too much about that here. As soon as two players have joined we will start the game.

The game is started by simply sending a "start" command to the client.


2) Handle the "userExit" and "userLost" events: here we change the status of the game based on which client left the room.
In case he was not a player the game status will not be altered, otherwise we will send a "stop" action to the clients.

IMPORTANT NOTE: in order to see if the user that left the room was a player or a spectator we don't call the getPlayerIndex() method as you may expect, but we use the oldPlayerIndex property received in the event object. The reason for this is pretty simple: if the user was lost (disconnected) you can't get its playerId and if he's now in another room calling the getPlayerIndex() method will tell you the playerId for the new room, which we don't need.

Now that we have seen how the main game events are handled we can move to the second part of the article and see how game moves are sent between client and server.


doc index next »