Serialize world into a savegame


(rostok) #1

(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


What is the performance impact when saving & loading from a SharedObject?
(Zachary Lewis) #2

Instead of trying to serialize a bunch of objects to read later, you can just use Adobe’s SharedObject class. It will automatically save data for you and load it when the .swf is run again, making game persistance easy, even on games hosted on the internet.

/** Save the current game to local storage. */
function saveGame():void
{
  // Get a reference to a SharedObject with the key "savedData".
  var so:SharedObject = SharedObject.getLocal("savedData");

  // Put whatever dang ol' objects you want in there.
  so.setProperty("playerLevel", currentLevel);
  so.setProperty("stars", collectedStars);
  so.setProperty("currentWorld", FP.world);

  // Okay. Everything is saved.
}

/** Load a game from local storage. */
function loadGame():void
{
  // Get a reference to a SharedObject with the key "savedData".
  var so:SharedObject = SharedObject.getLocal("savedData");

  // Pull whatever dang ol' thing we want out of here. Be sure to check for errors!
  currentLevel = so.data.hasOwnProperty("playerLevel") ? so.data.playerLevel : 1;
  collectedStars = so.data.hasOwnProperty("stars") ? so.data.stars : 0;
  FP.world = so.data.hasOwnProperty("currentWorld") ? so.data.currentWorld : new GameWorld();

  // Okay. Everything is loaded.
}

(rostok) #3

It looks like I have seriously overcomplicated things. I can’t get yours code running (however I will try harder later today). But storing stuff in shared object is serializing as well, and isn’t it like storing everything including spritemaps and other assets?


(Zachary Lewis) #4

I was looking back through my old stuff, and I noticed that I used a shared object through FlashPunk’s Data class.

Here’s the saving and loading logic from Escape From Music Manor.


(rostok) #5

Zach but this example saves only couple of integers. How can you serialize whole world automatically without registering all the classes including those with obligatory parameters in constructors?


(Zachary Lewis) #6

I may be highly mistaken. I remember trying to save stuff in the past and went way down a rabbit hole to try to find a good way to save data before discovering this simple solution.

Looking back, it turns out I wasn’t saving an entire world. Your way is a simple way to save the entire world.