Nobody’s perfect, right? Judi Dench has starred in bad films, Michael Jordan missed a basket or two, and Hideo Kojima couldn’t come up with a sensible name for a character if his life depended on it.Whether you’re a complete novice or a grizzled veteran, there’s always room for improvement, and coding with GameMaker is no exception.
This article includes
- Coding style
- Use local variables
- Data structures
- Structs and constructors
- Texture swaps and vertex batches
Before we begin...
The suggestions we’re about to make are rule-of-thumb best practices that can help you organise and optimise your game’s code, but there’s never a hard-and-fast right or wrong answer.
It all comes down to what makes your life easier and your projects more manageable, so see these tips as guides, rather than instructions.
It’s also important to remember that you shouldn’t try and squeeze a few extra FPS out of your game if it’s already running perfectly well. What you learn here may be more useful for your next project than your current one
With the disclaimers out of the way, let’s get going, shall we?
Level Up Your GML Code, GameMaker Coaching
When it comes to writing code, everyone has a style.
The way you place your brackets, how you indent the lines, and how you declare and name variables are all examples of the stylistic differences between coders.
It’s essential to make your code clear and readable to other people, and to your future self. You don’t want to come back from a two-week holiday in the Bahamas and wonder what on Earth this clump of coding means.
There are many programming styles to take inspiration from, but almost any style is fine as long as it’s consistent and clear.
#endregion to section off parts of your code manually. You can comment on regions, which you can read more about in the GameMaker Manual.
These things improve your code’s readability, but don't affect its usage. GameMaker strips out comments, removes unnecessary line breaks and whitespace, substitutes in constant/macro/enum values, and compresses your code when it compiles your game.
Use local variables
Continuing on from the above point about programming style, one thing that a lot of beginners do is to cram as much into one line of code as possible. For example:
draw_sprite(sprite_index, image_index, x + lengthdir_x(100, point_direction(x, y, mouse_x, mouse_y)), y + lengthdir_y(100, point_direction(x, y, mouse_x, mouse_y)));
While not completely unreadable, it is inefficient (the
point_direction() function is called twice, for example) and it is messy and awkward to look at. It would be far better expressed as:
var p_dir = point_direction(x, y, mouse_x, mouse_y);
var local_x = x + lengthdir_x(100, p_dir);
var local_y = y + lengthdir_y(100, p_dir);
draw_sprite(sprite_index, image_index, local_x, local_y);
The memory and resources required to create those local variables are negligible, and greatly improve the clarity of your code. It’s a no-brainer, really.
The same idea should be applied to functions too, where you should assign sensible names to arguments, and use clear formatting and local variables where required to make it as readable as possible.
Make your code cleaner with local variables
Local variables are fast to process in-game, so make the most of them. If the same function call or calculation appears in a code block two or more times, think about creating a local variable for it.
instance variables multiple times in a script or code block, assign them to a local variable at the start of your code and reference that local variable for greater performance.
Arrays are easy to use and require less memory than data structures, but they can still be further optimised.
When you create an array, memory is allocated to it based on its size. You should always try to initialise an array to its maximum size first, even if you don't plan on filling it until later on.
For example, if you know you need an array to hold a maximum of 100 values, you should initialise it to 100 slots straight away, using the
array = array_create(100, 0);
This allocates the memory in one "chunk", with all array values set to the default value of 0 to help keep things fast. If you don’t do it this way, every time you add a new value to the array, the entire memory has to be reallocated again.
You can free up the memory associated with an array by setting its variable to some other value, like
undefined. This way the reference to that array is lost, and if it was the last reference, the Garbage Collector will free its memory.
array = undefined;
Current GameMaker data structures run much faster than in previous GameMaker versions.
They still need to be destroyed when not used to free memory, and they can still be slower than arrays, but their simplicity and extra functions for dealing with the data they contain often outweigh the minimal speed difference, so don't be afraid to use them in your games.
It should be noted that of all the data structures, DS Maps in particular are lightning-fast, both to read from and write to, making them an excellent option for all types of tasks.
We’ve already mentioned accessors for arrays, but they’re also available for data structures, which can help clean up your code and make it much easier to read.
Structs and constructorsA struct allows you to store multiple variables inside it. Such variables can also hold functions (known as methods), making structs a powerful way to store multiple, related properties in one place, with functions for interacting with those properties.
new() to create a new struct from that template, which will have all of the defined variables and functions, ready to be read and modified.
There are multiple ways to deal with collisions in GameMaker, and most of them come with a bit of extra CPU overhead attached.
place_ functions, and
instance_ functions all rely on bounding box checks with all instances of a given type within the room, and there is little in the way of optimisation built into the engine for this.
It gets worse if you start to use precise collisions, as not only will you be doing bounding box checks, but you will also be doing per-pixel checks, which is very slow indeed. We have an older tech blog on this which is well worth a read:
That’s not to say that you shouldn't use these functions, as they can be very handy. However, you should know which ones to use and when, as they all work slightly differently and will have different speeds.
Rough rule of thumb is that the
place_ functions are faster than the
instance_ functions, which are faster than the
point_ functions, so read the manual and make sure to choose the most appropriate for every situation.
Alternatively, look into creating a tile-based collision system, which can be created using the tilemap functions or even a custom 2D array or DS Grid.
These will be very fast and will help boost your game's speed. However, if you are using irregular terrain or walls and objects that don't align to a grid they may not be appropriate.
You can find a very simple video tutorial on tilemap collisions from Shaun Spalding:
GameMaker Studio 2: Tile Collisions
Texture swaps and vertex batches
If you switch on the debug overlay in GameMaker, you’ll see that there are two figures in brackets at the top of your screen when testing. The first is the number of texture swaps being performed, and the second is the number of vertex batches.
A number of factors will influence these figures and you’ll never get them down to (0) (0), as the engine requires one or two of each every step, but you should aim to get these values down as low as possible.
For texture swaps, the best and most efficient way to do this is to optimise how your sprites and backgrounds are stored on the texture page. This is done from the sprite properties, and you can create texture pages in the Texture Group Editor.
For example, if you have a number of images that are only used in the main menu, put them together on a separate texture page. The same applies to level-specific images, the player and the enemies, etc.
Basically, you want to group them by use so that the swaps are reduced as much as possible. Additionally, to help keep VRAM optimised, you can use the different prefetch and flush to load and remove textures from memory as required.
NOTE: As mentioned at the start of this article, if your game runs fine with a decent FPS then don't worry too much about texture swaps...especially when creating projects on the desktop targets.
These optimisations are best when used with large games or on low-end mobile devices, and when used incorrectly can they actually negatively impact performance.
Vertex batchesVertex information is sent in "batches" to the GPU for drawing, and in general the larger the batch the better. You should avoid “breaking" the batch when drawing, as that will increase the number of vertex batches sent to the GPU.
- Blend modes
- Setting the draw colour
- Setting the draw alpha
- Drawing the built-in shapes and primitives
So, if you have a number of bullet instances that draw using the
bm_add blend mode, for example, you’ll be creating a new vertex batch for each one, which is definitely a bad thing.
Instead, have a controller object in your game that draws all the bullets, like this:
NOTE: This doesn't just apply to using
bm_add- any blending change will break the batch and will give a performance hit .
In this way, all the bullets will be drawn in the same batch. This method can be applied to alpha and draw colour too.
In fact judicious use of the functions
gpu_set_alphatestenable() can greatly improve performance and can be enabled/disabled as required throughout your project code, although they may not be appropriate for all types of graphics or projects.
NOTE: If you don't require the instance to draw itself when using a controller in this way then you can simply add a comment into the Draw Event to suppress the default drawing, or make the instance invisible (although this will prevent all code running for the instance in all draw events).
Another way to reduce these numbers is to disable the Separate Texture Page option for sprites unless absolutely necessary.
Each image stored in this way is sent to its own texture page and batched differently, so having these images on a regular texture page is better. You can then get the UVS coordinates using the
sprite_get_uvs() function and store them in variables for use later.
It may be a small amount of extra code, but the boost you will get is worth it. Note that this method will not permit texture repeats.
As with all these coding tips, if it makes your life more difficult to change it and your game runs fine, don't worry about it too much. You can always implement some of the ideas into your next project.
Particles offer a very quick and efficient way to create dynamic effects in your game.
However, it's worth noting that using additive blending, alpha blending and colour blending on the particles may lower your game’s performance, especially on mobile targets, so if you don't need it, don't use it.
Additive blending in particular can greatly increase your vertex batches and should be used with care.
On the non-WebGL HTML5 target, having multi-coloured, fading particles will require a lot of image caching and will be very slow.
However, since particle sprites can be animated, you can create an animated sprite that has sub-images which change colour and use that for the particle instead. It’ll still look like a gradual colour change, but not require the constant creation of cache images.
You can find out more about particles from the tech blog Guide To GameMaker Particles.
Ever since the 2.2.3 Beta update, GameMaker has allowed you to switch surfaces on and off the depth buffer, which can significantly optimise your game.
When you just use surfaces as normal, GameMaker will actually create a surface and an accompanying depth buffer to ensure proper depth sorting when drawing anything in 3D.
However, for most 2D games, this extra depth buffer simply isn’t needed, so it takes up extra memory space and processing time. This is where the function
surface_depth_disable() comes into play...
This function can be called before you create a surface to disable the depth buffer generation, and all surfaces created afterwards won’t have a depth buffer created for them.
You can enable/disable this function as required, and can even call it once at the start of the game to disable depth buffers for all subsequent surface calls (in most 2D games, this is fine).
It won't always provide a major performance boost, but it's certainly something to keep in mind if your game relies heavily on surfaces.
BONUS TIPSLet’s wrap things up with a little Brucie Bonus. Here are a couple more general coding tips that can help improve your productivity:
Don't be afraid to use the trigonometric functions. Contrary to popular belief, they’re pretty fast, especially when compared to particles, collisions, strings, etc.
Don't put code that isn’t for drawing things in the Draw events
Use Alarms or Time Sources to run any code that doesn't have to be called every step, instead of just adding it into the Step event anyway
As we’ve mentioned a few times now, all of these optimisations are optional and if your game runs fine with 60 vertex batches, 80 texture swaps, and additive blending, don't worry about it too much.
The important thing is creating a system that optimises your workflow, the accessibility of your code, and the stability of your game.