Many games have options or settings that are configurable. The specifics can vary widely, from graphics options to music and sound effects volume to to input configuration to in-game options such as difficulty level and which rules are enabled/disabled.
This article will cover how to create a basic configuration system, using external file i/o, ds_map, and json.
Define your configuration requirements
First, before you develop it, design the system. Things to consider:
- What variables does your game have?
- What variables would you like the user to be able to modify?
- What are the upper and lower bounds that make sense, or are reasonable for an enjoyable game? What values will break the game?
A little planning, and you can quickly list out all the variables you’ll want to manage with the configuration system.
Not every variable in your project needs to be subject to customization. Decide what configuration options you want, and define the variables that will be needed to control them, and decide on your default values.
That said, having an editable, external configuration file can be very valuable for testing and tweaking the game, as well as convenient for the player. By having an external config file, you can modify game constants and variables without having to re-build the project each time. Since building the project takes time, being able to avoid building the game each time you want to test a variable can potentially save you time — for larger projects, potentially minutes per build.
Storing the config externally will also enable a tester to play around with the game variables without the need for the tester to be a programmer, or have GameMaker Studio installed. They can simply edit the config file, save it, and run the game again.
Nobody said that you must create a UI in the game program that can access every variable that you store in the config file. So, your testers can play around with configuration in a text editor, while your will eventually include a configuration UI for the game, to be used by players, and accessed through a title screen menu or a pause menu. This UI only needs to expose those configurable options that you think it should, or can have a secret mode that exposes more (or all) of them.
The default configuration should be safe to run on all devices the game is intended to run on, and should have the “standard” or “recommended” settings for the best or recommended game experience.
Having defaults is important in case the external config file is missing or corrupted, and needs to be regenerated. Create a ds_map
called defaults
, and use it to store all the default values.
Coding the config system
Coding a simple config system is actually very easy.
First, define variables to store the default configuration and the actual configuration, and create ds_maps for them. The best time to create these ds_maps is immediately on launch, as soon as the program starts. You may want to make these global variables, so that they will be accessible anywhere and will persist for the entire runtime.
global.defaults = ds_map_create();
global.config = ds_map_create();
Of course, when we are about to exit the game, we will need to remember to destroy these data structures, to properly free up the RAM:
ds_map_destroy(global.defaults);
ds_map_destroy(global.config);
Next, initialize the defaults ds_map with all the variables that are part of the configuration, and their default values. It’s easiest to learn the shortcode for accessing ds_map values:
defaults[? "key"] = value;
In the code above, “key” is the name of your variable, in quotes, as a string. The value is the literal value of the variable.
So you can do this:
global.defaults[? "starting_lives"] = 3;
or
global.defaults[? "starting_lives"] = default_starting_lives;
As you can see, the ds_map’s key indexing allows you to choose meaningful names for the keys, which makes it easy to recall them later.
When you apply the configuration to the variables in your project, assign the value from the ds_map, like this:
variable = config[? "key"];
starting_lives = config[? "starting_lives"];
Once you have your ds_map populated with your configuration data, it’s time to write it to disk. This is will save the file so that the configuration state can be read and applied to the game state the next time the program runs.
The gml function json_encode()
makes it very easy to write the ds_map
to a file, by converting the ds_map
into json, or JavaScript Object Notation, which is just a commonly used data format, and can be thought of as a specially formatted string. You don’t need to know anything about the syntax of json in order to use the encoding/decoding functions — GML handles this for you.
Create a json-encoded string version of the config ds_map:
config_string = json_encode(global.config);
Check to see if an external config file exists already, and if not, create it and fill it with the default configuration:
if !file_exists(working_directory + "config.json")
{config_file = file_text_open_write(working_directory + "config.json");
file_text_write(config_file, defaults_string);
file_text_close(config_file);
}
If the config file already exists:
- read the content of the file into a string variable,
- decode the json string to convert it back into a ds_map,
- validate the data in the ds_map, and,
- if valid, apply the configuration data to the game variables.
//First read the json data out of the config file
config_file = file_text_open_read(working_directory + "config.json");
config_json = file_text_read_string(config_file);
file_text_close(config_file);
//Next, decode the json, converting it to a ds_map
global.config = json_decode(config_json);
//Now, validate the configuration data to verify the settings are good
if !config_validate(global.config) //config_validate is a script that you wrote to validate your custom configuration data.
{
//if we failed validation, destroy the config and create a good copy using defaults.
ds_map_destroy(global.config);
config = ds_map_copy(global.default);
//(Or, alternately, just correct the specific variables that were invalid, leaving any valid non-default values intact. This is more work for the programmer, but more convenient for the user.)
}
[...]
//apply data stored in global.config to the variables in your project as needed, where and when appropriate.
configurable_game_variable = config[? "key"];
The exact details will vary depending on your project, but the above is a good example to follow to get you started.
Implementation: Data and File I/O
It might help to explain the above, in plain English, without the code examples getting in the way.
When the game runs, the very first thing the program should do is check to see if a configuration file exists. If it exists, we read the data out of the file using file_text_string_read(fileid)
and convert the string to Javascript Object Notation (JSON) with json_decode(string)
. This returns a ds_map
of key:value
pairs that contain all the configuration data. If the config file does not exist, then we create it, populating it with our default data values.
If the config file exists, but contains invalid data, then we restore the invalid data using defaults.
Once we’ve successfully read data out of our config file, we need to validate the configuration data that we read from the file in order to ensure that the game is being run with sensible values that won’t result in the game crashing or glitching. If the configuration data is invalid, depending on the setting and the value, the game not work properly, and may crash, or may perform unexpectedly, or break.
How the invalid data got into the configuration, you’ll want to investigate and fix, if the program was at fault. Your program may have written the data incorrectly, or it might have a bug that results in a corrupted file, or you may have read the wrong value out of the file, and those are all errors that you would need to debug and fix.
But it’s also possible that the user may find the file and manually edit it, make a mistake, and thereby introduce errors. So check each value in the configuration ds_map, and verify that it is valid by checking to see if the key was found in the configuration file, that its value is of the correct data type (string, number, or boolean), and that it is within the range of acceptable values for that variable. However the bad data got there, your program will need to handle it and correct it. Write a script that does this, according to the particular needs of your game.
If validation fails, whether because the file is missing, or because one of the values found within it is incorrect, we can handle this in several ways. One way is to reset all values back to their default values. Another way is to correct just the bad value, on a per-setting basis, restoring the invalid value back to the default value. Or we can simply display an error message to the player and exit the program. Decide what is best for your project, and implement it that way.
Usually, I recommend checking whether the file exists, and if not, regenerate it using defaults, and if it exists, restore individual settings to default one by one if there is an invalid value, rather than resetting the entire configuration back to the defaults if any of the values is invalid. This way, a small error in the config file doesn’t blow out all the settings, and program will leave any valid customized settings alone, so that the user will only need to correct the values that were reset to default.
If we have a valid configuration, the next step is to apply those values to the game variables. We do this by assigning values to variables in the objects that use them, and the values we assign are copied out of the config ds_map, so that they are local to the owning object, and can be modified during the lifetime of the object without altering the configuration data.
Once the game variables have been set, the game is now ready to run.
Where did that file get written to?
While you’re developing the game, you might want to know where GameMaker is writing the files, in case you want to open them with a text editor and double-check that they’re being written correctly, or if you want to edit them. If you’re running the game through the IDE, the location may be trickier to find than if you built an executable and installed it. For help in finding the location of the writeable directory for your project, see this article.
Editing the configuration
One great thing about storing the configuration data in an external file is, we no longer need to re-compile the game every time we wish to tweak the settings. This can greatly speed up testing, as compilation can take a minute or more each time, and this rapidly adds up when you’re quitting, coding, re-launching again and again as you develop. So use the configuration system as you test your game design.
We can edit the configuration in various ways.
The simplest to develop is to develop nothing; just rely on the user to find the configuration file on disk, open it up with a text editor, and change values, save the file, and run the game again. You’ll find the writeable working directory for your game somewhere inside of %appdata%
in a Windows build, but if you’re building the project for other platforms, you’ll need to find it.
While this is the easiest approach, this isn’t the most user friendly solution. A better user experience would be to put a GUI in the game to change the settings, and let the game program edit the config file. This will be safer for the user, as you can constrain the input and check it for valid values before saving the config file.
The downside is that this can take a lot of extra work to build the user interface. Sadly, GameMaker does not provide a library of user input controls, such as buttons, checkboxes, text boxes, sliders, and so on. So to build the UI, we first have to build these controls.
This is a great opportunity for beginner programmers to get some experience designing and programming their own controls. But it’s also a time-consuming task, and can be a frustrating challenge for a newbie programmer to figure out how to make a collection of robust, bug-free UI controls that are easy to use, work together, and behave in a way that the user expects based on their experience using other software. And doing this takes time away from developing the game part of the project.
It’s a lot of work, so having an easy way out is a good thing. There are assets available through GameMaker marketplace, which can be purchased for a few dollars, and provide the needed functionality. Either way, it’s good to have a set of reusable controls that you can put into any project, so whether you buy some, or you decide to make your own, you can get a lot of value out of them.
Advanced concerns
Storing arrays, ds_structures inside json
Up until now, the examples I’ve given have all been simple data types. If you want to store something else, like an array, or a data structure, or graphics data, it gets more complicated. This article is just an intro level tutorial, so it won’t cover these advanced topics. But I expect I may cover them in the future, at some point. Many games will not need the advanced techniques to store the basic configuration data.
Applying config to application state
Certain configuration changes, such as display size settings, will require the room or game to be restarted before they take effect. In such case, you may need to use game_restart()
or room_restart()
. If you are confident that the data is being applied correctly to update the game variables, but you’re not seeing the change, try a room restart and see if the changes take effect.
But any room or game restart will restart the game in progress, and that may not be what you want to happen at all! If you have a game where you can change the configuration from the Pause screen, for example, you will not want to disrupt the running game. In that case, you’ll need to go further and handle this in some way, such as:
- Display a message to the user saying that the changes will take effect after they quit and relaunch the game.
- Give the play the option save the present game-state and then restore it with the new configuration in effect.
Details on exactly how to do this will vary considerably depending on your game’s design, but if you need to do this, you’ll essentially be building a save state feature for your game, and then calling it to save and then restore the game after the restart.
The basic logical flow of this process is as follows:
If <config change will require restart> and <game is in progress>{ save game-state to external file; restart;}
On re-start, check for existence of a game-state file, and if it exists, clear the room of default starting instances; restore room from game-state; then delete the game-state file.
This sounds fairly straightforward and simple, and, in concept at least, it is. The difficulty is with storing all of the instances and their state data. Depending on the game, you may have hundreds of instances, and each instance may have a lot of data, and all of it needs to be written to file, then read back out of file, in order to reconstruct the game in the state where it left off.
Making it tamper resistant
You may welcome the user tinkering with the config file, or you may want to protect the config file against unwanted tampering. Usually this isn’t critical for a config file, but for a save file or a high scores file, it might be important to prevent cheating. If your game uses password-protected user accounts, or stores any kind of financial data or purchase records, you should be storing that data securely.
This should be all you need. I won’t get into technical detail here, but will outline a few tools you can make use of.
- Use ds_map_secure_save() and ds_map_secure_load() to securely store the configuration data in an encrypted file. The encrypted file will not be editable by a curious user. The manual doesn’t give detail about what encryption is used. Depending on how serious you are about protecting the data, you will want to research encryption and use an algorithm that hasn’t been broken. Don’t attempt to invent your own encryption.
- Create a cryptographic hash of the configuration data string, and store the hash with the file. When you read the file, re-hash the data you read out of the file, and verify that the hashes match. If they don’t, something has changed, and you know the file has been tampered with or corrupted. In this case, you should re-generate the entire config file from defaults.
Look at GML’s md5 functions to get started. GML also provides sha1 hashing for this purpose. MD5 hashes are no longer considered secure, and sha1 is also no longer considered secure, but may be “good enough” for non-critical needs. While not “hacker proof” they will prevent casual tinkerers from being able to modify the data.
Saving external data for other purposes
Now that we know how to store data in an external file, retrieve it, validate it, and use it at runtime, there are other applications we can use these techniques for.
The most obvious one that comes to mind is save states. I’ve already touched on this above, in brief. Other ideas include:
- High score, leaderboard, and achievement data.
- Telemetry data for debugging or analytics.
- User profiles (to allow multiple users of the same machine to each have their own configuration and save file preference).
- Level Editors.
- Mod packs, to allow a community of players to make custom modifications to your game, such as level data, or other external resources like sprites and sound effects, etc.
As these features become more and more advanced and complicated, they’re really best left to professionals working on commercial projects. But by following the approach described in this article to do a simple configuration system, you’ll have taken the first steps toward getting your skills up to that level.