(repost from old FP site)
all players agree that saving game state is something worth implementing. for coders, however, it may seem pain in the ass. here’s my solution to this problem.
whole idea is divided into two parts. first thing is generic object serializer, that stores chosen properties into ByteArray. second one is more FlashPunk specific and concerns worlds and entities.
Serializer.as:
package rostok
{
import flash.utils.ByteArray;
import flash.utils.getDefinitionByName;
import flash.utils.getQualifiedClassName;
/**
* class to serialize and deserialize objects in ByteArray
* @author rostok
*/
public class Serializer
{
// an object of arrays, key is object's class name, value is array of property names
private static var objectsProperties:Object = new Object();
public function Serializer()
{
}
/* reisters class and selected properties
* @cl a class to save properties for
* @propname is a name of property or string of names separated by commas
*/
public static function registerClass(cl:Class, propname:String=""):void
{
var cn:String = getQualifiedClassName(cl);
var pn:Array;
if (objectsProperties[cn])
pn = objectsProperties[cn];
else
pn = objectsProperties[cn] = new Array();
var a:Array = propname.split(",");
for each (var pp:String in a) {
pp = pp.replace(" ", ""); // remove spaces
if (pn.indexOf(pp)<0)
pn.push(pp);
}
}
/* returns true if this class was added to Serializer
*/
public static function isSupported(o:*):Boolean
{
return objectsProperties[ getQualifiedClassName(o) ] != null;
}
/* saves properties to ByteArray
* only properties that were registered earlier are saved
* if object's parent class was registered parent't properties are saved as well
* if object has save() method it is called before writing to stream
* @src any object
* @return new ByteArray
*/
public static function serialize(src:*):ByteArray
{
if (src.hasOwnProperty("save")) src.save();
var o:Object = new Object();
o["___className___"] = getQualifiedClassName(src);
var defined:Boolean = false;
for (var cn:String in objectsProperties)
if (src is Class(getDefinitionByName(cn)))
{
defined = true;
var pn:Array = objectsProperties[cn];
for each (var p:String in pn) {
if (src.hasOwnProperty(p)) o[p] = Object(src)[p];
}
}
if (!defined) throw("Serializer:serialize() object not defined");
var ba:ByteArray = new ByteArray();
ba.writeObject(o);
return ba;
}
/* reads object from ByteArray's current position
* creates it and sets its properties
* if object has load() method it is called after all properties were set
*/
public static function deserialize(ba:ByteArray):*
{
var o:Object = ba.readObject();
var temp:* = new (getDefinitionByName(o["___className___"]));
for(var p:String in o) {
if (p != "___className___")
temp[p] = o[p];
}
if (temp.hasOwnProperty("load")) temp.load();
return temp;
}
}
}
this is a simple static class that offers only 4 methods: registerClass - call it to register class you want to save later, only chosen properties will be saved. if you register parent class its properties will be saved in descendants. isSupported - this method tells whether class was registered earlier serialize - returns new ByteArray with stored object’s properties deserialize - reads ByteArray, creates new instance and sets properties with values that were stored in a stream
to seralize complex types call registerClassAlias() earlier (see AS3 docs)
quick example:
// register
Serializer.registerClass(SomeComplexEntity,"x,y,type");
Serializer.registerClass(Player,"ammo,health"); // Player extends SomeComplexEntity
Serializer.registerClass(Enemy,"health"); // as above
// save
var ba:ByteArray;
ba = Serializer.serialize(myPlayer);
ba = Serializer.serialize(enemy);
// load
ba.position = 0; // read from start
myPlayer = Serialized.deserialize(ba);
enemy = Serialized.deserialize(ba);
tell me what you want by this is way more convenient than XML parsing. the generic approach however has some drawbacks - you can’t restore private members.
let’s move to FlashPunk. each save game is a ByteArray with all entities that should be saved in all worlds in games. of course i assumed that saving entity properties is enough to restore its state. savegame ByteArray is string with world’s name, number of entities in it, entities… and so on. last world is world that should be active.
// declarations
public var worlds:Object;
private var quickSave:ByteArray;
// somewhere near constructor initialize the serializer
registerClassAlias("flash.geom.Vector3D", Vector3D);
registerClassAlias("flash.geom.Point", Point);
registerClassAlias("Vector.<Vector3D>", Vector.<Vector3D> as Class);
Serializer.registerClass(Actor, "x,y,type,health,runStep,walkStep,aiStateLast,aiState,talkAlias,radius,dir,newDir,dirAdj,speed,fov,seeDist,move,limitAngle,decelerate,autoDecelarate,lastDir,lastAnim,newAnim,resetAnim,targetEnemyID,uniqueActorID,waypoints");
Serializer.registerClass(Player);
Serializer.registerClass(Wolf);
Serializer.registerClass(Dwarf);
Serializer.registerClass(Deer);
Serializer.registerClass(Spider, "venom");
Serializer.registerClass(Boomerang, "x,y,type,iterations,target,speed,start,dir,iterations,targetDistance,travelledDistance,spd,boomerangsInAir");
// load some levels into worlds object
worlds = new Object();
worlds["mines"] = lastWorld = new Game("mines_02.xml");
worlds["forest"] = lastWorld = new Game("forest_14.xml");
worlds["smallworld"] = lastWorld = new Game("smallworld.xml");
FP.world = lastWorld;
// and finally
/* saves very quickly
* @return ByteArray with name of world (string), number of entities, objects properties
*/
public function save():ByteArray
{
var saveData:ByteArray = new ByteArray();
var a:Array = new Array();
var wo:Object = new Object;
var worldNameList:Array = new Array();
var worldName:String;
for (worldName in worlds)
if (worlds[worldName] != FP.world)
worldNameList.unshift( worldName );
else
worldNameList.push( worldName );
for each (worldName in worldNameList) {
worlds[worldName].getAll(a);
saveData.writeObject(worldName);
var entitiesNumber:int = 0;
var entitiesByteArray:ByteArray = new ByteArray();
for each (var e:Entity in a)
if (Serializer.isSupported(e)) {
entitiesNumber++;
entitiesByteArray.writeBytes( Serializer.serialize(e) );
}
saveData.writeInt(entitiesNumber);
saveData.writeBytes(entitiesByteArray);
}
trace(saveData.length);
saveData.compress();
trace(saveData.length);
return saveData;
}
/* restores quickly
*/
public function load(saveData:ByteArray):void
{
if (!saveData) return;
saveData.uncompress();
saveData.position = 0;
var e:Entity;
var a:Array = new Array();
var wo:Object = new Object;
while (saveData.position < saveData.length) {
var worldName:String = saveData.readObject();
var entitiesNumber:int = saveData.readInt();
worlds[worldName].getAll(a);
for each (e in a) {
if (Serializer.isSupported(e)) worlds[worldName].remove(e);
}
worlds[worldName].updateLists();
for (; entitiesNumber > 0;entitiesNumber-- ) {
e = Serializer.deserialize(saveData);
worlds[worldName].add(e);
}
worlds[worldName].updateLists();
}
FP.world = worlds[worldName];
saveData.compress();
}
// and action!
if (Input.released("save")) quickSave = save();
if (Input.released("load")) load( quickSave );
saving is just parsing every world and serializing to stream entities with types registered in serializer.
what may be difficult is saving references to another objects. for this you will have to set unique ID to each of them before save and restore reference on load. but this is another story…
basic example http://rostok.3e.pl/download/SerializeIntoSaveGame.zip