Coding GUI Elements Using Structs
Hi, I’m Zack Banack, developer of the Shampoo framework. Shampoo changes the GUI-development workflow familiar to many GameMakers. You can use Shampoo to create interfaces in real-time using markup language. If you’ve written HTML or bbcode before, the syntax should feel natural!
My knowledge of GUI programming is why I’m brought onto the blog. Here, I’ll explain how I like to code a variety of GUI elements with structs in GameMaker Studio 2.3.0.
Using a combination of this post and the YYZ finished project linked below, you’ll have all these GUI elements at your disposal:
- Buttons
- Checkboxes
- Dropdown menus
- Range sliders
- Single-line text fields
- Grouped radio buttons
GUI Elements GML Project Download
Once you have it downloaded, open GameMaker Studio and select "Import". Then, browse to the downloaded YYZ and select it.
WHY USE STRUCTS?
Take a look at the Resource Tree and notice how there’s only one object. “But Zack,” you may ask, “how can we have all these different elements and only one object”? My friend, that’s the power of structs!
In simplest terms, a struct is a variable that holds other variables and functions. They’re lightweight versions of the Object resource. I like how versatile they are. Structs help me write reusable code.
If I ask you to compare two GUI elements—a checkbox and a text field—it might seem like comparing apples to oranges. Checkboxes you click to toggle on and off. Text fields you type stuff into. And yes, that’s true at face value. But translating these arbitrary elements into code reveals a lot of overlap. Structs take advantage of this overlap.
THE ELEMENT STRUCTURE
Let’s brainstorm a one-size-fits-all struct that all elements can fit into.
Well first off, most elements will hold some kind of value. We need functions to
set
get
Second, all elements will get clicked on in some way via a
click
x
y
width
height
Third, certain elements require a
listen
has_focus
set_focus
remove_focus
Taking all that and converting it to code, we get the following blueprint parent struct that will be inherited by all element types:
function GUIElement() constructor { // int, string, or bool based on type value = undefined; name = undefined; // unique name // dimensions static width = 200; static height = 32; static padding = 16; // focus-related static has_focus = function() { } static set_focus = function() { } static remove_focus = function() { } // value setter and getter static get = function() { return value; } static set = function(_value) { value = _value; } // interaction static step = function() { } static click = function() { set_focus(); } static listen = function() { } // drawing static draw = function() { } }
Some quick notes about the syntax. First, to declare structs, GameMaker uses the
constructor
static
static
THE CONTROLLER STRUCTURE
Unlike Objects, structs don’t have Step and Draw events that automatically run every gamestep. We need a way to trigger the GUI element’s
step
draw
function GUIElementController() constructor { // make this struct a global variable so all elements can reference easily global.__ElementController = self; // list of all GUI elements elements = ds_list_create(); // the GUI element struct in focus currently element_in_focus = undefined; // prevents click-throughs on overlapping elements can_click = true; /// @function step() static step = function() { if (mouse_check_button_pressed(mb_left)) element_in_focus = undefined; can_click = true; // call `step` function in all elements var count = ds_list_size(elements); for(var i = 0; i < count; i++) elements[| i].step(); } /// @function draw() static draw = function() { draw_set_halign(fa_left); draw_set_valign(fa_middle); draw_set_color(c_white); // call `draw` function on all elements in reverse-creation order for(var i = ds_list_size(elements)-1; i>=0; i--) elements[| i].draw(); } /// @function destroy() static destroy = function() { // free all elements from memory for(var i = ds_list_size(elements)-1; i>=0; i--) elements[| i].destroy(); ds_list_destroy(elements); // remove global reference delete global.__ElementController; global.__ElementController = undefined; } }
Next, create an instance of the controller. That’s done in the Create Event of Object1 using the
new
/// Create Event // create controller struct control = new GUIElementController();
In the Step and Draw Events of Object1, the controller’s
step
draw
/// Step Event // update controller every gamestep control.step(); /// Draw Event // draw elements to screen control.draw();
Before we forget, call the controller’s
destroy
elements
destroy
/// Clean Up Event // free controller (and elements) from memory control.destroy(); delete control; // delete reference to struct
FILLING IN THE GAPS
Now that we know the type of controller we’re working with, head back to the
GUIElement
Let’s start with adding and removing an element from the controller’s list of elements. Inserting the following code into the structure will do the trick:
// a reference to the controller controller = global.__ElementController; // add to controller's list of elements ds_list_add(controller.elements, self); /// @function destroy() static destroy = function() { // remove from controller's list of elements ds_list_delete(controller.elements, ds_list_find_index(controller.elements, self) ); }
The focus-related scripts are light. When
set_focus
element_in_focus
has_focus
element_in_focus
remove_focus
/// @function has_focus() static has_focus = function() { return controller.element_in_focus == self; } /// @function set_focus() static set_focus = function() { controller.element_in_focus = self; } /// @function remove_focus() static remove_focus = function() { controller.element_in_focus = undefined; }
Finally, the fleshed-out
step
/// @function step() static step = function() { // check for mouse click inside bounding box AND ensure no click already happened this gamestep if (mouse_check_button_pressed(mb_left) && controller.can_click && point_in_rectangle(mouse_x, mouse_y, x, y, x + width, y + height)) { // tell controller we clicked on an input this step controller.can_click = false; click(); } // if the element has focus, listen for input if (has_focus()) listen(); }
Sweet! Let’s get to creating the actual elements.
CHECKBOXES
Checkboxes sound like a good element to start with. They don’t require “listening” and its value will only ever toggle between
true
false
To write a
Checkbox
GUIElement
function Checkbox() : GUIElement() constructor
To give ourselves more control over checkboxes, the function should accept a few arguments.
function Checkbox(_name, _x, _y, _checked) : GUIElement() constructor
These arguments will allow each instance of the structure to have a unique name and position (
x
y
checked
Thanks to its parent, the
Checkbox
GUIElement
draw
click
/// @function Checkbox(string:name, real:x, real:y, bool:checked) function Checkbox(_name, _x, _y, _checked) : GUIElement() constructor { // passed-in vars x = _x; y = _y; name = _name; /// @function click() static click = function() { set_focus(); set(!get()); show_debug_message("You " + (get() ? "checked" : "unchecked") + " the Checkbox named `" + string(name) + "`!"); } /// @function draw() static draw = function() { draw_rectangle(x, y, x + height, y + height, !get()); // box draw_text(x + height + padding, y + (height * 0.5), name); // name } // set value set(_checked); }
Time to see checkboxes in action! In the Create Event of Object1, let’s add four checkboxes:
/// Create Event // create controller struct control = new GUIElementController(); // create checkboxes checkbox1 = new Checkbox("Checkbox A", 16, 16, false); checkbox2 = new Checkbox("Checkbox B", 16, 64, true); checkbox3 = new Checkbox("Checkbox C", 16, 112, true); checkbox4 = new Checkbox("Checkbox D", 16, 160, true);
Run the game, and click on the checkboxes! Take a look at the console output and you should see information about the element you interacted with.
Next, let’s tackle text fields.
TEXTFIELDS
Textfield constructor syntax is eerily similar to that of the Checkbox. The only difference is the fourth argument, which will be a string instead of a bool.
function Textfield(_name, _x, _y, _value) : GUIElement() constructor
Its inner-code also bears a striking resemblance to Checkbox’s. Again, we’re overriding parent functions with type-specific code. When a text field is clicked, we set the
keyboard_string
listen
/// @function Textfield(string:name, real:x, real:y, string:value) function Textfield(_name, _x, _y, _value) : GUIElement() constructor { // passed-in vars name = _name; x = _x; y = _y; /// @function set(string:str) static set = function(str) { // value hasn't changed; quit if (value == str) return; value = str; show_debug_message("You set the Textfield named `" + string(name) + "` to the value `" + string(value) + "`"); } /// @function click() static click = function() { set_focus(); keyboard_string = get(); } /// @function listen() static listen = function() { set(keyboard_string); if (keyboard_check_pressed(vk_enter)) remove_focus(); } /// @function draw() static draw = function() { draw_set_alpha(has_focus() ? 1 : 0.5); // bounding box draw_rectangle(x, y, x + width, y + height, true); // draw input text draw_text(x + padding, y + (height * 0.5), get()); draw_set_alpha(1); } // set value set(_value); }
Add a few text fields to the Create Event of Object1 and get typing!
// create textfields textfield1 = new Textfield("Textfield A", 600, 16, "Hello!"); textfield2 = new Textfield("Textfield B", 600, 64, ""); textfield3 = new Textfield("Textfield C", 600, 112, "");
SUMMARY
I’d like to keep this tutorial short, so buttons, range sliders, dropdown menus, and radio buttons structs are available in the source code linked in the beginning. If you’ve made it this far, the rest should read naturally. The project has some edge cases taken care of. It also contains a prettier version of text fields with overflow fix, placeholder strings, and a flashing typing indicator.
Hopefully, this tutorial has given you a better understanding of:
- GameMaker structs, their use cases, and how they can work in conjunction with objects
- Writing reusable and flexible code
- Using a parent to process and render its children
If you’re interested in GUI creation, consider checking out Shampoo. :)
Thanks for reading!