Hi everyone!
In this blog post, we'll be looking at making a system to simulate code injection and approach programming for Game Maker in a more object-oriented kind of way, which is something that's been puzzling game developers ever since Game Maker has been released. I'll show you the system I use, it's advantages and it's limitations. The system is fully compatible with Game Maker 1.4 and Game Maker 2
Introduction
Let's begin by looking at a simple problem, and how using the system in question could solve it. Let's pretend we're making an RPG with a chest system. Let's pretend the chests have varying conditions that need to be fulfilled in order to open the chests. We could have chests that require a certain level of lock-picking, chests that can be forced open by raw strength, or chests that require a key resource. Given that these chests all give the same rewards (from a loot table or whatever), we would probably instinctively code something like this :
Which really isn't too bad.
Now if we were to ever change the types of rewards you can get from the chests, it would complicate things a little bit. If the reward could come from a loot table or be a specific predetermined item, we would need our parent object 'obj_chest' to handle all the different cases. At this point, we don't want to have a parent chest for every possible reward, because we would end up with "(possible rewards) X (possible conditions for opening a chest)" objects to deal with and that's the worst thing we could do.
So how would we make our 'obj_chest' handle all the different rewards? There all many ways to do this, but one of the possibilities is that it would end up looking something like this :
In this scenario, we increase the complexity of our code a little bit, but it still isn't too bad, and not that bad of a solution. It could get messy if we keep finding new reward types to add to our chests, but still.
But what if we play devil's advocate and we try to push things a little further. What if you suddenly want to make chest that requires both lock-picking skills and strength to open (Because the lid is very heavy...)? Are you going to add a child object 'obj_chest_lockpick_strength' and copy paste the code from your two previous chests into this one? That would work, but that's how we end up with code duplication and that's what we want to prevent as much as possible.
So what are your options? There are probably a couple of things you can do, but none that I ever found really satisfactory. That's why I came up with the system I'm about to show you.
If you still don't see how what I'm about to show you could be useful, I understand. The given example is a bit forced and the scale is very limited, but bear with me. There might be some information that when used on a big scale commercial game might help quite a bit.
The System - Intro
So let's get back to our example. The problem we're trying to solve is that we're basically trying to find a way to write code that would look something like this :
if (condition)
{
execute(action);
}
Where 'condition' and 'action' are variables that we set from an outside source. (Either in a controller like we might do when we spawn things randomly or within and object's creation code in the room editor or anywhere really.) This would prevent us from having to create a child object for each possible condition to open a chest and 'if' statements dealing with all the different possible rewards.
So how can we do that? For a while I was trying to come up with something like :
if (script_execute(condition))
{
script_execute(action);
}
But script_execute requires us to know exactly the number of arguments to pass to the script. The trick would be to encapsulate all of the information necessary to run a script with it's arguments within an object. The object could look something like this :
And that's it. If we write a script 'scr_execute' that expects an obj_callable with predefined variables holding all the information necessary to execute a script, we can have our code with 'condition' and 'action' do exactly what we want by writing this :
if (scr_execute(condition))
{
scr_execute(action);
}
As long as 'condition' and 'action' are instances of obj_callable with their variables set correctly.
Even better, we can have our 'scr_execute' check if the object it receives is an array or not and execute all of the 'obj_callable' in the array. You can also combine all the returned values for all the script executions and return the result. That way we can check for multiple conditions using only one call of 'scr_execute'.
In the example above, 'condition' could be an array and we could make the chest check for any combination of strength, lock-picking skill or keys.
An other example where this could be useful is for changing the behavior of an object in game. Let's say that in your RPG, you have 'npcs' and 'doors'. When you activate an 'npc', it pops up a dialog and when you activate the door, it opens it. Let's say there's this special case where, when the player activates a door, you want a dialog to pop up saying "I can't go there right now, I should go this other way". You already have an object 'npc' with that behavior so that's great. You could swap the 'door' with an 'npc', change it's sprite and set a couple of variables to make sure the 'door' won't start walking around. But it might be even better to just let it be a 'door' and change the 'action' you want to execute when you activate it. Saves you the trouble of changing the sprite and making the other object behave like a 'door'.
So let's get into the details of the system itself. All of the required to scripts and objects to make the system work are downloadable in a .gmz format at the end of the post, but first let's take a look at them.
First of all, creating and setting every variables of the 'obj_callable' in the image above before assigning it to an other variable would get tedious really fast. One way make the process a little bit more user friendly is to create a script to do it for us.
What I use is a script called 'new_call' that I use this way :
Which means that our single 'obj_chest_callable' that can handle all the different behaviors would look something like this :
A thing to lookout for when using something like this is that you sometimes have to handle the destruction of the 'obj_callable' objects being created. By default none of the 'obj_callable' are persistent, so they might all end up being freed regardless, but sometimes it's better to handle things manually.
Let's say we create a chest in a room and we want the chest to check for strength instead of the default behavior. We can create the chest in the room and assign a new 'obj_callable' to it. Since an 'obj_callable' is already assigned to 'condition' in the create event of the object, it would be better to destroy the object or change it's variables to fit our need. It could look something like this :
The System - Details
- 'scr_error' is a script used to pop up a 'show_message' with an error message and crash the game if one of the value passed to 'scr_execute' is not valid. To change that behavior, just change 'scr_error'.
- There are three objects that come in the .gmz package called 'obj_call_nop', obj_call_false' and 'obj_call_true' if you need an 'obj_callable' that does nothing, always returns 'false' or always returns 'true'.
- The different 'obj_callable' used in an array will always be evaluated with a logical 'AND' operator (if they're used in a condition).
- If you want to create an array of 'obj_callable' that is bigger than the actual number of instances you need, you can set the unused array values to -11. That will ensure, if it's evaluating a condition, that the unused array values don't make the script return false and that the unused array values don't crash the script.
The System - Limitations
There are a few limitations to the system. One of the most glaring one is the overhead used by the system. Creating an entire object for only a few variables takes up more memory and CPU than necessary. Realistically, the impact is still minimal. The impact on the CPU is even less noticeable if you used the YoYo Compiler.
An other limitation is that you ideally need to know the value of the argument you want to pass to the 'obj_callable' at creation time. An unsatisfactory workaround is to have the object using the 'obj_callable' constantly update the appropriate 'obj_callable.argv[X]' with the right value. This is not fun.
A pitfall that I tended to fall into at the beginning was to try to use 'obj_callable' all the time in every situation. Some things in games are pretty clear cut and don't require extensive modularity. Also, most problems can be solved alternatively quite easily. So in these situation, it is sometimes best to use the easiest path rather than to try to force an overly complex system for the solution. After all, there is a small overhead in user friendliness in using 'obj_callable' objects.
Conclusion - Just the beginning
What I am posting and explaining in this first post is the bare minimum of the system. The system I use in 900 and Infini has a bit more complexity to it with a few other systems built on top of that one. I will, in later posts, show the other layers to that system. These layers include delayed calls (like alarms, but a bit more simple to use), assigning argument values at execute time instead of creation time and assigning a variable to receive the return of the execution of an 'obj_callable'. I wish I could've showed more right away, but this post already ended up a lot longer than I previously anticipated.
I will, eventually, put the whole system on git when I'm done showcasing it completely. In the mean time you can download what I showed here :
DOWNLOAD
Little note about the .gmz (since Game Maker is abysmal when it comes to importing projects...), the best way to get the scripts and objects from the .gmz is to open it in a new project and then use "Add Existing Objects" and "Add Existing Scripts" from the project in which you want to include the files.
You can have fun with it and try to build some systems on top of it too. If you do you can post the results in the comments for everyone.
For any questions, insult or comments, please feel free to comment on this post, email me and/or follow me on twitter.
Thanks!
//Émeric Morin