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
and get
these values. Checkboxes will hold bool values, textareas will hold string values, and sliders will hold real values.
Second, all elements will get clicked on in some way via a click
function. To know if a click happened on an element, they need defined bounding box coordinates (x
, y
, width
, and height
variables).
Third, certain elements require a listen
function. Text fields listen for keyboard presses and sliders listen for mouse movement. But only the most recently-clicked element should be listening. This is “focus” and it needs three helper functions: has_focus
, set_focus
, and 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
keyword after function definitions. Second, the static
keyword creates a variable that will maintain its value after being declared for the first time. Struct functions branded as static
won’t be re-created every time a new struct is created.
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
and draw
functions. That’s why we need a controller. Our controller will maintain a list of GUI elements and run their functions. The code is written below. At a glance, it looks overwhelming. But notice how similar the three struct functions are! We’re just iterating over the list of elements and calling their functions.
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
keyword.
/// Create Event
// create controller struct
control = new GUIElementController();
In the Step and Draw Events of Object1, the controller’s step
and draw
functions are called, respectively. We’re making good use of the fact an Object resource has always-running events.
/// Step Event
// update controller every gamestep
control.step();
/// Draw Event
// draw elements to screen
control.draw();
Before we forget, call the controller’s destroy
function in the Clean-Up Event. This frees the controller from memory when Object1 no longer exists. When a struct is no longer referenced, they’re automatically garbage collected. ds_lists aren’t, however. If we don’t destroy the controller’s elements
list (evoked in the destroy
function), we have a memory leak on our hands.
/// 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
structure so we can fill in some blanks.
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
is called, the controller’s element_in_focus
variable is set to the struct that called it. has_focus
returns a bool based on whether the element that called it matches the controller’s element_in_focus
variable. remove_focus
clears.
/// @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 handles clicking inside an element and listening for input if the element is in focus.
/// @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
and false
.
To write a Checkbox
struct that’s inherited from the GUIElement
parent struct, we use colons in the function definition.
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
) in the game room. Based on the fourth argument (checked
), a checkbox can be created as checked (true) or unchecked (false).
Thanks to its parent, the Checkbox
code is tiny. Checkboxes inherit all of GUIElement
’s variables and functions, except draw
and click
. Those two functions are overridden. Function overriding is perhaps one of the greatest advantages of using structs.
/// @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
to its value. Text fields also require listening for keyboard input. Every time a key is pressed, its value will be updated in the listen
function. And, of course, text fields are drawn differently than checkboxes.
/// @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!