Coffee-break Tutorials: Checkpoints Using Buffers (GML)


Coffee-break Tutorials: Checkpoints Using Buffers (GML)

In this Coffee-Break tutorial, we're going to look at creating a checkpoint in a game using buffers. However, before getting started, let's quickly explain what a buffer is and how it works, as even veteran users can sometimes be put off by the idea of using them and see them as being something complicated (they're not!).

NOTE: This tutorial is for people that use GML. If you prefer to use Drag and Drop™ to make your games, we have a companion article for you here.

Basically, a buffer is a space in memory that you tell the device running the game to "set aside" as a storage area for information. Simply put, you create a buffer and give it a size (the bytes of memory that it will require) then you write the data you want to store to it. Once filled, the buffer can be saved to a file and loaded again, and you can read the data back from it. In general, data is written to - and read from - a buffer in consecutive order, from the start of the buffer to the end, which makes it ideal for storing chunks of information that will be used all in one go, like a save game or a checkpoint!

Sounds simple enough, doesn't it? Now, there are nuances to using buffers and they can actually do a lot more than we've outlined above, but for the purposes of our tutorial today, that's really all you need to know to get started using them, and if you want to know a bit more of the in-depth details of how they work then you can check out the following section in the manual: Using Buffers.


GETTING STARTED

To start with, you will need to download and import into GMS2 the following YYZ file, as it will provide the base on which we are going to build our checkpoint system:

If you run the project, you'll see that this is a simple brick-breaker game controlled by the mouse, and it has 3 levels that can be played.

YoYo Brick Breaker DnD™

At the moment, if you die or leave the game on level 2 or 3 then you have to go back to level 1 and start again. What we're going to do is create a checkpoint that saves the game state information at the end of each room so that you can load the game from that point again, and not lose your progress. For that we'll need a new object, so go ahead and create a new object now and call it "oSaveControl". We want this object to be persistent so make sure to check that flag in the object properties:

Save Object Properties

Making this object persistent means we can add it into the first room of the game and it will be carried over to the rest of them when rooms change, making it ideal for storing information. With that done, we can start to add our GML code...


ROOM END EVENT

We're going to start by adding a Room End event (from the Other event category). We'll be using this event to check which room we are in, and if it is not the title room (ie: it is a game level room), then we will save the data for the room so that it can be used as a checkpoint. We also need to check the number of balls (lives) that the player has to make sure we don't save when the room ends because of a Game Over.

After those checks, we'll create a buffer, add some data to it, then save it and delete it. Let's look at the code first then we'll explain it:

if (room != rTitleScreen)
{
if (global.p_balls > 0)
    {
    var _b = buffer_create(64, buffer_grow, 1);
    buffer_write(_b, buffer_u8, global.p_balls);
    buffer_write(_b, buffer_u32, global.p_score);
    switch (room)
        {
        case rLevel1:
            buffer_write(_b, buffer_u8, 2);
            break;
        case rLevel2:
            buffer_write(_b, buffer_u8, 3);
            break;
        case rLevel3:
            buffer_write(_b, buffer_u8, 4);
            break;
        }
    buffer_save(_b, "save.dat");
    buffer_delete(_b);
    }
}

So here, after we check the room and the number of balls that the player has remaining, we create our buffer. When we do this we need to specify the size, the type and the alignment. We won't explain these details here as the manual covers it all in depth here, but the values we've chosen are just basic "starter" values for buffers where we aren't sure what amount of data, nor of what type, we are going to add.

Next we add in the number of balls as an unsigned 8 bit integer value. It's unlikely that the player will have more than 256 balls, so using an 8bit int is fine for this. For the score we've used an unsigned 32 bit integer as it will be greater than 256 and this type gives us plenty of room to store even larger score values. Finally, we check the current room and store a value for it as another 8 bit integer (if the game was going to have more than 256 levels, then this would be changed to a 16 or 32 bit integer). You'll notice here that for Level 1 we are saving 2 as the value, for Level 2 we save 3, etc... This is because we've completed the current level at this point, so we will want the load system to know that the room the player should go to when we load is the next one.

The final thing we do here is save the buffer to a file, then delete it, as we don't need to keep it around in memory. Like most dynamic resources that are created at runtime, you want to make sure that they are removed from memory when no longer needed to prevent memory leaks.


STEP EVENT

That's the code we need for saving, so let's now add what we need to load the game. This will go in the Step Event of our save controller object, so add that event now. We only want to load data if the player is in the main title room, and we also only want to permit the player to load a game if a save file actually exists, so we'll be starting with those checks. We currently use the left mouse button to start the game, so we'll keep using that for a new game and use the right mouse button to load the game. This means we also need to check those buttons, and then in the right mouse button section add in the code to actually load the data and go to the appropriate room.

The code for all of the above looks like this and should be added to the Step Event:

if (room == rTitleScreen)
{
if (file_exists("save.dat"))
    {
    if (mouse_check_button_pressed(mb_left))
        {
        file_delete("save.dat");
        global.p_balls = 3;
        global.p_score = 0;
        }
    else if (mouse_check_button_pressed(mb_right))
        {
        var _b = buffer_load("save.dat");
        global.p_balls = buffer_read(_b, buffer_u8);
        global.p_score = buffer_read(_b, buffer_u32);
        var _r = buffer_read(_b, buffer_u8);
        buffer_delete(_b);
        switch(_r)
            {
            case 1: room_goto(rLevel1); break;
            case 2: room_goto(rLevel2); break;
            case 3: room_goto(rLevel3); break;
            }
        }
    }
}

You'll notice that the left mouse button action will delete the save file file and reset the global variables for balls and score. This ensues that when a new game started, the save file created will be for the that game and that the data is saved correctly. Also notice that in the right mouse button code where we load the buffer, we read back the buffer data in the same order that it was written, ensuring that each of the items we read are of the same type that were written too. This is very important, as getting the order wrong or using the wrong type can give wrong values and even lead to errors that may crash the game.


DRAW GUI EVENT

We're almost ready to test things, but before we do we need to let the player know that they can load a saved game from the title screen. For that we'll add some very basic code into the Draw GUI Event so that it shows on the title screen if a save file is present. Add this event to the save controller object now and give it the following code:

if (file_exists("save.dat"))
{
if (room == rTitleScreen)
    {
    draw_text(175, 300, "Press RMB To Load Game");
    }
}

With that done, all that's left is to add this object into the room "rTitleScreen", so open that room now and then select the layer "Instances". Once selected, drag the object "oSaveControl" onto the layer (anywhere, although I usually put controllers in the top left corner).


SUMMARY

You can test the game now and see if the checkpoint save system is working. Simply play the game and pass the first level, then deliberately lose the second level so you are taken back to the title screen. On the title screen you should now see the text "Press RMB to load game", and if you do that then you should start the game on level 2 and with the same score and lives you had when you finished level 1.

Hopefully that all works and you have now got a better idea of how easy it is to use buffers for things like this. You could go ahead and expand on this example now by adding in more levels (in which case you'd need to expand the "Switch" actions for loading and saving), or adding in power-ups that are carried across levels (in which case you'd need to add further information into the buffer then read it back to generate any effects on load). As long as you are clear on the following important points you shouldn't have any issues using buffers in this project or in your own projects:

  • Buffers require memory and should be deleted when no longer required

  • You should always read data from a buffer in the same order that it was written to the buffer

  • You should always read and write values using the same data type

That's it for another Coffee-break Tutorial! Happy GameMaking!