Coding GUI Elements Using Structs


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.

GUI GIF

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!



Written by Ross Manthorp

Ross Manthorp handles all things community at the GameMaker team. When he’s not pulling the strings from behind the scenes he’s enjoying Nintendo games, indie games, and getting emotional over cartoons and comics.