Week of AFCS Revisited: SharedObject

After my trainwreck post on using CollectionNode in AFCS Nigel suggested that I try to recreate my example using the higher level APIs instead of trying to build everything from scratch with NodeCollection. As I mentioned in the previous post, the AFCS team did a great job of creating very basic APIs like CollectionNode and then building on top of those to make APIs that are easier to use. SharedObject is a perfect example of that.

Here’s what I wanted to accomplish: Create a basic multiplayer game where players try to match colors with each other using a ColorPicker. Using the SharedObject class it’s really pretty easy. The first thing I did was set up a room where guests are automatically promoted and no username/password is required. Then I used a combination of AFCS components and Flex components to create a user interface for entering a display name, picking the color, and showing how many players there are:

<rtc:AdobeHSAuthenticator id="auth" />
     <rtc:ConnectSessionContainer id="cSession" 
          authenticator="{auth}" 
          roomURL="{_roomURL}"
          synchronizationChange="cSession_synchronizationChangeHandler(event)"
     <mx:Panel id="panelColor" title="Pick a Color!"
          horizontalCenter="0" verticalCenter="0" alpha=".3" 
          width="200" height="200" enabled="false"
          <mx:ColorPicker id="color" change="color_changeHandler(event)" width="100%" height="100%" />
          <mx:Label text="Playing with {cSession.userManager.userCollection.length - 1} other people" />
     </mx:Panel>
 
     <mx:TitleWindow id="tw" horizontalCenter="0" verticalCenter="0"
          <mx:TextInput id="username" width="150" text="Enter A Username" focusIn="{username.text = ''}" />
          <mx:Button id="btnLogin" label="Login" click="btnLogin_clickHandler(event)" /> 
     </mx:TitleWindow>
</rtc:ConnectSessionContainer>

Nothing major there, notice we’re using the userManager class to get the number of other users in the room (and that it’s bindable). The big thing is the synchronizationChange event handler. In that function we’re going to configure our node and create the SharedObject.

public var sharedColor:com.adobe.rtc.sharedModel.SharedObject;
 
protected function cSession_synchronizationChangeHandler(event:SessionEvent):void
{
     if( event.type == SessionEvent.SYNCHRONIZATION_CHANGE )
     {
          var config:NodeConfiguration = new NodeConfiguration();
          config.userDependentItems = true;
 
          sharedColor = new com.adobe.rtc.sharedModel.SharedObject();
          sharedColor.sharedID = "color";
          sharedColor.setNodeConfiguration(config);
          sharedColor.subscribe();
          sharedColor.addEventListener(SharedObjectEvent.PROPERTY_ADD, onPropertyChange);
          sharedColor.addEventListener(SharedObjectEvent.PROPERTY_CHANGE, onPropertyChange);
          sharedColor.addEventListener(SharedObjectEvent.PROPERTY_REMOVE, onPropertyRemove);
     }
}

We have to use the full namespace when we reference the SharedObject in AFCS because it conflicts with the normal Flash SharedObject. We first do a check go make sure we’re in a SYNCHRONIZATION_CHANGE event and then start setting things up. The only change I make to the default NodeConfiguration is to make it so that when a user leaves the room, they take their items with them so we don’t have “ghost” matches. That’s done by setting the userDependentItems property to false.

Next we create our SharedObject. I give it a sharedID of “color” and then make sure the application is subscribed to the SharedObject so that it sees any changes. Finally I set up event handlers for whenever a property is changed, added, and removed. The add and change properties use the same function, onPropertyChange.

Let’s first take a look at how we create the SharedObject; something that happens whenever we change our color selection.

protected function color_changeHandler(event:ColorPickerEvent):void
{
     sharedColor.setProperty(cSession.userManager.myUserID,color.selectedColor);
}

We use the setProperty() method on the SharedObject to give it information. A SharedObject is just a name/value pair, or in this case, a propertyName/value pair. I set the propertyName to the userID so we can know where the color came from and then set the value to our selected color. Whenever we set that property it will create a property change event and call our event handler.

protected function onPropertyChange(event:SharedObjectEvent):void
{
     if( event.propertyName != cSession.userManager.myUserID)
     {
          if(event.value == color.selectedColor)
          {
               var user:UserDescriptor = cSession.userManager.getUserDescriptor(event.propertyName);
               sharedColor.removeProperty(cSession.userManager.myUserID);
               Alert.show("You matched with " + user.displayName + "!");
          }
     }
}

This is where the game starts to happen. The way AFCS works is that when you change something locally inside of your application it is sent to the server and then you get a change event when it comes back. This is so you can be sure the change was successful. In our game we want to make sure that we’re not dealing with the event we just sent so I use the propertyName property and compare it to myUserID to see if it’s the user’s own event. If it isn’t, we look for a match using the value property and our selectedColor. If we have a match I use the UserManager class to get the display name of the matched user and pop up an alert box to show that we found a match.

The other thing I do is to call the removeProperty() method on the user’s SharedObject. I ran into an issue where when a user selected a color that matched another one, the other user would get the popup but there was no easy way to notify the user who selected the color that it was a match. I solved this by removing the property which would then trigger a PROPERTY_REMOVE event and fire our event handler.

protected function onPropertyRemove(event:SharedObjectEvent):void
{
     var user:UserDescriptor = cSession.userManager.getUserDescriptor(event.propertyName);
     if( event.propertyName != cSession.userManager.myUserID && user != null)
     {
          Alert.show("You matched with " + user.displayName + "!");
     }
}

It looks similar to our change event handler. I first get the user information for the popup and then I check a couple of things. First, I make sure that the event isn’t coming from the current user, but the user I have the match for. I also want to make sure that the SharedObject wasn’t removed by someone just logging out (remember our NodeConfiguration settings). If my user is null then that means the user is gone and the event fired because the user left the room and not because I specifically removed it.

That’s pretty much all there is to it. You can grab the project here or I’ve embedded the game below (and here’s a direct link), so feel free to play and see if you can match colors!

New AIR 1.5.2 API Examples and Features

Despite the ‘.2′ naming this is a pretty cool release for Adobe AIR developers and there are a few important new APIs that I think you’ll find very useful as well as a big change that should make every developer’s life a lot easier.

Getting Set Up
The first thing you need to do is download the new SDK and replace the old AIR 1.5 or 1.5.1 SDK. It’s pretty easy and it just consists of dropping the new AIR SDK folder into your Flex SDK (back it up first). The ditto command on the Mac works beautifully. You also need to make sure to change the namespace in your appname-app.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://ns.adobe.com/air/application/1.5.2">

Catching the “ESC” Key in Full Screen Mode
Now you’re ready to go. One oft-requested feature for AIR (and Flash Player) is the ability to capture the “escape” key and not have it exit full screen mode. With AIR 1.5.2 this is easy. You set an event handler for the keyDown event and then inside of the event handler you call event.preventDefault(). Here’s a quick example:

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication 
	xmlns:fx="http://ns.adobe.com/mxml/2009" 
	xmlns:s="library://ns.adobe.com/flex/spark" 
	xmlns:mx="library://ns.adobe.com/flex/halo"
	keyDown="windowedapplication1_keyDownHandler(event)">
 
	<fx:Script>
		<![CDATA[
 
			protected function btn_fullScreen_clickHandler(event:MouseEvent):void
			{
				stage.displayState = StageDisplayState.FULL_SCREEN_INTERACTIVE;
				btn_fullScreen.label = "Try pushing escape!"
 
			}
 
			protected function windowedapplication1_keyDownHandler(event:KeyboardEvent):void
			{
				event.preventDefault();
				if( event.keyCode == Keyboard.ESCAPE )
				{
					btn_fullScreen.label = "Nope, escape won't work. Try pushing the Enter Key";
				}
				if( event.keyCode == Keyboard.ENTER )
				{
					stage.displayState = StageDisplayState.NORMAL;
					btn_fullScreen.label = "Go Full Screen!";
				} 
			}
 
		]]>
	</fx:Script>
 
	<s:Button id="btn_fullScreen" label="Go Full Screen!" click="btn_fullScreen_clickHandler(event)" />	
</s:WindowedApplication>

Once you call the event.preventDefault() method you can use the Escape keyCode like any other key.

Garbage Collection of XML Data
Another one that will be huge for performance optimization is the ability to mark an XML object for immediate garbage collection. To do this you can use the new System.disposeXML(xml:XML) method. Oliver Goldman gave me a great explanation of what’s happening.

Even then, the disposeXML() call doesn’t immediately dispose of the XML. The XML object is backed by a graph of objects with parent/child pointers between them. Those pointers make it difficult for the GC to collect all of those objects. The disposeXML() call traverses the graph, setting all of those pointers to null, and making it much easier for collection to occur. The objects still aren’t collected right away, however—that’s still pending on the GC activity.

To use the new API you just have to call the method and then null out your XML variable. That will make it easier for the garbage collector to clean up. In the example below I load in a big XML file, do some parsing to it so that it loads into memory, then call the System.disposeXML() method.

public var xml:XML;
 
protected function windowedapplication1_creationCompleteHandler(event:FlexEvent):void
{
	var file:File = File.applicationDirectory.resolvePath("assets/Untitled.gpx");
	var stream:FileStream = new FileStream();
		stream.open(file,FileMode.READ);
	var str:String = stream.readUTFBytes(file.size);
		stream.close();	
	xml = new XML(str);			
}
 
protected function btn_loadXML_clickHandler(event:MouseEvent):void
{
	xml.normalize();
	var xmlList:XMLList = xml.children();
	var text:XMLList = xml.text();
	var xmlStr:String = xml.toString();
}
 
 
protected function btn_disposeXML_clickHandler(event:MouseEvent):void
{
	System.disposeXML( xml );
	xml = null;
}
Memory Profiler using the disposeXML() method

Memory Profiler using the disposeXML() method

You can see how this behaves in the profiler screenshot above. If you want to immediately garbage collect you can call System.gc() but if you use this often it can have negative performance implications (thanks to Ethan for the tip).

The last one I wanted to touch on was a new, friendlier install screen as blogged about by Joseph Labrecque and Oliver Goldman. We got rid of the “System Access: Unrestricted” for signed applications so that if you sign your app your end users will have a nicer install experience.

Adobe AIR 1.5.2 Signed App Install Screen

Adobe AIR 1.5.2 Signed App Install Screen

Those are three of the biggies. You can download the sample project here if you want to run the full screen example and the System.disposeXML() example.

Flash Player 10 Penetration at 86.7%

flash_platform_blog

In case you missed it, the brand new Flash Platform Blog announced some new penetration numbers for Flash Player 10.

For our inaugural Flash Platform blog posting I want to begin with some important news. The Flash Player penetration statistics have just recently been updated and Flash Player 10 is now installed on 86.7% of Internet-connected desktop computers in mature markets, which is the fastest the Web has ever adopted new innovation.

It’s great to see the adoption speed and know that so many people can take advantage of Flash Player 10′s features. For those who want other datapoints you can see that MochiAds Zeitgeist is showing a similar percentage and RIAStats has penetration trending upwards but lagging about 10% behind our (and MochiAds’) studies.

It’s also a great inaugural post for the Flash Platform blog! The marketing team at Adobe has put a ton of work into this and I’m really happy with the content. I highly suggest you subscribe as this will be the place where you’ll see a lot of breaking news and general Flash Player momentum content. They’ve got a number of different authors so the entire range of the Flash Platform is covered. Plus the feed includes content from many of the evangelists.