1.3 Tutorials: C#/Unity3d SmartFoxTris PRO (p.2)

» Sending moves

The game logic is now running completely on the server side while the client will only take care of the visualization. The central part of the client code is the extension response handler in TrisGame.cs:

public void OnExtensionResponse(object data, string type) {
	// We only use XML based messages in this tutorial, so ignore string and json types
	if ( type == SmartFoxClient.XTMSG_TYPE_XML ) {
		// For XML based communication the data object is a SFSObject
		SFSObject dataObject = (SFSObject)data;
		switch ( dataObject.GetString("_cmd") ) {
			case "start":
				StartGame(	(int)dataObject.GetNumber("t"),
							(int)dataObject.GetNumber("p1i"),
							(int)dataObject.GetNumber("p2i"),
							dataObject.GetString("p1n"),
							dataObject.GetString("p2n"));
					break;
                    
			case "stop":
				UserLeft();
				break;
                    
			case "move":
				MoveReceived((int)dataObject.GetNumber("t"), (int)dataObject.GetNumber("x"), (int)dataObject.GetNumber("y"));
				break;
                    
			case "win":
				ShowWinner(dataObject.GetString("_cmd"), (int)dataObject.GetNumber("w"));
				break;
                    
			case "tie":
				ShowWinner(dataObject.GetString("_cmd"), -1);
				break;
		}
		// Restarts are send as cmd and not _cmd - possible bug in SFS extension
		if (dataObject.GetString("cmd") == "restart") {
			sfs.SendXtMessage(extensionName, "ready", null);
		}
	}
}

For each command coming from the extension we perform the appropriate action, for example if a "start" message is received we'll save the player names, ids and turn locally and call the startGame() function. If the "stop" action is received will halt the game and show a dialog window and so on...
The whoseTurn variable will keep track of the player turn during the game, so that each player will know if it's his turn or not.

This is what we do when the game starts:


/**
 * Start the game
 */	
private void StartGame(int whoseTurn, int p1Id, int p2Id, string p1Name, string p2Name) {
	this.whoseTurn = whoseTurn;
	player1Id = p1Id;
	player2Id = p2Id;
	player1Name = p1Name;
	player2Name = p2Name;

	ResetGameBoard();
	SetTurn();
	EnableBoard(true);
	gameStarted = true;

	// Reset GUI if this is a restart
	GameGUI gui = (GameGUI)GameObject.Find("Game GUI").GetComponent("GameGUI");
	gui.SetStartGame();
} 
    

Once we have performed these very simple actions, the players will be able to click on the game board and send moves.
The code we use for sending the player move is this:


/**
 * On board click, send move to other players
 */
public void PlayerMoveMade(int tileX, int tileY) {
	EnableBoard(false);
	Hashtable obj = new Hashtable();
	obj.Add("x", tileX);
	obj.Add("y", tileY);
	sfs.SendXtMessage(extensionName, "move", obj);
}


The PlayerMoveMade(...) function is called from the Unity callback OnMouseDown() in the script TileController.cs that's attached to each tile object. The tiles are all named tileXY, where X and Y are the coordinate in the grid they posses so all we do is extract the 5th and 6th characters from the name and send that as the coordinate to the server.

This is how the extension handles the "move" action:

function handleMove(prms, u)
{
        if (gameStarted)
        {
                if (whoseTurn == u.getPlayerIndex())
                {
                        var px = prms.x
                        var py = prms.y
                        
                        if (board[py][px] == ".")
                        {
                                board[py][px] = String(u.getPlayerIndex())
                                
                                whoseTurn = (whoseTurn == 1) ? 2 : 1
                                
                                var o = {}
                                o._cmd = "move"
                                o.x = px
                                o.y = py
                                o.t = u.getPlayerIndex()
                                
                                _server.sendResponse(o, currentRoomId, null, users)
                                
                                moveCount++
                                
                                checkBoard()
                        }
                }
        }
}


We have added some extra validations to avoid cheating: first we check that the game is really started, if not we'll refuse the request.
Then we verify if the player who sent the move was allowed to do so, in other words if it's his turn. Once these two checks are passed we can finally store the move in the board array, using the playerId as value.

In the next lines we set the turn for the other player and send the move data and turn id to both clients. Also we keep track of the number of moves by incrementing the moveCount variable and we call the checkBoard() method to see if there's a winner or a tie.

function checkBoard()
{
	var solution = []
	
	// All Rows
	for (var i = 1; i < 4; i++)
	{
		solution.push(board[i][1] + board[i][2] + board[i][3])
	}
	
	// All Columns
	for (var i = 1; i < 4; i++)
	{
		solution.push(board[1][i] + board[2][i] + board[3][i])
	}
	
	// Diagonals
	solution.push(board[1][1] + board[2][2] + board[3][3])
	solution.push(board[1][3] + board[2][2] + board[3][1])
	
	
	var winner = null
	
	while(solution.length > 0)
	{
		var st = solution.pop()
		
		if (st == "111")
		{
			winner = 1
			break
		}
		else if (st == "222")
		{
			winner = 2
			break
		}
	}
	
	var response = {}
	
	// TIE !!!
	if (winner == null && moveCount == 9)
	{
		gameStarted = false
		
		response._cmd = "tie"
		_server.sendResponse(response, currentRoomId, null, users)
		
		endGameResponse = response
	}
	else if (winner != null)
	{
		// There is a winner !
		gameStarted = false
		
		response._cmd = "win"
		response.w = winner
		_server.sendResponse(response, currentRoomId, null, users)
		
		endGameResponse = response
	}
}

Even if there is a lot of code the function works in a very simple way: it creates an empty array called "solutions" and fills it with all possible rows and columns where you can put three items in a row.

The available solutions are 8 in total: 3 columns + 3 rows + 2 diagonals.

When the array is populated we loop through it and if one combinantion of three is found then we have a winner! Also we check if there's no more moves available. In that case we'll have a tie.


» Receiving moves and client updates

As we've already seen the moves are received in the onExtensionResponse(). The method that handles the move update is called MoveReceived()

/**
 * Handle the opponent move
 */
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();
	}
	SetTurn();
	EnableBoard(true);
}


We dynamically create a string with the name of the tile and find the GameObject with that name. This will be our tile with the attached TileController script. Next we get the TileController component and call SetEnemyMove().

TileController::SetEnemyMove() simply set the tiles state to the opponents playing piece (TileState.RING or TileState.CROSS).


» Conclusions

In this tutorial we've learned how to program a game with the game logic separated from the game view by using a server side extension as the "game controller". There are many advantages to writing your game this way rather than having the logic on the client(s); better game code organization, better game security, better integration with external data etc...

We suggest to analyze both client and server code to fully understand how they work and experiment with your own variations and ideas. Good luck!


« previous doc index