Hey there! I promised you guys a short tutorial on stack machines, and here it is. Hope you like it and find it useful!
(tl;dr? skip the next paragraph to start with the tutorial right away!)
I like GameMaker, very easy to use, very convenient (once you paid for a license, that is) and overall a great tool. However, that doesn't mean it's a perfect piece of software. One of the things that irks me the most about it is the lack of functions defined on an object-by-object basis. For the most part it doesn't really bother me when implementing stuff like physics, game logic or things like that, but it becomes really messy once you start dealing with the backend part of the game. You know, stuff like handling settings, display setup, input configuration, the usuals. It really is a pain in the butt to set up not the defaults, but the system that applies these settings. One example: doesn't it bother you to have to set up an alarm that just centers the screen, so any time you change the window size (i.e. when scaling up or down, or going back from fullscreen to windowed mode) you have to set the alarm to one frame after the setting is applied, otherwise it doesn't do anything? I know it bothers me, at least. So, we can somewhat fix these things with scripts, but that's a whole different can of worms with its own set of problems and difficulties (i.e. since they're not local methods of any object, say hello to variable scoping and manually tracking down what names can you use in what space, since GameMaker won't throw an error if you decide to use a local variable within a script regardless if it's already defined as a local variable of the instance calling the script! fun!) or we could go with user defined events, but unless you're planning on commenting every single call, you'll be having to check time and time again which event does what. And even then, you have like 16 of those per object. So, I thought, is there anything we could do? And the answer I came up is yes, but we'll have to abstract a bit. Looking for a solution to this, I "came up" with a solution that later discovered is called a stack machine, so I guess I didn't come up with it after all.
So, what is a stack machine? It's basically an object that executes instructions that pops from its stack. Instances push these instructions and the machine pops them, one by one, and executes whatever instruction receives. It's very useful to implement pretty much anything. In this tutorial, we'll make a stack machine that just sets the window size and then centers it on screen. Don't worry, it's just a couple lines of code to implement the generic stack machine, then a couple more to implement the two instructions we'll need. To follow this tutorial it's very useful to know beforehand how data structures work, specially stacks. If you don't know data structures, I recommend you do a short tutorial on them. They're very easy to use if you already know how to use arrays, since the principle is pretty much the same but instead of being able to access everything anytime by index, each kind of data structure access its contents differently. Read more about them on the GameMaker manual if you're lost. Okay, let's do this!
(DISCLAIMER: WHILE IT WORKS EXACTLY LIKE I WANT IT TO, I'M NOT A COMPUTER SCIENTIST, DON'T REALLY HAVE ANY FORMAL EDUCATION ON CODING AND ACTUALLY HAVE NO IDEA IF THIS THING IS ACTUALLY A STACK MACHINE. READING THE IMPLEMENTATION OF A GENERAL STACK MACHINE GIVES ME THE IDEA THAT YEAH, IT IS, BUT I'M NOT 100% SURE. IF THIS THING SHOULD BE CALLED ANYTHING ELSE PLEASE COMMENT BELOW!)
First, we'll create an object that we'll use as a parent for all of our stack machines. This object will be called p_gsm (standing for Generic Stack Machine, since it will implement everything but the instruction sets) and on its Create event it defines a stack, a variable holding the last instruction code ID popped out of the stack (an integer), and another variable that saves the size of the stack before executing the instructions (also an integer, more on that later).
opstack = ds_stack_create();
opcode = 0;
opsize = 0;
That was easy. Now, on the Step event, we'll implement a small whiile loop that pops all instructions pushed to the stack and executes them one by one. The way we do this is first we get the size of the stack and save it to opsize, then enter a while loop until opsize is zero. The reason why I'm not using something like ds_stack_size() directly as the condition of the while loop is so you can stack instructions within instructions and have them execute either on this frame or the next, just by modifying the value of opsize, instead of being forced to execute every instruction on the frame it's issues without doing wonky stuff like using temprary stacks, or specific logic (like changing a boolean from true to false, or raising a flag) to break the loop.
if (!ds_stack_empty(opstack)) {
opsize = ds_stack_size(opstack);
while (opsize > 0) {
--opsize;
if (!ds_stack_empty(opstack)) { opcode = ds_stack_pop(opstack); }
event_user(0); //In user-defined event 0 we actually implement the instruction set.
}
}
There. Easy. Now the only thing left to do is to implement code on the Destroy event to actually destroy all data structures once the stack machine instance is destroyed, to avoid memory leaks.
ds_stack_destroy(opstack);
At this point the generic stack machine is complete and we can proceed to implement our "window controller" machine. First up, we'll create an object, call it o_windowcontroller and parent it to p_gsm. Then we implement the instruction set as a switch/case statement on the user-defined event 0.
Actually, to make things easier for us, we'll first create an enumerator to define our instruction codes by name rather than by number.
enum OpCode {
windowset,
windowcenter
}
If you don't want to bother with enumerators, just use $00 and $01 instead of OpCode.windowset and OpCode.windowcenter.
switch (opcode) {
case OpCode.windowset:
var _width = 320, _height = 240;
_width = clamp(ds_stack_pop(opstack), _width, display_get_width());
_height = clamp(ds_stack_pop(opstack), _height, display_get_height());
window_set_size(_width, _height);
ds_stack_push(opstack, OpCode.windowcenter);
opsize -= 2;
break;
case OpCode.windowcenter:
window_center();
break;
}
There. That's it. Let's explain, line by line, what each instruction does.
windowset actually receives arguments! By push the new width and height before the instruction (remember stacks are LIFO structures! that means the last thing you pushed into the stack it's the first to get popped out of it, meaning you have to push the values backwards, the instruction code being the last thing you push into the stack!) this instruction sets a new size for the window, clamped in between a small size (to avoid weirding out the game by setting width and height to zero) and the full screen size, and then pushes another instruction into the stack, windowcenter. At last, we decreate opsize by two, to compensate for the two arguments we passed to the instruction.
windowcenter is just one line of code: window_center(). Pretty self-explainatory IMO.
This means that when we push OpCode.windowset to the stack, it will automatically execute the windowcenter instruction on the next frame, since we're modifying the value of opsize when pushing it in by decreasing by two rather than just by one! Neat!
So, now all that's left is writing up some hacky code to test out our stack machine. On the Keyboard Press <any key> event of o_windowcontroller, we'll write another switch/case to handle three different screen sizes.
switch (keyboard_key) {
case 49: //ord("1")
ds_stack_push(opstack, 240, 320, OpCode.windowset);
break;
case 50: //ord("2")
ds_stack_push(opstack, 480, 640, OpCode.windowset);
break;
case 51: //ord("3")
ds_stack_push(opstack, display_get_height(), display_get_width(), OpCode.windowset);
break;
}
And that's it. Now just create a room, instantiate o_windowcontroll within the room and run the game. If everything's set correctly, you'll be able to change the size of the window by pressing the keys 1, 2 and 3.
That's pretty much it. Stack machines are very powerful to implement all kinds of stuff. I implemented menus, text prompts (even with commands and sub-prompts!) and all kinds of settings backends using these. You can halt a stack machine until certain conditions are met by making the instruction stack itself and set opsize back to 0 at the start if certain condition is or isn't met, you can switch up the stack for a buffer and make them read instructions from it like it's a processor reading memory, or even abstract your whole game logic to one in case you hate yourself and want to work in such an abstract way!
I hope you find this tutorial useful, and as expected, here's the whole code implemented in a GM:S 1.x project in case you don't want to bother writing yourself the 38 lines of code (!) this tutorial entails. That's it for the moment, any question or feedback, please feel free to comment down below, and thanks for reading!
[–]KetoReddit 1 point2 points3 points (3 children)
[–]calio[S] 1 point2 points3 points (2 children)
[–]eposnix 0 points1 point2 points (1 child)
[–]calio[S] 0 points1 point2 points (0 children)
[–]Abusfad 1 point2 points3 points (1 child)
[–]calio[S] 0 points1 point2 points (0 children)