Creating a Map Editor For a Game (4/6)

This series was initially expected to be 3 parts long, but the new estimated length is actually 6 parts(!). You can hopefully find the previous parts somewhere in the blog. First part is the problem statement, second part is data model, third part is the script.

In the previous post, we saw how the script generates the header declaration, and writes it to the *.h file. How about the implementation *.cpp code? Is that generated by the script, too? You bet it is, and that’s what we will be looking at today. Before we actually embark on that journey, we shall explore another data model that we are using extensively.

JSON

Honestly, there is nothing that can substitute the great documentation in the json official website, but we will go over it real quick, anyways.

JSON files are actually almost identical to plist files from a high level perspective. They can represent arrays, dictionaries, strings, numbers, booleans, and nulls. Not 100% identical to plists, but close enough. They need the root object to be either an array or a dictionary. The most desirable feature of JSON files is the lightweight format they have. It is also very readable, unlike XML files. Let’s look a a very simple JSON file:

[    
    {    
        "lname" : "Edogawa",    
        "fname" : "Conan"    
    },    

    {    
        "lname" : "Hattori",    
        "fname" : "Heiji"    
    },    
]

This JSON description has an array as the root object (indicated by the “[…]”), and the array contains two dictionaries (or objects)(indicated by the “{…}”). Each dictionary has two keys, “fname”, and “lname”, and the values associated with these keys are strings. I think that is enough about JSON files, so let us move on.

So, can you guess how the Map Editor exports the maps? Yup, it serializes the data into a JSON file! That JSON file is fed to the game, which parses the JSON file and creates the necessary entities in C++ land. So, let’s look at this so called parser.

The Parser

Our game utilizes the awesome JsonCPP library to parse the JSON file into a structured JSON::Value object structure. You can look at it this way: The json file is ultimately just a text file. The JsonCPP library takes this file, and parses it while instantiating objects from the JSON::Value class, and sets the type of these object as Array, Dictionary, String, … etc. based on what it finds in the JSON file. Let’s see how our code looks like regarding the JSONCpp parser:

void MapParser::parse(const std::string &mapFile)    
{    
    FileUtil f;    
    std::string fullpath = f.getAbsolutePath(mapFile);    
    std::ifstream filestream(fullpath.c_str());    

    Json::Value root;   // will contains the root value after parsing.    
    Json::Reader reader;    

    bool parsingSuccessful = reader.parse(filestream, root);    
    if ( !parsingSuccessful )    
    {    
        std::cout  << "Failed to parse FFX File\n" << reader.getFormatedErrorMessages();    
        return;    
    }    

    resolveJsonValuesRecuresively(root);    
}    

void MapParser::resolveJsonValuesRecuresively(const Json::Value &children)    
{    
    // base case    
    if (children.isNull())    
    {    
        return;    
    }    

    for (int i = 0 ; i<children.size() ; ++i)    
    {    
        const Json::Value& val = children[i];    
        const std::string& class_id = val.get("class_id", "").asString();    
        /* The resolution code will be generated */    
        // because I am lazy    
        if (class_id == "") {}    
#pragma START-ParserResolver-CODEGEN    
        else if (class_id == "MapMeta")    
        {    
            MapMeta obj(val["parameters_data"]);    
            m_pSink->OnMapMetaCreated(obj);    
        }    
        else if (class_id == "MapObject")    
        {    
            MapObject obj(val["parameters_data"]);    
            m_pSink->OnMapObjectCreated(obj);    
        }    
        else if (class_id == "MapTrigger")    
        {    
            MapTrigger obj(val["parameters_data"]);    
            m_pSink->OnMapTriggerCreated(obj);    
        }    
        else if (class_id == "MapZone")    
        {    
            MapZone obj(val["parameters_data"]);    
            m_pSink->OnMapZoneCreated(obj);    
        }    
        else if (class_id == "MapSpawnArea")    
        {    
            MapSpawnArea obj(val["parameters_data"]);    
            m_pSink->OnMapSpawnAreaCreated(obj);    
        }    
        else if (class_id == "MapActionSpawnUnit")    
        {    
            MapActionSpawnUnit obj(val["parameters_data"]);    
            m_pSink->OnMapActionCreated(obj);    
        }    

        ...    

#pragma END-ParserResolver-CODEGEN    

        const Json::Value& grandChildren = val.get("children", Json::Value::null);    
        resolveJsonValuesRecuresively(grandChildren);    
    }    
}

Starting with the MapParser::parse method, it first takes the map file name passed as an argument and loads it from the file-system using the fileUtils component. After that, it’s JSONCpp’s turn. We use the library to create the root JSON::Value, which is an array in our case. This might be a good time to go back to the second post in this series and review the map editor object format. After reviewing the format, you can see that the root is an array of dictionaries. That’s why, in the code we send the root object directly to the MapParser::resolveJsonValuesRecuresively. This method takes an array JSON::Value.

As the name implies, MapParser::resolveJsonValuesRecuresively is a recursive method, hence it needs a base case that breaks the recursion. The base case is when the “JSON::Value& children” is a null value. Please note, it is not the regular C++ null!! This is a JSON::Value that has a null type. It is awesome because it won’t crash your app when you try accessing it, as opposed to using C++ null. This is actually a design pattern called the Null Object pattern. Moving along, we then loop over the children array, which contains JSON::Values of the object (dictionary) type. Notice the difference here from the mentioned format, the use of “class_id” instead of “name” to indicate the type.

After fetching the class_id, we need to create an object that matches that class_id value, but these object types may change anytime! We may want to add new types, remove types, etc. Not to mention that the string checking is a very “error-prone” approach on its own. The whole idea is to make this data driven, right? HENCE, the #pragma START-ParserResolver-CODEGEN! :D This #pragma thing is actually the delimiter for the python script to start generating the “Resolution code”. I call it resolution code because it resolves the type of the object that needs to be created from a string. So, let us zoom into a single component of the resolution code:

else if (class_id == "MapObject")    
{    
    MapObject obj(val["parameters_data"]);    
    m_pSink->OnMapObjectCreated(obj);    
}

In this example, we are checking if the class_id is equal to MapObject. Of course, we saw in the previous post that we are generating these entity structs, right? This way, if the class_id is equal to the entity type, we simply send the JSON::Value’s “parameters_data” to the constructor. This is another difference from the format presented earlier, actually, so keep that in mind. Anyway, this “parameters_data” is a dictionary that has keys which are the parameters’ names (position, size, type, … etc), so we pass it to the object’s constructor so it can initialize the entity with the JSON values. REMEMBER, this code is generated, and so is the constructor code for those classes, as we will see in a bit.

But, how are these parser resolution if statements generated, you say? I have shown enough code generation scripts, so I am just gonna say it’s as simple as before. We have a template file (see below) that we fill with the info. There is a slight trick that I do here, which is if the class_id has a super class, I handle it differently. This has to do with the “m_pSink” thingy. Let’s move to that.

else if (class_id == "{prefab_name}")
{{
    {prefab_name} obj(val["parameters_data"]);
    m_pSink->On{prefab_sink_method}Created(obj);
}}

Let’s rewind a bit now, and go back to “We send the parameter_data to the constructor”. Ok, we send the parameter_data to the constructor, but what does it do with it? Let’s see the code first:

MapObject::MapObject(const Json::Value& val):
position(val.get("position", Json::Value::null).get("children", Json::Value::null)),
entity_id(val.get("entity_id", Json::Value::null).get("value", "").asString()),
commander(val.get("commander", Json::Value::null).get("value", 1).asInt()),
uid(val.get("uid", Json::Value::null).get("value", 1).asInt()),
tile_size(val.get("tile_size", Json::Value::null).get("children", Json::Value::null))
{

}

I am so glad I learned about the C++ initializer list!! I am referring to the code after the colon, and before the opening brace. This code is generated in a few simple steps:

  1. Write the variable name.
  2. Open a parenthesis to initialize the variable with a value.
  3. Fetch the variable data from the val, using get([VAR_NAME]). (1)
    • If the variable is a primitive, append .get("value").as[TYPE]. (2)
    • If the variable is a struct, append .get("children").

(1): We actually use get("[VAR_NAME]", DEFAULT_VALUE), which returns the DEFAULT_VALUE if the variable is not found. This is to make sure the parser is backward compatible and not freak out if VAR_NAME was not found. (2): So, if the type is a string, it will be .asString(), int would be .asInt(), and so on. Since tile_size above is a struct, we just send it the children JSON::Value, and the MapSize struct will fetch the width and height from it in its initializer list.

Integrating With the Engine

With the parsing out of the way, we ultimately need to send these created objects to the game engine to start using these objects in the game, right? So, when we created a MapObject, for example, we have to send that object to the engine, where it can (for example) load it visually in the game by creating a simulationObject for it. This is a bit too deep and outside the scope of this series, but the process of sending the object to the engine isn’t. We send the objects using the delegate pattern (or also called sink). This works as follows:

When someone creates a MapParser object, they have to send a pointer to an object that implements an interface defined by the MapParser, so the MapParser can do it’s job. In our case, this is the interface the MapParser defines:

class MapParserSink    
{    
public:    
    /* These methods will be automatically generated */    
#pragma START-ParserSink-CODEGEN    
    virtual void OnMapMetaCreated(const MapMeta&) {}    
    virtual void OnMapObjectCreated(const MapObject&) {}    
    virtual void OnMapTriggerCreated(const MapTrigger&) {}    
    virtual void OnMapZoneCreated(const MapZone&) {}    
    virtual void OnMapSpawnAreaCreated(const MapSpawnArea&) {}    
    virtual void OnMapActionCreated(const MapAction&) {}    
    virtual void OnMapOperatorCreated(const MapOperator&) {}    
    virtual void OnMapOperandCreated(const MapOperand&) {}    
#pragma END-ParserSink-CODEGEN    
};

As you can see, these methods are basically callbacks for every entity type that the parser can create. And before you ask, YES, THIS IS GENERATED, TOO! It is actually quite obvious that this is generated from the delimiters that are there. So, there you have it! That’s how the developer gets the objects and starts using ‘em! As simple as that!

(A keen C++ developer will notice that these are NOT pure virtual methods. This is because when we add a new type in the Map Editor, and this code is generated, the developer from the game engine side might not implement the new methods immediately for one reason or the other (maybe he’s busy doing something else). We don’t want the game to break just because of that, so we make the methods optional by providing a default empty implementation.)

Conclusion

Phew! Today, we had an overview of the JSON format, then we took a deep look at how the JSON file is parsed, and how the entity classes are created and initialized with the JSON values. The beauty here is that 99% of the code presented is generated by a script, and updates seamlessly with the JSON format! Anyways, I think the overview presented here was pretty clearn (clear and clean), if I do say so myself! Make sure to check out for the next part were we will take a 180 degrees turn and look at the map-editor-UI side of things. Till next time, keep the code comin’!!!