8.6 Tutorials: Realtime maze (part 2)
» 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 have 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 param
= "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 been already 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.
Also you can consult the Server Side
Actionscript framework docs for more detailed informations about the server
side methods used in this example.