Using 'File' to save scene object locations to rebuild later in AS3
I am attempting to use the 'File' function in ActionScript 3 to save the following information:
I have varying draggable display objects in the scene, the amount and type can vary. I want to save the amount and their position and then load them back in a future session.
I am struggling to use File to save anything, I have searched the Adobe documentation and cannot get my head round how to use it.
I have not yet developed any code using it.
Any help would be appreciated.
Thank you.
Solution 1:
You are trying to write a DisplayObject
into the file directly, this is prevented by Flash engine due to the way Flash handles default serialization of any object. In order to save a DisplayObject
into the external resource, you need to employ IExternalizable
on that object's class and any class of objects you will plan to store as well. The implementation of writeExternal
should save all data required to rebuild the said object from scratch, and readExternal
should also employ methods to restore the integrity of said DisplayObject
by performing addChild()
on nested display objects, or adding them into other internal structures that object might contain.
Note, other answers contain valid points for doing a custom serialization with XML or JSON, and also contain links to requires import, in particular, flash.utils.registerClassAlias
and flash.utils.getDefinitionByName
are gravely needed to recreate the structure from a serialized data chunk.
An example: Let's say you have a drawing board in a Board
class, and a set of rectangles that you can drag by using mouse, that differ by size and color. Rectangles are custom made MovieClip
s and don't have a class of their own, but each MovieClip
is also assigned a color
property to simplify their distinction. This means you need to implement IExternalizable
on Board
class only. Let's also assume Board
class has a pieces
array that contains all links to nested rectangles, and a method to create a new properly sized rectangle based on width, height and color supplied as parameters. (There might be more requirements to the data structure of Board
to meet in your case, so watch closely) So, the process of serializing Board
will be to collect all the data from nested MCs and stuff it in order into IDataOutput
supplied, and the process of restoring an instance of Board
should retrieve stored data, parse it to find what is where, create the nested MCs to be the same like they've been stored, position them properly, addChild() to self and rebuild the
pieces` array.
public class Board extends Sprite implements IExternalizable {
private var pieces:Array;
public function createRectangle(_width:Number,_height:Number,color:uint):MovieClip {
var mc:MovieClip=new MovieClip();
mc.graphics.beginFill(color);
mc.graphics.drawRect(0,0,_width,_height);
mc.graphics.endFill();
mc.color=color;
pieces.push(mc);
return mc;
}
A refinement to data structure is already visible - you need to store the passed _width
and _height
in the MC somewhere, because the actual width
of that MC will differ from what's passed by the default line thickness (1, 0.5 on either side). x
and y
are properly retrieved from MC's properties, though. So, adding both lines into createRectangle
is necessary.
mc._width=_width;
mc._height=_height;
With this, serializing the Board
becomes more easy.
public function writeExternal(output:IDataOutput):void {
var pl:int=pieces.length; // cache
output.writeInt(pl); // assuming we keep this array in integral state
for (var i:int=0;i<pl;i++) {
var _mc:MovieClip=pieces[i];
output.writeDouble(_mc.x); // this is usually not rounded when dragging, so saving as double
output.writeDouble(_mc.y);
output.writeDouble(_mc._width);
output.writeDouble(_mc._height);
output.writeInt(_mc._color);
}
// if anything is left about the "Board" itself, write it here
// I'm assuming nothing is required to save
}
To restore, you need to read the data out of IDataInput
in the very same order as it was written in writeExternal
and then process to rebuilding the display list we've stored.
public function readExternal(input:IDataInput):void {
// by the time this is called, the constructor has been processed
// so "pieces" should already be an instantiated variable (empty array)
var l:int;
var _x:Number;
var _y:Number;
var _width:Number;
var _height:Number;
var _color:uint;
// ^ these are buffers to read data to. We don't yet have objects to read these into
input.readInt(l); // get pieces length
for (var i:int=0;i<l;i++) {
input.readDouble(_x);
input.readDouble(_y);
input.readDouble(_width);
input.readDouble(_height);
input.readInt(_color);
// okay we got all the data representing the rectangle, now make one
var mc:MovieClip=createRectangle(_width,_height,_color);
mc.x=_x;
mc.y=_y;
addChild(mc); // createRectangle does NOT have addchild call
// probably because there are layers for the parts to be added to
// I'm assuming there are no layers here, but you might have some!
// pieces array is populated inside createRectangle, so we leave it alone
}
// read all the data you have stored after storing pieces
}
In case your nested MCs have a class that also implements IExternalizable
, you can save the entire array in a single instruction, writeObject(pieces)
, this will make Flash walk through the array, find all data it contains and call writeObject
on any nested object, essentially calling that class's writeExternal
function for each of the instance in the array. Restoring such an array should include rebuilding the display list by walking the array and calling addChild()
on each of the restored instances.
And last but not the least, registerClassAlias()
should be called prior to doing any serialization or deserialization of custom objects. Best place to call these is probably your main object's constructor, as this will surely be called before any other code your application contains.
Solution 2:
Assuming all your objects to save belong to the same parent, you could dosomething along these lines:
First, create a class file (let's call is SaveData.as
and put it in the root of your project directory). This will describe the data you want to save:
package
{
import flash.geom.Rectangle;
public class SaveData
{
public var bounds:Rectangle; //to save where an object is on the stage
public var classType:Class; //to save what kind of object it is
//you could add in more proterties, like rotation etc
public function SaveData() {
}
}
}
Next, on your save function, do something like this:
//this will hold all your data
//a vector is the same as an array only all members must be of the specified type
var itemList:Vector.<SaveData> = new Vector.<SaveData>();
//populate the array/vector with all the children of itemContainer
var tmpItem:SaveData;
//loop through all children of item container
for (var i:int = 0; i < itemContainer.numChildren; i++) {
tmpItem = new SaveData(); //create a new save record for this object
tmpItem.bounds = itemContainer.getChildAt(i).getBounds(itemContainer); //save it's bounds
tmpItem.classType = getDefinitionByName(itemContainer.getChildAt(i)) as Class; //save it's type
itemList.push(tmpItem); //add it to the array
}
//Now you have an array describing all the item on screen
//to automatically serialize/unserialize, you need this line (and you need to register every class nested in SaveData that isn't a primitive type - which would just be Rectangle in this case
registerClassAlias("SaveData", SaveData);
registerClassAlias("flash.geom.Rectangle", Rectangle);
//create a new File to work with
var file:File = File.applicationStorageDirectory; //or whatever directory you want
file.resolvePath("saveData.data"); //or whatever you want to call it
var fileStream:FileStream = new FileStream();
fileStream.open(file, FileMode.WRITE);
fileStream.writeObject(itemList); //write the array to this file
fileStream.close();
Now, to load it back in:
var itemContainer:Sprite = new Sprite(); //however you initialize this
addChild(itemContainer);
var file:File = File.applicationStorageDirectory;
file.resolvePath("saveData.data");
var fileStream:FileStream = new FileStream();
fileStream.open(file, FileMode.READ);
var itemList:Vector.<SaveData> = fileStream.readObject() as Vector.<SaveData>;
fileStream.close();
//now that you've read in the array of all items from before, you need to recreate them:
var tmpItem:DisplayObject;
var tmpClass:Class;
//loop through all items in the array, and create a object
for (var i:int = 0; i < itemList.length; i++) {
tmpClass = itemList[i].classType; //The type of item
tmpItem = new tmpClass() as DisplayObject; //create the item
//now move the item to it's former position and scale
tmpItem.x = itemList[i].x;
tmpItem.y = itemList[i].y;
tmpItem.width = itemList[i].width;
tmpItem.height = itemList[i].height;
//add the item back to the parent
itemContainer.addChild(tmpItem);
}
If you're not sure of the imports, here they are:
import flash.filesystem.File;
import flash.filesystem.FileMode;
import flash.filesystem.FileStream;
import flash.net.registerClassAlias;
import flash.utils.getDefinitionByName;
import flash.utils.getQualifiedClassName;