Optimizing Your games in GameMaker: Studio

Posted by James Foreman on 25 October 2013

One of the most frequently asked questions about making games with GameMaker:Studio, is how to optimize them so that they run as efficiently as possible. Unfortunately there is no easy answer to this! Each game is different and how you code them will vary depending on your own needs for the project... however there are a few general "rules of thumb" that can be followed in all projects which will help you to get the most from GameMaker.

 

Graphics

One of the most common causes of slow-down and stuttering in a game is the graphics pipeline. Game logic is a lot faster to process than it is to draw (in general) so you want to make sure that you draw everything in the most efficient way possible. Below you can find some tips on how to achieve this.

 

Texture Pages

GameMaker:Studio stores all your game graphics on Texture Pages. A texture page is simply a single image with all the game graphics spread out in such a way that they can be "pulled" from it at run time to draw on the screen.

 

Now, when you have created many graphics for your game they start to take up more than one texture page and you can have them spread out over several different ones. This means that for GameMaker:Studio to draw them on the screen, it may have to perform texture swaps to get the correct sprite or background from the correct page, which is not a problem when it's only a couple of pages, but when they are spread out of a lot of pages, this continuous swapping can cause lag and stutter.

 

How to avoid this? Well, you can create Texture Groups for your game and then flush the unused graphics from texture memory at the start of the room, meaning that only those graphics you are going to actually use are held in memory.

 

To start with, you should open the Global Game Settings and go to the Texture Groups Tab. Here you need to create the groups that you need -  for example, if you have some graphics that only appear in the Main Menu, create a group for them. If you have a series of sprites and backgrounds that only appear in a single level/room, then make a group for them.

 

TextureGroups.png

Once you have your groups, you can then go through the sprites and backgrounds and assign each one to a specific group from the drop-down menu in the asset window:

 

TexturePage_Assign.png

With that done, you have already optimized your game quite a bit as this will limit the texture swaps needed, since all the graphics for a specific room should now be on the same page.

 

As for flushing the unneeded pages from memory, you would do this in the Create Event of the very first instance of each room (this can be set from the room editor) using the function

 

draw_texture_flush();

 

This function clears all data from the texture memory. Note that this may cause a brief flash or flicker as the new textures are loaded for the first time when the draw event is run, and so, to avoid this, you should also "initialize" the textures in the same event, by simply calling a draw function (one for each of the needed pages). This will not be seen since it is in the Create Event, but will prevent any glitches or flickers when the game graphics are actually drawn. Your final Create Event would look something like this:

 

draw_texture_flush();

draw_sprite(spr_Menu1, 0, 0, 0);

draw_sprite(spr_Cursor, 0, 0, 0);

 

Remember! You only need to initialize ONE graphic for each texture page! You can find out how many texture pages are in a group by previewing them from the Global Game Settings Graphics tab for the target platform, which permits you to see how the finished texture pages will look for that platform. In this way you can see if all the images are on one page or multiple, and select one to draw after flushing from each page created as necessary.

 

TexturePage_Preview.png

Blend Modes

When drawing, GameMaker:Studio sends off "batches" of graphics data through the pipeline for drawing, and obviously you want to keep this number as low as possible. Normally it's not something you would need to worry about, but if you are using blend modes for drawing, then each call to set the blend mode breaks the current texture batch and multiple calls from multiple instances can have an adverse affect on your game.

 

How to solve this? Try and use just one instance to set the blend mode and draw everything that is required. For example:

 

draw_set_blend_mode(bm_add);

with (obj_HUD) draw_sprite(spr_Marker, 0, mx, my);

with (obj_Player) draw_sprite(spr_HaloEffect, 0, x, y);

with (obj_Cursor) draw_self();

draw_set_blend_mode(bm_normal);

 

That would set the blend mode for a single batch call, instead of having three separate ones for each of the instances referenced.

 

Alpha Blending and Alpha Testing

There are two draw functions included in GameMaker:Studio which are often over-looked, bu they can both dramatically speed up the draw pipeline. They are:

 

draw_set_alpha_test()

draw_enable_alphablend()

How can these help? Well, the first one can enable alpha testing which basically checks the alpha value of each pixel and if it is above the blend threshold (a value from 0 to 255), then it is drawn. Essentially this "throws away" any pixel with an alpha lower than the test value, meaning that it is never drawn (as even a pixel with zero alpha is still "drawn" normally), and is an excellent way to speed up games that have retro, pixel art graphics with no alpha gradients. Note that you can set the alpha test reference value using the function draw_set_alpha_test_ref_value().

 

The alpha blend function plays a different role, and can be used to switch off all alpha blending. This will mean that any sprites or backgrounds with alpha will be drawn completely opaque. This function is designed to be used at any time in the draw pipeline, so if you are drawing a background manually and it has no alpha then you can switch off alpha blending, draw the background, and then switch it back on again for all further drawing. On some games this can give a massive speed boost, so if you are drawing something that doesn't require alpha, consider switching this off.

 

Scaling

When working on a game for multiple platforms, it is more than likely that you will need to scale the game to suit a given resolution. This can easily be achieved using views (the Scaling and Resolution tutorial from the GameMaker:Studio RSS feed shows this), but scaling in this way can also adversely affect the game's performance, especially when scaling up to large screen sizes (like scaling a 320x240 Android game up to full 1080p resolution for the OUYA).

 

A VERY efficient method to use is to draw everything to a surface and then scale that to fit the display size. You would do this by first creating a surface the size of the view, then use view_surface_id[] to store the id of the newly created surface so that ll the contents of that view are drawn to it rather than the screen.

 

You would then have a Draw GUI event which draws the surface scaled using draw_surface_stretched().

 

Adding Assets At RunTime

Loading sprites and backgrounds from an external source can be done in GameMaker:Studio, as can creating new assets using functions like sprite_duplicate(). However each new asset that you create in this way will also create a new texture page, meaning that (for example) adding 10 new sprites will create 10 new texture pages! And each time you draw these sprites it is a new texture swap and a break in the batch to the graphics card.

 

As you can imagine, this is not very efficient, and so (unlike previous versions of GameMaker) it should be avoided, with all the graphic assets being added to the game bundle from the IDE. Note that you can use these functions for adding/creating small numbers of things and they won't adversely affect performance, but adding many, many images in this way should always be avoided as it will have an impact.

 

Particles

While particles are an excellent way to create cheap yet spectacular effects, they may adversely affect performance if you are using the built in particle images instead of custom sprites. This is because each particle sub-image is held on it's own texture page, and therefore having any number of them on the screen at any one time will greatly increase the number of texture swaps being done.

 

To prevent this happening, you should add the particles to the resources as sprites and then define them as normal, only using part_type_sprite() instead of part_type_shape(). You can find the particles themselves from the folder %appdata%/GameMaker-Studio/Windows8/html5game/particles. Adding them to your game as normal sprites will add them to a texture page along with the rest of your game's images and help reduce the swaps when they are drawn.

 

Sound

When adding sound to GameMaker:Studio, there are a number of available options for the format and quality of the final output sound file. These should be set automatically for you following these basic rules:

 

If it is a sound effect (or any short sound bite of only a few seconds), then it should be uncompressed.

 

If it is a sound effect but larger than a few seconds, or if it is only used very occasionally in the game, then it can be compressed.

 

If it is a large sound effect and used frequently in the game it should be compressed (uncompressed on load).

 

If it is music it should be compressed (streamed from disk).

 

Apart from the compression and streaming options, you also have settings for the sound quality. these should be set to be as close as possible to the settings used to create the original file that you are adding. So if your MP3 track is 22,050Khz and 56kbps, those are the settings you should use for the quality. If you are unsure about the actual values to use, then leave it as the default values that GameMaker:Studio sets for you.

 

Code Tips

 Giving advice on coding can be difficult, as each person has their own opinion about things and what works for one, may not work for another. But there are certain things that should be noted when working with GameMaker:Studio that are true for everyone.

 

Nest "if"

Currently GameMaker:Studio does not do any "easy out" evaluation of if. Consider the following code:

 

if mouse_check_button(mb_left) && mouse_x > 200 && global.canshoot == true

{

//do something

}

 

In many languages, the moment any one of the checks returns false, the rest are skipped and the next function is stepped into. However GameMaker:Studio will evaluate ALL of them, even if the first one is false. Therefore it is recommended that you "nest" your ifs like this:

 

if mouse_check_button(mb_left)

    {

    if mouse_x > 200

        {

        if global.canshoot == true

            {

            //do something

           }

        }

   }

 

While this may seem verbose, in a logic heavy game where multiple checks like this are necessary, it can help keep things optimized.

 

Global Variables

Using global variables is a fine way to have controller variables that are accessible to all instances. However it should be noted that script calls which reference them (especially when compiling to the YYC) can be slowed down by multiple lookups of global variables. For example, consider this script:

 

repeat(argument0)

{

with (obj_Parent)

    {

    if place_meeting(global.px, global.py, argument1) instance_destroy();

    }

}

 

The issue here is that each iteration of the repeat loop has to look up the values for the global variables, which is very slow. To avoid this, you should always assign any gobal variables that are going to be used like this to a local variable. So our code example would become:

 

var xx = global.px;

var yy = global.py; 

repeat(argument0)

{

with (obj_Parent)

    {

    if place_meeting(xx, yy, argument1) instance_destroy();

    }

}

 

Local Variables

A local variable is "local" to the script or code block that it has been created in, and they have a very fast look-up time. This means that they are an ideal option to store any function call values or operations that need to be used repeatedly in a code. For example, if you have to draw something relative to the view center, calculate the point once and store it's coordinates in a couple of local variables for use later:

 

var xx = view_xview[0] + (view_wview[0] / 2);

var yy = view_yview[0] + (view_hview[0] / 2);

draw_sprite(spr_Crosshair, 0, xx, yy);

draw_text(xx, yy, dist);

 

In that simple example code we have halved the operations being done, simply by assigning to local variables first. In large code blocks this can be a significant optimization, and you should always look at ways in which your code can be compacted to have the lowest number of operations or function calls.

 

 Arrays

One simple optimization trick for arrays is to initialize them in reverse order. In this way GameMaker:Studio will assign memory for the whole array in a block, rather than "bit by bit". So, for example, if you are just wanting to initialize the array to 0, instead of a loop you can do:

 

myarray[99] = 0;

 

And that will create a 100 value array, cleared to 0. Should you need to assign values to each of the array indices then use a loop, but start from the last value of the array, for example:

 

for(var i = 255; i > -1; i--;)

{

myarray[i] = make_color_hsv(irandom(255), 150, 255);

}

 

It is important to note that this is not the case for any of the JavaScript modules (HTML5, Tizen or Windows 8), as they treat arrays differently. This means that you should initialize them from 0 upwards, and not in reverse.

 

Loops

A minor optimization is to make sure that you use the correct loop function for the job at hand. In general, most people use either a for loop or a repeat loop to generate repeating data, and in many cases the choice comes down to whatever the user is comfortable using. However, it should be noted that they will perform differently depending on the use they are put to.

 

The general rule is that if you need to increment (or change) a value, use a for loop, and if you don't then use repeat. For example:

 

repeat(100000)

{

xx = irandom(500);

}

 

The above code would be faster to run than:

 

for (var i = 0; i < 100000; i++;)

{

xx = irandom(500);

}

 

But if you had this:

 

var i = 0;

repeat(100000)

{

xx = irandom(i);

i++;

}

it would be slower than doing this:

 

for (var i = 0; i < 100000; i++;)

{

xx = irandom(i);

}