Gurpreet S. Matharoo creates GameMaker courses and is known on YouTube as “GameMakerStation”. He has joined us on this blog to share tips on writing efficient code, using the new features in GameMaker Studio 2.3.0. Special thanks to Devon "Hyomoto" Mullane for help with the article.
Programming is hard. As beginner and intermediate level users, we are concerned with solving problems through code and making things work. However, what we and many tutorials often overlook is reusability. With some extra care put into creating abstract systems, we can make it easier to (1) reuse previously written systems, and (2) expand on them for additional functionality.
It’s easy to repeat yourself when you’re coding. You’ve probably found yourself needing a piece of code you’ve written previously, so you copy it and call it a day; however, reusing the same block of code everywhere means you can’t easily make changes to all of them, and have to copy it again to reuse it.
As your skills grow, you might throw that code into a script function. This is a huge improvement, as you can now reuse your code, and edit it all in one place. You might even start to pass in arguments that allow you to reuse this code in many contexts.
This is the basis of abstraction, but has its limitations: you are running the same piece of code again and again. This script might be useless in another project, because it's still tied to what the original project needed it to do. What we really want is to create systems to abstract away all basic functionality from your code, and to encapsulate variables and methods into an object to aid reusability.
Let’s start with a definition: What is Abstraction?
“Abstraction is the creation of abstract concept-objects by mirroring common features or attributes of various non-abstract objects or systems of study” - Wikipedia
What Abstraction means to me is the practice of creating systems that can be reused, instead of writing code for a single use case.
Let me give you a practical example. You’re working for a client, making a game for them from their blueprints, and you get a task that says:
Show the message “Game Active” on the screen when the player holds A
So you go and do this:
It’s working, problem solved! ...Or is it?
Solving a problem isn’t just about writing code that works; it’s also about creating systems that are reusable and modifiable.
The code above is very basic, so for any kind of additional functionality, we would need to add more lines to it. Then to reuse the same block, we’d have to copy it or make it into a function, essentially repeating it again and again.
So, how can we abstract the code given above? How can we break it down into its most basic concepts, its most essential inputs and outputs?
I would divide this functionality into two separate systems: Input and On-Screen Messages.
I want to create a system that registers all connected gamepads, and puts them into a list. So unlike the built-in
gamepad_ functions, the gamepad ID “0” would not refer to the first slot on the device, but the first gamepad actually connected.
This system would also allow us to define our inputs based on what they do in the game, as you can see in the “INPUT” enum below:
(Side-note: Gamepads are registered in the list using the Async System event)
This would abstract having to read a specific input device on a specific slot, and would allow me to mix up several input devices and have only one method (function) for reading input.
This method takes the device number and input type as parameters, and based on the input, reads buttons from the gamepad, keyboard and mouse. Note that the keyboard/mouse input is only read if the device is 0 -- meaning that they only affect the first player!
Secondly, I would create an On-Screen Messages system. This can be a manager object that has some properties, such as the font, its size, the margins around the GUI, etc. This way, we can encapsulate all relevant information within the object that needs it.
Each new message can be an instance of a separate object, which can have its own “timer” variable to make it disappear after a certain amount of time. You can build upon this object to make it as fancy as you want!
Then all you need for further abstraction is a method that creates such messages (and registers them with the manager), and you’re good to go!
To make it disappear, you can simply implement a timer functionality in the message object, as previously mentioned.
Our code that previously only used built-in functions, now looks like this:
(Notice how we don’t have to use the Draw event now! That is also abstracted into the On-Screen Messages object)
Our input function is now much more advanced, and allows us to get input without having to worry about the device. We specify which input we want (here, INPUT.ACTION) and it handles the device-specific inputs!
Without abstraction, if we wanted to expand on any of this or make it better, we would have had to modify this code and write some more lines. But now, because we are making use of our own systems, we can build on them without having to modify the usage of those systems.
And the best thing is that you can now reuse those systems, and implement similar features anywhere in your project, without having to rewrite similar code or rethink a solution. You can even use these systems in other projects, and potentially share them as libraries for the whole world to use!
Tip: Use GMS2’s Local Package feature to export your systems as standalone packages, which can be imported into other projects and shared on the marketplace!
The thing I would take away from this, is that you should strive to solve problems by creating systems that can be expanded and reused.
Now there are exceptions to this, such as when you’re making a prototype or a simple jam game -- you don’t want to waste time abstracting everything and creating systems, when you can build the features you want, to create a more effective prototype or jam game.
Abstraction can also be hard to achieve if a problem is particularly difficult to solve or involves a deadline. It’s okay to forget about systems and just code away to make something work, but remember, it’s also good to refactor your code to have more abstraction, especially if you want easier reusability!
You can discuss this topic further on our Discord server, and if there's anything that seems confusing, please do ask, as we love to help anyone that drops by!