By John Evanson  

Why a Config System?

Inevitably as you work on a game, you start having various debug options and tunable configuration parameters that you want to be able to control easily.  Her are a couple random examples from our own code:

  • FPSGraph: controls whether you want a graph of the frame rate over time to overlay your game screen.
  • BootLevel: name of a level you want the game to start immediately, skipping the main menu and getting you into debugging faster.
  • MaximumSelectionSize: maximum number of units you are allowed to select at once.

There are as many ways to approach this problem as there are programmers, but for us the goals were:

  • Very simple text format: We are hand-editing these files (maybe even using a phone keyboard), so we’d like to avoid any complex structure like JSON or XML.
  • Performance friendly access to configuration variables in code: We want to be able to access these values in code without performance overhead if we can help it.
  • Local overrides: We want users to be able to control these settings on their own specific device easily.
  • As little boilerplate as possible: We want to avoid boilerplate so we can get things done faster, with fewer errors, and with less programmer grumbling.

Format

Our chosen format looks something like this:

As you can see, there is very little structure here. It’s pretty easy to remember the format and not too bad to type, even on a phone. It’s not a robust format set up to handle a lot of complexity, but we don’t need that!

Performance-Friendly Access

With our solution you can simply access the variable directly as a field or property in a class. You can just write something like:

You basically can’t get any more performance friendly that that.

Local Overrides

We have a variety of ways to set config values. The most common are through a file called GameConfig.txt, which contains defaults, and UserConfig.txt that handles local overrides. GameConfig.txt is placed in the Resources folder so it gets bundled as an asset by Unity, and it’s checked in to source control. The UserConfig.txt file goes in the persistent data folder. It’s part of a local user’s data and can be changed via an in-game editor on devices or via your favorite text editor when running a standalone game or inside the Unity editor. When this file changes during runtime, we re-parse it to allow changes on the fly. During app startup, the UserConfig file is parsed after the GameConfig to allow those settings to override any previous values. If you have a console command system, it’s handy to make a command that lets you set values through console commands so you can tweak them on the fly.  You can process them from the command line for quick one-time changes at startup.  You can even pull values from a web server at startup to remotely tweak things for users that connect.

As Little Boilerplate as Possible

Our boilerplate is non-existent. If you want to add a new configuration variable, it’s as simple as declaring any other class variable:

Because these are just normal class fields/properties, it also makes accessing the variable from code completely boilerplate-free and (as mentioned previously) very efficient.

Time to Reflect

On previous projects written in C/C++, we had to resort to some template/macro trickery that still required extra boilerplate for each variable. In C#, however, we can use reflection to greatly simplify things. The key methods for implementing this are Type.GetField and Type.GetProperty, which look up fields and properties by their string names:

With the resulting PropertyInfo or FieldInfo, you can then set their values:

You must pass a value of the right type to these functions, so you need to convert to that type from the string you’ve parsed. First you’ll want to figure out what type the field or property is:

Now you know the type, you can do something like:

This approach works well (and is sometimes needed), but we have another C# trick up our sleeves to automagically parse a lot of the simple types you’ll be using in this type of system (simple types like floats, ints, strings, and even enums)!

You can, in fact, define TypeConvertors for your own classes if you’d like more things to be handled via that one-liner.  You can find more info here on MSDN.

Putting It All Together

Using those concepts, here’s a simplified version of our overall solution:

For this demo, we’ve divided the class into two .cs files, one with the core logic for the config system itself (BXPConfig.cs), and one for the config variables (BXPConfigs.cs). For our production version, we actually have a more complicated setup that allows derived classes so the core functionality can live separately from game specific configs.  If you want to get fancier still, you could also spread it across multiple classes and do something to register each class you want to search with the config system at startup.

In BXPConfigs.cs you can also see another handy trick in the implementation of TestInt, which is to have a setter that can take additional actions when a value changes, such as notifying other systems that might need to update based on the value changing.

Setting Configs

A simplified version of what you might do on startup is:

The first line loads GameConfig.txt from the game asset bundle (in the project it’s found in Assets/Resources). The second line reads UserConfig.txt from the user’s local persistent data folder, potentially overriding values from GameConfig.

If you have a console command system, you might do something like this to set configs at runtime:

Your configString would want to look something like “TestInt=4321”.

 

Wrapping Up

We now have a really simple way to set configuration values that can be modified easily for testing and debugging.  It requires little boilerplate and has good performance for usage from code.  For us, this is an extremely handy feature that we use for a wide array of simple tunable values.  You can just drop this system into your project as-is for your own use and easily extend it to have more features.

The same C# reflection concepts can make a lot of data driven features really easy to implement. For example we have an tech/upgrade system that uses reflection to allow basically any property on our units to be modified without having to add any annoying boilerplate per variable. Maybe that will be the subject of a future post!

 

By the way, you can grab a really boring sample project demonstrating this code here.  If you run from the Test scene, you’ll see some log output generated from the Test.cs file, which is using the BXPConfig system.