Using ByteArray to Serialize AS3 Classes for AIR, Web and Everywhere!
When using AIR's FileStream to write AS3 classes to ByteArray, I've found that the same file format works for loading in via URLLoader, as well as what's uploaded using FTP. This isn't surprising, but wanted to test anyway.
What this means is that ByteArray is a groovy ubiquitous format for storing full classes and dependencies and references, via ByteArray.writeObject and ByteArray.readObject (provided registerClass is used), without nececessarily having to parse them manually though that is still suggested for versioning and class evolution. Meaning you can create a file using AIR , send that file to a friend in email, and have them open it up in a flash based webpage, and they operating in a webpage could Http POST the change back to you. The ByteArray can have rich complexity, classes, self references, collections, etc. This could be used to save whole game states (even massive multiplayer, NPC brains, etc). Or in my target case debugging outside other non-AIR runtimes to speed development.
How to save to the same directory the swf AIR is running from
This took me a bit to figure out. Primarily as I'm using AIR to write the files in Flash CS3 and it's not immediately apparent in the docs, how to target the 'self' directory of the swf. This isn't recommended in the docs (which I agree) but in the case I'm using it for it makes sense to do so. The API for AIR is here
-
-
// this is in url path
-
var file:File = File.applicationDirectory.resolvePath( "example.jpg" );
-
trace("file " + file.url + " " +file.nativePath ); //outputs app:/example.jpg, and "C:\SomeDirectory\Somepath\example.jpg"
-
-
var wr:File = new File(file.nativePath);
-
// Create a stream and open the file for asynchronous reading
-
var stream:FileStream = new FileStream();
-
stream.open( wr, FileMode.WRITE );
-
stream.write.....
-
stream.close();
-
This would be swf._url in AS2, or in AS3 DisplayObject.loaderInfo.url , as when using relative links in URLLoader to load whatever you're saving this will resolve properly in AIR. Turns out flash.filesystem.File.applicationDirectory is the droid you are looking for. It's not fully qualified (e.g. file:C:\somedirectory...) when traced it looks like:
"app-resource:/yourSwfNameGoesHere.swf"
unless you use the file.nativePath which is required in order to actually write to the directory. So you can use still this in old school string name parsing the swf name out, so you don't have to use File.applicationResourceDirectory if your looking to make a single swf for both AIR and Web use, and on the web, the AIR libraries won't be accessible.
Here's two handy scripts to help inspect ByteArray's, the first looks at the 1's an 0's, the other looks at the objects in the stream, using the getQualifiedClassName, and if it finds an Array or sub ByteArray attempts to inspect it.
/* converts byteArray to binary like
TraceBits--------------------
0 = 110
1 = 1011
2 = 1001000
3 = 1100101
*/
-
-
function traceBits(bA:ByteArray) {
-
trace("TraceBits--------------------");
-
for (var i:int = 0; i < bA.length; i++) {
-
bA.position = i;
-
-
var inte:uint = bA.readByte();
-
trace(i+ " = " + inte.toString(2));
-
}
-
}
-
-
function traceByteArray(bA:ByteArray) {
-
var o;
-
var cnt:int = 0;
-
var cnm:String;
-
-
var o2;
-
var cnt2:int = 0;
-
var cnm2:String;
-
try {
-
while (true) {
-
o = bA.readObject();
-
cnm = getQualifiedClassName(o);
-
trace( cnt++ + " " + o + " : " + cnm);
-
if (cnm == "flash.utils::ByteArray") {
-
try {
-
cnt2 = 0;
-
cnm2 = "";
-
while (true) {
-
o2 = o.readObject();
-
trace(" " + cnt2++ + " " + o2 + " : " + getQualifiedClassName(o2));
-
-
}
-
} catch (err:Error) {
-
}
-
} else if (cnm == "Array") {
-
trace("array contents...");
-
try {
-
var ar:Array = o as Array;
-
trace("ar " + ar);
-
trace(ar.join("\r"));
-
} catch (err:Error) {
-
trace(err.toString());
-
}
-
}
-
}
-
} catch (err:Error) {
-
}
-
}
Serialization: IExernalizable and Evolving Classes
What is evolving a class? It's the inevitable:
- addition of a field
- change the superclass
- remove a field
- change the name of a field
- change a field to static (from non-static)
- change a field to transient (from non-transient)
- change the type of a field type (e.g. int to float)
As soon as you do any of the above whatever you've previously saved something to disk, be it local or remote will break upon loading back in.
This isn't common in most web based flash development which lives in isolation on a webpage and doesn't save anything, but introduce any sort of saving. With Desktop Flash apps (AIR, Zinc), and RIA's, offline online applications, it IS becoming something that is important.
There are 3 different strategies to serializing objects, only one is particularly useful for evolving classes.
- Direct:
writeObject(yourClassObject); - Customize with IExternalizeable: iterate over each attribute of your object and write each of those to the stream.
- Customize with IExternalizeable, iterate over each attribute of your object write it with a key to a Dictionary / HashTable.
Approach 1 is great for simple uses and first passes. However it doesn't scale well or at all. Approach 2 Is better but as the stream is sequential, this can make dealing with multiple versions difficult or impossible as you have to know the exact order in which to parse. Approach 3 is the best, it uses a Dictionary as an intermediate map, it's only a bit more code to write, and is a *whole* lot easier to parse. This is also the approach that Remoting uses when talking to the server with the ASObject, and in someways writing to disk is identical to writing to the filesystem.
Here is the list of imports:
-
-
import flash.utils.Dictionary;
-
import reflection.introspectable;
-
import flash.utils.IExternalizable;
-
import flash.utils.IDataInput;
-
import flash.utils.IDataOutput;
-
import flash.net.registerClassAlias;
Here is the version number, we'd need to increase everytime a significant data change has happened, and some of the static register class necessary to get the class to serialize and deserialize correctly:
-
-
public static const serialVersionUID:Number = 01;
-
private static const REG_DICT:* = registerClassAlias("flash.utils.Dictionary",Dictionary);
-
private static const REG:* = registerClassAlias("reflection.IntrospectableObj",IntrospectableObj);
-
[RemoteClass(alias="reflection.IntrospectableObj")]
-
Here are the modified IExternalizable functions. Note that the read from the stream has to deal with all permutations for a given version to version, as this number of changes gets large, the normalization can be quite large, thankfully good architecture up front can prevent much of this.
-
-
///////////////////////////////////////////////
-
public function readExternal(input:IDataInput):void
-
{
-
var d:Dictionary = input.readObject();
-
var serialVersionUID:Number = d["serialVersionUID"] as Number ;
-
trace("readExternal " + serialVersionUID);
-
/////////////// COMMON TO ALL VERSIONS ///////////////////////////
-
if(serialVersionUID == 1 || serialVersionUID == 2 || serialVersionUID == 3){
-
introspectable::introVarStr = d["introVarStr"]as String;
-
defVarStr = d["defVarStr"]as String;
-
-
pubByteArray =d["pubByteArray"] as ByteArray;
-
pubBitmapData= (d["pubBitmapData"] as BitmapDataWrapper).getBitMapData();
-
}
-
/////////////// COMMON TO TWO VERSIONS //////////////////////
-
if(serialVersionUID == 1 || serialVersionUID == 2){
-
}
-
-
if(serialVersionUID == 2 || serialVersionUID == 3){
-
-
}
-
if(serialVersionUID == 1 || serialVersionUID == 3){
-
-
}
-
///////////// COMMON TO ONE VERSION /////////////////////////
-
if(serialVersionUID == 1 ){
-
-
}else if(serialVersionUID == 2 ){
-
}else if(serialVersionUID == 3 ){
-
}
-
-
}
-
-
public function writeExternal(output:IDataOutput):void
-
{
-
trace("writeExternal....");
-
var d:Dictionary = new Dictionary();
-
d["serialVersionUID"] = serialVersionUID;
-
-
// name spaced var
-
d["introVarStr"] = introspectable::introVarStr;
-
d["defVarStr"] = defVarStr;
-
-
// this has already packed data
-
d["pubByteArray"] = pubByteArray;
-
-
// since BitmapData can't be retrieved, we've created a wrapper that can be.
-
d["pubBitmapData"] = new BitmapDataWrapper(pubBitmapData);
-
output.writeObject(d);
-
}
AS3 Customized Serialization and Deserialization, IExternalizeable to the rescue!
This and original off Pete's blog is great article on a little known feature critical for de/serialization in flash, and the knowledge is not just for client/server as the The flex documentation on the subject implies. Anywhere the AMF format is used: shared local objects, local connection, in AIR using fileStream. cloning objects using ByteArray etc. So many gems (might as well be easter eggs!) in the AS3 code, sadly barely documented. It's using IExternalizeable for custom serialization, which mimicks Java's already proven approach.
What's that mean? It means you can better control how objects are written and read back from the byteStream.
What if I don't want X to be written?
To compliment a great follow up on using the [Transient] metadata by Darron, with both the clone and when serializing to disk or the web. I've got to say the metadata format makes sense but also wierds me out.
How do I get my class type back
registerClassAlias is necessary for local cloning and maintaining Class type, when written to a ByteArray they forget which class they belong to. I would have hoped that the RemoteClass metadata would have worked but that was not the case. Anyway you can do this via a static var:
private static const REG:* = registerClassAlias("reflection.IntrospectableObj",IntrospectableObj);
this is called during static initialization, once which is all that's really needed as the linkage is static, not per instance.
What if you need to transcend versions of code?
basic approach to using serialization to deal with different versions of the class is
1) write a version number to the stream first, then a) save to hashmap, or b) iterate over the members that should be saved.
2) read the version number back from the stream, then reconstitue the members from a) the Dictionary (like Remotings's ASObject) or b) in the order they were written to the stream. Vary the parsing response upon the version number.
Summary of Serialization Tips:
1) Use IExternalizable to customize how your objects are written.
2) Use the [Transient] metadata to NOT serialize objects if you don't want them there, and are using writeObject().
3) registerClassAlias is necessary for local cloning and maintaining Class type. You can do this via a static var,
private static const REG:* = registerClassAlias("reflection.IntrospectableObj",IntrospectableObj);
4) keep a unique version number in the code, write a version first in the output stream then when reading it back perform different behavior based on that stream. The use of a Dictionary for key/values can be helpful too.
AS3 Serialization and Deserialization: ByteArray BitmapData registerClassAlias
Grr. Flash's registerClassAlias does not work with BitmapData (I wonder ?), so while you can save it to the ByteArray and wherever that can go... you can't get it back! As BitmapData complains about the constructor getting no args, presumeably that memory allocation is fixed upon the constructor.
This means you'll have to roll your own, which isn't too hard but it when having the BitmapData as members of other classes it might be a pain as each will have to serialize/deserialize the BitmapData using the IExternalizable.
Another option might have been overridding, but while BitmapData can be extended there isn't a no arg constructor, so the constructor can't really be overridden efficetively. A Proxy could be created for this.
Andrew has a good workaround, iterating over the pixels, though I've opted to write the width and height into the stream as well. You can use writeObject for the height and widht without problems (versus writing writeInt)
For additional details and others experiencing this problem: search terms "registerClassAlias BitmapData" "deserialize Bitmapdata"
- http://www.google.com/search?q=deserialize+BitmapData&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-US:official&client=firefox-a
- http://www.adobe.com/cfusion/webforums/forum/messageview.cfm?catid=641&threadid=1255888#4547075
- http://osflash.org/pipermail/red5_osflash.org/2007-April/011091.html
Solutions
- [1] http://www.cynergysystems.com/blogs/blogs/andrew.trice/BitmapData/SharedDesktop/srcview/
- [2] http://www.barncar.com/writejpg.mxml (sample code using corelib to save as Jpg)
- http://www.bytearray.org/?p=90 (a more flexible snapshot of video/swf to jpg using corelib, which is a completely different solution path)
- corelib is here http://code.google.com/p/as3corelib/
NOTE:
if you do need to use registerClassAlias you can do this trick:
private static const REG:* = registerClassAlias("reflection.IntrospectableObj",IntrospectableObj);
ArrayX and ArrayWeighting
com.troyworks.datastructures has a few useful collection types. One of the more frequently used is ArrayX, a drop in replacement for Array. ArrayX for eXtended. It has common used features in many collection management, media and game applications:
- swapPlaces
- shift (relative, absolute, number of positions) - useful for playlist
- shuffle - useful for cards and playlist
- remove (obj) - searches through and removes the item
- getFirstIndexOf(item, fromIdx, toIdx) - similar to String's use
- getLastIndexOf(item, fromIdx, toIdx) - similar to String's use
- getFilteredSet (fromIdx, toIdx, thatsNot, thatIsOneOf), this allows simple database querying of the collection, from a start position to an end position. With a filterout and filter in.
- getFilteredRandom - same as above, of the valid values in the collection return a random
- snapToClosest - useful for sorted number collections, gets the closest value in the collection.
- contains(item) -
- isBefore(itemA, itemB, inRange) - is itemA before itemB within inRange elements
- isAfter(itemA,itemB, inRange)- is itemA before itemB within inRange elements
Also paired with it is ArrayWeighting , a series of weights is paired with an array, that get's random elements based on the weighting/distribution. Imagine the source array represents group of pipe organ pipes, the element at [0] being one note, [1] being another, behind it is a bellows pushing air infinitely, but no sound is emitted until the valve is opened. A monkey is randomly jumping on the keyboard getting one peep at a time, if he's jumping fast enough there is a random chord, creating this random cacophony. However we can make this haromincally pleasing by weighting the notes. The weighting array is an array the same length of the pipes array, with a value inside of it that can be whatever, but it will get normalized relative to the other. Let's use 0-1 being 0% to 100%
- wts[0] = 1; //whatever is at pipes[0] will get called 100% of the time.
- wts[1] = 0; //whatever is at pipes[1] will get called 0% of the time.
Getting a stream of them would be
0,0,0,0,0,0,0, which might represent whistling the same note over and over again.
Setting the weights to
- 0 = .5
- 1 = .5
Getting a stream of them might be
0,1,0,1,1,0,0 which might be the jaws theme, or a two note chord depending on how fast they are played back.
The percentage of values gotten will get close to the weighting set. The weighting can be reset, so one can imagine a music game teaching the kid chords, at first one note at a time, then the weights being reset to teach them together, then the next section teaching a new chord, and after a review game, perhaps the valve will be stuck partly open to handle notes the kid is hard at hearing. Cogs is a good approach to managing these score sections.
The contents can be anything from quiz problems, particle, to enemies on a battlefield. The number of elements included can be arbitrary large, representing the game from beginning to end. The random ensures some variance, and replayability. Say we have a 100 different types of enemies that cover a game. The weights can be reset over time, their weights even tweened. The distribution might be
Level1: 1, .5, 0, 0...
Level2: .1, .7, .2, 0...
Level3: .1, .2, .5, .2 ...
Which graphically looks like a ripple moving from left to right, and from the users experience can be a smooth progression of difficulty. But it might not be exact from one replay to the other.
Last night I was able to get it and it's unit test converted to AS3 ( which took some wrangling, thankfully Array can be extended unlike most other types). Coming soon is CollectionManager which adds event management for collection changed, and toXML to present XML views (like used for TreeComponents)
AS3 Performance Optimizations
Summary, use linked lists instead of Array indexes for fastest performance in traversing collections.
http://blog.je2050.de/2007/06/06/lists-faster-than-array/
