View to GUI Conversion

Credit: Gizmo199

function view_to_gui_x(camera, x)
{
	return ((x - camera_get_view_x(camera)) / camera_get_view_width(camera)) * display_get_gui_width();
}

function view_to_gui_y(camera, y)
{
	return ((y - camera_get_view_y(camera)) / camera_get_view_height(camera)) * display_get_gui_height();
}

Usage

These are simple scripts that convert the given x/y coordinates in a room to the appropriate x/y coordinates on the GUI layer when using views. These scripts are essential, especially when dealing with shader effects that need to be drawn on the GUI layer (eg. Motion blur, Shockwaves, etc.). They are also particularly good for rendering elements such as text boxes or input prompts to the screen on the GUI layer where needed.

Optimization Tip

#macro inline gml_pragma("forceinline")

Add this somewhere in your game, and you will be able to simply type inline; in a function to make it faster to call in YYC mode. I would personally always inline simple helper functions like the two listed above.

Fancy Circular Healthbar

Written by me (Anixias).

function draw_circle_hollow(x, y, radius, width)
{
	draw_circle_hollow_quality(x, y, radius, 0, width, 64);
}

function draw_circle_hollow_quality(x, y, radius, offset, width, quality)
{
	draw_primitive_begin(pr_trianglelist);
	for(var i = offset; i < 360 + offset; i += 360 / quality)
	{
		var r = max(0, radius - width);
		var next_i = (i + (360 / quality));
			
		var inner_x = x + lengthdir_x(r, i);
		var inner_y = y + lengthdir_y(r, i);
		var outer_x = x + lengthdir_x(radius, i);
		var outer_y = y + lengthdir_y(radius, i);
			
		var next_inner_x = x + lengthdir_x(r, next_i);
		var next_inner_y = y + lengthdir_y(r, next_i);
		var next_outer_x = x + lengthdir_x(radius, next_i);
		var next_outer_y = y + lengthdir_y(radius, next_i);
			
		// First triangle
		draw_vertex(inner_x, inner_y);				// inner
		draw_vertex(outer_x, outer_y);				// outer
		draw_vertex(next_inner_x, next_inner_y);	// next inner
			
		// Second triangle
		draw_vertex(next_inner_x, next_inner_y);	// next inner
		draw_vertex(outer_x, outer_y);				// outer
		draw_vertex(next_outer_x, next_outer_y);	// next outer
	}
	draw_primitive_end();
}

function draw_healthbar_circular(x, y, radius, amount, backcol, mincol, maxcol, offset, direction, showback, showborder, width)
{
	draw_healthbar_circular_quality(x, y, radius, amount, backcol, mincol, maxcol, offset, direction, showback, showborder, width, 64);
}

function draw_healthbar_circular_quality(x, y, radius, amount, backcol, mincol, maxcol, offset, direction, showback, showborder, width, quality)
{
	if (direction < 0) offset += 180;
	
	quality = max(3, quality);
	
	// Using primitives
	var start_color = draw_get_color();
	
	// Back
	if (showback)
	{
		// Border
		if (showborder)
		{
			draw_set_color(c_black);
			draw_circle_hollow_quality(x + 1, y    , radius, offset, width, quality);
			draw_circle_hollow_quality(x - 1, y    , radius, offset, width, quality);
			draw_circle_hollow_quality(x    , y + 1, radius, offset, width, quality);
			draw_circle_hollow_quality(x    , y - 1, radius, offset, width, quality);
		}
		
		// Back
		draw_set_color(backcol);
		draw_circle_hollow_quality(x, y, radius, offset, width, quality);
	}
	
	// Lerp color
	var r1, g1, b1;
	var r2, g2, b2;
	var r3, g3, b3;
	
	r1 = (mincol & 0x0000FF);
	g1 = (mincol & 0x00FF00) >> 8;
	b1 = (mincol & 0xFF0000) >> 16;
	
	r2 = (maxcol & 0x0000FF);
	g2 = (maxcol & 0x00FF00) >> 8;
	b2 = (maxcol & 0xFF0000) >> 16;
	
	var amt = clamp(amount / 100, 0, 1);
	r3 = lerp(r1, r2, amt);
	g3 = lerp(g1, g2, amt);
	b3 = lerp(b1, b2, amt);
	
	// Border
	if (showborder)
	{
		if (!showback)
		{
			var _x = x;
			var _y = y;
			
			draw_set_color(c_black);
			for(y = _y - 1; y <= _y + 1; y += 2)
			{
				for(x = _x - 1; x <= _x + 1; x += 2)
				{
					draw_primitive_begin(pr_trianglelist);
					for(var i = offset; i < 360 * amt + offset; i += 360 / quality)
					{
						var r = max(0, radius - width);
						var this_i = direction < 0 ? -i : i;
						var next_i = (i + (360 / quality)) * (direction < 0 ? -1 : 1);
						
						var inner_x = x + lengthdir_x(r, this_i);
						var inner_y = y + lengthdir_y(r, this_i);
						var outer_x = x + lengthdir_x(radius, this_i);
						var outer_y = y + lengthdir_y(radius, this_i);
						
						var next_inner_x = x + lengthdir_x(r, next_i);
						var next_inner_y = y + lengthdir_y(r, next_i);
						var next_outer_x = x + lengthdir_x(radius, next_i);
						var next_outer_y = y + lengthdir_y(radius, next_i);
						
						// First triangle
						draw_vertex(inner_x, inner_y);				// inner
						draw_vertex(outer_x, outer_y);				// outer
						draw_vertex(next_inner_x, next_inner_y);	// next inner
						
						// Second triangle
						draw_vertex(next_inner_x, next_inner_y);	// next inner
						draw_vertex(outer_x, outer_y);				// outer
						draw_vertex(next_outer_x, next_outer_y);	// next outer
					}
					draw_primitive_end();
				}
			}
			x = _x;
			y = _y;
		}
		else
		{
			draw_set_color(c_black);
			
			var border_offset = 4;
			var progress = 0;
			
			draw_primitive_begin(pr_trianglelist);
			for(var i = offset; amt > 0 && i <= 360 * ceil(amt * 1000) / 1000 + offset; i += 360 / quality)
			{
				var r = max(0, radius - width);
				var this_i = direction < 0 ? -(i - border_offset) : i - border_offset;
				var next_i = (i + (360 / quality) + border_offset) * (direction < 0 ? -1 : 1);
				
				var inner_x = x + lengthdir_x(r, this_i);
				var inner_y = y + lengthdir_y(r, this_i);
				var outer_x = x + lengthdir_x(radius, this_i);
				var outer_y = y + lengthdir_y(radius, this_i);
				
				var next_inner_x = x + lengthdir_x(r, next_i);
				var next_inner_y = y + lengthdir_y(r, next_i);
				var next_outer_x = x + lengthdir_x(radius, next_i);
				var next_outer_y = y + lengthdir_y(radius, next_i);
				
				if (i + (360 / quality) > 360 * ceil(amt * 1000) / 1000 + offset)
				{
					var amt_within = (amt - progress) / (1 / quality);
					next_inner_x = lerp(inner_x, next_inner_x, amt_within);
					next_inner_y = lerp(inner_y, next_inner_y, amt_within);
					next_outer_x = lerp(outer_x, next_outer_x, amt_within);
					next_outer_y = lerp(outer_y, next_outer_y, amt_within);
				}
				
				// First triangle
				draw_vertex(inner_x, inner_y);				// inner
				draw_vertex(outer_x, outer_y);				// outer
				draw_vertex(next_inner_x, next_inner_y);	// next inner
				
				// Second triangle
				draw_vertex(next_inner_x, next_inner_y);	// next inner
				draw_vertex(outer_x, outer_y);				// outer
				draw_vertex(next_outer_x, next_outer_y);	// next outer
				
				progress += 1 / quality;
			}
			draw_primitive_end();
		}
	}
	
	var progress = 0;
	
	draw_set_color(r3 | (g3 << 8) | (b3 << 16));
	draw_primitive_begin(pr_trianglelist);
	for(var i = offset; amt > 0 && i <= 360 * ceil(amt * 1000) / 1000 + offset; i += 360 / quality)
	{
		var r = max(0, radius - width);
		var this_i = direction < 0 ? -i : i;
		var next_i = (i + (360 / quality)) * (direction < 0 ? -1 : 1);
		
		var inner_x = x + lengthdir_x(r, this_i);
		var inner_y = y + lengthdir_y(r, this_i);
		var outer_x = x + lengthdir_x(radius, this_i);
		var outer_y = y + lengthdir_y(radius, this_i);
		
		var next_inner_x = x + lengthdir_x(r, next_i);
		var next_inner_y = y + lengthdir_y(r, next_i);
		var next_outer_x = x + lengthdir_x(radius, next_i);
		var next_outer_y = y + lengthdir_y(radius, next_i);
		
		// If this is the last iteration before the loop stops
		if (i + (360 / quality) > 360 * ceil(amt * 1000) / 1000 + offset)
		{
			var amt_within = (amt - progress) / (1 / quality);
			next_inner_x = lerp(inner_x, next_inner_x, amt_within);
			next_inner_y = lerp(inner_y, next_inner_y, amt_within);
			next_outer_x = lerp(outer_x, next_outer_x, amt_within);
			next_outer_y = lerp(outer_y, next_outer_y, amt_within);
		}
		
		// First triangle
		draw_vertex(inner_x, inner_y);				// inner
		draw_vertex(outer_x, outer_y);				// outer
		draw_vertex(next_inner_x, next_inner_y);	// next inner
		
		// Second triangle
		draw_vertex(next_inner_x, next_inner_y);	// next inner
		draw_vertex(outer_x, outer_y);				// outer
		draw_vertex(next_outer_x, next_outer_y);	// next outer
		
		progress += 1 / quality;
	}
	draw_primitive_end();
	
	draw_set_color(start_color);
}

Notes

This can be easily modified to use textures, since it uses primitives to render. The border rendering is slightly wrong on the partial circle (the one that shows the amount), but it’s good enough for my purposes. You can use draw_healthbar_circular_quality to actually render non-circular shapes (quality of 3 is a triangle, quality of 4 is either a diamond or rectangle based on the offset you provide, quality of 5 is a pentagon, quality of 6 is a hexagon, etc.).

Modular Multiplication

Written by me (Anixias).

function mulmod(a, b, modulo)
{
	var res = 0;
	a %= modulo;
	while(b > 0)
	{
		if (b % 2 == 1)
		{
			res = (res + a) % modulo;
		}
		
		a = (a * 2) % modulo;
		
		b = b >> 1;
	}
	
	return res % modulo;
}

Usage

This function will multiply two numbers, a and b, and ensure the product doesn’t overflow past modulo. It avoids actually multiplying a by b so GMS2 doesn’t lose precision. After several tests with large 32-bit integers, it is perfectly accurate, whereas using normal operations (literally using (a * b) % 0x100000000) produced slightly inaccurate results. Here is a simple function to multiply two unsigned 32-bit integers, ensuring the product is an unsigned 32-bit integer (via overflow):

function multiply_u32(a, b)
{
	return mulmod(a, b, 0x100000000);
}

Circular Healthbar

Credit: Gizmo199

function draw_healthbar_circular(_x, _y, _radius, _amount, _backcol, _mincol, _maxcol, _direction, _showback, _thickness)
{
	// Get our color
	var _r1, _r2, _g1, _g2, _b1, _b2;
	
	_r1 = color_get_red(_maxcol);
	_g1 = color_get_green(_maxcol);
	_b1 = color_get_blue(_maxcol);
	
	_r2 = color_get_red(_mincol);
	_g2 = color_get_green(_mincol);
	_b2 = color_get_blue(_mincol);
	
	// Calculate the difference between each color channel
	var _rVal = _r2 + (((_r1 - _r2) * _amount) / 100);
	var _gVal = _g2 + (((_g1 - _g2) * _amount) / 100);
	var _bVal = _b2 + (((_b1 - _b2) * _amount) / 100);
	
	var _col = make_color_rgb(_rVal, _gVal, _bVal);
	
	// Draw our circular healtbar
	draw_set_color(_backcol);
	
	for(var i = 0; i < 360; i++)
	{
		// get rotation
		var _ldx = lengthdir_x(_radius, _direction + i);
		var _ldy = lengthdir_y(_radius, _direction + i);
		
		var _ldx2 = lengthdir_x(_radius + _thickness, _direction + i);
		var _ldy2 = lengthdir_y(_radius + _thickness, _direction + i);
		
		// Back ring
		if (_showback)
		{
			draw_line(_x + _ldx, _y + _ldy, _x + _ldx2, _y + _ldy2);
		}
		
		// Default is set to 100 as the maximum percentage to calculate
		var _percent = 100;
		var _val = ( _amount * 360 ) / _percent;
		
		// health ring
		if ((360 - _val) <= i)
		{
			draw_set_color(_col);
			draw_line(_x + _ldx, _y + _ldy, _x + _ldx2, _y + _ldy2);
		}
	}
}

Usage

draw_healthbar_circular(x, y, radius, amount, backcol, mincol, maxcol, direction, showback, showborder, thickness)


For drawing general circular healthbars, meters, etc.

Remapping a Range

Written by me (Anixias).

function map_range(value, srcmin, srcmax, destmin, destmax)
{
	return destmin + (destmax - destmin) * ((value - srcmin) / (srcmax - srcmin));
}

Usage

You can use this function to map one range to another. For example, say you want to convert a point in the view to a point in the GUI. You could call something like (pseudo-code):

point_x_in_gui = map_range(point_x_in_view, view_x, view_x + view_width, 0, display_get_gui_width());
point_y_in_gui = map_range(point_y_in_view, view_y, view_y + view_height, 0, display_get_gui_height());

Smooth Wrapping Lerp

Written by me (Anixias).

function wrap(val, minval, maxval)
{
	while(val < minval) val += maxval - minval;
	while(val >= maxval) val -= maxval - minval;
	
	return val;
}

function lerp_wrap(src, dest, amt, minval, maxval)
{
	if (dest > src)
	{
		if (abs(dest - src) >= abs((dest - (maxval - minval)) - src))
		{
			dest -= (maxval - minval);
		}
	}
	else if (dest < src)
	{
		if (abs(dest - src) >= abs((dest + (maxval - minval)) - src))
		{
			dest += (maxval - minval);
		}
	}
	else return wrap(dest, minval, maxval);
	
	var val = lerp(src, dest, amt);
	return wrap(val, minval, maxval);
}

Usage

You can use this to lerp from one value to another, but it takes into account that there is a wrapping range. For example, say you want to change image_angle to lerp towards some direction D every frame, but it should rotate in the direction closest to the goal. Let’s assume image_angle is 350 degrees, and D is 15 degrees. Obviously, we should increase image_angle, since 15 is the same as 375, and going up from 350 to 375 is faster than going down from 350 to 15. This script takes that into account. You can achieve this by calling lerp_wrap(image_angle, D, 0.1, 0, 360) in the Step Event. 0.1 here can be any number you want, from 0 to 1.

Note: This script works in the range [minval, maxval). AKA, it can equal minval, but it will never equal maxval.

Positional Pseudorandom Noise

Written by me (Anixias). Warning: This does not work well, and you should instead look up squirrel3 or see the GDC video on YouTube on noise to replace RNGs.

function pseudorandom_position(x, y)
{
	var z = ((x + 10) * 6587 + (y + 43) * 9811) % 1;
	
	var xt = (dcos(360 * 8 * x) + 1) / 2;
	var yt = (dsin(360 * 8 * y) + 1) / 2;
	var zt = abs(dsin(360 * 8 * z)) % 1;
	
	return (xt + yt + zt) % 1;
}

Usage

Call this function on a position (integer coordinates) to get a seemingly random number from 0 – 1 (never equals 1). You can use this to, without messing with the random seed and affecting subsequent random calls, render tiles on a grid with random variations. Multiply the returned value by the number of subimages (in a grass tile, for example), then floor that value to get a “random” subimage to use for rendering.

Surface Stack

Written by me (Anixias).

global.surface_stack = ds_stack_create();
global.surface_is_set = false;

/// @arg id
function surface_set(surf)
{
	if (global.surface_is_set) surface_reset_target();
	
	surface_set_target(surf);
	ds_stack_push(global.surface_stack, surf);
	global.surface_is_set = true;
}

function surface_reset()
{
	if (global.surface_is_set)
	{
		surface_reset_target();
		global.surface_is_set = false;
	}
	ds_stack_pop(global.surface_stack);
	if (!ds_stack_empty(global.surface_stack))
	{
		var surf = ds_stack_top(global.surface_stack);
		if (surface_exists(surf))
		{
			surface_set_target(surf);
			global.surface_is_set = true;
		}
		else show_error("Error: Reset surface didn't exist", true);
	}
}

Usage

Simply replace surface_set_target and surface_reset_target throughout your code with surface_set and surface_reset, respectively. This allows you to nest surface rendering. I wrote it because I needed a global UI surface, but I use surfaces throughout the UI. This allows me to use surface rendering wherever I need it without worrying about resetting the surface target every time I need to render to a surface, since I can’t know at runtime whether I’m currently rendering to a surface or not.

You can also use global.surface_is_set to know if you are currently rendering to a surface, and ds_stack_top(global.surface_stack) to get the id of the surface you are currently rendering to.

Event Bus

Credit to u/galitork on reddit.com.

function Events() constructor {
	signals = [];
	
	static subscribe = function(event, fn) {
		var i = array_length(signals);
		
		signals[i] = {
			name: event,
			func: fn
		}
	}
	
	static unsubscribe = function(event) {
		var newSignals = [];
		var j = 0;
		
		for(var i = 0; i < array_length(signals); i++) {
			var signal = signals[i];
			
			if(signal.name != event) {
				newSignals[j] = signals[i];
				j++;
			}
		}
		
		signals = newSignals;
	}
	
	static emit = function(event, data) {
		for(var i = 0; i < array_length(signals); i++) {
			var signal = signals[i];
			
			if(signal.name == event) {
				signal.func(data);
			}
		}
	}
}