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

Unity 2 uSimpleChat Tutorial, ActionScript 2.0 Version

PART 3

The third version of uSimpleChat will show how to:

In parts 1 and 2 of this tutorial, we focused on simple ways to quickly create and join a room in order to communicate with other clients. For the sake of that simplicity, we ignored the client-side representation of the room we joined.

In uSimpleChat version 3, we'll start to make use of the client-side objects that represent server side rooms and namespaces. Specifically, we'll use the NameSpace class and the URoom class. These classes will let us implement more complex functionality in our multiuser applications. The NameSpace and URoom classes give us:

Our uSimpleChat application architecture will evolve substantially in version 3. Let's take a look at where we've come from and where we're going.

In uSimpleChat version 1, our client-side application logic was:

  1. Assume server has created room udefault.chat.
  2. Connect to server.
  3. Assume server automatically puts client in room udefault.chat.
  4. When connection succeeds, display chat interface.

In uSimpleChat version 2, our client-side application logic was:

  1. Connect to server.
  2. When connection succeeds, create room udefault.chat and join it.
  3. Display chat interface.

In uSimpleChat version 3, our client-side application logic will be:

  1. Connect to server.
  2. When connection succeeds, create room udefault.chat.
  3. If room is created or already exists, join it.
  4. When client-side NameSpace object is created for udefault, register to listen to its events.
  5. When udefault NameSpace object reports new room, chat, register to listen to chat room's events.
  6. When chat room reports that it was joined, display chat interface.

To implement the above version 3 logic, we'll modify the class USimpleChat, and we'll add two new classes: UdefaultNamespaceView, and ChatRoomView. The names of these two new classes reflect their role in the application: they are the "View" classes of a traditional Model-View-Controller setup. Each "View" class observe changes in its respective model: UDefaultNamespaceView observes the udefault NameSpace instance, and ChatRoomView observes the chat URoom instance. (Note that a prior understanding of the MVC design pattern is not required here, but if you happen to be familiar with MVC, you'll have an easy time understanding the motivation behind version 3's architecture.)

Let's look at version 3's classes in the context of our above logic flow.

The Revised USimpleChat.onClientReady() Method
According to step 2 of our version 3 logic flow, our application no longer joins the room udefault.chat after creating it in the onClientReady() method. Instead, our application waits for Unity to report the result of the room creation attempt before joining the room. Hence, our version 3 USimpleChat.onClientReady() method has lost the joinRoom() statement from version 2. However, it has also gained a new statement that displays the application status to the user in an onscreen Label component named status. The status Label has been added to our uSimpleChat.fla file at the frame labeled "main".

Here's the new version of onClientReady():

public function onClientReady ():Void {
  // Make a new room in the default namespace.
  getRoomManager().createRoomOnServer("chat", "udefault", false, false, 50);

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

The client-side udefault NameSpace and chat URoom objects will give us very specific information about the creation and joining of the room udefault.chat. We'll pass that information on to the end user via the status Label.

The New USimpleChat.onCreateRoomResults() Method
Unity reports the results of our room-creation attempt via the RoomManagerListener.onCreateRoomResults() method. All UClient instances are automatically registered to receive RoomManagerListener events, so all we need to do handle the onCreateRoomResults() event is implement that method in our USimpleChat class. Here's the method. Read it over, then we'll look at it line by line.

public function onCreateRoomResults (e:RoomManagerEvent):Void {
  // Invoke superclass's version of this method to preserve logging.
  super.onCreateRoomResults(e);

  // Store the creation status in a local variable.
  var statusMsg:String = e.getStatus();

  // If the creation of "udefault.chat" succeeded or the room 
  // is already there, join the room. Otherwise, display an error message.
  if (e.getNamespaceID() == "udefault" && e.getRoomID() == "chat") {
    if (statusMsg == "ROOM_CREATED" || statusMsg == "ROOM_EXISTS") {
      joinRoom("udefault.chat");
    } else {
      getTargetMC().status.text = "Room '" + e.getRoomID() 
                                + "' could not be created. Reason: "
                                + statusMsg;
    }
  } else {
         getTargetMC().status.text = "Room creation results "
                              + "received for unknown room:\n" 
                              + e.getNamespaceID() + "." + e.getRoomID();
  }
}

The first line of the method invokes the superclass (UClient) version of the method.

super.onCreateRoomResults(e);

By default, UClient provides basic logging for all methods in the RoomManagerListener interface. Invoking the UClient version of the method preserves that logging.

Next we retrieve the status of the room-creation attempt: did the room creation succeed or not? The status is passed to the method via the RoomManagerEvent instance, e. The RoomManagerEvent.getStatus() method tells us the result of the room creation attempt. We store that result in a local variable named statusMsg:

var statusMsg:String = e.getStatus();

Next we add a nested branch to our code: if the room for which creation results are being returned is in fact udefault.chat then check if the room creation succeeded.

if (e.getNamespaceID() == "udefault" && e.getRoomID() == "chat") {

If the udefault.chat room was created or if the room already exists, then join it.

  if (statusMsg == "ROOM_CREATED" || statusMsg == "ROOM_EXISTS") {
         joinRoom("udefault.chat");

Otherwise, report the room-creation error to the user.

  } else {
         getTargetMC().status.text = "Room '" + e.getRoomID() 
                                  + "' could not be created. Reason: "
                                  + statusMsg;
  }

Finally, if the room for which creation results are being returned is not udefault.chat, then display an error message.

  } else {
         getTargetMC().status.text = "Room creation results "
                              + "received for unknown room:\n" 
                              + e.getNamespaceID() + "." + e.getRoomID();
  }

The New USimpleChat.onAddNamespace() Method
Every time a client joins a room in a given namespace, the UClient class checks for the existence of: 1) a corresponding client-side NameSpace instance 2) a corresponding client-side URoom instance. If those instances do not exist, the UClient class instructs the RoomManager to create them. When the RoomManager creates the NameSpace instance, it invokes onAddNamespace() on its listeners. Recall that every UClient instance registers as a RoomManager listener. Hence, we can respond to the creation of new namespaces in our application by implementing USimpleChat.onAddNamespace().

Here's the sequence that causes USimpleChat.onAddNamespace() to fire when the udefault NameSpace is created:

  1. Client asks Unity to create room udefault.chat.
  2. Unity sends results via RoomManagerListener.onCreateRoomResults().
  3. If room was created or exists, client asks to join room.
  4. Unity returns room-join results to client.
  5. UClient receives room-join results for room udefault.chat.
  6. UClient checks if namespace udefault exists.
  7. Namespace udefault does not exist, so UClient asks RoomManager to create it.
  8. RoomManager creates namespace udefault.
  9. RoomManager invokes onAddNamespace() on registered RoomManager listeners (including USimpleChat), reporting that a new namespace was created, with id: udefault.

When USimpleChat learns that the udefault NameSpace was created, it creates a new instance of the UdefaultNamespaceView class, and registers that instance to receive events from the udefault NameSpace. Here's the code. Read it over, then we'll dissect it.

public function onAddNamespace (e:RoomManagerEvent):Void {
  // Store a reference to the namespace that was added.
  var ns:NameSpace = getRoomManager().getNamespace(e.getNamespaceID());

  if (e.getNamespaceID() == "udefault") {
    // Add a listener to the udefault namespace.
    ns.addNamespaceListener(new UdefaultNamespaceView(ns));      
  }
}

Our first task in onAddNamespace() is to retrieve a reference to the NameSpace instance that was just created. To retrieve that reference, we use the RoomManager.getNamespace() method, passing it the id of the namespace we want. We store the NameSpace reference in a local variable named ns. Notice that we retrieve the id of the newly added NameSpace via RoomManagerEvent.getNamespaceID().

var ns:NameSpace = getRoomManager().getNamespace(e.getNamespaceID());

Next, if the namespace that was added is "udefault", then we create a new UdefaultNamespaceView instance, and register it to receive events from the udefault NameSpace instance (stored in the variable ns). We pass our UdefaultNamespaceView instance a reference to the udefault NameSpace instance, allowing the view to refer back to the namespace it's observing if required.

  if (e.getNamespaceID() == "udefault") {
    // Add a listener to the udefault namespace.
    ns.addNamespaceListener(new UdefaultNamespaceView(ns));      
  }
Note that in order to refer to the UdefaultNamespaceView class directly by name, the USimpleChat class imports the package org.moock.unity.simplechat.v3. The following line of code is added to the top of USimpleChat:
import org.moock.unity.simplechat.v3.*;

The UdefaultNamespaceView Class
The UdefaultNamespaceView class has one purpose: to watch for new rooms being created. Specifically, when the room chat is created, the UdefaultNamespaceView registers a ChatRoomView instance to listen to the room's events. (For a list of all URoom events see URoomListener.)

The UdefaultNamespaceView class actually extends the general NamespaceView class, which provides do-nothing implementations for all the methods in the NamespaceListener interface. Extending NamespaceView relieves us of the obligation to provide those implementations in the UdefaultNamespaceView class. As a result, the UdefaultNamespaceView class contains only one method: onAddRoom().

Here's the entire listing for the UdefaultNamespaceView class. Notice the close similarities between the method UdefaultNamespaceView.onAddRoom() and our earlier USimpleChat.onAddNamespace() method.

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

class org.moock.unity.simplechat.v3.UdefaultNamespaceView
      extends NamespaceView {

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

  /**
   * A 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));
    }
  }
}

The ChatRoomView Class
The ChatRoomView class listens to events broadcast by the URoom instance, chat. Just as UdefaultNamespaceView extends NamespaceView, ChatRoomView extends the URoomView class. Extending URoomView relieves us of the obligation to implement all the methods in the URoomListener interface. It also provides us with access to the URoomView.client property, which in our case refers to the USimpleChat client instance of our application. We'll use the client property to access the main timeline of our Flash movie.

The ChatRoomView class has only one purpose, and, hence, implements only one method: ChatRoomView.onJoin(). That method fires automatically when the client joins the udefault.chat room. Or, more precisely, when attempt to join the udefault.chat room completes. The status of the join attempt is passed to the method via a URoomEvent object, e. The body of the method performs one of two actions, depending on the result of the room-join attempt. If the room was joined successfully, the chat interface is displayed and the user is told that the chat is ready to use. If the room could not be joined, the user is told why.

Here's the code for the ChatRoomView class.

import org.moock.unity.*;

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

  /**
   * 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") {
      client.getTargetMC().gotoAndStop("simpleChatInterface");
      client.getTargetMC().status.text = "Chat now active. Send your message.";
    } else {
      client.getTargetMC().status.text = "Could not join chat room. Status: " + e.getStatus();
    }
  }
}

We've now finished making structural changes to our uSimpleChat application. To polish it off and make it more functional, we'll add support for the Enter key in our chat interface.

Handling the Enter Key
To add support for the enter key in our chat application, we'll add an enter key event listener to the outgoing TextInput component. The listener is defined and registered on the frame labeled simpleChatInterface. We place the following new enter key listener code just below the event handling code for the Send button:

// Listen for enter key presses in outgoing.
var enterHandler:Object = new Object();
enterHandler.enter = function (e:Object):Void {
  sc.sendMessage();
};
outgoing.addEventListener("enter", enterHandler);

The USimpleChat Class, version 3
Here's the final code listing for the USimpleChat class, version 3. Code that is new in this version is highlighted in bold.

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

class org.moock.unity.simplechat.v3.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);
  }

  /**
   * UClient event handler
   */
  public function onClientReady ():Void {
    // Make a new room in the default namespace.
    getRoomManager().createRoomOnServer("chat", "udefault", false, false, 50);

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

  /**
   * RoomManagerListener event handler
   */
  public function onCreateRoomResults (e:RoomManagerEvent):Void {
    // Invoke superclass's version of this method to preserve logging.
    super.onCreateRoomResults(e);

    // Store the creation status in a local variable.
    var statusMsg:String = e.getStatus();

    // If the creation of "udefault.chat" succeeded or the room 
    // is already there, join the room. Otherwise, display an error message.
    if (e.getNamespaceID() == "udefault" && e.getRoomID() == "chat") {
      if (statusMsg == "ROOM_CREATED" || statusMsg == "ROOM_EXISTS") {
        joinRoom("udefault.chat");
      } else {
        getTargetMC().status.text = "Room '" + e.getRoomID() 
                                      + "' could not be created. Reason: "
                                      + statusMsg;
      }
    } else {
           getTargetMC().status.text = "Room creation results "
                                + "received for unknown room:\n" 
                                + e.getNamespaceID() + "." + e.getRoomID();
    }
  }

  /**
   * RoomManagerListener event handler
   */
  public function onAddNamespace (e:RoomManagerEvent):Void {
    // Store a reference to the namespace that was added.
    var ns:NameSpace = getRoomManager().getNamespace(e.getNamespaceID());

    if (e.getNamespaceID() == "udefault") {
      // Add a listener to the udefault namespace.
      ns.addNamespaceListener(new UdefaultNamespaceView(ns));      
    }
  }

  /**
   * Takes user input from outgoing_txt and sends it to all clients
   * in the room "udefault.chat".
   */
  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", "udefault.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 {
    getTargetMC().incoming.text += "User" + clientID + ": " + msg + "\n";
    getTargetMC().incoming.vPosition = getTargetMC().incoming.maxVPosition;
  }
}


Changes to uSimpleChat.fla
To make our chat easy to move from development to production (or from server to server), we'll change our USimpleChat constructor call to provide the location of an external config file. In this version, the constructor call no longer specifies a host or port for Unity; that information is provided via the config file. In addition, we no longer explicitly connect to Unity using UClient.connect(); when the config file has loaded, UClient automatically connects to Unity, so the call to connect() is not required.

Here's the revised code for the frame labeled "main" in uSimpleChat.fla. Changes to this version are shown in bold.


// Create USimpleChat instance. When the config file loads
// USimpleChat will automatically attempt to connect to Unity.
import org.moock.unity.simplechat.v3.USimpleChat;
var sc:USimpleChat = new USimpleChat(this, null, null, "uSimpleChatConfig.xml", true);

// Wait here. When connection succeeds, the application
// will proceed to the frame labeled "simpleChatInterface".
stop();

Here's the content of uSimpleChatConfig.xml:

<?xml version="1.0"?>
<config>
  <server>localhost</server>
  <port>9100</port>
  <logLevel>DEBUG</logLevel>
</config>

Onward!
Well you can stick version 3 in your back pocket. Now head on to part 4, where we'll learn how to maintain the all-important chat user list! (Among other useful features.)



Documentation Version

1.0.1