GMCloud and GameMaker Part 2: Game Configs

The downside of a released game is it’s done. It’s a complete package. You throw it out into the world, and players play your game. If you want to send re-balance the game, change the configuration of the game slightly, then you’ll have to somehow track down your players and ask them to update to a new version. This seems like a lot of work.

Instead, what would be more immensely easier and more useful, would be if your game could check-in with a server online that you can control, and grab the latest configuration that you provide. This opens up several possibilities, you would then be able to have:

  • Message of the Day (MotD): A small message to display to the player. This might be as simple as showing a message to remind players to upgrade their game when a new release comes out.
  • Timed unlocks and special events: Perhaps at a certain time, a new area or new feature of the game unlocks. You don’t want players to be able to just fast-forward their system clock and fool the game into thinking it’s the future already.
  • Game balancing: Maybe you want to be able to re-balance the stats in your game or tune certain game parameters. Normally you’d need to send out a new update to do this, but pulling game balancing values from a server instead would allow you to update the game balance across every copy of your game without making your players update.

This tutorial describes how to pull your game config off GMCloud in your GameMaker Studio 2 project. It assumes you’ve set up your secrets and the gmcloud_handler in Part 1

Step 1: Write a message

Before we dive into GML code, you should set up some actual data to pull off GMCloud (by default it’s blank). Go to your game in the GameDev section of the website, scroll down to the “Game Config”, and enter some data. In this tutorial, I’ll be using some JSON-encoded data to easily turn into a ds_map.

JSON-formatted game config

Other formats are possible, for example:

  • plaint text: maybe you don’t need anything fancy, just a single string to show
  • ini: you could write an ini data just like an ini file, and have GameMakerini_open_from_string()
  • buffer: you could store a base64-encoded buffer, and have GameMakerbuffer_base64_decode()
  • ds_grid or other datastructures: GameMaker’sds_grid_read()and other _read() functions of other datastructures use string-based serialization. You could generate the grid data in a GM program, write out the serialized version with ds_grid_write() and copy the result into the config.
  • any other string-serialized data: as long as it’s a string, and you have the appropriate deserializers, any data is possible

Size limit approximately 1MB

Step 2: Issue the request for the Game Config

At some point in the game, just issue one request for the config once:

uuid = gmcloud_get_config();

That’s it! gmcloud_get_config() takes no arguments. It’ll use the secrets you set up in the gmcloud_handler object, and make a request to GMCloud for the game configs.

Note: make sure this only runs once! If you run it every step at 60fps, the server will not be happy with you, and I will be very upset with you. See the appendix of possible implementations for ways to do this.

The returned value, here stored in the variable uuid, is the “request ID”. It’s not the actual game config yet — it takes a few frames for the config to be requested and downloaded, and due to the uncertain nature of the internet, it’s hard to predict exactly how long this will take, so this uuid variable is returned and is used to periodically check to see if the requested game config has arrived yet. A diagram of how this kind of async polling works looks a bit like this:

Asynchronous polling for the result

While this may look resource intensive at first glance, under the hood each poll for the result is only one ds_map_exists(), so has minimal resource usage, even if you had several simultaneous requests.

Step 3: Check for a result

Once the request ID is grabbed, this means the request has been fired off to the GMCloud server, and all that is needed is to sit back and wait for the response.

We periodically poll with this request ID to see if a result exists. This can be done using:

if (gmcloud_result_exists(uuid)) {
...
}

It is fine to run this every frame, as it doesn’t require much resources to do so.

Step 4: Fetch the result

Once we’ve confirmed that a result exists for the request ID in question, we can fetch the data of the result using:

if (gmcloud_result_exists(uuid)) {
var result = gmcloud_result_pop(uuid);
// ... do something with result
}

The variable result now contains the requested data, in this case the game config you set up in Step 1.

Note: Had we not done a gmcloud_result_pop(uuid) inside that if statement, then the result would still be sitting there waiting for us to grab it, and gmcloud_result_exists(uuid) would continue to return true until we did.

Step 5: Parse the result

Now that we have the whole config string, since in this example we’ve set a JSON formatted string for easy access to the data, we need to run a json_decode() on the results to put the data in a map. For the purpose of the demo, we’ll just show_message the Message of the Day, and update some global value assumed to already exist for the stat update.

if (gmcloud_result_exists(uuid)) {
var result = gmcloud_result_pop(uuid);
var data = json_decode(result);

show_message(data[? "motd"]);
global.some_stat = data[? "some_stat"];
}

This very simple example shows the bare minimum of what you’d need to fetch and decode your game config from GMCloud. In a real implementation, you would probably want to better handle possible error conditions.

The extension includes some demo objects with moderately more complete implementations to serve as examples.

In Part 3, we’ll look at how to link a user’s profile so you can start accessing player data.


Tips for implementing the asynchronous result poll

As previously mentioned, it is important to make sure the request only runs once, rather than every frame (which would upset the server); and once the request is issue, the result must be polled for. This appendix describes a couple of methods and evolutions of the methods for doing so.

Method 1: Use the create event

The simplest method is to just issue the request in the create event of an object. This guarantees that the request is sent once, and then the step event can be dedicated to checking for the result:

Create event:

uuid = gmcloud_get_config();

Step event:

if (gmcloud_result_exists(uuid)) {
var result = gmcloud_result_pop(uuid);
// ... do something with result
}

Method 2: Use an undefined value

Another method is to use undefined as a way to signify that no uuid is set (request not made yet).

Create event:

uuid = undefined;

Step event:

if (is_undefined(uuid)) {
uuid = gmcloud_get_config();
}
else {
if (gmcloud_result_exists(uuid)) {
var result = gmcloud_result_pop(uuid);
// ... do something with result
}
}

In the above code, uuid starts out as undefined. At the first step, because it is undefined, the gmcloud_get_config() will run, and set the uuid variable to the request id. On subsequent step events, because uuid now holds a request id, it’ll skip the request and go straight on to checking for the result.

Method 2b: Add a timeout and retry

The previous method can be extended to add a timeout to abort the get if it takes too long (and optionally try again).

Create event:

uuid = undefined;
timeout_counter = 0;

Step event:

if (is_undefined(uuid)) {
uuid = gmcloud_get_config();
timeout_counter = 0;
}
else {
timeout_counter ++;
if (timeout_counter > 600) {
gmcloud_result_cancel(uuid);
uuid = undefined;
}

if (gmcloud_result_exists(uuid)) {
var result = gmcloud_result_pop(uuid);
// ... do something with result
}
}

In the above code, a timeout_counter has been added, which ticks up ever step, and when it hits 600 (10 seconds on a 60fps game), issues a gmcloud_result_cancel() to cancel the request, and set uuid back to undefined. Because undefined signifies that a request has not been made, this causes a new request to be fired off next step and the process is repeated.

Method 2c: Add error checking

In addition to checking for a result, it is also possible to check if an error occurred instead.

Create event:

uuid = undefined;
timeout_counter = 0;

Step event:

if (is_undefined(uuid)) {
uuid = gmcloud_get_config();
timeout_counter = 0;
}
else {
timeout_counter ++;
if (timeout_counter > 600) {
gmcloud_result_cancel(uuid);
uuid = undefined;
}

if (gmcloud_result_exists(uuid)) {
var result = gmcloud_result_pop(uuid);
// ... do something with result
}
else if (gmcloud_error_exists(uuid)) {
var error = gmcloud_error_pop(uuid);
// ... do something with error
uuid = undefined;
}
}

In the above code, an extra check is made to see if an error was returned. If the error is returned, the error message is grabbed, and uuid is again reset to undefined to retry (though you could at this point do something else like abort rather than retry)

On to Part 3!