Tweakable Constants

Published January 19, 2010 by Joel Davis, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
This snippet presents a useful oxymoron. It's just a simple hack, but like the best hacks, it's easy to implement and can save hours of time. This isn't my idea -- I first learned of this technique after posting about a bad experience with .ini files (more on that in a bit). A commenter pointed me to a thread on mollyrocket, and it worked so well I wondered why this simple idea wasn't more well-known. This article is an attempt to publicize this useful technique.


Tweaking == Rapid Iteration
I do a lot of prototyping and weekend programming contests like LD48. I've learned that the key to getting good gameplay is iteration, and the faster you can iterate, the better. Having a value like this might not seem too hard to change: #define TRAVEL_SPEED (10.0f)

Simply recompile, and test it again. Bingo! However, that can take a few minutes even for a small game, and then you need to go through your menus, and play until the point you left off. And when you have several gameplay values that are interacting, this starts to really eat up time. This has led to several common approaches:

  • Debug Mode Menus -- Any reasonably complex game should have a debug mode menu (or console) to allow changing game behavior without restarting. Usually invoked from an uncommon key or button combination (like tilde, or B + START), this allows a way to change gameplay values or modes (such as entering debug drawing mode or no-clip). These modes are often disabled or compiled out entirely in a final build, or left in for modders and tinkerers, but either way they can be vital to the development process.
  • Config Files -- Using a simple config file for gameplay values. A text file or .ini file is easy to change, easy to parse, and allows you to change things without a recompile. By watching the file modification time, you don't even need a restart, your game can automatically reload the values when the file is changed. For a recent weekend project, I choose to take the latter approach and used a config file. It took a little while to set up but once it was going it was great. I could change a value and see the effect instantly in the running game. The only problem was that it added an extra step to add a new value. My code looked like this:

    if (arg_map.count("gameplay.travel_speed")) { m_travelSpeed = arg_map["gameplay.travel_speed"].asFloat(); } I also needed to create a m_travelSpeed member somewhere, be sure to initialize it with some default, and decide what to call it in the config file. Certainly, this is not going to take more than a minute or two, and I'm sure you could come up with a more concise API for getting a value out of an .ini file, but the fact that there is any mental friction at all means that there's a disincentive to use it. For my project, I was very pleased with the tweakable travel speed, but I kept telling myself, "I won't need to change that", and not adding it to the .ini file. In particular, values that would not need to change at all -- like the vertical spacing between buttons on the menu screen -- I resisted adding. After all, wasn't I going to go back and rip that out once I found an acceptable value? After a weekend of work, I still only had three tweakable values in my .ini file.


    The Constant Gardener
    Enter Tweakable Constants. Here's the idea, in a nutshell: The code itself is the data file. Simply wrap a constant in a macro like this: // Move at constant (tweakable) speed float newPos = oldPos + _TV(5.2f) * dt; // Clear to a solid color glClearColor( _TV( 0.5 ), _TV( 0.2 ), _TV( 0.9 ), 1.0 ); glClear( GL_COLOR_BUFFER_BIT ); // initial monster health Monster *m = new Monster( _TV( 10 ) ); Here, the _TV stands for TweakableValue. Others call these "Hot Values" or something, but the result is the same. I can run my program, change the value in my IDE, and as soon as I save the file the still-running program updates. I can dial in exactly what I want for the value without having to recompile, change my .ini file, or even leave my editor. And unlike changing it in the debugger, I don't have to remember to go back and apply the change to the code. This might seem a tiny savings, but multiplied over the hundreds of values in a reasonably sized project, and thousands of compile/test cycles, it really adds up. Compared to three tweakable values in my .ini file version, I used this 45 times in my next weekend project and saved literally hours of work.

    I noticed that I was using it mostly for really insignificant things, such as background colors and button placements, as well as for values which took a little trial and error but didn't need to change, like a near-clip or a good offset distance to prevent z-fighting.


    How does this magic work?
    It's really pretty simple. The _TV macro expands to a function call like this: #define _TV(Val) _TweakValue( __FILE__, __COUNTER__, Val ) float _TweakValue( const char *file, size_t counter, float origVal ); int _TweakValue( const char *file, size_t counter, int origVal ); void ReloadChangedTweakableValues(); Where _TweakValue is a function that stores a list of files, and their modification times, and the current value for each counter value. _TweakValue just looks up the most recent value, or returns Val unmodified if it hasn't seen that value yet. You also need a function like ReloadChangedTweakableValues, which you call once per frame (or whatever interval you want to check for changes). This function just checks the file modification time for each source file, and if it is more recent than the last time we loaded it, it reloads the values from that file. C++ takes care of the type overloading for you, and if you want to add more types, it's pretty easy (though I'm not really sure why you'd need to do this).

    Most people have seen the __FILE__ macro before, it simply expands to the full path to the current source file. The __COUNTER__ macro is a bit newer, it's been available in VC++ since version 6, and in gcc in version 4.3 and higher. The counter macro simply expands to a count of the number of times the compiler has encountered it.

    Wait, you mean it actually parses the C++? Well, not really. We can just implement a quick and dirty parser to simply count the number of occurrences of the _TV() macro, and just pull the values themselves out of the parenthesis.

    If your compiler doesn't support the __COUNTER__ macro, and you can't upgrade, you can also use the ubiquitous __LINE__ macro, but then you need to either avoid using more than one _TV on a line, or use an additional index. See the mollyrocket forum post for more details on this solution.

    Doesn't all of this slow the game to a crawl? In practice, not so much. Since it only has to rescan the source code when there's an actual change, it goes pretty quick, especially if you use a map or a hash to lookup the values.

    In release builds, it compiles out to the constant itself, so the compiler has the extra opportunity to optimize and it can be even faster than a .ini file solution where you have a variable in your release build that doesn't actually ever change.

    And in case it's not obvious, this is only going to be applicable to PC development, where the game can see the filesystem where the code lives. For an iPhone game, for example, this would only work when running from the simulator. However, many console development environments offer ways for a running game to communicate with the host platform, so there might be a way to shoehorn it in there somewhere.


    But Constants are EVIL??!
    Okay, we've all had data-driven design drilled into our heads. But often the cure is worse than the disease. If you go too far down the path of configurability, you wind up with some xml-driven abstraction that doesn't actually do anything. Making something configurable is only useful if you're actually going to change it someday. The larger your team is, the less applicable this trick is going to be. If you're a one-man shop writing indie games in your mom's basement (or your yacht -- I'm not judging here), this can be a real gem. Where I found this the most useful so far was in setting up the hacky, hard-coded gui for my games. If you're using gameswf or (God help you) CEGUI or something and your game menus are all in flash or xml then obviously you don't want to use this for any gui tampering.

    In general, use common sense. I find that a good rule of thumb is to ask yourself, who will need to change this value?

    • End Users -- Then it better be changeable from a settings GUI screen somewhere. I.e. Screen Resolution.
    • Modders -- A clear, easy to read text file; .ini files work great for this.
    • A level or GUI designer -- Depends on the toolchain, but it should be available wherever they build the gui.
    • A game designer -- Debug menus or your level authoring tools.
    • Just you, the coder -- Go ahead and use TweakVal. :)
      Sample Implementation
      My sample implementation is available, and you can always find the most up-to-date version here. There are some caveats to be aware of (feel free to send me patches):
      • Wrapping _TV macros in /* C style */ comments or #if 0 blocks will confuse it. // C++ style comments are OK.
      • _TV macros probably won't work in header files.
      • You must call ReloadChangedTweakableValues() once per frame (or however often you want updates)
      • Right now requires compiler support for the __COUNTER__ macro (I've had reports that it's pretty trivial to modify it to use __LINE__ instead).
      • If you're using __COUNTER__ for anything else, you'll need to modify the code to also count those macros.
        References
        [1] "Watched Constants", Casey Muratori
Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

A simple means to add changeable data to your game that doesn't require a restart or recompile to update - very well suited for rapid prototyping

Advertisement
Advertisement
Advertisement