3

I am in the final stages of a game development and i have a bunch of objects like this;

roomBedroom = function () {
    this.title = "Bedroom";
    this.description = "I'm in a bedroom";
    this.noun = "bed";
    this.entities = new Array();
}

var bedroom = new roomBedroom();

What I want to do now is place all of my game objects into an array;

var savedGameObjects = {};
savedGameObjects['bedroom'] = bedroom;
var jsonGame = JSON.stringify(savedGameObjects);

The plan is to then save the savedGameObjects array and then recall it when the user loads the game again.

If I replace savedGameObjects['bedroom'] = bedroom; with savedGameObjects['bed'] = 'slappy'; it works but not when I have the object.

I really need to save the objects in their current state. I'd rather not go through each object saving key pieces of information one by one.

8
  • toSTring wont work? Commented Dec 12, 2013 at 2:19
  • 1
    where the code for 'bed' object? Commented Dec 12, 2013 at 2:20
  • 1
    @JonathandeM., the problem is when you convert it back to an object, the objects won't have the proper type/prototype. Commented Dec 12, 2013 at 2:20
  • Oops, typo. edited thanks Commented Dec 12, 2013 at 2:20
  • 1
    Are you trying to stringify the class or function expression? If so forget it, it's not the right way to do it. Create proper objects that holds the values so they can be stringified, and don't try to stringify classes. Commented Dec 12, 2013 at 2:24

5 Answers 5

1

This feels like a bit of a hack, but its the best I can come up with right now

Your serialization/deserializtion utility

This is going to attach obj.constructor.name to obj.__prototype before serialization. Upon deserializing, the prototype will be put back in place.

(function(global) {

  function serialize(obj) {
    obj.__prototype = obj.constructor.name;
    return JSON.stringify(obj);
  };

  function deserialize(json) {
    var obj = JSON.parse(json);
    obj.__proto__ = global[obj.__prototype].prototype;
    return obj;
  }

  global.serialize = serialize;
  global.deserialize = deserialize;

})(window);

A sample "class"

(function(global) {

  function Foo() {
    this.a = "a";
    this.b = "b";
  }

  Foo.prototype.hello = function() {
    console.log("hello");
  }

  global.Foo = Foo;

})(window);

Let's try it out

var foo = new Foo();

var json = serialize(foo);
console.log(json);

var newFoo = deserialize(json);
console.log('a', newFoo.a); // a
console.log('b', newFoo.b); // b

newFoo.hello(); // hello

Watch out for some gotchas

If you use an expression to define your "class", you will have a nameless constructor

var Foo = function() {};
var foo = new Foo();
foo.constructor.name; // ""

As opposed to a named function

function Foo() {}
var foo = new Foo();
foo.constructor.name; // Foo

In order for serialize and deserialize to work, you will need to use named functions


Another gotcha

The deserialize method expects your "classes" to exist on the in the same namespace (window in this case). You could encapsulate your game object classes in another way, just make sure that you reconfigure the deserialize method so that it can find the prototypes as needed.


Making this better

Instead of attaching serialize to the global window, you could have serialize live on (e.g.) the GameObject.prototype then your individual classes could inherit from GameObject. Serializing an object would then be as simple as

var json = foo.serialize();
// {"a":"a","b":"b","__prototype":"Foo"}

You could then define deserialize as GameObject.deserialize and restoring foo would be

var foo = GameObject.deserialize(json);

An alternative solution

Instead of implementing a custom serializer and deserializer, you could make very clever use of the Factory Method Pattern.

This might be a little verbose, but it does give you individual control over how a game object should be deserialized/restored.

var savedData = // your normal JSON here

var player = Player.create(savedData.player);

var items = [];
for (var i=0, i<savedData.items.length; i++) {
  items.push(Item.create(savedData.items[i]));
}

var map = Map.create(savedData.map);

This was a pretty interesting problem and I'm sure you're not the first to encounter it. I'm really curious to see what other people come up with.

Sign up to request clarification or add additional context in comments.

Comments

0

If I run the following code in a browser there is no problem getting the JSON string of the bedroom object, not sure what the problem is.

Note that JSON is data and bedroom is an object, bedroom may have behaviour like turnOffLight() that JSON doesn't have.

roomBedroom = function () {
    this.title = "Bedroom";
    this.description = "I'm in a bedroom";
    this.noun = "bed";
    this.entities = new Array();
}

var bedroom = new roomBedroom();

var savedGameObjects = {};
savedGameObjects['bedroom'] = bedroom;
//logs {"bedroom":{"title":"Bedroom","description":
//  "I'm in abedroom","noun":"bed","entities":[]}}
console.log(JSON.stringify(savedGameObjects));

So if you want to re create object instances from JSON data then you can change your constructor:

roomBedroom = function (args) {
    //following fails fast and loud, you could silently
    //fail by setting args to {}
    if(typeof args!=="object")
      throw new Error("Have to create roomBedroom by passing an object");
      //or do args={} to silently fail
    this.title = args.title||"Bedroom";
    this.description = args.description||"I'm in a bedroom";
    this.noun = args.noun||"bed";
    //if entities are objects with behavior
    //  you have to re create them here passing the JSON data
    //  as I've done with roomBedroom
    this.entities = args.entities||new Array();
}
var jsonString='{"bedroom":{"title":"Bedroom",'+
  '"description":"I\'m in a bedroom",'+
  '"noun":"bed","entities":[]}}';
var bedroom = new roomBedroom({});
bedroom.entities.push({hi:"there"});
bedroom.title="Master Bedroom";
//serialize bedroom to a json string
var jsonString = JSON.stringify(bedroom);
//create a roomBedroom instance named br2 using
// the serialized string
var br2=new roomBedroom(JSON.parse(jsonString));

//compare if they are the same
console.log(JSON.stringify(bedroom)===JSON.stringify(br2));//true

Comments

0

I have an approach that might work for you. You can see it in action on JSFiddle.

The main point is to use the reviver parameter to JSON.parse to reconstruct your object when it's parsed.

I do this with a general-purpose reviver that can be configured for multiple different types, although here the only one used is the RoomBedroom constructor. This implementation assumes that you have simple copy constructors that create new objects using a reference to an existing one. (For other, more sophisticated possibilities, see an answer to another question I gave in February.) To make it easy to have a copy constructor, I have one more function that accepts a very simple constructor function and a set of default values and builds a copy constructor function for you.

var MultiReviver = function(types) {
    return function(key, value) {
        var type;
        for (var i = 0; i < types.length; i++) {
            type = types[i];
            if (type.test(value)) {
                return new type.constructor(value);
            }
        }
        return value;
    };
};


var makeCloningConstructor = (function() {
    var clone = function(obj) {return JSON.parse(JSON.stringify(obj));};
    var F = function() {};

    return function(Constructor, defaults) {
        var fn = function(obj) {
            Constructor.call(this);
            var self = this;
            var config = obj || {};
            Object.keys(defaults).forEach(function(key) {
                self[key] = clone(defaults[key]);
            });
            Object.keys(config).forEach(function(key) {
                self[key] = clone(config[key]);
            });
        };
        F.prototype = Constructor.prototype;
        fn.prototype = new F();
        fn.constructor = Constructor;

        return fn;
    };
})();

// Note: capitalize constructor functions
var RoomBedroom = makeCloningConstructor(function RoomBedroom() {}, {
    title: "Bedroom",
    description: "I'm in a bedroom",
    noun: "bed",
    entities: []  // Note: use `[]` instead of `new Array()`.
});

RoomBedroom.prototype.toggleLight = function() {
    this.lightOn = !this.lightOn;
};

RoomBedroom.prototype.checkLights = function() {
    return "light is " + (this.lightOn ? "on" : "off");
};

var bedroom = new RoomBedroom();
bedroom.windowCount = 3; // add new property
bedroom.noun = "king-sized bed"; // adjust property
bedroom.toggleLight(); // create new propery, use prototype function
console.log(bedroom.checkLights());

var savedGameObjects = {};
savedGameObjects['bedroom'] = bedroom;
var jsonGame = JSON.stringify(savedGameObjects);

var reviver = new MultiReviver([{
    constructor: RoomBedroom,
    test: function(obj) {
        var toString = Object.prototype.toString, str = "[object String]", 
            arr = "[object Array]";
        return toString.call(obj.title) == str && 
               toString.call(obj.description) == str && 
               toString.call(obj.noun) == str && 
               toString.call(obj.entities) == arr;
    } 
}]);

var retrievedGameObjects = JSON.parse(jsonGame, reviver);

// data comes back intact
console.log(JSON.stringify(retrievedGameObjects, null, 4));
// constructor is as expected
console.log("Constructor: " + retrievedGameObjects.bedroom.constructor.name);
// prototype functions work
console.log(retrievedGameObjects.bedroom.checkLights());

I don't know if it's precisely what you were looking for, but I think it's at least an interesting approach.

Comments

0

the faster route

It is better — from an optimisation point of view — to do as Adeneo states, which is power each of your Game Objects by an exportable simple object i.e:

roomBedroom = function(){
    this.data = {};
    this.data.title = 'Bedroom'
    /// and so on...
}

These can then be easily stored and re-imported just by JSON.Stringifying and overwriting the data property. For example, you could set-up the system that Maček mentions (+1) which is to give each of your game objects serialize and deserialize functions:

roomBedroom.prototype.serialize = function(){
    return JSON.stringify( this.data );
};

roomBedroom.prototype.deserialize = function( jstr ){
    this.data = JSON.parse(jstr);
};


the quicker way

However, you can make a simple addition to what you already have using the following:

First enhance your Game Objects with an objectName property. This is because constructor.name and function.name are unreliable and do strange things the further back in time you go, far better to use a string you have set in stone.

var roomBedroom = function ( title ) {
    this.objectName = "roomBedroom";
    this.title = title;
    this.description = "I'm in a bedroom";
    this.noun = "bed";
    this.entities = new Array();
};

Then the additional code to help with storage:

var storage = {};

/// add your supported constructors to this list, there are more programmatic
/// ways to get at the constructor but it's better to be explicit.
storage.constructors = {
    'roomBedroom' : roomBedroom
};

/// take an instance and convert to simple object
storage.to = function( obj ){
    if ( obj.toStorage ) {
        return obj.toStorage();
    }
    else {
        var keep = {};
        for ( var i in obj ) {
            if ( obj.hasOwnProperty(i) && !obj[i].call ) {
                keep[i] = obj[i];
            }
        }
        return keep;
    }
}

/// take simple object and convert to an instance of constructor
storage.from = function( obj ){
    var n = obj && obj.objectName, c = storage.constructors[n];
    if ( n && c ) {
        if ( c.fromStorage ) {
            return c.fromStorage( obj );
        }
        else {
            var inst = new c();
            for ( var i in obj ) {
                if ( obj.hasOwnProperty(i) ) {
                    inst[i] = obj[i];
                }
            }
            return inst;
        }
    }
    else {
        throw new Error('`' + n + '` undefined as storage constructor');
    }
}

Once you have that you can use it like so:

var savedGameObjects = {};
    savedGameObjects['bedroom'] = storage.to(new roomBedroom("bedroom"));
    savedGameObjects['bedroom2'] = storage.to(new roomBedroom("bedroom2"));

var jsonGame = JSON.stringify(savedGameObjects);
console.log(jsonGame);

savedGameObjects = JSON.parse(jsonGame);
for( var i in savedGameObjects ) {
    savedGameObjects[i] = storage.from(savedGameObjects[i]);
    console.log(savedGameObjects[i]);
}


extras

You can also be specific about the way objects get stored/unstored by supplying toStorage and fromStorage methods on your constructed instances and constructors respectively. For example, you could use the following if you only wanted to store titles of roomBedrooms. Obviously this is an unrealistic use-case, you'd more often use this to avoid storing cached or computed sub-objects and properties.

roomBedroom.prototype.toStorage = function( obj ){
    var ret = {};
    ret.title = obj.title;
    return ret;
};

roomBedroom.fromStorage = function( obj ){
    var inst = new roomBedroom();
    inst.title = obj.title;
    return inst;
};

The above also means you can take advantage of improving your Game Object construction by providing parameters, rather than iterating over properties which can be slow and error-prone.

roomBedroom.fromStorage = function( obj ){
    return new roomBedroom( obj.title );
};

Or even:

roomBedroom.fromStorage = function( obj ){
    return new roomBedroom( obj ); // <-- the constructor processes the import.
};


fiddle

http://jsfiddle.net/XTUdp/


disclaimer

The above code relies on the existence of hasOwnProperty which is not present cross-browser yet, a polyfill should be used until it is... or, if you aren't doing anything complicated with prototype inheritance you don't need to worry and can remove it from the code.

Comments

-2

you can declare a big variable like

var world = {};

and each small variable declare as

var bedroom = world.bed = (world.bed || new roomBedroom());

remember never change bedroom to another object, i think this will work fine, but looks too long winded

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.