[GameMaker] Basic Saving and Loading with JSON & Buffers

Categories: GameMaker

I’m writing this guide as I’ve noticed that there’s not a lot of up-to-date reading material that covers a good way of saving/loading important data. You can find videos of it online, but I prefer to scroll through webpages myself and study the material. The main topic we’re covering of course is saving/loading data from JSON. But, there’s a few things that we need to cover before we can get to that.

Preparations

Before we actually begin to learn how to save and load our data, we first need to understand how we can store our data as JSON. GameMaker fortunately does provide us some easy-to-use functionality, which are json_stringify and json_parse. These two will allow us to turn data into JSON and vice versa. Data specifically being structs.

What are structs, anyway?

If you’re familiar with instances/objects in GameMaker, they’re a form of that, but without events or built-in variables. Put simply, they only contain data that you, the developer, specify. They’re by far the easiest to deal with, since they are garbaged collected. Which means that you don’t even need to delete them, like with arrays. You can just set and forget them.

How do we make a struct?

Defining a struct is relatively straight forward.

var _struct = {
	foo: "bar",
	key: "value",
	bool: false,
	number: 26
}

A struct can have as many keys as you desire, and they can hold whatever value you choose. In this example, foo is our key, and "bar" is the value. At this point of time, we can easily reference them. Just by either using the dot operator (_struct.key) or using the function variable_struct_get(_struct, “key”) (or the accessor _struct[$ "key"])

show_debug_message(_struct.foo);
show_debug_message(_struct[$ "foo"]);
show_debug_message(variable_struct_get(_struct, "foo"));

Do take care in mind, that both the accessor and the function do the same thing, and are different from the dot operator.

The major difference being that:

  • Dot Operators are several times faster, as they precalculate the pointer in memory. However, doing _struct.foo on something that doesn’t have the variable foo, will cause the game to crash. We can get around this by checking with variable_struct_exists(_struct, "foo") prior to actually attempting to retrieving the value from the variable.
  • Accessor/function only calcuate the pointer when called by the string value, and either returns the value being held in the variable, or undefined if nothing exists. This won’t crash the game, but will be slower than using the Dot Operators, due to having to calculate the pointer each time it is called.

Setting up our example player

Let’s assume that we have an obj_player object. And in it’s main Create/Step event, we have the following code:

// Create Event
x = random(room_width);
y = random(room_height);

// Step Event
if (keyboard_check_pressed(vk_space)) {

	x = random(room_width);
	y = random(room_height);

}

Great! We’ve got our code. Now every time we press spacebar, our player will place itself in a random position in the room.

Saving our data

The fun part! We’ll use the obj_player x and y to save from our lil example. But to start off, we’ll take the knowledge from making a struct and applying it to our example case.

var _struct = {
	x: obj_player.x,
	y: obj_player.y
}

var _json = json_stringify(_struct);

Now that we have our struct stringified, we just need to store into a buffer. Now I did sorta gloss over buffers entirely, but I’ll go down in some basic details shortly.

var _buff = buffer_create(string_byte_length(_json), buffer_fixed, 1);
buffer_write(_buff, buffer_text, _json);
buffer_save(_buff, "player.json");
buffer_delete(_buff);

Breaking it down what we’ve just done here.

  • With buffer_create, we created a buffer with a set size given by string_byte_length, followed by determing the type of buffer it is. Since we already know how big our buffer is, we’ve gone with buffer_fixed, and the 1 is the alignment, or how many bytes it will attempt to “fill” for blank spaces. Leaving it as 1 is fine for our case here. And stored the new buffer in _buff.
  • Since our JSON is a string, we used string_byte_length to get the raw size total of all of the bytes from our string.
  • With buffer_write, we supplied our previously created buffer, and given it the type buffer_text, and then passed our JSON results.
  • With buffer_save, we then save our buffer results to a file called player.json. The contents themselves will be shown as a singleline JSON string.
  • Since buffers aren’t garbage collected, we then delete the buffer with buffer_delete.

You might’ve noticed that in the manual for buffer_write, we have some additional types. We’re going with buffer_text since it’s by the most straight forward, and we’re not storing anything else. To complete our functionality, we’ll add them together and add a separate block of code of our obj_player‘s step event.

Note: Saving buffers ontop of files that already do exist, will overwrite them.

if (keyboard_check_released(ord("S"))) {

	var _struct = {
		x: obj_player.x,
		y: obj_player.y
	}

	var _json = json_stringify(_struct);

	var _buff = buffer_create(string_byte_length(_json), buffer_fixed, 1);
	buffer_write(_buff, buffer_text, _json);
	buffer_save(_buff, "player.json");
	buffer_delete(_buff);
}

Loading our data back in

Loading in our data is just as easy as saving our data. We’ll start by loading in the buffer and converting it back into a struct.

if (keyboard_check_released(ord("L"))) {
	var _buff = buffer_load("player.json");
	var _json = buffer_read(_buff, buffer_text);
	buffer_delete(_buff);
	var _struct = json_parse(_json);

We’ve now loaded in a buffer, extracted the JSON and then converted it back into a struct. Retrieving the x and y is as simple as what we’ve done above with our structs example.

	obj_player.x = _struct.x;
	obj_player.y = _struct.y;
}

That’s all we really need to do for that. Though, you might ask some good questions. What if the JSON doesn’t have an x and or y value, the JSON is invalid, or if the file doesn’t exist, then what?

Avoiding errors upon loading

In most cases, as long as we know that the file does exist, then we don’t have to worry too much. We can easily adapt what we have above by checking first to ensure that said file exists.

if (keyboard_check_released(ord("L"))) && (file_exists("player.json")) { 
	var _buff = buffer_load("player.json");
	var _json = buffer_read(_buff, buffer_text);
	buffer_delete(_buff);
	var _struct = json_parse(_json);

Very little of the code needs changing to check that player.json already exists before attempting to load in. But, what if someone were to modify the save file to pass in an invalid JSON string? Well, in this case GameMaker will throw an exception. Frankly I find that to be an annoyance. But, we can get over that by simply adding in try/catch, which results our code looking like this so far.

if (keyboard_check_released(ord("L"))) && (file_exists("player.json")) { 
	var _buff = buffer_load("player.json");
	var _json = buffer_read(_buff, buffer_text);
	var _struct = undefined;
	buffer_delete(_buff);
	try {
		_struct = json_parse(_json);
	} catch(_ex) {
		show_debug_message(_ex.message);
	}

	if (is_struct(_struct)) {
		obj_player.x = _struct.x;
		obj_player.y = _struct.y;
	}
}

This will do for the most part, and will load our data in just fine. Though, we still run into the issue that if any of the values aren’t available (either by the developers end, or by players modifying the save file), while our try/catch will prevent the game from crashing, it will also skip the rest of the loading. That’s not good! Some of our values might be beneficial! How we can overcome this is by using a tenary operator. (It’s basically a one line version of an if/else block), represented as
statement ? value_if_true : value_else;

obj_player.x = !is_undefined(_struct[$ "x"]) ? _struct[$ "x"] : obj_player.x;
obj_player.y = !is_undefined(_struct[$ "y"]) ? _struct[$ "y"] : obj_player.y;

Which makes up the rest of our loading system. Now we’re left with this.

if (keyboard_check_released(ord("L"))) && (file_exists("player.json")) { 
	var _buff = buffer_load("player.json");
	var _json = buffer_read(_buff, buffer_text);
	buffer_delete(_buff);
	try {
		var _struct = json_parse(_json);
	} catch(_ex) {
		show_debug_message(_ex.message);
	}


	if (is_struct(_struct)) {
		obj_player.x = _struct[$ "x"] ?? obj_player.x;
		obj_player.y = _struct[$ "y"] ?? obj_player.y;	
	}
}

Now not only do we check to ensure that the player file exists, but we also check to ensure that json_parse doesn’t throw an error, and that each and every variable from the struct is there, skipping over what’s not there. This makes our system step over many of the errors without completely halting the game. You might also be tempted to try out the idea of looping all of the structs variables (via variable_struct_get_names()) and then applying it to obj_player, but this would also mean that each and every entry that wasn’t intended at all, will be added. So I wouldn’t ever recommend doing that, unless you want to cause unintentional behaviour down the road.

Conclusion

While this concludes the basics, I will be looking to make further tutorials on the matter. Including saving multiple instances, asynchronous saving/loading and much more. As always, I’m happy to take in any suggestions and feedback between tutorials.

And while I won’t cover it today, there is a library with parsers for CSV, XML, YAML, Messagepack, Binary, Ini, JSON and more! I highly recommend checking it out if you’re interested in other formats as well as JSON. They work just as well with the code above. Just swap out json_stringify and json_parse for the equivilants.
Link here: https://github.com/JujuAdams/SNAP

In our next blog post on this topic, I’ll cover asynchronous saving/loading.
Repo link (for the demo code above): https://github.com/tabularelf/BasicSaveLoadJSON

«
»

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.