Buffers

GameMaker:Studio has introduced a series of functions to the GameMaker Language (GML) to deal with buffers. Most people should be familiar with this term as it is used all the time when dealing with computers and programming, but knowing the word doesn't mean you actually know what it means! So this tutorial aims to explain what a buffer is and how to use them within the context of GameMaker:Studio programming, although the general way they work is the same no matter the language or the technology, which is one of the reasons they are so important.

What Is A Buffer?

A buffer (in programming) is basically a space within the system memory that is used to store small "packets" of data for just about anything (for example data transfer, collisions, colour data etc...). Since it is held in system memory it is very fast to access, and a buffer would generally be used for very short-term storage, like receiving network information before processing it, or for storing a checkpoint in your game (this is explained in the example given further down the page).

Buffer_Memory.png

Buffers are created by allocating a space in the system memory, calculated in bytes, which is then reserved for your game as long as your game is running or until you delete the buffer using the appropriate function. This means that even when your game is not in focus (for example, on a mobile device when you take a call the game will be put into the background) the buffer will still exist, however if the game is closed or re-started the buffer will be lost.

NOTE: Restarting the game will not clear or delete the buffer! But it will prevent any further access to the previously created buffer as the id "handle" will have been lost, causing a memory leak which will crash your game eventually. So, when re-starting a game, remember to delete the buffer first.

Buffer Types

GameMaker:Studio permits the creation of four different buffer types. The reason for this is that buffers are designed to be a highly optimized temporary storage medium, and as such you should create a buffer that is appropriate to the type of data that you wish it to store, otherwise you could get errors or cause a "bottleneck" in your code. Before explaining this further, let's look at the four available buffer types (defined as constants in GML):

Buffer Constant Description

 buffer_fixed

A buffer of a fixed size in bytes. The size is set when the buffer is created and cannot be changed again.

 buffer_grow

A buffer that will "grow" dynamically as data is added. You create it with an initial size (which should be an approximation of the size of the data expected to be stored), and then it will expand to accept further data that overflows this initial size.

 buffer_wrap

A buffer where the data will "wrap". When the data being added reaches the limit of the buffer size, the overwrite will be placed back at the start of the buffer, and further writing will continue from that point.

 buffer_fast

This is a special "stripped down" buffer that is extremely fast to read/write to. However it can only be used with buffer_u8 and buffer_s8 data types, and must be 1 byte aligned. (Information on data types and byte alignment can be found further down the page).

Those are the buffer types available to you when using GameMaker:Studio, and which one you choose will greatly depend on the use you wish to put it to. For example, a grow buffer would be used for storing a "snapshot" of data to create a save game since you do not know the actual amount of data that is going to be placed in it, or a fast buffer would be used when you know that the values you are working with are all between 0 and 255 or -128 and 127, for example when processing RGB data from an image.

Buffer_Types.png

When creating a buffer, you should always try to create it to a size that is appropriate to the type, with the general rule being that it should be created to accommodate the maximum size of data that it is to store, and if in doubt, use a grow buffer to prevent overwrite errors.

The actual code to create a buffer would look something like this:

player_buffer = buffer_create(16384, buffer_fixed, 2);

That would create a fixed buffer of 16348 bytes and byte aligned to 2, with the function returning a unique id value that is stored in a variable for later referencing of this buffer. now we have the basic concept of a buffer explained you should learn about data types and the previously mentioned byte alignment.

Data Types

When reading and writing data to a buffer, you do it in chunks of data defined by their data type. The data type sets the number of bytes allocated within the buffer for the value being written, and it is essential that you get this correct otherwise you will get some very strange results (or even errors) for your code.

Buffers are written to (and read from) sequentially, in that one piece of data is written after another, with each piece of data being of a set type. This means that you should ideally be aware of what data you are writing to the buffer at all times. these data types are defined in GML by the following constants:

Data Type Constant Bytes  Description

 buffer_u8

 1 byte

An unsigned, 8bit integer. This is a positive value from 0 to 255.

 buffer_s8

 1 byte

A signed, 8bit integer. This can be a positive or negative value from -128 to 127 (0 is classed as positive).

 buffer_u16

 2 bytes

An unsigned, 16bit integer. This is a positive value from 0 - 65,535.

 buffer_s16

 2 bytes

A signed, 16bit integer. This can be a positive or negative value from -32,768 to 32,767 (0 is classed as positive).

 buffer_u32

 4 bytes

An unsigned, 32bit integer. This is a positive value from 0 to 4,294,967,295.

 buffer_s32

 4 bytes

A signed, 32bit integer. This can be a positive or negative value from -2,147,483,648 to 2,147,483,647 (0 is classed as positive).

 buffer_f16

 2 bytes

A 16bit floating point number. This can be a positive or negative value within the range of +/- 65504. (Not currently supported!)

 buffer_f32

 4 bytes

A 32bit floating point number. This can be a positive or negative value within the range of +/-16777216.

buffer_f64

8 bytes

A 64bit floating point number. This can be a positive or negative value from -(252) to 252 - 1.

buffer_bool

1 byte

A boolean value. Can only be either 1 or 0 (true or false)

buffer_string

N/A

This is a UTF-8 null terminated (0x00) string. Basically a GameMaker string is dumped in the buffer, and a 0 put at the end.

So, say you have created a buffer and you want to write information to it, then you would use something like the following code:

buffer_write(buff, buffer_bool, global.Sound);
buffer_write(buff, buffer_bool, global.Music);
buffer_write(buff, buffer_s16, obj_Player.x);
buffer_write(buff, buffer_s16, obj_Player.y);
buffer_write(buff, buffer_string, global.Player_Name);

As you can see, you can write different types of data to a buffer (you are only limited to a specific data type when using the fast buffer type), and this data will be added into the buffer consecutively (although its actual position in the buffer will depend on its byte alignment, explained below). This is the same for reading information from the buffer too, and in the case of the example given above, you would read from the buffer in the same order that you wrote the data, checking for the same data type, eg:

global.Sound = buffer_read(buff, buffer_bool);
global.Music = buffer_read(buff, buffer_bool);
obj_Player.x = buffer_read(buff, buffer_s16);
obj_Player.y = buffer_read(buff, buffer_s16);
global.Player_Name = buffer_read(buff, buffer_string);

As you can see, you read out information in the same order that you read that you read it into the buffer. For further information on how to add and remove data from the buffer please see the Examples section below.

Byte Alignment

If you have been reading through this tutorial you will have seen references to the byte alignment of a buffer. This basically refers to the position that new data will stored at within a given buffer. How does this work? Well, for a single byte aligned buffer, each piece of data is written to the buffer sequentially, with each new data piece being added directly after the previous. However a 2 byte aligned buffer will write each piece of data to intervals of 2 bytes, so that even if your initial write is 1 byte of data, the next write will be moved to align to two bytes.

Buffer_Byte_Alignment.png

So, if your byte alignment is set to, say, 4 bytes and you write a single piece of data which is 1 byte in size then do a buffer tell (a "tell" gets the current position for reading/writing for the buffer), you'll get an offset of 1 (the offset in this case is the number of bytes from the start of the buffer to the current read/write position).

However, if you write another piece of data, also 1 byte in size, then do a buffer tell, you'll get an offset of 5 bytes (even though you have only written 2 bytes of data) as the alignment has "padded" the data to align it with the 4 byte buffer alignment.

Basically, what this means is that alignment will only affect where things are written to, so if you do a buffer tell after you write something, it'll return the current write position which immediately follows the data you've written. Note, however, that if you then write another piece of data, internally the buffer will move the write position along to the next multiple of the alignment size before actually writing the piece of data.

The following is a general guide to show which values are most appropriate for each data type but it should be noted that in GameMaker:Studio there is no real speed benefit to using a specific alignment, so it's not something you need to generally worry about (although future updates may change this).

  • Strings should be aligned to 1 byte.
  • Signed or unsigned 8bit integers can be aligned to any value, but note that for a fast buffer (see buffer_write) it must be aligned to 1.
  • Signed or unsigned 16bit integers should be aligned to 2 bytes.
  • Signed or unsigned 32bit integers should be aligned to 4 bytes
  • Floats of up to 16bits should be aligned to 2 bytes. (Not currently supported!)
  • Floats of up to 32bits should be aligned to 4 bytes.
  • Floats of up to 64bits should be aligned to 8 bytes.

Examples

Below you can find a couple of examples of buffers being used.

Making Checkpoints With A Buffer

An simple example of how a buffer can be used in any GameMaker:Studio game for any platform, is the function game_save_buffer. This function will take a "snapshot" of the current game state and save it to a pre-defined buffer, which can then be read from to load the game at that point again.

NOTE: This function is very limited and it is designed for the beginner to get a checkpoint system up and running quickly, but more advanced users may prefer to code their own system using the File functions, due to the fact that the game will not save any of the dynamic resources that you can create at run-time like data structures, surfaces, added backgrounds and sprites etc...

The first thing we need to do is create a new object to control the saving and loading, so make one now and give it a Create Event. In this event, place the following code:

SaveBuffer = buffer_create(1024, buffer_grow, 1);
StateSaved = false;

The first line creates a grow buffer (since we don't know the final size of the saved data) of 1024 bytes and aligned to 1 byte. A variable is then created to check against and see if the game has been saved or not (this will be used for loading).

Now we would add a Keypress Event (for example) in which we will save the current game state to the created buffer:

StateSaved = true;
buffer_seek(SaveBuffer, buffer_seek_start, 0);
game_save_buffer(SaveBuffer);

The above will first set the control variable to true (so that this is saved when we save the game to the buffer) and then seek to the start of the buffer before writing the current save state into it. Why do we use buffer_seek()? Well, as mentioned in the tutorial previously, you read and write to a buffer from the last position that data was added to. This means that if you don't set the buffer tell back to the start then when you save you will be adding the data into the buffer at the current buffer read/write position, so we use the function buffer_seek() to move the tell to the buffer start.

We have now saved the current game state to a buffer. The next step would be to code how to load it, probably in a Keypress Event:

if StateSaved
{
buffer_seek(SaveBuffer, buffer_seek_start, 0);
game_load_buffer(SaveBuffer);
}

The game will then be loaded at the end of the event in which you place the above code.

'NOTE: This is only for use in the same room, and not for generating complete saved games for after your game has been closed or restarted!

The final thing to add to the controller object is a "clean up" code. Buffers are stored in memory and as such if you do not clean up when you are finished with them, you can get memory leaks that will eventually lag and crash your game. So add a Room End Event (from the Other event category) with:

buffer_delete(SaveBuffer);

This object can now be placed into a room and on a keypress save and load the room state from a buffer.

Networking Buffers

When working with the GameMaker:Studio networking functions, you have to use buffers to create the data "packet" that is being sent over the network connection. This example intends to show how this is done, but due to the scope of the networking possibilities, it is only designed to show how to use the buffers themselves, and not the full networking system (there is a Demo file included with GameMaker:Studio that shows this.

The first thing we will show is the creation and use of a buffer for the client side of the network connection. This buffer will be used to create small data packets that can then be sent to the server, so in the Create Event' of an instance we would assign a buffer like this:

send_buff = buffer_create(256, buffer_grow, 1);

We make the buffer small (256bytes) as it is not intended for holding large amounts of data, we make it a "grow" buffer to ensure no errors should we need to add more data to be sent at any time., and the alignment is set to one for convenience.

Now, lets say that we want our client to send data to the server. For that we need to create a buffer "packet", and in this example we are going to send a Key Press Event, like when the player presses Left to move around the game. To do this we write the necessary data to the buffer first then send it off:

buffer_seek(buff, buffer_seek_start, 0);
buffer_write(buff, buffer_u8, 1);
buffer_write(buff, buffer_s16, vk_space);
buffer_write(buff, buffer_bool, true);
network_send_packet(client, buff, buffer_tell(buff));

Before writing to the buffer we have set the "tell" to the start of the buffer as networking always takes the data from the start of a buffer. We then write the check value (this will be used by the server to determine the type of event to be processed), then the key being used, and then the state of the key 8in this case "true" for pressed). This buffer is then sent as a data packet by the network function. Note that we do not send the whole buffer! We only send the data written, using the buffer_tell() function to return the current read/write position of the buffer (remember that writing to the buffer moves the "tell" to the end of what has been written).

What about receiving the data on the server? The received data packet that must be written into the buffer on the server and then used to update the game. For that we would use the Networking Asynchronous Event in the network controller object of the server, as this simplified code below shows:

var buff = ds_map_find_value(async_load, "buffer");
if cmd == buffer_read(buff, buffer_u8);
    {
    key = buffer_read(buff, buffer_s16 );
    key_state = buffer_read(buff, buffer_bool);
    }

The asynchronous event will contain a special temporary ds_map (it is removed at the end of the event automatically) which contains different information depending on the type of incoming data from the network. In this case, we are assuming that the map has been checked and found to be a buffer data packet sent from a client. We now check the first piece of data that is in the buffer to see what kind of event has been sent - in this case the value "1" represents a key event, however when coding these things you should define constants to hold these values to simplify things - and then store the key being pressed and its state (true = pressed, false = released). This information would then be used to update all the clients with the new status of the sending client player.

NOTE: The buffer that is created from the ds_map is automatically removed at the end of the Network Asynchronous Event so there is no need to use buffer_delete() here.
Have more questions? Submit a request

0 Comments

Article is closed for comments.
Powered by Zendesk