The related source file for this article is found in the "Examples/mx2004/pro_smartFoxTris" folder.
INTRODUCTION
In this article we'll develop a complete turn-based game using server side extensions in SmartFoxServer PRO. The example will be based on the well known SmartFoxTris game, a multiplayer version of tic-tac-toe. We have already seen how to build the game keeping all the application logic on the client side. Now it's time to use a different approach and move all the game logic on the server side.
What are the advantages? 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 game we're going to develop is based on the tutorial that you can find in the SmartFoxServer documentation: a tic-tac-toe game for two players with support for spectators.
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 Flash interface. This way we can finally split the game logic, by moving it on the server, from the game view which is handled by the Flash 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. |
spectator enter |
|
A spectator has entered the room. We'll need to send him the current state of the game. |
spectator switch |
|
A spectator was turned into a player. We'll have to check if two players are available, if so a new game can start. |
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 under the "chat" label in the main timeline of the source .FLA file:
function createRoom(name:String, pwd:String, spec:Number)
{
hideWindow("newGameWindow")
var gameRoom:Object = new Object()
gameRoom.name = name
gameRoom.password = pwd
gameRoom.maxUsers = 2
gameRoom.maxSpectators = spec
gameRoom.isGame = true
gameRoom.isTemp = true
xt = {}
xt.name = "tris"
xt.script = "sfsTris.as"
gameRoom.extension = xt
smartfox.createRoom(gameRoom)
}
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".
Now we can move the playhead to the next frame called "game" and have a look at the first lines of code:
import it.gotoandplay.smartfoxserver.*
stop()
_global.gameStarted = false // global flag for tracking the game status
var extensionName:String = "tris" // Name of the extension that we'll call
var win:MovieClip // A movieclip used for dialogue windows
var myOpponent:User // My opponent user object
var player1Id:Number // Id of player 1
var player1Name:String // Name of player 1
var player2Name:String // Name of player 2
var player2Id:Number // Id of player 2
gamePaused("")
As you can see we setup the basic variables we'll use in the game to keep track of the players and we pause the game, calling the gamePaused() method.
At this point the SWF file will stay idle and wait for events coming from the server which we'll take care of in the onExtensionResponse() handler.
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.
The first thing to do is to see whether the client is a player or a spectator by checking his playerIndex (remember playerIndex == -1 means that the user is a spectator). Based on this information we can check if the game should start or if we should update the new spectator.
The game is started by simply sending a "start" command to the client. The code for sending the game status to the spectator is this:
function updateSpectator(user)
{
var res = {}
res._cmd = "specStatus"
res.t = whoseTurn
res.status = gameStarted
res.board = board
if (users[p1id] != undefined)
{
res.p1n = users[p1id].getName()
res.p1i = p1id
}
else
res.p1n = " "
if (users[p2id] != undefined)
{
res.p2n = users[p2id].getName()
res.p2i = p2id
}
else
res.p2n = " "
_server.sendResponse(res, currentRoomId, null, [user])
}
The code sends the board 2D array to the client together with the info (id and name) about the two players.
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.
3) Handle the "spectatorSwitched" event: when a spectator is turned into a player we should see if the room has now two players, and in this case start the game.
Now that we have seen how the main game events are handled we can talk about how the game moves are sent between client and server.
(continues on page 2)
|