Creating User Interfaces with GameMaker’s Flex Panel Functions

cover

GameMaker 2024.800 beta has added flex panel functionality, as runtime backbone for a UI editor in the future.

A flex panel is a rectangular container that can contain other flex panels. Each flex panel has properties for its layout and the layouts of its children.

flexpanel_example1.png

Using this system, calculating UI layout values becomes trivial. The new runtime functionality is only a first step towards an improved UI system in GameMaker, and functions as a wrapper for Yoga Layout (as such there is no rendering support with this).

Once a UI editor is added in a future release, our hope is that you won’t need to use these runtime functions, unless you want to modify specific properties of your UI layers at runtime.

However, if you wish to understand how these functions work, perhaps to start using them in your projects right away, this blog will take you through my journey of turning the mockup below into an interface in GM.

image2.png
Each element you see in this image will be its own flex panel. Here is my process for this from start to finish:

  1. Figure out layout properties (size, alignments, etc.) by using a real-time editor
  2. Convert the layout to GML and generate layout data
  3. Use the layout data (i.e. position and dimensions) to create object instances for each element
  4. Have the object instances draw images and text

Figuring Out The Layout

First, I needed to figure out the properties of each flex panel before I generated the panels in GML. I used Yoga layout’s online editor to build this UI.

Using their documentation, I designed the interface and learned what each property does. This is the code I used in the online editor to generate my layout:

<Layout config={{useWebDefaults: false}}>
  <Node style={{width: 400, height: 240, padding: 0, alignItems:'center', justifyContent: 'center'}}>
    <Node style={{width: 140, height: 60, left: 10, bottom: 10, position: 'absolute', flex: 1, padding: 5, flexDirection: 'row', gap: 5}}>
      <Node style={{aspectRatio: 1, flexDirection: 'column', justifyContent: 'flex-end'}}>
        <Node style={{height: 14}}/>
        <Node style={{height: 14}}/>
      </Node>
      
      <Node style={{width: 'auto', flexGrow: 1, flexDirection: 'column', justifyContent: 'flex-end', gap: 5}}>
        <Node style={{height: 10}}/>
        <Node style={{height: 10}}/>
        <Node style={{height: 10}}/>
      </Node>
    </Node>
    
    
    
    <Node style={{width: 140, height: 30, right: 10, bottom: 10, position: 'absolute', flexDirection: 'row', gap: 5, padding: 5}}>
     <Node style={{aspectRatio: 1}}/>
     <Node style={{aspectRatio: 1}}/>
     <Node style={{aspectRatio: 1}}/>
     <Node style={{aspectRatio: 1}}/>
     <Node style={{aspectRatio: 1}}/>
     <Node style={{aspectRatio: 1}}/>
    </Node>
    
    
    
    <Node style={{width: 140, height: 30, top: 10, position: 'absolute', flexDirection: 'row', justifyContent: 'space-between'}}>
     <Node style={{aspectRatio: 1}}/>
     <Node style={{aspectRatio: 1}}/>
    </Node>
    
    
    
    <Node style={{width: '30%', height: '40%', padding: 5, gap: 5, flexDirection: 'column'}}>
      <Node style={{height: 20, flexDirection: 'row', gap: 5, justifyContent: 'space-between'}}>
        <Node style={{aspectRatio: 1}}/>
        <Node style={{flexGrow: 1}}/>
        <Node style={{width: 30}}/>
      </Node>
      <Node style={{height: 20, flexDirection: 'row', gap: 5, justifyContent: 'space-between'}}>
        <Node style={{aspectRatio: 1}}/>
        <Node style={{flexGrow: 1}}/>
        <Node style={{width: 30}}/>
      </Node>
      <Node style={{height: 20, flexDirection: 'row', gap: 5, justifyContent: 'space-between'}}>
        <Node style={{aspectRatio: 1}}/>
        <Node style={{flexGrow: 1}}/>
        <Node style={{width: 30}}/>
      </Node>
    </Node>
    
  </Node>
</Layout>

NOTEGameMaker’s debug overlay also has a similar testing editor, which takes JSON to build layouts in real time. All members you can use in the JSON are documented here. You can then pass this JSON directly into a flexpanel_create_node() call to create your entire layout structure in GML with one call, which makes this process faster. However I have used the Yoga code example in this blog instead as it takes up less lines.

I’ll explain this in short (feel free to skip this explanation and jump straight ahead to the GML implementation):

  1. The root node is a 400x240 canvas - no particular reason for this besides it being the largest canvas size that would fit on my screen with the editor. It has the alignItems and justifyContent properties set to ‘center’, for the panels that appear at the top and in the centre.
  2. The first child node in the root is the weapons and health view in the bottom left. It’s placed in the bottom left using the left and bottom inset properties. Its position is ‘absolute’ so it does not move within the canvas container, and sticks to its corner.
    1. This node contains the weapon view container, which itself contains two nodes of 14 height, to display information about the weapon. The container has its flexDirection set to ‘column’ so items are laid out vertically, and justifyContent to ‘flex-end’ so items begin from the bottom rather than the top.
    2. The node then contains a container for three health bars, each having a height of 10 pixels. Note that when moving to GML, I’ll actually replace all these hard-coded pixel values with percentage values so these elements scale with the resolution. This container has flexGrow set to 1 so it takes up the remaining space in its parent container. It has a gap of 5 which defines the distance between each child element (health bar).
  3. The second child of the root canvas is the inventory you see in the bottom-right. This container has a flexDirection of ‘row’ meaning its child nodes are laid out horizontally. The child nodes have their aspectRatio values set to 1 so they maintain a square layout.
  4. The third child of the root canvas is the score display at the top. This only has a top value of 10 which makes it stick to the top edge of its parent container, and the alignItems value of its parent allows it to stick to the center horizontally, as no left or right value is provided.
    1. It contains two children with square shapes. Because of the justifyContent: ‘space-between’ value of its parent, each square shape is placed on either side of the container, maintaining space between the nodes.
  5. The final child of the root canvas is the list window you see in the center. This has a flexDirection of ‘column’ so its child nodes are laid out vertically.
    1. This contains a child container which is a ‘list item’ that you see in the window. This container has a flexDirection of ‘row’ so items are laid out horizontally, justified as ‘space-between’.
      1. This container has three children. The first is a square. The second is flexible and takes up the remaining space in the container. The last one maintains a width of 30 pixels.
    2. The other two child containers are just copies of the first one, to ‘populate’ the list.

This results in the layout shown below, as rendered by the preview in Yoga’s online editor:

image1.png

Note the corner rounding is not a property of any of the layouts and is only part of the Yoga editor’s preview (Yoga cannot support corner rounding, as it does not include any rendering support!).

Creating The Layout in GML

After creating a project in GameMaker, I created an object (let’s call it obj_flexpanel) to initialise the flex panels and handle creating UI elements. I also created an object obj_ui_element to represent each node in the tree.

In the Create event of obj_flexpanel, I used the flex panel GML functions to create nodes.

As mentioned before, you can set up a JSON structure which you can test directly in the Debug Overlay. You can pass that JSON, or a struct version of that same structure, into flexpanel_create_node() to create your entire layout tree at once, optionally after testing it in the debug overlay.

I converted my Yoga XML code into JSON, which includes information about the root nodes and about all of its children, which I tested with the Debug Overlay tester first:

image4.png

I could now create my entire layout tree in GML with one call, by passing this same JSON into flexpanel_create_node():

NOTE: Here I have changed the root’s width and height to “100%” so it fits whatever resolution I choose when calculating layouts later.

n_root = flexpanel_create_node(@'{
	"name":"root", "data":{}, "width":"100%", "height":"100%", "justifyContent":"center", "alignItems":"center",
	"nodes":[
		{
			"name":"weap", "data":{}, "width":"30%", "positionType":"absolute", "height":"20%", "flexDirection":"row", "bottom":10,
			"nodes":[
				{
					"name": "weap1", "data":{}, "aspectRatio":1, "gap":10, "alignSelf":"auto", "justifyContent":"flex-end",
					"nodes":[
						{
							"name": "weap_info1", "data":{}, "height":"20%", "alignSelf":"auto"
						},
						{
							"name": "weap_info2", "data":{}, "height":"20%", "alignSelf":"auto"
						}
					],
				},
				{
					"name": "weap2", "data":{}, "gap":10, "alignSelf":"auto", "flexGrow":1, "justifyContent":"flex-end",
					"nodes":[
						{
							"name": "weap_bar_magic", "data":{}, "height":"20%", "alignSelf":"auto"
						},
						{
							"name": "weap_bar_stamina", "data":{}, "height":"20%", "alignSelf":"auto"
						},
						{
							"name": "weap_bar_health", "data":{}, "height":"20%", "alignSelf":"auto"
						}
					],
				}
			],
			"gap":10, "padding":10, "left":10, "alignSelf":"auto"
		},
		{
			"name": "inv", "data":{}, "width":"40%", "positionType":"absolute", "height":"10%", "flexDirection":"row", "right":10, "bottom":10,
			"nodes":[
				{
					"name": "inv_slot", "data":{"num": 0}, "aspectRatio":1, "alignSelf":"auto"
				},
				{
					"name": "inv_slot", "data":{"num": 1}, "aspectRatio":1, "alignSelf":"auto"
				},
				{
					"name": "inv_slot", "data":{"num": 2}, "aspectRatio":1, "alignSelf":"auto"
				},
				{
					"name": "inv_slot", "data":{"num": 3}, "aspectRatio":1, "alignSelf":"auto"
				},
				{
					"name": "inv_slot", "data":{"num": 4}, "aspectRatio":1, "alignSelf":"auto"
				},
				{
					"name": "inv_slot", "data":{"num": 5}, "aspectRatio":1, "alignSelf":"auto"
				}
			],
			"gap":10, "padding":10, "alignSelf":"auto"
		},
		{
			"name": "score", "data":{}, "width":"40%", "positionType":"absolute", "height":"10%", "top":10, "flexDirection":"row", "justifyContent":"space-between", "alignSelf":"auto",
			"nodes":[
				{
					"name": "score1", "data":{}, "aspectRatio":1, "alignSelf":"auto"
				},
				{
					"name": "score2", "data":{}, "aspectRatio":1, "alignSelf":"auto"
				}
			],
		},
		{
			"name": "play", "data":{}, "width":"40%", "height":"50%",
			"nodes":[
				{
					"name": "play1", "data":{}, "gap":10, "height":30, "alignSelf":"auto", "flexDirection":"row",
					"nodes":[
						{ "name": "play_icon", "data":{}, "aspectRatio":1, "alignSelf":"auto" },
						{ "name": "play_name", "data":{}, "alignSelf":"auto", "flexGrow":1 },
						{ "name": "play_ping", "data":{}, "width":30, "alignSelf":"auto" }
					],
				},
				{
					"name": "play2", "data":{}, "gap":10, "height":30, "alignSelf":"auto", "flexDirection":"row",
					"nodes":[
						{ "name": "play_icon", "data":{}, "aspectRatio":1, "alignSelf":"auto" },
						{ "name": "play_name", "data":{}, "alignSelf":"auto", "flexGrow":1 },
						{ "name": "play_ping", "data":{}, "width":30, "alignSelf":"auto" }
					],
				},
				{
					"name": "play3", "data":{}, "gap":10, "height":30, "alignSelf":"auto", "flexDirection":"row",
					"nodes":[
						{ "name": "play_icon", "data":{}, "aspectRatio":1, "alignSelf":"auto" },
						{ "name": "play_name", "data":{}, "alignSelf":"auto", "flexGrow":1 },
						{ "name": "play_ping", "data":{}, "width":30, "alignSelf":"auto" }
					],
				},
				{
					"name": "play4", "data":{}, "gap":10, "height":30, "alignSelf":"auto", "flexDirection":"row",
					"nodes":[
						{ "name": "play_icon", "data":{}, "aspectRatio":1, "alignSelf":"auto" },
						{ "name": "play_name", "data":{}, "alignSelf":"auto", "flexGrow":1 },
						{ "name": "play_ping", "data":{}, "width":30, "alignSelf":"auto" }
					],
				},
				{
					"name": "play5", "data":{}, "gap":10, "height":30, "alignSelf":"auto", "flexDirection":"row",
					"nodes":[
						{ "name": "play_icon", "data":{}, "aspectRatio":1, "alignSelf":"auto" },
						{ "name": "play_name", "data":{}, "alignSelf":"auto", "flexGrow":1 },
						{ "name": "play_ping", "data":{}, "width":30, "alignSelf":"auto" }
					],
				},
				{
					"name": "play6", "data":{}, "gap":10, "height":30, "alignSelf":"auto", "flexDirection":"row",
					"nodes":[
						{ "name": "play_icon", "data":{}, "aspectRatio":1, "alignSelf":"auto" },
						{ "name": "play_name", "data":{}, "alignSelf":"auto", "flexGrow":1 },
						{ "name": "play_ping", "data":{}, "width":30, "alignSelf":"auto" }
					],
				},
				{
					"name": "play7", "data":{}, "gap":10, "height":30, "alignSelf":"auto", "flexDirection":"row",
					"nodes":[
						{ "name": "play_icon", "data":{}, "aspectRatio":1, "alignSelf":"auto" },
						{ "name": "play_name", "data":{}, "alignSelf":"auto", "flexGrow":1 },
						{ "name": "play_ping", "data":{}, "width":30, "alignSelf":"auto" }
					],
				},
				{
					"name": "play8", "data":{}, "gap":10, "height":30, "alignSelf":"auto", "flexDirection":"row",
					"nodes":[
						{ "name": "play_icon", "data":{}, "aspectRatio":1, "alignSelf":"auto" },
						{ "name": "play_name", "data":{}, "alignSelf":"auto", "flexGrow":1 },
						{ "name": "play_ping", "data":{}, "width":30, "alignSelf":"auto" }
					],
				}
			],
			"gap":10,
			"padding":10,
			"alignSelf":"auto"
		}
	],
	"alignSelf":"auto"
}');

Note: Each node can have a name property and a data property which is a struct containing any data you’d like. I’ve initialised each node with a unique name, which I’ll later use in my UI instances to identify what the node should do. If you don’t pass a data property while creating a node, it will still be initialised with an empty data struct which I will use later to store the instance created for a node.

Now, there is an alternate way of setting up the layout in GML, by calling a flexpanel_create_node() function for each node and inserting each node into its parent with flexpanel_node_insert_child(). I’ve shown this method below only for demonstrating these functions. I’ve also made use of the flexpanel_node_style_set_*() functions to set properties on the nodes, again, for demonstrating how they can be used:

///// UI properties
// Dimensions of the final canvas, where the layout will be ‘applied’, which we can change at runtime
target_w = room_width;
target_h = room_height;

// Shared element properties
ui_padding = 10; // ‘ui_’ prefix as it’s for the root canvas only
gap = 5;
padding = 5;

////// Create layout
// Root (main canvas)
// This is defined as 100% of its container’s size, which is defined later in flexpanel_calculate_layout()
n_root = flexpanel_create_node({name: "root"});
flexpanel_node_style_set_width(n_root, 100, flexpanel_unit.percent);
flexpanel_node_style_set_height(n_root, 100, flexpanel_unit.percent);
flexpanel_node_style_set_justify_content(n_root, flexpanel_justify.center);
flexpanel_node_style_set_align_items(n_root, flexpanel_align.center);

// All the _set* functions can be replaced by struct properties, this is only for demonstration of the _set* functions
// Weapon and health gauge
var _n_weap = flexpanel_create_node({width:"30%", height:"20%", name: "weap"});
flexpanel_node_style_set_position_type(_n_weap, flexpanel_position_type.absolute);
flexpanel_node_style_set_position(_n_weap, flexpanel_edge.left, ui_padding, flexpanel_unit.point);
flexpanel_node_style_set_position(_n_weap, flexpanel_edge.bottom, ui_padding, flexpanel_unit.point);
flexpanel_node_style_set_padding(_n_weap, flexpanel_edge.all_edges, padding);
flexpanel_node_style_set_flex_direction(_n_weap, flexpanel_flex_direction.row);
flexpanel_node_style_set_gap(_n_weap, flexpanel_gutter.all_gutters, gap);
flexpanel_node_insert_child(n_root, _n_weap, 0);

var _n_weap1 = flexpanel_create_node({flexDirection: "column", justifyContent: "flex-end", gap: gap, name:"weap1"});
flexpanel_node_style_set_aspect_ratio(_n_weap1, 1);
flexpanel_node_insert_child(_n_weap, _n_weap1, 0);

flexpanel_node_insert_child(_n_weap1, flexpanel_create_node({height: "20%", name: "weap_info1"}), 0);
flexpanel_node_insert_child(_n_weap1, flexpanel_create_node({height: "20%", name: "weap_info2"}), 1);

var _n_weap2 = flexpanel_create_node({name: "weap2", flexDirection: "column", justifyContent: "flex-end", gap: gap});
flexpanel_node_style_set_width(_n_weap2, 0, flexpanel_unit.auto);
flexpanel_node_style_set_flex_grow(_n_weap2, 1);
flexpanel_node_insert_child(_n_weap, _n_weap2, 1);

flexpanel_node_insert_child(_n_weap2, flexpanel_create_node({height: "20%", name: "weap_bar_magic"}), 0);
flexpanel_node_insert_child(_n_weap2, flexpanel_create_node({height: "20%", name: "weap_bar_stamina"}), 1);
flexpanel_node_insert_child(_n_weap2, flexpanel_create_node({height: "20%", name: "weap_bar_health"}), 2);

// Inventory
var _n_inv = flexpanel_create_node(
{
	name: "inv", width: "40%", height: "10%",
	right: ui_padding, bottom: ui_padding, position: "absolute",
	flexDirection: "row", gap: gap, padding: padding
});
flexpanel_node_insert_child(n_root, _n_inv, 1);

for (var i = 0; i < 6; i++)
{
	flexpanel_node_insert_child(_n_inv, flexpanel_create_node({name: $"inv_slot", data:{num: i}, aspectRatio: 1}), i);
}

// Score display
var _n_score = flexpanel_create_node({name: "score", width: "40%", height: "10%", top: ui_padding, position: "absolute", flexDirection: "row", justifyContent: "space-between"});
flexpanel_node_insert_child(n_root, _n_score, 2);

flexpanel_node_insert_child(_n_score, flexpanel_create_node({name: "score1", aspectRatio: 1}), 0);
flexpanel_node_insert_child(_n_score, flexpanel_create_node({name: "score2", aspectRatio: 1}), 1);

// Player list
var _n_play = flexpanel_create_node(
{
	name: "play", width: "40%", height: "50%",
	padding: padding, gap: gap, flexDirection: "column"
});
flexpanel_node_insert_child(n_root, _n_play, 3);

for (var i = 0; i < 8; i++)
{
	var _n = flexpanel_create_node({name: $"play{1+i}", height: 30, flexDirection: "row", gap: gap, justifyContent: "space_between"});
	flexpanel_node_insert_child(_n, flexpanel_create_node({name: $"play_icon", data:{num: i}, aspectRatio: 1}), 0);
	flexpanel_node_insert_child(_n, flexpanel_create_node({name: $"play_name", data:{num: i}, flexGrow: 1}), 0);
	flexpanel_node_insert_child(_n, flexpanel_create_node({name: $"play_ping", data:{num: i}, width: 30}), 0);
	flexpanel_node_insert_child(_n_play, _n, i);
}

This takes care of the layout tree. Before using it, we need to call flexpanel_calculate_layout() which modifies the nodes’ positions and dimensions based on the “container” canvas dimensions you pass the function.

Then, we’ll create a generate_instance() function which creates an instance for each node in the tree. We call it on the root node first, and the function then recursively calls itself for each child node in the tree.

////// Calculate layout
flexpanel_calculate_layout(n_root, target_w, target_h, flexpanel_direction.LTR);

////// Generate object instances
generate_instance = function(_node, _depth)
{
	// Get layout data
	var _pos = flexpanel_node_layout_get_position(_node, false);
	var _name = flexpanel_node_get_name(_node);
	
	// Create instance
	instance_create_depth(_pos.left, _pos.top, _depth, obj_ui_element,
	{
		name: _name,
		width: _pos.width,
		height: _pos.height
	});
	
	// Call for children (recursive)
	var _children_count = flexpanel_node_get_num_children(_node);
	for (var i = 0; i < _children_count; i++)
	{
		var _child = flexpanel_node_get_child(_node, i);
		generate_instance(_child, _depth - 1);
	}
}

generate_instance(n_root, 0);

While calculating layout, you can pass it the direction to use, from either left-to-right or right-to-left.

The generate_instance() function does the following:

  1. It takes the node to generate an instance for, and the depth for that instance. We need the depth here so we can have each child node be closer to the camera so it draws above its parent.
  2. It gets the positional data for the node, passing in false for the second argument. This ensures we get the absolute coordinates for the node, rather than coordinates relative to the parent, which is the function’s default behaviour.
  3. It gets the node’s name which we use as a unique identifier.
  4. It then creates an instance, with the node’s left and top values as x and y, and applies the namewidth and height as variables. These three variables must be defined in obj_ui_element’s Variable Definitions window.
  5. At the end, it loops through all the child nodes for the given node, and calls the generate_instance() function on it, with a lower depth.

After defining the function, we call it on the root node.

Just so we see something, I added the following code to the Draw GUI event of obj_ui_element:

draw_rectangle(x, y, x + width, y + height, 1);

draw_text(x, y, name);

And with this, we get this result:

image3.png

Resizing Canvas

I wanted this interface to be able to be resized at any time, which would involve calling flexpanel_calculate_layout() again with new dimensions and then have those changes be applied to the already existing instances.

The first part of the problem is simple, however for the latter part – applying changes to existing instances – I needed to make some changes to the generate_instance() function.

In short, it will check the data struct of the node to see if it has an inst property. If it doesn’t, it will create a new instance and store its reference in the inst property of the data struct. However if it does, it will simply update the instance stored in that variable.

Here’s what the function looks like now:

generate_instance = function(_node, _depth)
{
	// Get layout data
	var _pos = flexpanel_node_layout_get_position(_node, false);
	var _name = flexpanel_node_get_name(_node);
	
	// Update instance
	var _data = flexpanel_node_get_data(_node);
	if (!struct_exists(_data, "inst"))
	{
		// Create instance
		var _inst = instance_create_depth(_pos.left, _pos.top, _depth, obj_ui_element,
		{
			name: _name,
			width: _pos.width,
			height: _pos.height
		});
	
		_data.inst = _inst;
	}
	else
	{
		var _inst = _data.inst;
		_inst.x = _pos.left;
		_inst.y = _pos.top;
		_inst.width = _pos.width;
		_inst.height = _pos.height;
	}
	
	// Call for children (recursive)
	var _children_count = flexpanel_node_get_num_children(_node);
	for (var i = 0; i < _children_count; i++)
	{
		var _child = flexpanel_node_get_child(_node, i);
		generate_instance(_child, _depth - 1);
	}
}

You can see that in the else block, where it finds that the inst member exists in the data struct, it updates the xywidth and height of the existing instance.

Now we need to test changing the canvas size, so we can call flexpanel_calculate_layout() again and then run this function to modify the instances. For that, I added a Step event to obj_flexpanels with the following code:

if (mouse_check_button_released(mb_right))
{
	target_w = mouse_x;
	target_h = mouse_y;
	
	flexpanel_calculate_layout(n_root, target_w, target_h, flexpanel_direction.LTR);
	
	generate_instance(n_root, 0);
}

Any point I click in the room will become the bottom-right corner of the canvas. It will re-calculate the layout using those dimensions, and then update all instances using generate_instance().

image6.gif

This article is getting too long already, ideally I would give each node max/min size values so they maintain a minimum size when scaled down too much and a maximum size when scaled up too much.

Displaying Actual Information

Whatever we are drawing so far is only for testing. The interface isn’t showing actually useful information right now.

For this I programmed the obj_ui_element object with some properties. I’ll show you its update Draw GUI event first:

// Draw background, used for containers
if (draw_back)
{
	draw_sprite_stretched(spr_box, 0, x, y, width, height);
}

// Draw image (e.g. an icon)
if (image != -1)
{
	draw_sprite_stretched(image, index, x, y, width, height);
}

// Draw bar image (e.g. for health bars)
if (bar_image != -1)
{
	draw_sprite_stretched_ext(bar_image, 0, x, y, width * bar_level, height, bar_colour, 1);
}

// Draw text, can optionally be centred
if (text != "")
{
	if (center_text)
	{
		draw_set_halign(fa_center);
		draw_set_valign(fa_middle);
	}
	
	draw_text(x + (width / 2) * center_text, y + (height / 2) * center_text, text);
	
	if (center_text)
	{
		draw_set_halign(fa_left);
		draw_set_valign(fa_top);
	}
}

It basically allows this object to display any kind of UI element. These variables are set in the Create event, based on the name applied to the instance, taken from the node.

I won’t show you every single case for the switch statement below, for that you can download the project at the end of this article, however I will show you some cases as an example:

draw_back = false;
image = -1;
index = irandom(10);
text = "";
bar_image = -1;
bar_colour = -1;
bar_level = random_range(0, 1);
center_text = false;

switch (name)
{
            // Display background for containers only
	case "weap": case "inv": case "score": case "play":
		draw_back = true;
	break;
	
	case "weap1":
		image = spr_weapon;
	break;
	
	case "weap_info1":
		text = "DUR 168";
	break;
	
	case "weap_info2":
		text = "LVL 26";
	break;
	
	case "weap_bar_health":
		bar_colour = merge_colour(c_red, c_gray, 0.4);
		bar_image = spr_bar;
	break;

            // More cases…
}

This way I have now set up my UI rendering system which is abstracted away from the layout calculation system. You can just as easily program all of this to work in obj_flexpanels directly, with all of the image and text data being stored in a node’s data struct. However with an abstracted system, you can swap out either of the systems at any time without having the other system affected, as long as the same inputs are maintained.

image5.png

Download this project here.

Now that you have these objects created from your layout, you can program them to do anything, e.g. interact with the mouse, display changing information, etc.

As mentioned before, these functions are the backbone for an upcoming UI editor, so keep an eye out! It will make things a thousand times easier and you won’t have to worry about these functions in almost all cases.

Happy GameMaking!

avatar
Lead Technical Writer at GameMaker, Gurpreet creates documentation and tutorials to make game making easier for you. He loves using the computer to bring new things to life, whether it's games, digital art, or Metal music.
Back to blogs