JSON Parser: Part 1

For those who might not know, JSON stands for “JavaScript Object Notation”. It’s a pretty popular method for serializing objects into human-readable files, while still being easy to parse by an engine. A quick and dirty example:

//this is an example object for a player in a game
struct Player
{
  //health of the player, etc
  int health;
  int positionX;
  int positionY;
};

In JSON, this object might look like this:

{
"health" : 100,
"positionX" : 0,
"positionY" : 0,
},

Pretty easy! Obviously these files will be bigger than some binary format, but these are easier to debug. Our engine uses pseudo-JSON, but for all intents and purposes, they’re the same. Now, for our parser; I wanted to ease the burden placed upon our game designer. This took a couple tries… For loading/unloading data from the JSON file, the very first iteration looked something like this:

void LoadData(JSONLoader *loader)
{
  //the name to look for, and the variable to load.
  loader->LoadData("health", MyHealth_);
  loader->LoadData("positionX", MyPositionX_);
  //....
}

void SaveData(JSONSaver *saver)
{
  //the name to look for, and the variable to save.
  saver->SaveData("health", MyHealth_);
  saver->SaveData("positionX", MyPositionX_);
  //....
}

There’s a lot of similar code, and it’s a pain to have to write two different loader functions for every object. You also can’t define default parameters and such. This got me working on a solution, a universal JSON Loader. The new (current) implementation looks something like this:

void Transform::ArchiveLoader(I_Archive *loader)
{
  //the loader takes the name of the variable, the variable itself,
    //and a default value
  loader->Field("Translation", Translation, XMFLOAT3(0, 0, 0));
  loader->Field("Rotation", Rotation, XMFLOAT4(0, 0, 0, 0));
  loader->Field("Scale", Scale, XMFLOAT3(1, 1, 0));
}

Much better! This is straight from the source code of our transform component. It’s clean, easy to change, and quick to write. However, even this can be shortened with a macro:

//this assumes the loader is called loader
#define FIELD(VarName, Default) loader->Field(#VarName, VarName, Default)
FIELD(Scale, XMFLOAT3(1, 1, 0));

Fancy macro definitions! Now, you’ll notice that the parameter passed to the function is now an ambiguous “I_Archive”. This is my base class for my JSON loader/saver. The definitions for the main class are as follows:

class I_Archive
{
public:
 //loading, handling objects
 virtual bool LoadArchive(const char *filepath) = 0;
 virtual void UnloadArchive(void) = 0;
 virtual bool BeginObject(const char *name = 0) = 0;
 virtual void EndObject(void) = 0;

 //variable loading
 virtual void Field(const char *name, float &value, float defval) = 0;
 virtual void Field(const char *name, bool &value, bool defval) = 0;
 virtual void Field(const char *name, std::string &value, const std::string &defval) = 0;
 virtual void Field(const char *name, int &value, int defval) = 0;
 virtual void Field(const char *name, unsigned &value, unsigned defval) = 0;
 virtual void Field(const char *name, DirectX::XMFLOAT2 &value, DirectX::XMFLOAT2 defval) = 0;
 virtual void Field(const char *name, DirectX::XMFLOAT3 &value, DirectX::XMFLOAT3 defval) = 0;
 virtual void Field(const char *name, DirectX::XMFLOAT4 &value, DirectX::XMFLOAT4 defval) = 0;
 virtual void Field(const char *name, float &x, float &y, DirectX::XMFLOAT2 defval) = 0;
 virtual void Field(const char *name, float &x, float &y, float &z, DirectX::XMFLOAT3 defval) = 0;
 virtual void Field(const char *name, float &x, float &y, float &z, float &w, DirectX::XMFLOAT4 defval) = 0;
};

Every function is virtual. This is because the actual implementation exists within either “JSONLoader.cpp” or “JSONSaver.cpp”. I wanted the same syntax for both, so both the loader and the saver derive from this base class. Through the power of polymorphism and C++, one function can do the work of two! A quick rundown of the functions:

virtual bool LoadArchive(const char *filepath) = 0;

This opens a path to the file. For loading, it tries to find the file. For saving, it creates a new file for writing.

virtual void UnloadArchive(void) = 0;

This cleans up the loader as a whole, closing files, writing data, or doing anything that needs to happen last.

 virtual bool BeginObject(const char *name = 0) = 0;

This steps into an object. While loading, it will check to see if we are loading variables, or loading objects. The saver will add a “{” to the file, and recognizes that we are entering the scope of a new object.

 virtual void EndObject(void) = 0;

This function will end an object. The loader will do some stack manipulation, while the saver adds a “},”, and recognizes that we are leaving the scope of an object.

 virtual void Field(const char *name, float &value, float defval) = 0;

There are a lot of there, so I won’t cover every single one (since they’re mostly the same), but each one of these functions deals with a different variable type. This one is for floats. The loader will attempt to find a variable with the given name. If it exists, it sets the value to that variable. If we did not find a variable, or the data is messed up, the default value is used. For saving, we will write the name, followed by our data. This process varies from function to function, which is why they need to be individually defined. Making it templated wouldn’t be much of a help unless I overloaded all the output operators.

This is the basis of my parser! For now, I’ll start with this, but when I have the time, I’ll talk about the implementation of my saver and loader.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s