| back to uSimpleChat (actionscript 2 version) home |
Unity 2 uSimpleChat Tutorial, ActionScript 2.0 Version
PART 3The third version of uSimpleChat will show how to:
- use an external application configuration file
- display connection status in a status text field
- use client-side NameSpace and URoom objects
- send outgoing chat messages when the Enter key is pressed
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:
- detailed information about operations such as creating a room, adding a client to a room, and leaving a room
- access to room attributes and client attributes, which let us share and store information about rooms and clients
- support for multiroom applications, where each room corresponds to a separate interface, (e.g, a different chat window for chatting in the "Dogs" room and the "Snowboarding" room).
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:
- Assume server has created room
udefault.chat.- Connect to server.
- Assume server automatically puts client in room
udefault.chat.- When connection succeeds, display chat interface.
In uSimpleChat version 2, our client-side application logic was:
- Connect to server.
- When connection succeeds, create room
udefault.chatand join it.- Display chat interface.
In uSimpleChat version 3, our client-side application logic will be:
- Connect to server.
- When connection succeeds, create room
udefault.chat.- If room is created or already exists, join it.
- When client-side NameSpace object is created for
udefault, register to listen to its events.- When
udefaultNameSpace object reports new room,chat, register to listen tochatroom's events.- When
chatroom 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
udefaultNameSpace instance, and ChatRoomView observes thechatURoom 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 roomudefault.chatafter 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 thejoinRoom()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 namedstatus. ThestatusLabel 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
udefaultNameSpace andchatURoom objects will give us very specific information about the creation and joining of the roomudefault.chat. We'll pass that information on to the end user via thestatusLabel.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 namedstatusMsg: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.chatthen check if the room creation succeeded.if (e.getNamespaceID() == "udefault" && e.getRoomID() == "chat") {If the
udefault.chatroom 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
udefaultNameSpace is created:
- Client asks Unity to create room
udefault.chat.- Unity sends results via RoomManagerListener.onCreateRoomResults().
- If room was created or exists, client asks to join room.
- Unity returns room-join results to client.
- UClient receives room-join results for room
udefault.chat.- UClient checks if namespace
udefaultexists.- Namespace
udefaultdoes not exist, so UClient asks RoomManager to create it.- RoomManager creates namespace
udefault.- RoomManager invokes onAddNamespace() on registered RoomManager listeners (including USimpleChat), reporting that a new namespace was created, with id:
udefault.When USimpleChat learns that the
udefaultNameSpace 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
udefaultNameSpace instance (stored in the variablens). We pass our UdefaultNamespaceView instance a reference to theudefaultNameSpace instance, allowing the view to refer back to the namespace it's observing if required.Note that in order to refer to the UdefaultNamespaceView class directly by name, the USimpleChat class imports the packageif (e.getNamespaceID() == "udefault") { // Add a listener to the udefault namespace. ns.addNamespaceListener(new UdefaultNamespaceView(ns)); }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 roomchatis 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 theURoomView.clientproperty, which in our case refers to the USimpleChat client instance of our application. We'll use theclientproperty 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.chatroom. Or, more precisely, when attempt to join theudefault.chatroom 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 labeledsimpleChatInterface. 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