5.14 Tutorials: Advanced Board Game
The source FLA of this example is available under the Examples/AS2/sfsTris_advanced folder. |
» 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.
doc index |