Wednesday, May 19, 2010

From Raphaël.js to Fab.js: A Tale of One Player's Journey

‹prev | My Chain | next›

Up tonight, I want to kick players out of my (fab) game. I am not trying to drop unruly players, just unresponsive players.

I start with the client code. When a Player quits, I need to remove the raphaël.js drawable object:
Player.prototype.quit = function() {
this.drawable.remove();
}
("drawable" refers to a raphaël.js circle and remove() is a raphaël.js method).

For good measure, I delete the attributes from the player object:
Player.prototype.quit = function() {
this.drawable.remove();
delete this.x;
delete this.y;
delete this.id;

}
I am not trying to save memory or anything of the sort. I remove these attributes so that the Player no longer quacks like a Player. If anything tries to treat it as such, errors will happen.

Now that a Player can quit a room, I need to expose an interface for fab.js to be able to communicate with browsers to inform them of a departure. The interface for this is the PlayerList in the browser. The PlayerList holds the current player (me) as well as all the other current players. I will handle removing these other players first:
PlayerList.prototype.remove_player = function(id) {
this.get_player(id).quit();
delete this.other_players[id];
};
Actually, that is quite easy. I call the quit() method that I just defined on the Player leaving the room. Then I delete the player from the player list.

If the player quitting the room is me (e.g. I have been idle too long), then I need to quit the room as do all other players. From my perspective, if I leave, I no longer care about the other players. So:
PlayerList.prototype.remove_player = function(id) {
if (this.me.id == id) {
// I am leaving and so is everyone else (as far as I'm concerned)
this.me.quit();
for (var oid in this.other_players) {
this.other_players[oid].quit();
}
}

else {
this.get_player(id).quit();
delete this.other_players[id];
}
};
With that, I am ready to get started in the fab.js backend. First up, I do a little refactoring of the code so that the comet communication method is a bit more general. It had been specific to walking players in the rooms of each client. Now the broadcast() function can send any command to the PlayerList in the connected browsers:
function broadcast(comet_command) {
var num = 0;
for (var id in players) {
var player = players[id];
player.listener({body: comet_command});
num++;
}
puts("broadcasting to "+num+" players");
}
It can send any command, such as the new remove_player() method. To get started with timing out players, I add a simple setTimeout() immediately after a player is added in fab.js:
// ...
puts("adding: " + obj.body.id);
players[obj.body.id] = {status: obj.body, listener: out};

setTimeout(function() {
drop_player(obj.body.id);
}, 5000);


broadcast(comet_walk_player(JSON.stringify(obj.body)));
...
With that, 5 seconds after connecting, the drop_player() method will be called which, in turn, broadcasts to all connected clients that said player should be removed:
function drop_player(id) {
puts("Dropping player \""+ id +"\"");
broadcast(comet_quit_player(id));
}

function comet_quit_player(id) {
return comet_wrap('player_list.remove_player("'+ id +'")');
}
Trying this out, and after moving the player "bob" about the room, I am dropped:
cstrom@whitefall:~/repos/my_fab_game$ node game.js
adding: bob
broadcasting to 1 players
broadcasting to 1 players
updating player status: bob
Dropping player "bob"
broadcasting to 1 players
The last step is to move this into per-player idle watching inside the fab.js. In the idle_watch() function, I clear any existing timeout, then set a new one. If 60 seconds pass, then the Player will be removed:
function idle_watch(id) {
if (players[id].idle_timeout) {
clearTimeout(players[id].idle_timeout);
}

players[id].idle_timeout = setTimeout(function() {
drop_player(id);
}, 60*1000);
}
Calling this method anytime a player updates its status will ensure that active players remain in the room as long as they like:
function update_player_status(status) {
puts("updating player status: " + status.id);
players[status.id].status = status;
idle_watch(status.id);
}
That is a good stopping point for tonight. Up tomorrow, more raphaël.js work, I think. I would like to include player names next to the player icons. Then it'll be onto the next killer feature (in my son's eyes): chatting.

Day #108

No comments:

Post a Comment