Friday, June 21, 2013

Setting Settings of Arbitrary Types in C++

It's been a while, but things are still slowly moving along. Working a 40-hour week doing web development leaves me with little time or energy to make game development progress!

I've begun a new iteration of my rendering engine, IronClad, now dubbed Zenderer. Over the past few weeks I've slowly been adding various modules and utilities to it, such as audio, file parsing, and asset management. Nothing is being rendered on the screen just yet, but that's next!

This post is primarily dedicated to creating a settings module that will accept arbitrary types, such as ints, floats, std::strings, and even bools. The final result will allow something like this:

// Optional file to parse immediately
CSettings Settings("SettingsFile.dat");
Settings.Init();

// Set settings
Settings["WINDOW_WIDTH"]    = 800;
Settings["WINDOW_HEIGHT"]   = 600;
Settings["WINDOW_NAME"]     = "Zenderer Window";
Settings["WINDOW_FS"]       = false;
Settings["SCROLL_SPEED"]    = 5.2;
Settings["FRAME_RATE"]      = 60;

// Retrieve settings
if((size_t)Settings["FRAME_RATE"] < 20)
{
    std::cerr << "[FATAL] Frame rate is too low for adequate gameplay.\n";
    exit(1);
}

// Change settings, regardless of type
Settings["WINDOW_FS"] = true;
Settings["WINDOW_NAME"] = 42.335f;

We will do this by creating an extremely dynamic COption class that accepts all different types for values and turns them into strings behind the scenes.



The COption Class

We need implicit conversion, assignment, and comparison operators for many many different types. Thankfully, with implicit template parameter deduction in C++, we don't need to hard-code a lot of these, as long as they are supported by std::to_string (new in C++11). And in the opposite direction, we can easily compare everything with a floating-point value (except actual floating point values due to comparison inaccuracies, we will use math::compf for that). As you can see, this class is basically nothing but a wrapper allowing it to be assigned to, compared with, and assigned from many different types. If it's not obvious, the implementation can be viewed here.

The CSettings Class

Now we are going to create an array-like wrapper class that will allow us to set, retrieve, and modify different settings values using identical syntax.
To achieve this, we will return an instance of COption& when using operator[] on a string_t index. Internally, we will add a new COption to the internal list if it does not exist, and then return a direct reference to it for modification regardless. Since we defined the type above to be compatible with all primitive types, this will easily allow for operations like this:

// Set.
Settings["HEALTH"] = 100;

// Get.
std::cout << Settings["HEALTH"] << std::endl;

// Compare
int h = Settings["HEALTH"];
if(h < 50) std::cout << "Dying.\n";

// In-line compare.
if(static_cast<int>(Settings["HEALTH"]) < 50)
    std::cout << "Dying.\n";
// Change type. Settings["HEALTH"] = "Dead";

As you can see, the only thing that is slightly tricky is comparison. To remedy this, you would need to overload operator<, operator<=, operator>, operator>=, and operator!= for every single type you wanted to do comparisons with. It's a bit simpler to just assign it to the variable type you want to work with and do the comparison that way, or cast to a type with an operator.
Below is the implementation of operator[]. The CSettings class internally defines an option dictionary associating a string value (or a hash in release builds for speed) with a COption instance.


So there you have it. A high-level abstraction layer for an array-like settings module that allows for assignment and modification of a variety of types through unified syntax.

No comments:

Post a Comment