back to uSimpleChat (actionscript 2 version) home  back to  
unify the web

Unity 2 uSimpleChat Tutorial, ActionScript 2.0 Version

PART 4

The fourth version of uSimpleChat will show how to:

In the first three versions of uSimpleChat, we created our chat room in the udefault namespace. In this version, we'll give the chat room its very own namespace. By providing our application with its own namespace, we ensure that no other application will interfere with our chat room. This allows multiple projects to be developed and deployed on the same Unity server. It also provides a way to group rooms logically, so that they can be manipulated en masse. For example, if a chat application keeps all its sports-related rooms in a namespace called "moockchat.sports", then an administration tool can easily be written to send a message to all rooms related to sports ("ALERT: Wayne Gretzky's chatting in the Hockey room right now!!").

The addition of a custom namespace changes our application logic somewhat. In uSimpleChat version 4, the client-side application logic is:

  1. Connect to server.
  2. When connection succeeds, create namespace usimplechat.
  3. When client-side NameSpace object is created for simplechat:
    • Register to listen to its events.
    • Create room simplechat.chat.
  4. When simplechat NameSpace object reports new chat room:
    • Register to listen to chat room's events.
    • Join chat room.
  5. When chat room reports that it was joined, display chat interface.

In addition to the above revised logic, this version adds new features such as a client list and user names. Those features do not affect the basic logic involved in connecting to the server, creating a room, and joining it. In fact, even very complex Unity applications generally do not deviate much from the basic architecture of uSimpleChat version 4. This version of our chat application makes an excellent boilerplate for many kinds of multiuser apps.

Creating the simplechat Namespace
Unlike the previous two versions of uSimpleChat, version 4's USimpleChat.onClientReady() method does not tell Unity to create the chat room. Instead, it only tells Unity to create the simplechat namespace, and then waits to be notified when the corresponding client-side NameSpace object is created by the RoomManager. We use the RoomManager.createNamespaceOnServer() method to create the namespace. Here's the code for USimpleChat.onClientReady(), version 4:

public function onClientReady ():Void {
  // Make a new namespace, "simplechat", and start observing it.
  getRoomManager().createNamespaceOnServer("simplechat", true);

  // Tell the user what's happening.
  getTargetMC().status.text = "Connected to Unity. Joining simplechat room...";
}

In the call to createNamespaceOnServer(), the second argument (true) specifies that the client wants to "observe" the namespace. When a namespace is observed by a client, Unity automatically notifies that client of changes to the namespace (i.e., additions or removals of rooms or subnamespaces). For example, later, when we're informed that the simplechat namespace has been created, we'll add the chat room to it. In response, Unity will notify us that the room was added. The RoomManager will automatically create the corresponding client-side URoom instance, and the simplechat NameSpace instance will invoke onAddRoom() on all its listeners (in our case, a SimpleChatNamespaceView instance).

Notice that we don't bother implementing the RoomManagerListener.onCreateNamespaceResults() method. That method tells us whether the namespace creation was successful or not. However, UClient already implements onCreateNamespaceResults(), and outputs debugging information to the client log. (We'll learn how to enable the client log later in this tutorial.) For our purposes, debugging info is all we need. If there's a problem creating the namespace, we can check the log and attempt to debug. In more complete applications, it might also be nice to display a generic error message to the user when namespace creation fails. That's left as an exercise for the reader. Hint: implement USimpleChat.onCreateNamespaceResults(), and use it to display an error message in the status Label.

Creating the simplechat.chat Room
When our client-side simplechat NameSpace instance is created, USimpleChat.onAddNamespace() executes. From that method, we'll register an event listener for the NameSpace, just like we did version 3. This time, Our listener will be an instance of the SimpleChatNamespaceView class, which bears much in common with version 3's UdefaultNamespaceView class. In addition to registering a NameSpace listener, the version 4 USimpleChat.onAddNamespace() will create our chat room in the new namespace.

Here's the code for USimpleChat.onAddNamespace():

  public function onAddNamespace (e:RoomManagerEvent):Void {
    // Invoke superclass's version of this method to preserve logging.
    super.onAddNamespace(e);
    // Retrieve a reference to the new NameSpace object.
    var ns:NameSpace = getRoomManager().getNamespace(e.getNamespaceID());

    if (e.getNamespaceID() == "simplechat") {
      ns.addNamespaceListener(new SimpleChatNamespaceView(ns));
      // This line is new to v4.
      getRoomManager().createRoomOnServer("chat", "simplechat", true, true, 50);
    } else {
      log.warn("Unexpected namespace added to chat application.");
    }
  }
Notice that the arguments to our createRoomOnServer() method have changed since version 3. This time, the third and fourth arguments are true instead of false, indicating that:

We'll respond to clients joining and leaving the room from our ChatRoomView class, which will implement the URoomListener.onAddClient() and URoomListener.onRemoveClient() methods.

Joining the simplechat.chat Room
If the server successfully creates the simplechat.chat room, it will notify all clients observing the simplechat namespace that the room was added. We respond to that notification from the SimpleChatNamespaceView.onJoin() method (exactly like we did in version 3 with UdefaultNamespaceView.onJoin()). The onJoin() method has three tasks: 1) create a new ChatRoomView instance, 2) register the ChatRoomView instance to receive events from the chat room, 3) join the chat room.

Here's the code for SimpleChatNamespaceView:

import org.moock.unity.*;
import org.moock.unity.simplechat.v4.*;

class org.moock.unity.simplechat.v4.SimpleChatNamespaceView
      extends NamespaceView {

  /**
   * Constructor
   */
  public function SimpleChatNamespaceView (namespace:NameSpace) {
    super(namespace);
  }

  /**
   * NamespaceListener event handler
   */
  public function onAddRoom(e:NamespaceEvent):Void {
    // When a new room is added to this namespace...
    // If it's the chat room...
    if (e.getRoomID() == "chat") {
      // First get a reference to the room object.
      var room:URoom = ns.getRoom(e.getRoomID());

      // Then create a view for the room and register it to 
      // receive the room's events.
      room.addURoomListener(new ChatRoomView(room));

      // Finally, join the room.
      room.join();
    }
  }

}

Displaying the Chat Interface
We respond to the joining of the simplechat.chat room via ChatRoomView.onJoin(). That method has two responsibilities: 1) move the main timeline's playhead to the frame labeled "simpleChatInterface", and 2) create the List component instance that will display the user list for the room. Note that we're forced to create the List component programmatically rather than placing it on the Stage manually (as we did with the rest of the chat interface). If we were to place the component on the Stage manually, its methods wouldn't be initialized until a frame had passed, which would prevent us from adding user names to it for the duration of one frame.

Here's the code for onJoin():

public function onJoin (e:URoomEvent):Void {
  // If the room join attempt was successful...
  if (e.getStatus() == "ROOM_JOINED") {
    // Display the interface frame.
    client.getTargetMC().gotoAndStop("simpleChatInterface");

    // Attach the "users" List component.
    users = client.getTargetMC().createClassObject(List, "users", client.getNewTargetDepth());
    users.move(407, 148);
    users.setSize(117, 159);

    // Tell the user the application is ready to use.
    client.getTargetMC().status.text = "Chat now active. Send your message.";
  } else {
    client.getTargetMC().status.text = "Could not join chat room. Status: " + e.getStatus();
  }
}
Note that in order to be able to create the users List component dynamically, we must add it to the uSimpleChat.fla file, and so that the component can be preloaded, we must make sure that it doesn't export in the first frame of the movie. Here are the steps we follow to add the List component to uSimpleChat.fla:

The instance of the List component that we added to the dummy list layer forces the component to load at that frame.

Displaying the User List
Whenever a client joins the chat room, ChatRoomView.onAddClient() executes. Whenever a client leaves the chat room, ChatRoomView.onRemoveClient() executes. Those two methods let us populate the user list for the room. Immediately after we join a room, onAddClient() executes once for each client in the room at the time we joined it.

Like all other event methods in the uClientCore API, the onAddClient() and onRemoveClient() methods are passed an event object, e, which provides information about the event. The event object passed to onAddClient() and onRemoveClient() contains the id of the client that was added or removed. In the simplest case, we could add that ID to, or remove it from, our ListBox as follows:

// Add.
this.users.addItem(e.getClientID());

// Remove.
var clientID:String = e.getClientID();
for (var i:Number = this.users.length; --i >= 0;) {
  if (this.users.getItemAt(i).label == clientID) {
    this.users.removeItemAt(i);
    return;
  }
}

However, the above code would result in a user list containing only numeric user ids. Not very much fun. Our application allows users to set their name as a "shared client attribute". Client attributes for a particular client are retrieved via the RemoteClient.getAttribute() method. For example, the following code retrieves a global shared client attribute named "username" for a RemoteClient instance (the null argument indicates that the scope of the client attribute is global):

theRemoteClient.getAttribute(null, "username");

RemoteClient instances themselves are retrieved via the RemoteClientManager.getClient() method, which takes the client ID for the RemoteClient to retrieve as its only argument. The RemoteClientManager for an application is accessed via the UClient.getRemoteClientManager() method. Hence, to retrieve a remote client with ID 45, we use:

someUClientSubclass.getRemoteClientManager().getClient(45);

Here, then, is the ChatRoomView.onAddClient() code that retrieves the username attribute for a client that has been added to the room:

var remoteuser:RemoteClient = client.getRemoteClientManager().getClient(e.getClientID());
var username:String = remoteuser.getAttribute(null, "username");

Once the username has been retrieved, it is displayed in the users List via the addItem() method. The label for the List item is the username, and the data for the List item is the client id. We store the the client's unique id as each item's data so that we can identify each item in the list uniquely (in this application, two users could have the same name, but would have different client ids).

Here's the complete code for the onAddClient() method:

public function onAddClient (e:URoomEvent):Void {
  // Retrieve the username for the client that just joined.
  var remoteuser:RemoteClient = client.getRemoteClientManager().getClient(e.getClientID());
  var username:String = remoteuser.getAttribute(null, "username");

  // Use the client id as a user name if the user hasn't set a name.
  if (username == undefined) {
    username = "User" + e.getClientID();
  }

  // Add the new user to the list box.
  users.addItem(username, e.getClientID());
  users.sortItemsBy("label", "asc");
}

When a client leaves the room, we must remove it from the List component. That's a little bit tricker than adding it. The List component does not provide a means of removing an item according to a specific data value. Instead, we have to manually find the data value we're looking for, determine its index in the list, and then remove it. The onRemoveClient() method, shown next, does just that: it removes an item for a particular client id from the users List.

public function onRemoveClient (e:URoomEvent):Void {
  var clientID:String = e.getClientID();
  for (var i:Number = users.length; --i >= 0;) {
    if (users.getItemAt(i).data == clientID) {
      users.removeItemAt(i);
      return;
    }
  }
}

Setting a User Name
Our application allows each client to set a shared client attribute, username, which stores the client's chosen user name. When the attribute is defined or changed, all clients in room at the time will be notified of the new value. We'll define the username attribute as a global attribute, which means that if the client is in multiple rooms, all clients in all those rooms will be notified when the attribute changes. A client attribute can also be scoped to a particular room, so that only clients in that room are notified when it changes. For details, see UClient.setClientAttribute().

To allow end-users to set a user name, we'll add a new TextInput component (nameInput) and a new Button component (setName) to our uSimpleChat.fla file. We'll use the method UClient.setClientAttribute() to set the username attribute, but we'll wrap that method in a USimpleChat method called setName(). To wire setName button clicks to the USimpleChat.setName() method, we'll add the following code to our .fla, at the frame labeled "simpleChatInterface":

setName.clickHandler = function (e:Object):Void {
  sc.setName();
}
Here's the USimpleChat.setName() method:
public function setName ():Void {
  if (getTargetMC().nameInput.text.length > 0) {
    setClientAttribute("username", getTargetMC().nameInput.text, null, true, false, false);
    getTargetMC().nameInput.text = "";
  }
}
The arguments to setClientAttribute() have the following meaning:

When the username attribute is set, ChatRoomView.onUpdateClientAttribute() automatically fires on all clients in the simplechat.chat room at the time. That method is responsible for updating the user name displayed in the users List component.

Here's the code for ChatRoomView.onUpdateClientAttribute(). Notice that it subcontracts the work of updating the users List to another method, renameUser() (shown following).

public function onUpdateClientAttribute (e:URoomEvent):Void {
  var attrName = e.getChangedAttr().attrName;
  var attrVal  = e.getChangedAttr().attrVal;
  var fqRoomID = e.getChangedAttr().fqRoomID;

  // If the username attribute was changed, display it on screen.
  if (attrName == "username") {
    renameUser(e.getClientID(), attrVal); 
  }
}

public function renameUser (clientID:String, newName:String):Void {
  for (var i:Number = users.length; --i >= 0;) {
    if (users.getItemAt(i).data == clientID) {
      users.replaceItemAt(i, newName, clientID);
      users.sortItemsBy("label", "asc");
    }
  }
}

The uSimpleChat version 4 application is now functionally complete. However, as a final bit of polish for our application, we'll add code to handle a connection failure and to enable the application log.

Handling Connection Failure
When a connection failure occurs, the application's SocketManager automatically invokes one of three methods on USimpleChat:

Here's the implementation for those methods in USimpleChat, version 4.

  public function onConnectFailure (e:SocketEvent):Void {
    // Invoke superclass's version of this method to preserve logging.
    super.onConnectFailure(e);
    getTargetMC().status.text = "Connection to Unity failed.";
    getTargetMC().gotoAndStop("disabled");
  }

  public function onServerKillConnect (e:SocketEvent):Void {
    // Invoke superclass's version of this method to preserve logging.
    super.onServerKillConnect(e);
    getTargetMC().status.text = "Unity has closed the connection.";
    getTargetMC().gotoAndStop("disabled");
  }

  public function onClientKillConnect (e:SocketEvent):Void {
    // Invoke superclass's version of this method to preserve logging.
    super.onClientKillConnect(e);
    getTargetMC().status.text = "Connection to Unity closed.";
    getTargetMC().gotoAndStop("disabled");
  }

In each of the three disconnection scenarios, we display a frame labeled "disabled", which contains a "reconnect" button. On the "scripts" layer of the "disabled" frame, we add the following code:

reconnect.clickHandler = function (e:Object):Void {
  sc.reconnect();
}

When the reconnect button is clicked, it invokes reconnect() on our USimpleChat instance, which attempts to re-establish the connection to Unity. Here's the code for the USimpleChat.reconnect() method:

public function reconnect ():Void {
  getTargetMC().status.text = "Reconnecting to Unity...";
  connect();
}

Cleaning Up After the Party
When a connection is lost, all NameSpace and URoom instances are automatically removed from the client. Immediately before each URoom is deleted, it fires onLeave() on its listeners. From that method, each room view object is expected to clean up any resources it may have created. In the case of our ChatRoomView class, we must remove the users List component that we created when the chat room was joined. Here's the ChatRoomView.onLeave() method, which does just that.

public function onLeave ():Void {
  client.getTargetMC().destroyObject("users");
}

We didn't create any resources in the SimpleChatNamespaceView class, so we don't need to do anything special when the simplechat namespace is deleted. If we had needed to clean up after that namespace, we'd have implemented the SimpleChatNamespaceView.onDie() method.

Enabling the Client Log
As a final feature in our application, we'll add an application log, which will display lots of information about what's happening internally in the uClientCore classes. The log is enabled in the call to the USimpleChat constructor (on the frame labeled "main"). The last argument of the constructor call is set to false, meaning "don't disable the log", as follows:

var sc:USimpleChat = new USimpleChat(this, null, null, "uSimpleChatConfig.xml", false);

Within the constructor function itself, the log display is customized with the following code:

logPanel.setPosition(17, 376);
logPanel.setSize(511, 250);
logPanel.minimize();

The above code controls the logPanel movie clip instance, which is dynamically generated automatically by the UClient class when the log is enabled.

Mission Complete
Over four stages, we've created a solid, reasonably sophisticated multiuser application. The listings for the final application classes are provided below. This is the end of the tutorial but it should only be the beginning of your own exploration. If you create something interesting with Unity, let us know at unity@moock.org! We may include your project in the Unity Application Showcase.

The code listings for the final versions of the classes in our uSimpleChat application follow.

Code Listing for USimpleChat Class, Version 4

import org.moock.unity.*;
import org.moock.unity.simplechat.v4.*;

class org.moock.unity.simplechat.v4.USimpleChat
      extends UClient {

  /**
   * Constructor
   */
  public function USimpleChat (target:MovieClip, 
                          host:String, 
                          port:Number, 
                          configURL:String, 
                          disableLog:Boolean) {
    // Invoke UClient constructor.
    super(target, host, port, configURL, disableLog);

    // Adjust the display of the log panel.
    logPanel.setPosition(17, 376);
    logPanel.setSize(511, 250);
    logPanel.minimize();

    // Tell the user we're connecting.
    getTargetMC().status.text = "Connecting to Unity...";
  }

  /**
   * UClient event handler
   */
  public function onClientReady ():Void {
    // Make a new namespace, "simplechat", and start observing it.
    getRoomManager().createNamespaceOnServer("simplechat", true);

    // Tell the user what's happening.
    getTargetMC().status.text = "Connected to Unity. Joining simplechat room...";
  }

  /**
   * SocketListener event handler
   */
  public function onConnectFailure (e:SocketEvent):Void {
    // Invoke superclass's version of this method to preserve logging.
    super.onConnectFailure(e);
    getTargetMC().status.text = "Connection to Unity failed.";
    getTargetMC().gotoAndStop("disabled");
  }

  /**
   * SocketListener event handler
   */
  public function onServerKillConnect (e:SocketEvent):Void {
    // Invoke superclass's version of this method to preserve logging.
    super.onServerKillConnect(e);
    getTargetMC().status.text = "Unity has closed the connection.";
    getTargetMC().gotoAndStop("disabled");
  }

  /**
   * SocketListener event handler
   */
  public function onClientKillConnect (e:SocketEvent):Void {
    // Invoke superclass's version of this method to preserve logging.
    super.onClientKillConnect(e);
    getTargetMC().status.text = "Connection to Unity closed.";
    getTargetMC().gotoAndStop("disabled");
  }

  /**
   * RoomManager event handler
   */
  public function onAddNamespace (e:RoomManagerEvent):Void {
    // Invoke superclass's version of this method to preserve logging.
    super.onAddNamespace(e);
    // Retrieve a reference to the new NameSpace object.
    var ns:NameSpace = getRoomManager().getNamespace(e.getNamespaceID());

    if (e.getNamespaceID() == "simplechat") {
      ns.addNamespaceListener(new SimpleChatNamespaceView(ns));
      getRoomManager().createRoomOnServer("chat", "simplechat", true, true, 50);
    } else {
      log.warn("Unexpected namespace added to chat application.");
    }
  }

  /**
   * Takes user input from outgoing_txt and sends it to all clients
   * in the room "simplechat.chatroom".
   */
  public function sendMessage ():Void {
    // Only send the message if there's text
    // in the outgoing_txt text field.
    if (getTargetMC().outgoing.text.length > 0) {
      // The message typed by the user.
      var msg:String = getTargetMC().outgoing.text;

      // The message we'll send to the server.
      var safeMsg:String = '<![CDATA[' + msg + ']]>';

      // Send the message to the server.
      invokeOnRoom("displayMessage", "simplechat.chat", true, safeMsg);

      // Clear the user input text field.
      getTargetMC().outgoing.text = "";
    }
  }

  /**
   * Displays text sent by a client.
   */
  public function displayMessage (clientID:String, msg:String):Void {
    // Retrieve the username for the client that sent the message.
    var remoteuser:RemoteClient = getRemoteClientManager().getClient(clientID);
    var username:String = remoteuser.getAttribute(null, "username");

    // Use the client id as a user name if the user hasn't set a name.
    if (username == undefined) {
      username = "User" + clientID;
    }

    // Display the message on screen.
    getTargetMC().incoming.text += username + ": " + msg + "\n";
    getTargetMC().incoming.vPosition = getTargetMC().incoming.maxVPosition;
  }

  /**
   * Sets the username attribute for this client.
   */
  public function setName ():Void {
    if (getTargetMC().nameInput.text.length > 0) {
      setClientAttribute("username", getTargetMC().nameInput.text, null, true, false, false);
      getTargetMC().nameInput.text = "";
    }
  }

  public function reconnect ():Void {
    getTargetMC().status.text = "Reconnecting to Unity...";
    connect();
  }
}

Code Listing for SimpleChatNamespaceView Class, Version 4

import org.moock.unity.*;
import org.moock.unity.simplechat.v4.*;

class org.moock.unity.simplechat.v4.SimpleChatNamespaceView
      extends NamespaceView {

  /**
   * Constructor
   */
  public function SimpleChatNamespaceView (namespace:NameSpace) {
    super(namespace);
  }

  /**
   * NamespaceListener event handler
   */
  public function onAddRoom(e:NamespaceEvent):Void {
    // When a new room is added to this namespace...
    // If it's the chat room...
    if (e.getRoomID() == "chat") {
      // First get a reference to the room object.
      var room:URoom = ns.getRoom(e.getRoomID());

      // Then create a view for the room and register it to 
      // receive the room's events.
      room.addURoomListener(new ChatRoomView(room));

      // Finally, join the room.
      room.join();
    }
  }
}

Code Listing for ChatRoomView Class, Version 4

import org.moock.unity.*;
import mx.controls.List;

class org.moock.unity.simplechat.v4.ChatRoomView extends URoomView {

  private var users:List;

  /**
   * Constructor
   */
  public function ChatRoomView (room:URoom) {
    super(room);
  }

  /**
   * URoomListener event handler
   */
  public function onJoin (e:URoomEvent):Void {
    // If the room join attempt was successful...
    if (e.getStatus() == "ROOM_JOINED") {
      // Display the interface frame.
      client.getTargetMC().gotoAndStop("simpleChatInterface");

      // Attach the "users" List component.
      users = client.getTargetMC().createClassObject(List, "users", client.getNewTargetDepth());
      users.move(407, 148);
      users.setSize(117, 159);

      // Tell the user the application is ready to use.
      client.getTargetMC().status.text = "Chat now active. Send your message.";
    } else {
      client.getTargetMC().status.text = "Could not join chat room. Status: " + e.getStatus();
    }
  }

  /**
   * URoomListener event handler
   */
  public function onAddClient (e:URoomEvent):Void {
    // Retrieve the username for the client that just joined.
    var remoteuser:RemoteClient = client.getRemoteClientManager().getClient(e.getClientID());
    var username:String = remoteuser.getAttribute(null, "username");

    // Use the client id as a user name if the user hasn't set a name.
    if (username == undefined) {
      username = "User" + e.getClientID();
    }

    // Add the new user to the list box.
    users.addItem(username, e.getClientID());
    users.sortItemsBy("label", "asc");
  }

  /**
   * URoomListener event handler
   */
  public function onRemoveClient (e:URoomEvent):Void {
    var clientID:String = e.getClientID();
    for (var i:Number = users.length; --i >= 0;) {
      if (users.getItemAt(i).data == clientID) {
        users.removeItemAt(i);
        return;
      }
    }
  }

  /**
   * URoomListener event handler
   */
  public function onUpdateClientAttribute (e:URoomEvent):Void {
    var attrName = e.getChangedAttr().attrName;
    var attrVal  = e.getChangedAttr().attrVal;
    var fqRoomID = e.getChangedAttr().fqRoomID;

    // If the username attribute was changed, display it on screen.
    if (attrName == "username") {
      renameUser(e.getClientID(), attrVal); 
    }
  }

  /**
   * Changes a user's name in the player list.
   */
  public function renameUser (clientID:String, newName:String):Void {
    for (var i:Number = users.length; --i >= 0;) {
      if (users.getItemAt(i).data == clientID) {
        users.replaceItemAt(i, newName, clientID);
        users.sortItemsBy("label", "asc");
      }
    }
  }

  /**
   * Clean up UI when the client leaves the room or the server
   * connection is lost. (When the server connection is lost,
   * all rooms and namespaces are removed, and onLeave() fires 
   * for every removed room.)
   */
  public function onLeave ():Void {
    client.getTargetMC().destroyObject("users");
  }
}


Documentation Version

1.0.1