Table of contents
Introduction
At some stage of your application development you will realise that your code is rife with "magic numbers" which may be string literals, integer constants or Ogre::Vector3s that control and define the layout of your scene, how your game logic works, mouse move speed or many other things. You may also be in a "code and test" phase where you are changing constants within the source, recompiling and then testing. The recompile step may be making this process a real pain.
The standard method for solving these problems is to move these configuration directives into an external file (XML, txt, .material etc), parsing the config file once at runtime and then propogating these settings around the application. At this stage you can simply change the configuration file and then re-run the application without requiring a costly recompile. This also benefits teams where you may have testers, designers or other non-programmers who may want to change or tune settings and can now do so without bothering a programmer.
This article demonstrates a simple config parser system that uses config files that look just like the ogre.cfg, resources.cfg and plugins.cfg files and uses the excellent Ogre built-in classes Ogre::StringConverter and Ogre::ConfigFile
This parser is pretty simple, for a more flexible or powerful parser you can see All-purpose script parser
The Method
We'll define a header and source file to manage all of our config parsing and value retrieval needs. All parsed config will be stored in a std::map<std::string, std::string> as key-value pairs. This configuration class will be accessed as a singleton with a bunch of associated getters which cast the config directive into the correct data type for class users.
Sample Config File
Here we've got a short config file that contains
- Ogre::Real for mouse movement
- Ogre::ColourValue for the background/viewport
- std::string for the Player's name
- int for the players starting cash
- Ogre::Vector3 for the camera's starting position
- bool for setting whether or not we'll be enabling LevelOfDetail
These values are grouped into sections for System, Player and Scene.
Sections are denoted by square brackets " [ sectionName ] " and you can create as many of these as you like.
Just like the standard Ogre configuration files, leading and trailing spaces are removed from the 'value' part of the line.
Place this file into a directory that's referenced in your resources.cfg file.
[System] MouseMoveScaling = 1.75 BackgroundColour = 0.47 0.67 0.96 0 [Player] Name = Player1 StartingResources = 20 [Scene] CameraInitialPosition = 0 80 180 EnableLOD = true
MyConfig.h
#include <OgreConfigFile.h> #include <map> class MyConfig { public: static MyConfig &getInstance(); //various getters used by class clients to get configuration directives int getValueAsInt(std::string key); Ogre::Real getValueAsReal(std::string key); std::string getValueAsString(std::string key); Ogre::Vector3 getValueAsVector3(std::string key); bool getValueAsBool(std::string key); Ogre::ColourValue getValueAsColourValue(std::string key); bool getKeyExists(std::string key); private: MyConfig(); ~MyConfig(); HALConfig(const MyConfig &); //standard protection for singletons; prevent copy construction HALConfig& operator=(const MyConfig &); //standard protection for singletons; prevent assignment //this is our config file Ogre::ConfigFile m_ConfigFile; //this is where our configuration data is stored std::map<std::string, std::string> m_Configs; };
MyConfig.cpp
#include "MyConfig.h" #include <OgreResourceGroupManager.h> #include <OgreStringConverter.h> MyConfig::MyConfig() { m_ConfigFile.load("MyConfig.txt", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, "=", true); Ogre::ConfigFile::SectionIterator seci = m_ConfigFile.getSectionIterator(); Ogre::String sectionName; Ogre::String keyName; Ogre::String valueName; while (seci.hasMoreElements()) { sectionName = seci.peekNextKey(); Ogre::ConfigFile::SettingsMultiMap *settings = seci.getNext(); Ogre::ConfigFile::SettingsMultiMap::iterator i; for (i = settings->begin(); i != settings->end(); ++i) { keyName = i->first; valueName = i->second; m_Configs.insert(std::pair<std::string, std::string>(sectionName + "/" + keyName, valueName)); } } } MyConfig::~MyConfig() { } MyConfig& MyConfig::getInstance() { static MyConfig Instance; return Instance; } bool MyConfig::getKeyExists(std::string key) { if (m_Configs.count(key) > 0) { return true; } return false; } std::string MyConfig::getValueAsString(std::string key) { if (getKeyExists(key) == true) { return m_Configs[key]; } else { throw Ogre::Exception(Ogre::Exception::ERR_ITEM_NOT_FOUND,"Configuration key: " + key + " not found", "MyConfig::getValue"); } } int MyConfig::getValueAsInt(std::string key) { return atoi(getValueAsString(key).c_str()); } Ogre::Real MyConfig::getValueAsReal(std::string key) { return Ogre::StringConverter::parseReal(getValueAsString(key)); } bool MyConfig::getValueAsBool(std::string key) { return Ogre::StringConverter::parseBool(getValueAsString(key)); } Ogre::Vector3 MyConfig::getValueAsVector3(std::string key) { return Ogre::StringConverter::parseVector3(getValueAsString(key)); } Ogre::ColourValue MyConfig::getValueAsColourValue(std::string key) { return Ogre::StringConverter::parseColourValue(getValueAsString(key)); }
The detail
The first thing you'll notice is how similar to the code looks like to the code you already have in your project to parse your resources.cfg file (outlined in Basic Tutorial 6) that's because we're using Ogre's built-in config file parsing class, hooray for code-reuse. The only difference here is that instead of passing the config values to the resource manager we're storing them in our own little std::map
m_Configs.insert(std::pair<std::string, std::string>(sectionName + "/" + keyName, valueName));
Stores it like this
KEY = sectionName/keyName
VALUE = valueName
The hard work done for us, the rest of the class is just accessor methods. Only one function does the retrieval from the std::map, the getValueAsString(std::string key) function. The other functions just call this function and then cast the returned string to whatever format the user asked for as a convenience. The casting, or more accurately, parsing of these string values is achieved using the Ogre::StringConverter class Ogre API Reference
If at any time the user requests a key that does not exist an Ogre exception is thrown letting the user know what was asked for that annoyed the class so much.
How to use
Any class that wants to use this configuration simply includes the MyConfig.h header, gets the singleton and then asks for the appropriate key. The format for requesting a key is to ask for the section name, a forward slash, and then the keyname; "System/MouseMoveScaling", "Player/Name" etc
Going back to our sample MyConfig.txt config file, we could access value like this
#include "MyConfig.h" void doStuff() { Ogre::Real MouseMoveScaling = MyConfig.getInstance().getValueAsReal("System/MouseMoveScaling"); std::string playerName = MyConfig.getInstance().getValueAsString("Player/Name") Ogre::Vector3 cameraPos = MyConfig.getInstance().getValueAsVector3("Scene/CameraInitialPosition") }
Extras
I like to create comments in the config file to specify the format for a key's value format to make the user's life easier, for example
#this is an Ogre::ColourValue; R G B A playerColour = 0.1 0.2 0.3 0.4
You can easily extend this class to make it return all sorts of other data types, any of the other types that Ogre::StringConverter can return could be easy added as you require. Just create another accessor method and use the appropriate Ogre::StringConverter::parse* function.
Caveats
- Ogre::StringConverter will usually suceed in returning the type that you asked for but incorectly formatted config will cause you to receive incorrect data! Luckily that API reference for this class explicitly lists the format that it expects.
- atoi may return a garbage value for very large (positive or negative) numbers.
- All Keys are case sensitive.
- As the config.txt file is requested from Ogre's ResourceManager you won't be able to use this class until after the ResourceManager is initialised.