Save/Load System using JSON

🚀 This is Cosmo, signing on for a tutorial about saving and loading in GameMaker!

GameMaker has a handful of built in save functions and systems. If you want to read about game_save() and using ini files first, have a read below. Otherwise, let’s jump straight to JSON.

Saving Everything: game_save(filename: String), game_load(filename: String)

These functions save and load a “game state” directly to the given filename. However, according to the documentation, these functions are legacy (from an old version of GM and exists for compatibility), and not recommended for use.

This is likely for a few reasons. GameMaker has other methods of saving data, namely exporting to .ini and .json files. Also, game_save() and game_load() saves EVERYTHING in the game, except for dynamic resources (eg. data structures and surfaces), but it is highly unlikely we need to do this.

That is, we don’t need to save the location, variables, states, etc, of every single object. Not only is it unnecessary as we may not care about some of this data, but it may produce unpredictable and buggy results. Not to mention, this method of saving only works for Desktop, and has no direct analogues in other platforms or programs.

To make our lives simpler (and faster), we ought to be specific about the data we are saving.

Saving small data with INI files

Next up are .ini files (“ini” for “ini”tialisation). GameMaker has easy to use functions and documentation for saving and loading to .ini files. These are used to store small amounts of data, such as configuration or user settings for a program, and are compatible with most platforms. “Small” here is key, as these files have a limit of only 64KB, limiting their use for saving large amounts of data. For this reason, we’re going to move on to a different save format for most of our game data.

However, it is not unusual to also use an .ini file alongside other save folders and files, as it is lightweight and quite a readable serialisation format, making it relatively easy for even non technical users to tweak settings.

📦The Box: JSON

JSON (JavaScript Object Notation) is a widely used file format both for storing data, and transmitting it via web applications and servers. Though JSON was derived from JavaScript (hence its name), it is language independent. Luckily for us, GML (GameMaker Language) is also very similar to JavaScript, so JSON should look quite familiar.

You will find APIs/libraries in pretty much any program or language which supports saving to and loading from JSON. This makes it a very safe option to use for data storage.

Think of JSON like the box in a delivery.

👉 > 📦 > 👋

The sender 👉 and receiver 👋 both have rules:

> Sender must package their goods into a box. > The box is delivered to the receiver. > Receiver receives the box and unpacks it.

In this relationship, the receiver 👋 doesn’t have to care about who is sending the goods, or the previous state of them. They just know they’re getting a box 📦 they can unpack, and use as they wish.

In programming, this process of packing and unpacking the “box” is referred to as serialisation and deserialisation. But instead of a box, we are converting data into different formats.

serialise: 🔢 > 📦

deserialise: 📦 > 🔢

For example, when saving, we serialise our game data by converting it from a GML struct into a valid JSON object. Same data, just packed up into a different format. So, just as before with our delivery box analogy:

The serialiser converts their data to another format, like JSON. 👉 > 🔢 > 📦 The data in JSON form sits around in a file until it is needed. 📦 The deserialiser reads the JSON, and converts it into the format it wants eg. a specific data structure in its programming language.📦 > 🔢 > 👋

Because of this clean handoff, the serialiser 👉 and deserialiser 👋 don’t need to know anything about each other. They can be in entirely different languages or programs! One outputs a box, and the other receives a box.

Note: This is true of many save formats and not unique to JSON; JSON is simply a widely-used format. Some of your favourite programs or data likely exports or converts to JSON!

🔢 Serialising and Deserialising

Luckily, this process of serialisation and deserialisation in GML is relatively simple, as there are built-in functions which take care of the conversion for us:

json_stringify(value: Struct or Array) Returns -> String
json_parse(json_string: String) Returns -> Struct or Array

Let’s use these functions with an example GML struct. Just like ds_maps, these comprise of key-value pairs:

player = { name: "Cosmo", score: 999, inventory: [ { "item": "bean", "count": 42 }, { "item": "radio", "count": 1 }, ] }

We have declared the above struct “in-line” (literally, in the middle of a line) and assigned it to the variable player. This struct has three keys, or value fields: name, score, and inventory. Respectively, the types of the values are a String, Number, and finally our inventory is a nested value: an Array which itself contains Structs.

If we run json_stringify(player), we will convert our GML object to JSON, which will look like this (as a string):

{ "name": "Cosmo", "score": 999.0, "inventory": [ { "item": "bean", "count": 42.0 }, { "item": "radio", "count": 1.0 } ] }

As you can see, it’s very similar to our original struct. However, you will notice a few differences:

  • Numbers are all converted to Floats (ie. decimals)

  • All keys or fields of our object have been converted into Strings.

  • The formatting may be slightly different (removing trailing commas).

Once we have our JSON string, we just have to save it to a file!

Note: while json_stringify() does a great job of converting Structs or Arrays to JSON, even when they are quite complicated and nested like in our inventory example above, it will not convert other GML data structures as expected eg. Lists and Grids. You will first have to convert these into Arrays and/or Structs, otherwise, json_stringify() will only save the data structure’s ID (a number). Read more here.

💾 Saving to a File

So, now we have a String containing valid JSON. We just need to get it into a file. We could use get_open_filename() and get_save_filename() to access files, but these will create popups and require explicit input from the user. This is quite immersion-breaking if it happens in the middle of our game; not to mention, it will only work on Desktop.

Instead, we can use buffers (essentially, a special Array) to save to our “sandboxed” (ie. protected) area. Our computer uses buffers to temporarily store data for transport to a different place in memory. Consider them the 🚚 delivery drivers for our data “box”, moving it to and from storage.

Save: 👉 > 🔢 > 📦 > 🚚 > 💾

Load: 💾 > 🚚 > 📦 > 🔢 > 👋

It may strike you as a little strange that this is necessary, but consider how potentially dangerous it is to handle data. Computers must be precise, down to the bit, and not allow data to move into places it is not allowed. After all, if we write our buffer incorrectly, we may corrupt our data.

Even worse, if we save to a piece of crucial system memory and overwrite it, we could cause some serious trouble for our computer!

Luckily for us, the data we have is homogenous – that is, it’s all the same, one big string. So to “load our delivery truck”, we just need to make a buffer that is the exact same size as our string. And by “size” here, I mean we specifically need to know how many bytes our string takes up.

Remember, each bit is a 1 or 0, and there are eight bits in a byte. To get our length, we shouldn’t simply use string_length(), as this will get how many characters there are in the string. String characters are encoded in UTF8, which is kind of like if each letter had a byte “barcode” of eight 1’s or 0’s. Only, some characters take up more than one byte.


There are 256 unique combinations we can make with a byte (arrangements of 1’s and 0’s)… but there are more characters than that, due to punctuation, special characters, language alphabets, etc. This is how UTF8 can encode thousands of characters to create the widely-used library of characters called Unicode.

So now we know how long our buffer will be, we just have to create it. The buffer_create() function also asks us to specify what type of buffer it is; does it grow, wrap, etc. We know it won’t change in length (ie. has a fixed length), and we will only write to it once, so we can put buffer_fixed. The final argument asks us to specify the alignment of bytes, and since we know our data is a String, we can put 1.

//We need to make a buffer the same size as json_str //Here we get how many bytes are in a string var len = string_byte_length(json_str);
//Creates a fixed buffer the same length as the JSON string var buff = buffer_create(len, buffer_fixed, 1);

Now that we have our buffer stored in the variable buff, we need to write our string into it. Essentially, we hand over our “box” to the delivery driver.

📦 > 🚚

When we write to a buffer, we also need to tell it what type of data we are giving it. This is so it knows how far up a piece of memory (ie. how many bytes) to move each time it adds data. We already know all of our data is the same: a String full of characters!

We can find a list of constants in the docs for this function, and see that we need to give it buffer_text (not buffer_string as we don’t have a null terminating character at the end).

buffer_write(buff, buffer_text, json_str);

Finally, we save the buffer to a file! We also need to delete the buffer since we don’t need it anymore, and like ds_lists and surfaces, we have to handle the garbage collection ourselves.

//Saves the buffer to the file_path buffer_save(buff, file_path); //Delete the buffer buffer_delete(buff);

💫 Putting it All Together

👉 > 🔢 > 📦 > 🚚 > 💾

Let’s create some helpful functions so we can save any Struct or Array to a JSON file. That is, we will be able to call: save_to_json(data, filename)

//Saves the given Struct or Array as JSON function save_to_json(data, file_path) {//Turn our data into a string var json_str = json_stringify(data);
//Create our buffer and write to it var len = string_byte_length(json_str); var buff = buffer_create(len, buffer_fixed, 1); buffer_write(buff, buffer_text, json_str);
//Save and delete buffer_save(buff, file_path);buffer_delete(buff); }

We can also create a load function, which has a very similar structure – just reading and parsing instead of saving and stringify-ing.

//Loads a Struct or Array from the given filepath function load_from_json(file_path) { //Loads the buffer from file; this creates buffer var buff = buffer_load(file_path);
//Convert the data into a string var str = buffer_read(buff, buffer_text);
//Convert our JSON string into GML data var data = json_parse(str);
//All done; delete our buffer and return the databuffer_delete(buff);
return data; }

And we’re done! Only… we’ve left out one crucial point. What filepath should we be giving these functions? We might know we want to save it as “player.json”, but where will this file be created?

🪧Filepaths

To continue our delivery box analogy, we need an 🪧address to send our box to, ie. the filepath where our save data will be stored.

👉 > 🔢 > 📦 > 🚚 > 🪧 > 💾

For security, GameMaker (like all programs) only has permissions to access and create files in very specific areas. Beyond that, the Operating System (Windows, Linux, etc) will require us to get explicit input from the user to access files.

We can read more about how GameMaker is so-called “sandboxed” here, and learn there is only one place we can write to, ie. create our save files, and that there is a handy constant for it: game_save_id.

So, if we want to create a path to a file, we could simply write:

save_to_json(data, game_save_id + "player.json");

Though, we don’t actually need to put the “game_save_id” part… GameMaker will default to this path (after all, it is the only place we are allowed to write to). So, we can just put player.json after all!

We can even create paths involving folders by using "/" in our string, for example:

save_to_json(data, "saves/data/player.json");

But, now that we know more about GameMaker’s “sandboxing”, we also understand how to find the save file: following where game_save_id points. We will want to do this to check if our save/load system has worked as expected.

The path game_save_id leads to is different for each target platform. Below is an excerpt from GameMaker’s documentation:

  • Windows and Windows UWP: Windows has all files in the %localappdata%\<Game Name> directory (on Windows 7 this is the /Users/<User Name>/AppData/Local/<Game Name> directory).

  • HTML5: Everything is done through the local storage.

  • macOS: Storage will depend on whether the application is sandboxed or not (following Apple’s rules, with the path usually being ~/Library/Application Support/<Game Name>).
    Note: if you are testing your game on Mac, ie. not from an exported executable, you may find your output instead at
    /Library/Application Support/com.yoyogames.macyoyorunner/

  • Ubuntu (Linux): Files are stored in the Home/.config/gamename where “Home” is the users home directory – /home/<username>

  • iOS / tvOS: Storage is the standard location (as viewed through iTunes).

  • Android: Files are in the standard location (which is invisible unless the device is rooted) /data/<package name>.

The File System

Summary

And that’s it! We should now have all the pieces needed to save and load Structs and Arrays to JSON from our game. There’s more to discuss about different ways to structure save files and folders, ie. how much data we should store in one file before causing performance problems (or human headaches), and gathering all of our “serialising” and “deserialising” of objects into one place…

However, this is also where things can start getting very specific depending on your game. So for now, I will leave things for future Cosmo to explore.

This is Cosmo, signing off! 🚀