How To Optimise Your Games


How To Optimise Your Games

One of the most frequently asked questions about making games with GameMaker is: how to optimise them so that they run as efficiently as possible.

While there is no easy answer that works for all projects, there are a few general "rules of thumb" that can be followed in all projects which will help you to get the most out of GameMaker.

Graphics

One of the most common causes of slowdown and stuttering in a game is the graphics pipeline, 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 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 runtime to draw on the screen.

Now, when you have created many graphics for your game they may 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 to draw them on the screen, it may have to perform texture swaps to get the correct sprite from the correct page, which is not a problem when it's only a couple of pages, but when they are spread out over 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 and pre-fetch 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 Texture Group Editor from the Tools menu at the top of the IDE. Here you can create the texture 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, etc.

undefined

Once you have your groups, you can then go through the sprites and backgrounds and assign each one to a specific group from here using the "Add Asset" button, or you can go to each sprite and set the texture group from the Group drop-down menu in the asset window:

undefined

With that done, you have already optimised 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 image 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 "initialise" the textures in the same event, by simply calling a pre-fetch 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();
sprite_prefetch(spr_logo);
sprite_prefetch(spr_menu);

Remember! You only need to pre-fetch ONE graphic for each texture page! You can actually see how the sprites are packed onto each texture page from the Game Options for each platform.

undefined

This is done by clicking the "Preview" button in the Graphics section of the Game Options, and permits you to see how the finished texture pages will look. In this way you can see if all the images are on one page or multiple, and decide on how your groups and pages should be created and assigned.

undefined

Dynamic Textures

You can also mark a texture group as dynamic. While you can prefetch and flush texture groups to load and unload them from video memory (VRAM), they are still held in RAM. A dynamic texture group allows you to keep a texture group on disk and only load it into RAM (and, subsequently, into VRAM) when it's needed. Loading a dynamic texture group is done with the function:

texturegroup_load(groupname, [prefetch=true])

Unloading is done using the function:

texturegroup_unload(groupname)

Adding Assets At Runtime

Loading sprites from an external source can be done in GameMaker, as can creating new assets using functions like sprite_add(). 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.

NOTE: The function sprite_add() blocks further execution of your code until the sprite has finished loading. You can use sprite_add_ext() instead, which loads sprites asynchronously.

Drawing Optimisations

Depth Buffer

The depth buffer stores the depth value (or z value) of every pixel on a surface, which is the distance of the pixel to the camera. All surfaces are, by default, created with a depth buffer. Whenever you draw something new to the surface, the depth value is written to the depth buffer when z-writing is enabled (the default):

gpu_set_zwriteenable(true);

If your game permits it, however, you can disable the depth buffer when working with surfaces using surface_depth_disable and let everything be handled by the draw order. When the depth buffer is disabled, surfaces aren't created with a depth buffer. To disable the depth buffer, call the following before creating a surface:

surface_depth_disable(true);

When the depth buffer is disabled, there is also no need to do depth comparisons between pixels and you don't need to write any depth information:

gpu_set_zwriteenable(false);
gpu_set_ztestenable(false);

After all, everything is drawn back-to-front so anything that gets drawn later is drawn on top of what's drawn before it.

Blend Modes

When drawing, GameMaker 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:

gpu_set_blendmode(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();
gpu_set_blendmode(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.

NOTE: Other things that can break the batch are drawing shapes, using the draw health bar functions, using a shader, setting uniforms and changing render targets.

Alpha Blending and Alpha Testing

There are two draw functions included in GameMaker which are often overlooked, but they can both dramatically speed up the draw pipeline. They are:

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 gpu_set_alphatestref().

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 (note that it can be enabled and disabled as often as required with very little overhead).

Layer Begin and End Scripts

You can also make use of layer begin and end scripts. You make the desired changes in the layer begin script and reset it back in the layer end script. This gives a clean, organised way to draw all instances or assets on a given layer in the same way while keeping their code in their Draw Event, and without breaking the batch.

function blend_additive()
{
   if (event_type == ev_draw && event_number == ev_draw_normal)
   {
       gpu_set_blendmode(bm_add);
   }
}
function blend_normal()
{
   if (event_type == ev_draw && event_number == ev_draw_normal)
   {
       gpu_set_blendmode(bm_normal);
   }
}

The scripts are run at the start of each of the different draw events so you may want to check if the current Draw event is the one you want to execute them for.

In the Room Creation Code or in an instance's Create Event/Room Start event you then assign the scripts:

var _layer_id = layer_get_id("Instances");
layer_script_begin(_layer_id, blend_additive);
layer_script_end(_layer_id, blend_normal);

Everything on the "Instances" layer is then drawn using additive blending.

NOTE: In this case you should make sure that no instance on the layer breaks the batch in its draw code.

Sound

When adding sound to GameMaker, 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 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 that are true for everyone.

Early Out If

GameMaker has an "early out" evaluation of if. Consider the following code:

if (mouse_check_button(mb_left) && mouse_x > 200 && global.canshoot == true)
{
   // Do something
}

Here we are evaluating three different expressions and if they are all true then the rest of the code will run. However, if any one of them returns false, then the code won't run. The great thing about this is that if the first one is false, then the rest are not even checked, meaning that when creating "if" statements with multiple checks, put the most expensive one last, and try to put the least likely one to evaluate true first to make the most of this "early out" system.

NOTE: This is also called short-circuit evaluation.

Don't Calculate Every Step

Sometimes you may have a complex algorithm for pathfinding, enemy A.I., ... Having this run every step may be too much for the CPU to maintain the game speed that you're aiming for. In these cases it can be useful to set an alarm and only execute the code when the alarm triggers.

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 global 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

As explained above, a local variable is "local" to the script or code block that it has been created in, and has 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 its coordinates in a couple of local variables for use later:

var _xx = camera_get_view_x(view_camera[0]) + (camera_get_view_width(view_camera[0]) / 2);
var _xx = camera_get_view_y(view_camera[0]) + (camera_get_view_height(view_camera[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 optimisation, and you should always look at ways in which your code can be compacted to have the lowest number of operations or function calls. It's also worth noting that any variable lookup done on any instance will benefit from being stored in a local variable if used more than once in any code, and this is especially true when compiling using the YYC.

Arrays

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

myarray[99] = 0;

// or

array_create(100, 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 HTML5, as it treats arrays differently. This means that you should initialise them from 0 upwards, and not in reverse for this platform.

Structs

Another optimisation is related to structs. When you access struct variables, GameMaker calculates a "hash" from the variable name (as a string), which is basically a key to the location in memory of the variable. Accessing the variable using the hash is fast but calculating the hash itself is relatively slow - using the analogy of the key: using the key is easy, creating it is hard and continuously creating a new one should be avoided. When the compiler detects that a variable name is constant, it calculates the variable's hash in advance and replaces the hash calculation with the "hard-coded" hash value in your game's executable. This way, the hash to access the variable never needs to be calculated when your game is running. For example:

/// Create Event
my_struct =
{
   a: 7,
   b: 8,
   c: 9
};

/// Step Event
my_struct.a = x;  // The variable name "a" never changes throughout the game so the compiler can optimise this
// OR:
// my_struct[$ "a"] = x;

When the variable name is not constant at compile-time, GameMaker cannot know in advance what the hash should be (it depends on the value that the variable holds at that moment) and it needs to recalculate the hash. You can still optimise things yourself in this situation by getting the hash of the variable once using variable_get_hash and getting and setting the struct variable with struct_get_from_hash and struct_set_from_hash:

/// Create Event
my_struct =
{
   a: 7,
   b: 8,
   c: 9
};

randomise();
varname = choose("a", "b", "c");            // varname cannot be known at compile-time
varname_hash = variable_get_hash(varname);  // Get the hash of the variable name that varname currently holds

/// Step Event
// my_struct[$ varname] = x;                // Here, GameMaker needs to recalculate the hash from the name every step
struct_set_from_hash(my_struct, varname_hash, x);  // Here, the hash is used directly

/// Key Pressed Event - Space
varname = choose("a", "b", "c");            // Change to another random struct variable to access
varname_hash = variable_get_hash(varname);  // Update the hash accordingly!

Using the hash value directly effectively bypasses the calculation of the hash from the variable name. As long as varname remains unchanged, GameMaker won't have to recalculate the hash.
Note that when you do change varname, you also have to update the hash.