Skip to content

7. Creating a New Collision Tileset

Triangly edited this page Aug 7, 2025 · 12 revisions

Tile Types and Format

As described in the first chapter, collision tiles are divided into three types:

  • Full Solid
  • Top Solid
  • LBR Solid

Collision is represented by a tileset made up of 16x16 tiles, and it must follow a specific format. Below is a screenshot showing collision tiles from Sonic the Hedgehog 3:

chapter7_image1

As you can see, all three types of tiles are shown in the same order as listed above. By default, the framework supports 256 unique tiles in a single tileset (1 empty tile - the first one - and 255 customisable tiles). This limitation is defined by the ENGINE_TILE_COUNT macro, along with the tile size via the ENGINE_TILE_SIZE macro. These constraints apply to the entire project: if you change these macros, all tilesets must adhere to the new values.

Creating a Custom Tileset

To create your own tileset, duplicate the sprite spr_ts_collision_template in the project. The framework only requires data from Full Solid tiles. You duplicate these tiles into the corresponding cells of other groups so that they can be used when placing collision tiles in levels.

chapter7_image2

However, simply adding the tileset to the game is not enough. The collision system does not rely on built-in collision functions but on a set of custom width, height, and angle data.

More information about tile arrangement and usage can be found in this discussion.

The colour of the tiles does not matter.

Generating Collision Data

In the scr_fw_collision_setup() script, call the collision_generate() function for your tileset sprite, and the game will generate the width and height data at runtime. Afterward, you can load this data using the collision_load() function in a Create Event of your room controller (or c_stage).

If using the template grid above, the arguments for the collision_generate() function will be:

collision_generate(your_tileset_sprite_here, your_angle_array_here, 0, 0, 2, 2, 16);

Angle Data

To properly generate collision data, you need to pass an array of angles. Each value in the array represents the angle of the corresponding tile. These angles are represented by integer values ranging from 0 to 255, covering a full 360-degree circle.

The angles progress clockwise, starting from 0 at the downward direction:

  • 0 is downward (0 degrees)
  • 64 is leftward (270 degrees)
  • 128 is upward (180 degrees)
  • 192 is rightward (90 degrees)

Each integer represents a single step in the 360-degree circle. With 256 possible values in the array, each step corresponds to 1.40625 degrees (360° / 256 = 1.40625°).

The array must have one value for each tile, starting from the second tile (the first tile is empty and is practically non-existent). If the array is shorter than the total number of tiles defined by ENGINE_TILE_COUNT, any missing tiles past the angle array data will default to an angle of 0.

You can also pass an empty array if you want to quickly test collision functionality, in which case all tiles will default to an angle of 0. Additionally, you may use this tool to visualise the angle format, assisting you with generating angles.

As an example, here the data you'd use to assign angles to the Sonic 3 collision tileset:

        var _s3_data =
	[
		     0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0, 
		252, 252, 252, 252, 252, 252, 252, 252, 248, 248, 248, 0,   240, 240, 226, 208, 
		208, 200, 200, 200, 200, 200, 212, 214, 214, 0,   222, 0,   250, 238, 232, 242, 
		242, 240, 252, 252, 250, 248, 246, 246, 248, 250, 252, 252, 242, 244, 244, 226, 
		228, 240, 238, 254, 252, 248, 244, 244, 240, 244, 248, 252, 252, 236, 236, 224,
		228, 220, 242, 246, 250, 252, 232, 236, 224, 212, 208, 202, 198, 194, 236, 244,
		252, 214, 224, 204, 196, 212, 216, 224, 212, 240, 236, 236, 110, 114, 120, 0, 
		96,  100, 0,   92,  236, 244, 252, 132, 140, 146, 160, 168, 10,  32,  178, 56, 
		188, 70,  194, 84,  202, 112, 106, 96,  210, 108, 112, 224, 190, 180, 168, 160, 
		152, 140, 132, 216, 226, 232, 242, 246, 250, 0,   254, 250, 248, 242, 224, 238, 
		246, 252, 224, 224, 252, 246, 238, 196, 202, 210, 210, 202, 196, 62,  56,  52, 
		48,  48,  52,  58,  62,  0,   40,  40,  40,  40,  40,  168, 168, 168, 168, 168, 
		104, 112, 120, 124, 126, 96,  96,  80,  72,  72,  68,  66,  40,  40,  40,  32, 
		24,  24,  16,  12,  8,   4,   244, 244, 250, 252, 238, 238, 242, 224, 232, 224, 
		232, 254, 238, 236, 254, 252, 248, 52,  224, 224, 196, 204, 224, 212, 252, 244, 
		236, 0,   0,   0,   250, 250, 244, 240, 224, 0
	]

Additionally, you may replace everything in the collision_generate() script with the code below and leave _angle_data argument as undefined, which will make the framework calculate angles for you.

/// @self scr_fw_collision_setup
/// @description Generates collision data for the tilemap associated with the specified sprite.
/// @param {Asset.GMSprite} sprite_id The sprite used in the tilemap.
/// @param {Array<Real>|Undefined} _angle_data An array of values representing the angles for each tile. Leave undefined to let the framework calculate it automatically.
/// @param {Real} off_x The horizontal offset of the sprite on the tilemap.
/// @param {Real} off_y The vertical offset of the sprite on the tilemap.
/// @param {Real} sep_x The horizontal spacing between tiles on the tilemap.
/// @param {Real} sep_y The vertical spacing between tiles on the tilemap.
/// @param {Real} row_length The amount of tiles in each row.
function collision_generate(_sprite_id, _angle_data, _off_x = 0, _off_y = 0, _sep_x = 0, _sep_y = 0, _row_length)
{
    // Initialise arrays and variables for collision data
	var _height_arr = array_create(ENGINE_TILE_COUNT, 0);
	var _width_arr = array_create(ENGINE_TILE_COUNT, 0);
	var _angle_arr = array_create(ENGINE_TILE_COUNT, 0);
	var _xcell_size = ENGINE_TILE_SIZE + _sep_x;
	var _ycell_size = ENGINE_TILE_SIZE + _sep_y;

	// Set up the sprite and create the surface
	sprite_set_offset(_sprite_id, 0, 0);
	
	var _obj = instance_create(0, 0, obj_tile);
	var _surface = surface_create(sprite_get_width(_sprite_id), sprite_get_height(_sprite_id));
	surface_set_target(_surface);
	draw_clear_alpha(c_black, 0);
	draw_sprite(_sprite_id, 0, 0, 0);
	surface_reset_target();

	// Initialise the first tile's height and width arrays
	for (var j = 0; j < ENGINE_TILE_SIZE; j++)
	{
		_height_arr[0][j] = 0;
		_width_arr[0][j] = 0;
	}
	
	_angle_arr[0] = 0;
	
	// Generate collision data for each tile
	for (var i = 1; i < ENGINE_TILE_COUNT; i++)
	{		
		var _tile = sprite_create_from_surface(_surface, _off_x + _xcell_size * (i % _row_length), _off_y + _ycell_size * floor(i / _row_length), ENGINE_TILE_SIZE, ENGINE_TILE_SIZE, false, false, 0, 0);

		_height_arr[i] = array_create(ENGINE_TILE_SIZE);
		_width_arr[i] = array_create(ENGINE_TILE_SIZE);

		sprite_collision_mask(_tile, true, 1, 0, 0, 0, 0, 0, 0);
		_obj.sprite_index = _tile;

		for (var m = 0; m < ENGINE_TILE_SIZE; m++)
		{	
			for (var n = 0; n < ENGINE_TILE_SIZE; n++)
			{
				if collision_point(_obj.x + m, _obj.y + n, obj_tile, true, false)
				{
					_height_arr[i][m]++;
					_width_arr[i][n]++;
				}
			}
		}
		
		if _angle_data == undefined
		{
			#region ANGLE CALCULATION
			
			var _limit = ENGINE_TILE_SIZE - 1;	
			var _top_dist_y = 0;
			var _bottom_dist_y = 0;
			var _top_dist_x = 0;
			var _bottom_dist_x = 0;
			
			var _x1 = _obj.x;
			var _y1 = _obj.y;
			var _x2 = _obj.x + _limit;
			var _y2 = _obj.y;
			var _x3 = _obj.x;
			var _y3 = _obj.y + _limit;
			var _x4 = _obj.x + _limit;
			var _y4 = _obj.y + _limit;
			
			// Move top-left
			while !collision_point(_x1, _y1, obj_tile, true, false) && _y1 < _limit
			{
				_y1++;
				_top_dist_y++;
			}
			
			// Move top-right
			while !collision_point(_x2, _y2, obj_tile, true, false) && _y2 < _limit
			{
				_y2++;
				_top_dist_y++;
			}
			
			// Align with the tile
			if (_y1 < _y2)
			{
				while collision_point(_x1 + 1, _y1, obj_tile, true, false) && _x1 < _limit
				{
					_x1++;
					_top_dist_x++;
				}
			
				while !collision_point(_x2, _y2, obj_tile, true, false) && _x2 > 0
				{
					_x2--;
					_top_dist_x++;
				}
			}
			else if (_y1 > _y2)
			{
				while !collision_point(_x1, _y1, obj_tile, true, false) && _x1 < _limit
				{
					_x1++;
					_top_dist_x++;
				}
				
				while collision_point(_x2 - 1, _y2, obj_tile, true, false) && _x2 > 0
				{
					_x2--;
					_top_dist_x++;
				}
			}
				
			// Move bottom-left
			while !collision_point(_x3, _y3, obj_tile, true, false) && _y3 > 0
			{
				_y3--;
				_bottom_dist_y++;
			}
			
			// Move bottom-right
			while !collision_point(_x4, _y4, obj_tile, true, false) && _y4 > 0
			{
				_y4--;
				_bottom_dist_y++;
			}
			
			// Align with the tile
			if (_y3 < _y4)
			{
				while !collision_point(_x3, _y3, obj_tile, true, false) && _x3 < _limit
				{
					_x3++;
					_bottom_dist_x++;
				}
			
				while collision_point(_x4 - 1, _y4, obj_tile, true, false) && _x4 > 0
				{
					_x4--;
					_bottom_dist_x++;
				}
			}
			else if (_y3 > _y4)
			{
				while collision_point(_x3 + 1, _y3, obj_tile, true, false) && _x3 < _limit
				{
					_x3++;
					_bottom_dist_x++;
				}
				
				while !collision_point(_x4, _y4, obj_tile, true, false) && _x4 > 0
				{
					_x4--;
					_bottom_dist_x++;
				}
			}
			
			/// @feather ignore GM2018
			var _is_upside_down;
			if (_top_dist_y == _bottom_dist_y)
			{
				_is_upside_down = _top_dist_x > _bottom_dist_x;
			}
			else
			{
				_is_upside_down = _bottom_dist_y > _top_dist_y;
			}
			
			var _angle;
			if (_is_upside_down)
			{
				_angle = point_direction(_x4, _y4, _x3, _y3);
			}
			else
			{
				_angle = point_direction(_x1, _y1, _x2, _y2);
			}
			
			_angle_arr[i] = math_get_angle_rounded(_angle);
			
			#endregion
		}
	
		sprite_delete(_tile);
	}
	
	if _angle_data != undefined
	{
		var _angle_data_length = array_length(_angle_data);
		
		// Set angle data based on input array or default to zero
		if _angle_data_length == 0
		{
			for (var i = 1; i < ENGINE_TILE_COUNT; i++)
			{
				_angle_arr[i] = 0;
			}
		}
		else
		{
			var i = 1;
		
			for (; i <= _angle_data_length; i++)
			{
				_angle_arr[i] = math_get_angle_degree(_angle_data[i - 1]);
			}
		
			for (; i < ENGINE_TILE_COUNT; i++)
			{
				_angle_arr[i] = 0;
			}
		}
	}
		
	// Clean up
	surface_free(_surface);
	instance_destroy(_obj);
	
	// Store the generated data
	if !global.tools_binary_collision
	{
		global.generated_tile_height_data[? _sprite_id] = _height_arr;
		global.generated_tile_width_data[? _sprite_id] = _width_arr;
		global.generated_tile_angle_data[? _sprite_id] = _angle_arr;
	}
	
	// Save the generated data into the binary format
	else
	{
		var _prefix = sprite_get_name(_sprite_id);
		
		show_debug_message(
		"=============================================================================================================\n"
		+ $"GENERATED COLLISION FOR {_prefix} IS SAVED INTO THE BINARY FILES. IT IS NOT REGISTERED IN THE GAME!\n"
		+ "============================================================================================================="
		);
		
	    var _width_filename = _prefix + "_widths.bin";
	    var _height_filename = _prefix + "_heights.bin";
	    var _angle_filename = _prefix + "_angles.bin";
		
	    var _width_file = file_bin_open(_width_filename, 1);
		
	    // Write width data
	    for (var i = 0; i < ENGINE_TILE_COUNT; i++)
	    {
	        for (var j = 0; j < ENGINE_TILE_SIZE; j++)
	        {
	            file_bin_write_byte(_width_file, _width_arr[i][j]);
	        }
	    }
    
	    file_bin_close(_width_file);
		
	    var _height_file = file_bin_open(_height_filename, 1);
    
	    // Write height data
	    for (var i = 0; i < ENGINE_TILE_COUNT; i++)
	    {
	        for (var j = 0; j < ENGINE_TILE_SIZE; j++)
	        {
	            file_bin_write_byte(_height_file, _height_arr[i][j]);
	        }
	    }
		
	    file_bin_close(_height_file);
    
	    var _angle_file = file_bin_open(_angle_filename, 1);
		
	    // Write angle data
		for (var i = 0; i < ENGINE_TILE_COUNT; i++)
		{
		    file_bin_write_byte(_angle_file, math_get_angle_raw(_angle_arr[i]));
		}
		
	    file_bin_close(_angle_file);
	}
}

Optimising with Binary Data

If you want to optimise collision data, set the flag global.tools_binary_collision to true. This will tell the collision_generate() function not to load the generated data into the game's memory but instead save it in binary format in your project’s save directory. You can then move this data to the datafiles/collision/ folder in your project and load it using collision_load_binary(). This method reduces memory usage and improves loading times.

Do not forget to remove the collision_generate() function call for this tileset as it will no longer be needed.

Clone this wiki locally