[SmartFox] Advanced Board Game
[ April 14, 2005 ] by Marco Lapi, a.k.a Lapo
Article 15: how to add support for spectators in game rooms; spectators are a particular class of users that can join a game room but can't interact with the game


[ Introduction ]

In this tutorial we will learn how to add support for spectators in game rooms. Spectators are a particular class of users that can join a game room but can't interact with the game. When one of the players in the room leaves the game one of the spectators can take its place.
To demonstrate how SmartFoxServer handles spectators we will use the previous "SmartFoxTris" board game and we'll add spectators to it.
By the end of this tutorial you will have learned how to create a full turn-based games with spectator support all done on the client side. Also using the previous tutorials you will be able to add extra features like a buddy list or multi-room capabilities.

[ Requirements ]

Before proceeding with this tutorial it is necessary that you're already familiar with the basic SmartFoxServer concepts and that you've already studied the "SmartFoxTris" board game tutorial.

[ Objectives ]

We will enhance the previous "SmartFoxTris" board game by adding the following features:

» new options in the "create room" dialogue box: you will be able to specify the maximum amount of spectators for the game room
» new options in the "join" dialogue box: the user will have to choose if joining as a spectator or player
» the ability to switch from spectator to player when a player slot is free

[ Creating rooms with spectators and handling user count updates ]

Before we dive in the game code we'd like to have a look at the createRoom(roomObj) command.
The roomObj argument is an object with the following properties:

name the room name
password a password for the room (optional)
maxUsers the max. number of users for that room
maxSpectators the max. number of spectator slots (only for game rooms )
isGame a boolean, true if the game is a game room
variables an array of room variables (see below)

As you can see, not only you can specify the maximum number of users but also how many spectators you want for each game room. When you have created a game room with players and spectators you will receive user count updates not only for players but for spectators too.
As you may recall we can handle user count updates through the onUserCountChange(roomId) event handler where the roomId parameter tells us in which room the update occured.

Here follows the code used in this new version of "SmartFoxTris":

function updateRoomStatus(roomId:Number)
{
        var room:Room = smartfox.getRoom(roomId)
        var newLabel:String
        
        if (!room.isGame())
        newLabel = room.getName() + " (" + room.getUserCount() + "/" + room.getMaxUsers() + ")"
        else
        {
                newLabel = room.getName() + " (" + room.getUserCount() + "/" + room.getMaxUsers()
                newLabel += ")-(" + room.getSpectatorCount() + "/" + room.getMaxSpectators() + ")"
        }
        
        
        for (var i:Number = 0; i < roomList_lb.getLength(); i++)
        {
                var item:Object = roomList_lb.getItemAt(i)
                
                if (roomId == item.data)
                {
                        roomList_lb.replaceItemAt(i, newLabel, item.data)
                        break;
                }
        }
}
            

In the first line we get the room object, then we check if the room is a game and we dynamically create a label for the game list component we have on screen.

The label will be formatted like this: "roomName (currUsers / maxUsers) - (currSpectators / maxSpectators)" and we get the updated values by calling the following room methods:

room.getUserCount() returns the number of users in the room
room.getMaxUsers() returns the max. amount of users for that room
room.getSpectatorCount() returns the number of spectators currently in the room
room.getMaxSpectators() returns the max. number of spectators for that room

[ Handling Spectators ]

Adding spectators to the game introduces a few difficulties which we'll overcome by using Room Variables. The first problem is how to keep an "history" of the game in progress so that when a spectator joins a game room he's immediately updated to the current status of the game.

If you go back to the previous version of this board game you will notice that we've been using the sendObject() command to send moves from one client to the other. For the purpose of that game it was very easy to send game moves. Unfortunately in this new scenario using the sendObject is not going to work because moves are sent only between clients: if a spectator enters the room in the middle of a game we wouldn't be able to synch him with the current game status.

The solution to the problem is to keep the game status on the server side by storing the game board data in a Room Variable that we will call "board". By doing so each move is stored in the server side and a spectator joining in the middle of the game can easily read the current game status and get in synch with the other clients. In order to optimize the Room Variable as much as possible we will make our "board" variable a string of 9 characters, each one representing one cell of the 3x3 board.

We'll use a dot (.) for empty cells a "G" for green balls and an "R" for red balls: this way we send a very small amount of data each time we make a move.

Also we need another Room Variable to indicate which cell the player clicked and who sent the move: the new variable will be called "move" and it will be a string with 3 comma separated parameters: p, x, y

p = playerId
x = x pos of the cell
y = y pos of the cell

In other words this move: "1,2,1" will mean that player 1 has clicked on the cell at x=2 and y=1

Each time a move is done we will send the new status of the board plus the move variable to the other clients.
Summing up we will have four room variables in each game room: player1, player2, move, board. As you remember "player1" and "player2" are the names of the users playing in the room. These variables tell us how many players are inside and if we can start/stop the game.

[ Advanced Room Variables features ]

If you look at the documentation of the onRoomVariablesUpdate() event you will notice that it sends two arguments: roomObj and changedVars. We already know the roomObj argument but whe should introduce the second one: changedVars is an associative array with the names of the variables that were updated as keys.

In other words if you want to know if a variable called "test" was changed in the last update, you can just use this code:

smartfox.onRoomVariablesUpdate = function(roomObj:Room, changedVars:Object)
{
        // Get variables
        var rVars:Object = roomObj.getVariables()
        
        if (changedVars["test"])
        {
                // variable was updated, do something cool here...
        }
}
            

This feature may not seem particularly interesting at the moment, however it will become very useful as soon as we progress with the analysis of the code.
The actionscript code located in the frame labeled "chat" is very similar to the one in the previous version of the game, however we have added an important new flag called "iAmSpectator" which will indicate if the current player is a spectator or not.

Let's see how this flag is handled in the onJoinRoom event handler:

smartfox.onJoinRoom = function(roomObj:Room)
{
        if (roomObj.isGame())
        {
                _global.myID = this.playerId;
                
                if (_global.myID == -1)
                	iAmSpectator = true
                
                if (_global.myID == 1)
                	_global.myColor = "green"
                else if (_global.myID == 2)
                	_global.myColor = "red"
                
                // let's move in the "game" label
                gotoAndStop("game")
        }
        else
        {
                var roomId:Number 		= roomObj.getId()
                var userList:Object 	= roomObj.getUserList()
                
                resetRoomSelected(roomId)
                
                _global.currentRoom = roomObj
                
                // Clear current list
                userList_lb.removeAll()
                
                for (var i:String in userList)
                {
                        var user:User 		= userList[i]
                        var uName:String 	= user.getName()
                        var uId:Number		= user.getId()
                        
                        userList_lb.addItem(uName, uId)
                }
                
                // Sort names
                userList_lb.sortItemsBy("label", "ASC")
                
                chat_txt.htmlText += "<font color='#cc0000'>>> Room [ " 
				+ roomObj.getName() + " ] joined</font>";
        }
}
            

In the past tutorials you have learned that every player in a game room is automatically assigned a playerId, which will help us recognize player numbers. When a spectator joins a game room you will be able to recognize him because his/her playerId is set to -1. In other words all players will have their own unique playerId while the spectator will be identified with a playerId = -1

In the first lines of the code, after checking if the currently joined room is a game, we check the playerId to see if we'll be acting as a regular player or as a spectator. The rest of the code is just the same as the previous version so we can move on the next frame, labeled "game".

[ The game code ]

The first part of the code inside this frame sets up the player based on the "iAmSpectator" flag.

var vObj:Array = new Array()

// If user is a player saves his name in the user variables
if (!iAmSpectator)
{
        vObj.push({name:"player" + _global.myID, val:_global.myName})
        smartfox.setRoomVariables(vObj)
}

// If I am a spectator we analyze the current status of the game
else
{
        // Get the current board server variable
        var rVars:Object = smartfox.getActiveRoom().getVariables()
        var serverBoard:String = rVars["board"]
        
        // If both players are in the room the game is currently active
        if (rVars["player1"].length > 0 && rVars["player2"].length > 0)
        {
                _global.gameStarted = true
                
                // Show names of the players
                showPlayerNames(rVars)
                
                // Draw the current game board
                redrawBoard(serverBoard)
                
                // Check if some has won or it's a tie
                checkBoard()
        }
        
        // ... the game is idle waiting for players. We show a dialog box asking 
				// the spectator to join the game
        else
        {
                win = showWindow("gameSpecMessage")
                win.message_txt.text = "Waiting for game to start!" 
				+ newline + newline + "press [join game] to play"
        }
}
            

As you will notice in each action we will take, we'll check if the current user is a player or not and behave appropriately. In this case we save the user name in a room variable if the client is a player. On the contrary, if we are handling a spectators, we have to check the status of the game.

In the first "else" statement we first verify if the game is currently running or not. If the game is not ready yet (i.e. there's only one player in the room) a dialogue box will be shown on screen with a button allowing the user to join the game and become a player. If the game is running we set the _global.gameStarted flag, show the player names on screen and call the redrawBoard method passing the "board" room variable (which represents the game status).

Also the checkBoard() method is invoked to verify if there's a winner in the current game: this covers the case in which the spectator enters the room when a match has just finished with a winner or a tie. Now it's time to analyze the onRoomVariablesUpdate handler which represents the core of the whole game logic. Don't be scared by the length of this function, we'll dissect it in all its sections:

smartfox.onRoomVariablesUpdate = function(roomObj:Room, changedVars:Object)
{
        // Is the game started?
        if (inGame)
        {
                // Get the room variables
                var rVars:Object = roomObj.getVariables()
                
                // Player status changed!
                if (changedVars["player1"] || changedVars["player2"])
                {
                        // Check if both players are logged in ...
                        if (rVars["player1"].length > 0 && rVars["player2"].length > 0)
                        {
                                // If game is not yet started it's time to start it now!
                                if (!_global.gameStarted)
                                {
                                        _global.gameStarted = true
                                        
                                        if (!iAmSpectator)
                                        {
                                                hideWindow("gameMessage")
                                                _root["player" + opponentID].name.text 
						= rVars["player" + opponentID]
                                        }
                                        else
                                        {
                                                hideWindow("gameSpecMessage")
                                                showPlayerNames(rVars)
                                        }
                                        
                                        // It's player one turn
                                        _global.whoseTurn = 1
                                        
                                        // Let's wait for the player move
                                        waitMove()
                                }
                        }
                        
                        // If we don't have two players in the room we have to wait for them!
                        else
                        {
                                // Reset game status
                                _global.gameStarted = false
                                
                                // Clear the game board
                                resetGameBoard()
                                
                                // Reset the moves counter
                                moveCount = 0
                                
                                // movieclip reference used for showing a dialog box on screen
                                var win:MovieClip
                                
                                // If I am a the only player in the room I will get a 
				// dialogue box saying we're waiting
                                // for the opponent to join the game.
                                if (!iAmSpectator)
                                {
                                        win = showWindow("gameMessage")
                                        win.message_txt.text = "Waiting for player " 
					+ ((_global.myID == 1) ? "2" : "1") 
					+ newline + newline + "press [cancel] to leave the game"
                                        
                                        // Here we reset the server variable called "board"
                                        // It represents the status of the game board 
					// on the server side
                                        // Each dot (.) is an empty cell of the board (3x3)
                                        var vv:Array = []
                                        vv.push({name:"board", val:".........", persistent: true})
                                        
                                        smartfox.setRoomVariables(vv)
                                }
                                
                                // The spectator will be shown a slightly different dialogue box, 
				// with a button for becoming a player
                                else
                                {                                      
                                        win = showWindow("gameSpecMessage")
                                        win.message_txt.text = "Waiting for game to start!" 
					+ newline + newline + "press [join game] to play"
                                }
                        }
                }
                
                // The game restart was received
                else if (changedVars["move"] && rVars["move"] == "restart")
                {
                        restartGame()
                }
                
                // A move was received
                else if (changedVars["move"])
                {
                        // A move was done
                        // the MOVE room var is a string of 3 comma separated elements
                        // p,x,y
                        // p = player who did the move
                        // x = pos x of the tile
                        // y = pos y of the tile
                        
                        // Get an array from the splitted room var
                        var moveData:Array = rVars["move"].split(",")
                        var who:Number = moveData[0]
                        
                        var tile:String = "sq_" + moveData[1] + "_" + moveData[2]
                        var color:String = (moveData[0] == 1) ? "green" : "red"
                        
                        // Draw move on player board
                        if (!iAmSpectator)
                        {
                                // Ignore my moves
                                if (who != _global.myID)
                                {
                                        // Visualize opponent move
                                        setTile(tile, color)
                                        moveCount++
                                        
                                        checkBoard()
                                        
                                        nextTurn()
                                }
                        }
                        
                        // Draw move on spectator board
                        else
                        {
                                redrawBoard(rVars["board"])
                                
                                checkBoard()
                                
                                nextTurn()
                        }
                }
        }
}
			

Before we start commenting each section of the code it would be better to isolate the most important things that this method does.
Basically the code checks three different conditions:

1) If there's been a change in the player room variables, called player1 and player2. When one of these vars changes, the game must be started or stopped, based on their values. The code related with this condition starts with this line:

if (changedVars["player1"] || changedVars["player2"])

2) If the "move" variable was set to "restart". This is a special case and it's the signal that one of the players has clicked on the "restart" button to start a new game. The code related to this section start with this line:

else if (changedVars["move"] && rVars["move"] == "restart")

3) If the "move" variable was updated with a new player move. In this case we'll update the game board, check for a winner and switch the player turn. The code related to this section start with this line:

else if (changedVars["move"])

Let's start by analyzing section one: the code should look familiar as it is very similar to the one used in the first "SmartFoxTris" game.
If one of the two player variables was changed then a change in the game status will occur: if the game was already started (_global.gameStarted = true) and one of the player left, we have to stop the current game showing a message window. The message is going to be slightly different if you are a player or a spectator. The latter will be shown a button to join the game and become a player.

Please also note that the player that remains in the game will clear both his board game and the "board" room variable which in turn will update the other spectators. On the contrary if the game was idle and now the two player variables are ready, we can start a new game.

The second section of the code is much simpler: when the "move" variable is set to "restart" the restartGame() method is called which will clear the game board making it ready for a new match.

Finally the third section is responsible of handling the moves sent by the opponent.
As we said before in this article we have used a comma separated string to define a single player move. By using the split() String method we obtain an array of 3 items containing the playerId followed by the coordinates of the board cell that was clicked.

[ Turning spectators into players ]

As we have mentioned before when one of the player slots is free, spectators will be able to join the game and become players.

The message box showed to spectators is called "gameSpecMessage" and you can find it in the library under the "_windows" folder. By opening the symbol you will notice a button called "Join Game" that calls the switchSpectator() function in the main game code:

function switchSpectator()
{
        smartfox.switchSpectator(smartfox.activeRoomId)
}
            

This very simple function invokes the switchSpectator() command of the SmartFoxServer client API which will try to join the spectator as player
in the game room. There's no guarantee that the request will succeed as another spectator might have sent this request before, filling the empty slot before our request gets to the server. In any case the server will respond with a onSpectatorSwitched event:

smartfox.onSpectatorSwitched = function(success:Boolean, newId:Number, room:Room)
{
        if (success)
        {
                // turn off the flag
                iAmSpectator = false
                
                // hide the previous dialogue box
                hideWindow("gameSpecMessage")
                
                // setup the new player id, received from the server
                _global.myID = newId
                
                // Setup the player color
                _global.myColor = (_global.myID == 2) ? "red" : "green"
                
                // Setup player name
                _root["player" + _global.myID].name.text = _global.myName
                opponentID = (_global.myID == 1) ? 2 : 1
                
                // Store my new player id in the room variables
                var vObj:Array = []
                vObj.push({name:"player" + _global.myID, val:_global.myName})
                
                smartfox.setRoomVariables(vObj)
        }
        
        // The switch from spectator to player failed. Show an error message
        else
        {
                var win:MovieClip = showWindow("gameMessage")
                win.message_txt.text = "Sorry, another player has entered"
        }
}
            

As you can see we get a "success" boolean argument which will tell us if the operation was successfull or not. If it was we can turn the user into a player by assigning him the newId paramater as playerId, then setting the appropriate player color and finally we update the room variables with the new player name.

[ Conclusions ]

In this tutorial you have learned how to use server-side variables to keep the game status and handle spectators in a turn-based game. We reccomend to examine the full source code of this multiplayer game to better understand every part of it. Once you will be confident with this code you will be able to create an almost unlimited number of multiplayer turn-based games and applications by combining the many features we have discussed in the the other tutorials.

Also you if you have any questions or doubts, post them in our forums.


    
 
 
Name: Marco Lapi, a.k.a Lapo
Location: Fossano, Italy
Age: 34
Flash experience: started out with Flash 4 back in 1999
Job: web designer/developer
Website: http://www.gotoandplay.it/
 
 
| Homepage | News | Games | Articles | Multiplayer Central | Reviews | Spotlight | Forums | Info | Links | Contact us | Advertise | Credits |

| www.smartfoxserver.com | www.gotoandplay.biz | www.openspace-engine.com |

gotoAndPlay() v 3.0.0 -- (c)2003-2008 gotoAndPlay() Team -- P.IVA 03121770048