Writing Flash Clients
From J2Play
|
Structure of the J2Play network library
The j2play_fe library implements the J2Play communication protocol and functions similar to the J2ME and J2SE libraries.
The main library components are:
- Buddy list
- Remote profile
- Score
- Game channel
- Lobby channel
All these components are managed by a main class - the IController - which provides access to them.
interface ca.j2x.flash.IController {
public function getBuddyList() : IBuddyList;
public function getRemoteProfile() : IRemoteProfile;
public function getScore() : IScore;
public function getJoinedChannel(channelName : String) : IGameChannel;
public function getLobbyChannel() : ILobbyChannel;
...
The controller also defines the network configuration, manages channels, and provides functions to connect, login and register a user:
public function init(serverURL : String, serverPort : Number, networkID : String,
gameID : Number, gameServerPrefix : String) : Void;
public function login(username : String, password : String, nickname : String,
store : Boolean) : Void;
public function registerUser(username : String, nickname : String, password : String,
store : Boolean) : Void;
public function joinGameChannel(channel : String, type : Number, visible : Boolean,
create : Number, filter : String) : Void;
All these functions are similar to micro and standard edition and target the same server procedures. For more detailed messages description check the flash-doc for the interfaces.
Notification of Events
When using the J2Play platform, the visual components need to be notified on status changes or external events. This is achieved with the event/listeners pattern. Most of the events are defined in the Event class:
/** * Event to notify that controller successfully connected to the server. * Dispatched by IController */ public static var CONNECT_SUCCESS : String = "connect-sucess"; /** * Event to notify that login was successful. * Dispatched by IController */ public static var LOGIN_SUCCESS : String = "login-sucess"; /** * Event to notify that a chat message was received on lobby. * Dispatched by ILobbyChannel */ public static var LOBBY_CHAT_MESSAGE : String = "lobby.chat-message"; /** * Event to notify that a chat message was received in game. * Dispatched by IGameChannel */ public static var GAME_CHAT_MESSAGE : String = "game.chat-message";
Almost all the J2Play components implement IEventSource interface that gives us the ability to subscribe/unsubscribe for events:
interface ca.j2x.flash.event.IEventSource {
public function addEventListener(event : String, handler) : Void;
public function removeEventListener(event : String, handler) : Void;
}
The listeners must implement a handleEvent(evt) function which is called when the specified event fires. Here is an example how Chat Panel subscribes and reacts to chat messages that are sent on the lobby and game channels:
class ca.j2x.flash.ui.ChatPanel {
public function init(aPeer : ChatPanelMC) : Void {
...
j2play.addEventListener(Event.CREATE_SUCCESS, this);
j2play.addEventListener(Event.JOIN_SUCCESS, this);
j2play.addEventListener(Event.LEAVE_SUCCESS, this);
}
private function handleEvent(evt : Event) : Void {
if(evt.type == Event.JOIN_SUCCESS || evt.type == Event.CREATE_SUCCESS) {
// joined game, add chat panel
var channelJoined : IGameChannel = evt.params;
addGameChat(channelJoined);
// start listening on the game channel for chat messages
channelJoined.addEventListener(Event.GAME_CHAT_MESSAGE, this);
} else if(evt.type == Event.LEAVE_SUCCESS) {
// left game, remove the chat panel
var channelLeft : IGameChannel = evt.params;
removeGameChat(channelLeft);
// stop listening on the game channel for chat messages
channelJoined.removeEventListener(Event.GAME_CHAT_MESSAGE, this);
}
...
}
The handleEvent() function is called with one parameter of type Event. The event has the following fields:
class ca.j2x.flash.event.Event {
...
/**
* Defines the event type, one of the string constants.
* For example, Event.JOIN_SUCCESS = "join-success"
*/
public var type : String;
/**
* Optional and contains parameter(s) associated with
* the event. For example, for event Event.JOIN_SUCCESS these params will
* be a IGameChannel that was successfully joined.
*/
public var params : Object;
/**
* Defines the object that fired the event (optional)
*/
public var target : Object;
Whoever fires the event can pass additional parameters in params attribute. For example, the JOIN_SUCCESS event is fired with one parameter - the game channel that was just joined. However, some events don't carry any parameters, e.g. CONNECT_SUCCESS. To see a list of parameters that are expected from a particular event, check the Event class documentation.
Note that it's unsafe to use simple event listeners in a movie clip. The problem is that if you define a handleEvent(evt) method in different frames of the same movie clip, then they override each other and the last frame played keeps the latest version of the method.
To solve this problem, a FunctionListener was created. The FunctionListener wraps a function and an object to call when an event fires. This lets developers give the event-listening functions names different from handleEvent. The object to call the specified function on is usually the movie clip itself. Here is the simple implementation of FunctionListener:
class ca.j2x.flash.event.FunctionListener {
/**
* Function to call
*/
private var _function : Function;
/**
* Object to call the function on
*/
private var _callee : Object;
/**
* Called by EventSource-based classes when the underlying event is fired
*/
function handleEvent(evt) : Void {
_function.call(_callee, evt);
}
And here is the example how function listeners can be added to an EventSource-based component from a movie clip:
controller.addFunctionListener(Event.CONNECT_SUCCESS, onConnected, this);
controller.addFunctionListener(Event.CONNECT_FAILURE, onConnectionFailed, this);
function onConnected() {
play();
}
function onConnectionFailed() {
loader.removeMovieClip();
gotoAndStop("connection_error");
}
Just remember to give functions different names in one movie clip.
Asynchronous execution
Due to the asynchronous nature of remote calls in Flash, most of the remote call functions return immediately with a Void result and notify the caller when the result is available. This is achieved by using callbacks. For example, IScore provides a function to retrieve scores from the server and returns the result in the callback function. Look at the method signature:
interface ca.j2x.flash.IScore {
/**
* Requests scores from server and
* calls back the specified function upon scores retrieval.
*
* @param scoreType the type of the scores
* @param ascending true if low scores are better (like golf),
* false if high scores are better (like baseball)
* @param firstUser a username. The received scores will start at this user's best score.
* @param firstRank the received scores will start at this rank.
* If firstUser is defined, this argument is ignored
* @param limit the maximum number of scores to return, or -1 for all scores
* @param callback the callback to call on successful scores retrieval.
* The callback function will be called * with an array of IScore elements.
*/
public function getScores(scoreType : String, ascending : Boolean, firstUser : String,
firstRank : Number, limit : Number, callback : Callback) : Void;
This method returns Void immediately. However, when the scores are retrieved from server, the callback is executed with the scores data sent as parameters. The Callback class encapsulates a function and an object to execute on. It is similar to the FunctionListener used in movie clips with addition of a stacking feature to form a chain of calls. Note that listeners are notified with one argument - evt:Event, while callback functions may be called with any number of arguments based on the component's context.
class ca.j2x.flash.Callback {
/**
* Reference to a function to call
*/
private var func : Function;
/**
* Reference to an object to execute the function on
*/
private var objt : Object;
/**
* Optional callback allowes to link callbacks into stack.
*/
private var callee : Callback;
public function call() : Void {
// append the callee callback as the last parameter if any
if(callee != null)
arguments.push(callee);
// execute the callback function with all given arguments
func.apply(objt, arguments);
}
}
Here is an example showing the ProfileView using the remote request with a callback:
class ca.j2x.flash.ui.ProfileView
private function loadProfile() : Void {
...
// request game statistics
var statCallback : Callback = new Callback(onStatReceived, this);
score.getScores(SCORE_RECORD, false, player.username, 0, 1, statCallback);
}
/**
* Callback function executed by IScore.
*/
private function onStatReceived(scoresArray : Array) : Void {
var statScore : IScore = IScore(scoresArray[0]);
....
}
}
The same technique is used in IRemoteProfile for profile data management.
Some remote calls may return with failed result. For example, if one asks for scores of non-existent user, or tries to remove a buddy who wasn't on the buddy list in the first place. In these cases, the callback class is extended with an exception handler which is called when remote call is unsuccessful:
class ca.j2x.flash.Callback {
...
/**
* Optional exception handler.
* This function is executed only if asyncronous operation
* failed. The argument passed into the function is
* a:Error which describes the exception.
*/
private var exceptionHandler : Function;
/**
* When asyncronous operation failed (e.g. there is no Scores for
* specified user during Score.getScores() call)
*/
public function fail(e : Error) : Void {
if(exceptionHandler != null) {
if(callee == null)
exceptionHandler.call(objt, e);
else
exceptionHandler.call(objt, e, callee);
}
}
}
Further illustration can be seen in the Score, RemoteProfile and BuddyList components, which use a RemoteException to notify the user when some operations fail.
Accessing the Controller
The j2play_fe library is packed into a compiled .swf file. This library can be loaded by calling one of the following methods at the startup:
- MovieClip.loadMovieClip()
- MovieClipLoader.loadClip()
It is recommended to use MovieClipLoader to load the library becuase it provides loading status notifications so that you know when the library has been loaded.
The library publishes the controller in the global variables by the name _global.controller and can be accessed from any point.
To help execute methods on controller and related components, the library comes along with interfaces that can be imported to your project. Here is an example of how to access the controller:
import ca.j2x.flash.IController;
import ca.j2x.flash.IScore;
class ca.j2x.flash.ui.HighScoresView {
private var j2play : IController;
private var score : IScore;
/**
* Constructor for the high score view with spefied movie clip component
*/
public function HighScoresView(aPeer : HighScoresMC) {
this.j2play = _global.controller;
if(j2play == null) {
logger.warn("cannot link to the controller, it is NULL");
return;
}
this.score = j2play.getScore();
...
}
}
Here is the example how the visual framework loads the J2Play communication library (stored in j2play_fe.swf) along with other external resources during startup:
class ca.j2x.flash.J2PlayInitializer {
private var loader : MovieClipLoader;
private var currentMovie : Number;
private var moviesToLoad : Array;
/**
* Loads next resource if any.
*/
private function nextResource() : Void {
currentMovie++;
if(currentMovie < moviesToLoad.length) {
currentHolder = _root.createEmptyMovieClip("preload_mc" + currentMovie,
_root.getNextHighestDepth());
currentHolder._visible = false;
loader.loadClip(moviesToLoad[currentMovie], currentHolder);
}
} else {
// everything is loaded, waiting to connect
initStatus = 2;
// clear the timer
clearInterval(timer);
delete timer;
}
}
As soon as the loading process is finished, the connect() method is called from the movie clip to establish a connection to the server:
function checkInited() {
if (ca.j2x.flash.J2PlayInitializer.getInstance().completed()) {
// all external movies are pre-loaded
// also communication library is loaded and Controller should be shared
start();
}
}
function start() {
// add listeners to the controller
_global.controller.addFunctionListener(Event.CONNECT_SUCCESS, onConnected, this);
_global.controller.addFunctionListener(Event.CONNECT_FAILURE, onConnectionFailed, this);
...
// try to connect and by notified by the connection events
ca.j2x.flash.J2PlayInitializer.getInstance().connect();
}
The connect() method simply initializes the controller with parameters obtained from the properties file:
public function connect() : Void {
var j2play : IController = _global.controller;
if(j2play == null) {
logger.warn("cannot load j2pay controller");
} else {
var props : Properties = Properties.getInstance();
var serverURL : String = props.getString("SERVER_URL");
var serverPort : Number = props.getNumber("SERVER_PORT");
var networkID : String = props.getString("NETWORK_ID");
var gameID : Number = props.getNumber("GAME_ID");
var gameServerPrefix : String = props.getString("GAME_SERVER_PREFIX");
j2play.init(serverURL, serverPort, networkID, gameID, gameServerPrefix);
}
}
Note that listeners for connection events CONNECT_SUCCESS and CONNECT_FAILURE are linked with the controller before the call to connect. This ensures that they will be notified on connection attempt result. Similarly, we add listeners after the communication library is loaded and the controller is shared via the global variable.
Basically, the startup process consists of the following steps:
- Load the J2Play communication library. Wait until it is completely loaded.
- Access the Controller and add listeners for connection results.
- Call the connect() method on Controller and wait to be notified by the events.
- If connected successfully then show the game, otherwise a warning.
When the game is being closed, the Controller should be destroyed through the destroy() method. This will issue the logout() request to the server to clear up the session resources. Otherwise, the session stays alive for two minutes and other players will see you online.
