[SmartFox Pro] Realtime maze
[ July 29, 2005 ] by Marco Lapi, a.k.a Lapo
Article 6: create a real-time maze game protype for two players, concentrating on handling the basic logic of the game in an extension, and optimizing the amount of data sent between clients and server


(continues from page 1)

Player movements and updates

The "start" event is sent from the server extensions using this code:

function startGame()
{
        gameStarted = true
        
        var res = {}
        res._cmd = "start"
        
        res.p1 = {id:p1id, name:users[p1id].getName(), x:1, y:1}
        res.p2 = {id:p2id, name:users[p2id].getName(), x:22, y:10}
        
        _server.sendResponse(res, currentRoomId, null, users)
}

The command is sent along with the informations about the two players: playerId, name and the initial position on the game board (position is expressed in tile coordinates, not pixels).

In the client code the "start" event is handled in the onExtensionResponse() method:

if (protocol == "xml")
{
        switch(cmd)
        {
                case "start":
                
                player1Id = resObj.p1.id
                player2Id = resObj.p2.id
                player1Name = resObj.p1.name
                player2Name = resObj.p2.name
                
                myX = resObj["p" + _global.myID].x
                myY = resObj["p" + _global.myID].y
                
                _global.opID = _global.myID == 1 ? 2 : 1
                
                opX = resObj["p" + _global.opID].x
                opY = resObj["p" + _global.opID].y
                
                startGame()
                break
                
                case "stop":
                	_global.gameStarted = false
                	delete this.onEnterFrame
                	gamePaused(resObj.n + " left the game" + newline)
                break
        }
}

The "start" and "stop" commands are sent using the default XML protocol, so we check this before proceeding. In fact for the movement of the player we will use the string-based protocol and you will learn from the code that it is very simple to mix both of them in a transparent way.

Upon the reception of the "start" action we store the data received (player names, ids and x-y positions) in our global variables. You will notice that we used a little "trick" here to simplify the assignement of the variables: the resObj variable contains two objects called p1 and p2 with the respective data for player1 and player2. We have used the _global.myID (player ID) and _global.opID (opponent ID) to dynamically read those properties and assign them to myX, myY and opX, opY which represent the positions of the client sprite and his opponent sprite.

The startGame() function attaches the red and green balls, representing the two players, in the right positions and starts the main onEnterFrame that will take care of listening to the keyboard and animating the opponent:

function mainThread()
{
   if (_global.gameStarted)
   {
      if(!mySprite.moving)
      {
         if(Key.isDown(Key.LEFT) && obstacles.indexOf(map[mySprite.py][mySprite.px - 1]) == -1)
         {
            sendMyMove(mySprite.px-1, mySprite.py)
            moveByTime(mySprite, mySprite.px-1, mySprite.py, playerSpeed)
         }
         else if(Key.isDown(Key.RIGHT) && obstacles.indexOf(map[mySprite.py][mySprite.px + 1]) == -1)
         {
            sendMyMove(mySprite.px+1, mySprite.py)
            moveByTime(mySprite, mySprite.px+1, mySprite.py, playerSpeed)
         }
         else if(Key.isDown(Key.UP) && obstacles.indexOf(map[mySprite.py - 1][mySprite.px]) == -1)
         {
            sendMyMove(mySprite.px, mySprite.py-1)
            moveByTime(mySprite, mySprite.px, mySprite.py-1, playerSpeed)
         }
         else if(Key.isDown(Key.DOWN) && obstacles.indexOf(map[mySprite.py + 1][mySprite.px]) == -1)
         {
            sendMyMove(mySprite.px, mySprite.py+1)
            moveByTime(mySprite, mySprite.px, mySprite.py+1, playerSpeed)
         }
      }
                
      // If the moves queue of the opponent contains data and the opponent is not
      // being animated we update its position
      if(!opSprite.moving && opSprite.moves.length > 0)
      {
            moveByTime(opSprite, opSprite.moves[0].px, opSprite.moves[0].py, playerSpeed)
      }
   }
}

The first line checks if the game is running, then we proceed by checking the four directional keys. If one of them is pressed and the player can move in that direction we send the move to the server and start the animation of the sprite.
In order to check if the player move is valid we used an interesting technique: at the top of the code we've declared a variable called obstacles which should contain all the characters that represent a non walkable tile in the map. In our simple map there's only one character, the "X", however you could add more of them. When we check the new position where the player wants to move we just need to see if the character contained at that position in the map is found in the obstacles string, using the indexOf() method.
The last part of the function checks if we have new updates in the moves queue of the opponent. Each element in such queue is an object with x and y position.

The moveByTime() function will take care of animating the sprites and it will also skip some of those animations if it finds that they are out of synch. Here's the code:

function moveByTime(who, px, py, duration)
{
        who.moving = true
        
        if(who.moves.length > 1)
        {
                who._x = who.moves[who.moves.length - 2].px *tileSize
                who._y = who.moves[who.moves.length - 2].py *tileSize
                px = who.moves[who.moves.length - 1].px
                py = who.moves[who.moves.length - 1].py
        }
        who.moves = []
        
        var sx, sy, ex, ey
        
        sx = who._x
        sy = who._y
        
        ex = px * tileSize
        ey = py * tileSize
        
        who.ani_startTime = getTimer()
        who.ani_endTime = who.ani_startTime + duration
        who.duration = duration
        
        who.sx = sx
        who.sy = sy
        who.dx = ex - sx
        who.dy = ey - sy
        
        who.onEnterFrame = animateByTime
}


The paramater called who is the sprite to move. Each sprite has a moving flag that tells us if it is already performing an animation, and it is usually tested before calling this function, as you don't want to start a new animation when the old one is still running.

The next lines take care of that synching function we were talking about. If we find more than one element in the moves queue of this object, we are already out of synch with the game and we need to skip to the second-last item and perform the animation from there to the last one. The effect on screen will be of an immediate jump of the sprite from one position to the other and it could be more noticeable when many moves are skipped.

Finally we can take a look at how our move is sent to the server:

function sendMyMove(px:Number, py:Number)
{
        var o = []
        o.push(px)
        o.push(py)
        
        smartfox.sendXtMessage(extensionName, "mv", o, "str")
}


The only big difference to note when sending raw messages, is that the object containing the data to send is an Array instead of an object. Another important things is that all parameters are treated as strings, so you may need to cast them back to numbers, booleans, etc., depending on the data type you're using.
As you can see from the code we simply add our px and py variables to the array and send it to the server using the raw format (4th parameter = "str").

The action name we use here is "mv": as a general rule, the shorter the name, the better, as you use less bytes in your message.

Now let's see how this data is handled on the server side:

function handleRequest(cmd, params, user, fromRoom, protocol)
{
        if (protocol == "str")
        {
                switch(cmd)
                {
                        case "mv":
                        handleMove(params, user)
                        break
                        
                }
        }
}

The parameters received are passed to the handleMove() function:

function handleMove(params, user)
{
        if (gameStarted)
        {
                var res = []		// The list of params
                res[0] = "mv"		// at index = 0, we store the command name
                res.push(params[0])	// this is the X pos of the player
                res.push(params[1])	// this is the Y pos of the player
                
                // Chose the recipient
                // We send this message only to the other client
                var uid = user.getUserId()
                var recipient = (uid == p1id) ? users[p2id]:users[p1id]
                
                _server.sendResponse(res, currentRoomId, user, [recipient], "str")
        }
}

First we validate the move by checking the gameStarted flag: it's always a good idea to add some server-side validation in order to avoid hacking attempts.
In the next lines we prepare the message to send to the opponent with the x and y positions of the client. A new array is created where the name of the action is at index = 0 and the other parameters follow.
We choose the user that will receive the message by comparing the userId of the sender, and finally send the message to the other client using the raw protocol.

Now we can move back to the client code, in the onExtensionResponse() event handler and see how we receive this data:

smartfox.onExtensionResponse = function(resObj:Object, protocol:String)
{
        var cmd:String = resObj._cmd
        
        if (protocol == "xml")
        {
                // ...
        }
        else if (protocol == "str")
        {
                
                var cmd = resObj[0] // command name
                var rid = Number(resObj[1]) // roomId
                
                switch(cmd)
                {
                        case "mv":
                        handleOpponentMove(Number(resObj[2]), Number(resObj[3]))
                        break
                }
        }
}


For the sake of simplicity we have reported only the code relative to the raw-protocol, since the other messages ("start", "stop") have already been covered.

The first two lines inside the else block are very important: as you can see we obtain the command name and the roomId from the first two positions in the array that was received. This is a convention that you will always use: each time you receive a raw-protocol based message, the first two indexes (0, 1) of the array will contain those informations. All the custom parameters sent from the server will always start at index = 2.

As you can see we pass resObj[2] (which expect to be the x position) and resObj[3] (which we expect to be the y position) to the handleOpponentMove() function casting them to numbers (rember that raw-protocol uses strings only):

function handleOpponentMove(x:Number, y:Number)
{
        if (opSprite.moves == undefined)
        	opSprite.moves = []
        
        opSprite.moves.push({px:x, py:y})
}

The code will just add the new move received in the opponent moves queue, and the onEnterFrame we have talked about before will do the rest.


Conclusions

We have covered a lot of different topics in this tutorial, so we reccomend to take your time with it and to experiment on your own with the source code provided with SmartFoxServer PRO.
Also you can consult the Server Side Actionscript framework documentation for more detailed informations about the server side methods used in this tutorial.


                      
 
 
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