Friday, May 21, 2010

Tracer Bullets for Raphaël.js and Fab.js

‹prev | My Chain | next›

The killer feature in my (fab) game (as least as far as my son is concerned) is chatting. Sure I can have multiple people in a room, I could probably get rudimentary collision detection working in it, I can even animate players as they move about the room. None of that matters in my son's eyes. He wants to be able to chat.

Kids.

I have a lot of fairly decoupled pieces at the moment. In the frontend, largely raphaël.js with a sprinkle of jQuery, I have a bunch of Players that roam about a Room. I also have a PlayerList that holds the list of players in a room and, as such, serves as the middleman for comet push communication from the fab.js backend. Speaking of the backend, there are two parts that will factor into chatting: notifying the backend of the message and broadcasting the message to all players.

That is a lot of moving pieces. To make sure I do this right, I am going to use the tried-and-true method of tracer bullets to make sure I know my target before I hit it for real. I start with a simple form at the bottom of my Room, which I add via a jQuery insertAfter:
Room.prototype.draw = function() {
var self = this;

// Initialize the Raphaël.js room
this.paper = Raphael(this.container, 500, 500);
this.paper.clear();
this.paper.
rect(0, 0, 500, 500, 4).
attr({fill: "#fff", stroke: "none"});

// Form to capture chat
$('<div id="chat">' +
'<form id="chat-form" action="#">' +
'<label for="change-message">Chat:</label>' +
'<input type="text" name="message" id="chat-message" />' +
'<input type="submit" name="commit" value="Go" />' +
'</form>' +
'</div>').insertAfter($(self.paper.canvas).parent());


}
I know that this message eventually needs to hit the server, but first, the room needs to tell player that I said something so that my player can tell the fab.js backend. But even before that, I need to know that I can capture the form input. So, my first tracer bullet:
Room.prototype.draw = function() {
var self = this;

// Initialize the Raphaël.js room

// Form to capture chat

// handle submits
$('#chat-form').submit(function(e) {
console.debug($('#chat-message').val());
subscriber.notify(event);
$('#chat-message').val('');
return false;

});
}
Now, when I enter a message and press "Go", I get nice debug output in Chrome's javascript console. Obviously, this submit handler is of little use to me at this point, but that is the whole point of tracer bullets. I make sure that I can hit the target (handling chat messages) before I do it for real.

So let's do it for real. This is being done in my Room, but my current player needs to be told that a message was posted. I could hold a reference to my player in the room and call the Player.say() method directly, but that would pretty heavy handed coupling. Instead I have a pub/sub for events which allows me to send events, like a new chat message, to all interested parties (like my Player):
  $('#chat-form').submit(function(e) {
self.subscribers.forEach(
function(subscriber) {
var event = {
type: "message",
value: $('#chat-message').val()
};
subscriber.notify(event);
$('#chat-message').val('');
}
);

return false;
});
Time for another tracer bullet at this point. My Player currently ignores all notifications other than move:
Player.prototype.notify = function(evt) {
switch(evt.type) {
case "click":
this.stop();
this.walk_to(evt.x, evt.y);
this.notify_server('move', {id:this.id, x:evt.x, y:evt.y});
break;
}
};
Rather than going for full blown support of the "message" event, I add some more simple debugging to make sure that I am aiming the right way:
Player.prototype.notify = function(evt) {
switch(evt.type) {
case "click":
this.stop();
this.walk_to(evt.x, evt.y);
this.notify_server('move', {id:this.id, x:evt.x, y:evt.y});
break;
default:
console.debug("[notify] type: " + evt.type + " value: " + evt.value);

}
};
Now, when I send a chat message, I get more nice debug output in Chrome's javascript console. Since my aim is good, I can add the real stuff to handle message notifications:
Player.prototype.notify = function(evt) {
switch(evt.type) {
case "click":
this.stop();
this.walk_to(evt.x, evt.y);
this.notify_server('move', {id:this.id, x:evt.x, y:evt.y});
break;
case "message":
this.stop();
this.walk_to(this.x, this.y);
this.notify_server('move', {id:this.id, x:evt.x, y:evt.y});
this.notify_server('chat', {id:this.id, say:evt.value});
break;

default:
console.debug("[notify] type: " + evt.type + " value: " + evt.value);
}
};
New in there is notifying the server of a chat event. For that, I need to move down into my fab.js backend where a unary will handle the request:
  ( /chat/ )
( function() {
var out = this;
return function listener( obj ) {
if ( !obj ) out();
else if ( obj.body ) {
broadcast(comet_player_say(obj.body));
}
return listener;
};
} )
Nothing too fancy there. It is a simple unary app with a listener to grab the data being posted (the chat message payload). I then use the already-in-place broadcast method to tell all attached clients that someone had something to say. I am not using tracer bullets here because I know these fab.js targets well.

What I am not so sure about is the ultimate broadcast call. For that, I will tell the PlayerList on each connected browser that somebody said something:
function comet_player_say(player_string) {
return comet_wrap('player_list.player_say('+ player_string +')');
}
This brings me to my final bullet point for the night, the player_say method in PlayerList:
PlayerList.prototype.player_say = function(attrs) {
alert(attrs.id + " says " + attrs.say);
};
Here I use good, old fashioned alert() for my tracer bullets. Eventually, I would like what the player said displayed immediately above the player in the room. But, for now, an alert will provide a reasonable facsimile:

video

Nice!

Bit of a whirlwind, but I have basic chatting working. Tomorrow, I will tidy things up a bit and replace the last of my tracer bullets with in-room chat messages.

Day #110

No comments:

Post a Comment