| back to uSimpleChat (actionscript 2 version) home |
Unity 2 uSimpleChat Tutorial, ActionScript 2.0 Version
PART 4The fourth version of uSimpleChat will show how to:
- create a new namespace from ActionScript
- display a list of clients in a room
- use a client attribute to store each client's username
- display Unity connection status to the end user
- reconnect to Unity when the connection is lost
- add logging to our application
In the first three versions of uSimpleChat, we created our
chatroom in theudefaultnamespace. In this version, we'll give thechatroom 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:
- Connect to server.
- When connection succeeds, create namespace
usimplechat.- When client-side NameSpace object is created for
simplechat:
- Register to listen to its events.
- Create room
simplechat.chat.- When
simplechatNameSpace object reports newchatroom:
- Register to listen to
chatroom's events.- Join
chatroom.- When
chatroom 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 thechatroom. Instead, it only tells Unity to create thesimplechatnamespace, 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 thesimplechatnamespace has been created, we'll add thechatroom 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 thesimplechatNameSpace 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
statusLabel.Creating the simplechat.chat Room
When our client-sidesimplechatNameSpace 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 ourchatroom in the new namespace.Here's the code for USimpleChat.onAddNamespace():
Notice that the arguments to our createRoomOnServer() method have changed since version 3. This time, the third and fourth arguments arepublic 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."); } }trueinstead offalse, indicating that:
- the room should be removed from the server if it becomes empty (no harm done: it will be recreated by the next client that joins)
- the server should send the room's client list when we join the room, and should notify us when a client enters or leaves the room
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 thesimplechat.chatroom, it will notify all clients observing thesimplechatnamespace 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 thechatroom, 3) join thechatroom.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 thesimplechat.chatroom 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():
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: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(); } }
- create a new layer called
dummy list- add a blank keyframe at frame 8 and 9 of the
dummy listlayer- drag an instance of the List component to frame 8 of the
dummy listlayer- in the Library, select Linkage for the List component, then uncheck "Export in first frame".
The instance of the List component that we added to the
dummy listlayer forces the component to load at that frame.Displaying the User List
Whenever a client joins thechatroom, ChatRoomView.onAddClient() executes. Whenever a client leaves thechatroom, 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
nullargument 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
usernameattribute 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
usernamehas been retrieved, it is displayed in theusersList via theaddItem()method. Thelabelfor the List item is theusername, and thedatafor 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
usersList.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 theusernameattribute 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 theusernameattribute, but we'll wrap that method in a USimpleChat method called setName(). To wiresetNamebutton clicks to the USimpleChat.setName() method, we'll add the following code to our .fla, at the frame labeled "simpleChatInterface":Here's the USimpleChat.setName() method:setName.clickHandler = function (e:Object):Void { sc.setName(); }The arguments to setClientAttribute() have the following meaning:public function setName ():Void { if (getTargetMC().nameInput.text.length > 0) { setClientAttribute("username", getTargetMC().nameInput.text, null, true, false, false); getTargetMC().nameInput.text = ""; } }
- "username", the name of the attribute
- this.getTargetMC().nameInput.text, the value of the attribute
- null, the scope of the attribute (global)
- true, the attribute should be shared with other clients in the room
- false, the attribute is not persistent (saved on the server when the client disconnects)
- false, the supplied attribute value should not be appended to the existing attribute value on the server
When the
usernameattribute is set, ChatRoomView.onUpdateClientAttribute() automatically fires on all clients in thesimplechat.chatroom at the time. That method is responsible for updating the user name displayed in theusersList component.Here's the code for ChatRoomView.onUpdateClientAttribute(). Notice that it subcontracts the work of updating the
usersList 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:
- onConnectFailure(), when attempt to connect cannot be completed
- onServerKillConnect(), when an existing connection is terminated by the server (either the server went down, or the client was kicked off)
- onClientKillConnect(), when an existing connection is terminated by the client (via UClient.disconnect())
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 theusersList component that we created when thechatroom 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
simplechatnamespace 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 tofalse, 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
logPanelmovie 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