-
Notifications
You must be signed in to change notification settings - Fork 16
7. Creating a New Collision Tileset
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:

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.
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.

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.
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);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
]You can 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);
}
}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.