[ 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.
|