Legacy GM On Slopes and Grids II: Autotiling with 45° Slopes.


GM Version: 1.4.1757
Target Platform: ALL

This tutorial expands on my previous one by adding slope and squares combined autotiling with the use of grids. As a little bonus I've included a first attempt at 45° pathfinding.


Thanks for your interest!
A few things to note first:

  • Feel free to try the demo project first to see what’s being worked on! Click the right mouse button to the toggle the pathfinder, cycle the visualizations with "B". Be warned: Pressing 'G' will show the mp_grid and abosolutely TANK your framerate.
  • Have a look at my previous tutorial on slopes and grids, which explains basic collisions, line of sight, place_meeting and an introduction to 2D topdown slope movement code, all purely based on grids. https://forum.yoyogames.com/index.p...ment-and-collision-line-without-objects.4073/
  • Check out Shaun's Tutorial: "Game Maker Studio: Automatic Tiles Tutorial" on youtube, to get an in depth explanation of what the general idea is, and how its implemented for square walls. My Tutorial will then expand your knowledge to also include 45° slopes.
  • Cell Dimensions are a fixed 32x32. For simplicity and readibility there is no cell dimension macro variable.
  • Yes, its unusual to have the grid stored in the player object - hes not designed to die.
  • How you set up your grid is up to you! This tutorial assumes the grid is a given. In the demo (most) of the tiles are randomly generated.
  • For readability the code is represented in images with comments. See the .gmz file for details.
  • If you are very familiar with this kind of concept, feel free to jump down to the atg_check script - which is where all the magic happens.
Part 1: Assets and Setup

A square wall can have 47 different forms, depending on its 8 adjacent cells. A slope (of which 4 different kinds exist) can take on 5 different forms for auotiling. The scripts in this tutorial will thus return a number between [0-46] for squares, and [0-19] for slopes. I have provided an asset.zip with a .png, .psd [photoshop] and .pdn [paint.net] files.

Square walls in my tutorial check against an array. It uses what is sometimes referred to as the binary method. Thus before we begin autotiling, we have to initialize the array. There are 8 adjacent cells, which can each hold either true or false. 2^8=256 possibilities, and thus an array of [255]. The array is set up in the script atg_init_array();. Its basically a long list of values, representing the resulting tile, based upon the binary input. More on that in the atg_squares script.

The grid in this tutorial may contain the following values.
  • 0: Free
  • 1 to 4: Sloped Walls
  • 5: Square Wall

Initializing the grid - these variables are referenced constantly by the scripts. All autotiling scripts need to be called from the instance setting up the grid.
// Grid
grid=ds_grid_create((room_width div 32),(room_height div 32));
gw=ds_grid_width(grid)-1; // grid_width
gh=ds_grid_height(grid)-1; // grid_height
The spoiler below contains the main loop and array setup of the autotiling function.
A switch statement is called to distinguish each grid entry, except for case [0] which can be ignored, as it is simply a completely tiled background picture for the empty floor. Slopes and Squares have their own autotile script - atg_slopes and atg_squares, where the former requires an extra input argument, in the form of its respective slope type [1,2,3,4].

Both scripts return a single tile value. [0-46] for squares and [0-19] for slopes. As can be seen in the asset picture above, the sqaures are 12 across and 4 deep. For slopes its 12 across to get to the initial slope, AND THEN 5 across and 4 deep. This is because both squares and slopes are in the same background asset.

The resulting value is then used to place the correct tile in the room, at a given position determined by the loop.
'tile div' and 'tile mod' is where the location of the tile within the background asset is happening, based on the tile values resulting from the atg_x scripts.

Part 2: Autotile Grid Squares [atg_sqaures]

Any square wall's tile value is determined by its 8 neighbors. All of which need to be subsequently checked.

Before any grid checks can be run though, it is vital to first ensure that these checks do not happen outside of the grid boundaries. So no checking for grid[# -1,-1]. This will throw an error and if worst comes to worst, crash your game.

A precautionary measure is facilitated by defaulting all 8 directions to false. Afterwards we check if the grid boundaries are exceeded and if so automatically assume true for the given directions. Should these directional variables still hold false, we can then perform the actual check by calling the atg_check script - which is at the heart of this tutorial.

Depending on the result [true or false] for each of the 8 directions [up,down,left,right,upleft,upright,downleft,downright] we return the approriate tile value. Convert the 8 variables into a binary and check against the array.

tile = up*128+down*64+left*32+right*16+upleft*8+upright*4+downleft*2+downright;
return atg_array[@ tile];

Part 3: Autotile Grid Slopes [atg_slopes]

Works exactly akin to the squares function, the only difference being that a slope only requires checks for 3 of its adjacent cells. For example slope[1] would only require checks for left,up,upleft - as these represent its open sides. By providing the slope[#] ('sid') when calling this script we can use a switch upon it and only calculate and check the 3 directions neccesary. The resulting 3 direction variables contain [true of false] => 2^3 => 8 possibilites. These are again represented in binary format. As the number is so small, this time around we use a simple switch statement to determine the correct tile value.

EDIT: Tiny bug in the code: case4: gw,gh were accidentally swapped in the code. X checks vs grid width, and y the height. In my sqaure room 6400x6400 this mistake had no effect!

Part 4: Autotile Grid Check [atg_check] - the heart of this tutorial.

This script is where the actual neighbor check takes place. It needs to know the origin cell checking (xx,yy), and the direction its checking [target cell] (xx+xp,yy+yp) - where xp,yp are xplus and yplus [-1,0,1]. This script needs to called for each cell that is to be checked seperatly. So in effect 8 times per square cell and 3 times per slope. It replaces the simple grid[xx,yy]==WALL check that we can use for purely square walls.

What happens here is a tad more complex than before, thus I've provided a visualization to help you understand whats going on. The picture also indicates which parts of the code lead to which results, and why they are neccesary. For further explanation see the detailed clarification below!

It is important to note that the slope values in the grid [1,2,3,4] are NOT chosen at random - and actually matter here, as it alings so nicely with yet another binary check.


In depth clarification:

By implementing the code shown in the second sub-picture above [inside the spoiler] - next to the switch statement - we already get good results, but there are alot of little "triangles" missing inside the wall units, which are visually unappealing. The following logic solves this issue:

Checking up, down, left or right [90° angles]

  • If the target cell is a square wall - simply return true. This case is the same as without any slopes.
  • For slope target cells we need to know where we are checking from, and which sides of the slope point towards the origin cell and thus connect nicely. For slope 1 this is either left, up, or upleft. But upleft can be excluded, as we're only concerned with 90° angles here. Thus any check from the right (xp>0) or down (yp>0) from the origins points of view, returns true for slope 1.
  • Slope 3 for example may connect (open sides) to the origin cell on its left or down sides. Thus a check coming from the right (xp>0) or up (yp<0), from the origins point of view, return true;

Checking upleft, upright, downleft, downright [45° angles]

  • The check is now always along both axis simultaneously. Thus xplus and yplus (xp,yp) may each only hold [-1,1]. We can represent the direction [xp,yp] in binary.
    • bnry = (xp>0)+(yp>0)*2
      • 0 for [-1,-1],
      • 1 for [1,-1] ,
      • 2 for [-1,1] ,
      • 3 for [1,1].
    • Looking at the picture below, where all slope types are combined to form a single diamond block, we can see that if we get binary 0 for example, thus [-1,-1] direction, slope 4 has both of its open sides facing towards that cell. The same holds true for all other slopes and binary values. Thus a slope with both open sides towards the origin is present if
      • 4-binary==slope_id. [slope_id, or slope value, is stored in the grid, refered to as grid value (gv) in my code]
  • For a square target cell to belong to the same wall unit, its neighbors along atleast one of the seperate axis needs to be a square, or a slope, which connect the target cell and the origin cell. For slopes, this is dependant on the angle we are checking for. Infact the only time this is not the case is IF EITHER adjacent cell along a single axis is perfectly connecting to the origin cell. This would close off the target cell from the wall unit performing the check. Return false :not part of the unit - consider this target cell "free", and place the little missing triangle inside the origin cell. Again: No connection to the target cell is established if 4-binary==slope id, as this slope is a perfect connection to the origin (both free sides face the origin, non connect to the target). The slope id is stored in the grid value (gv) variable).
    • Example as shown in the picture hidden in the spoiler directly above this explanation: refer to subpictures 2 (with the boatload of little colored squares and the yellow text, and picture 3, which shows the solved status).
    • we are checking for a square target cell in direction [1,-1], and see that both neighbors along the seperate axis are slope 3, thus cutting off the target square from the origin square cell.
      • We check xp[+1] and yp [-1]
      • bnry= (xp>0)+(yp>0)*2 = 1; as xp>0 true, and yp>0 false;
      • 4-bnry[1] = slope 3. Slope 3 perfectly connects to the origin and thus excludes the target cell from the wall unit. Consider the target cell free.
  • For a slope target cell to belong to the same wall unit, it must first perfectly fit there (as in both its open sides need to perfectly connect // face towards the checking origin cell - this is represented as 4-binary==slope id. AND BOTH its adjacent cells along a single axis may not be identical to the target slope cell, as these would in turn close of the target cell from the wall unit. See picture below to see how the target cell would theoretically fit perfectly, but the adjacent cells along each individual axis are both also slope 4, thus denying any connection between the target and the checking origin cell [square 5]. Therefore consider the target cell to free and place the little missing triangle inside the origin cell.

Final thoughts:

I sincerely hope this tutorial helped you and i would love to see my system, or some other verion of it in one of your games :)
Whilst the logic behind slopes may at times be complex, they are fairly easy to implement with the help of my scripts and I feel like they really open up new possibilites in term of gameplay. Especially since all of this, including my previous tutorial is purely grid based, and thus very light on fps.

Go check out my other tutorial if you haven't on how to make grid collisions work, including line of sight, place_meeting and an example of a 2D topdown movement code to go along with it. All are featured in this tech demo .exe + .gmz aswell. Therefore there is no need to download the old tutorial!

p.s.: if you are an artist and would like to share one or two of your cool tile assets with me (preferably including slopes ! ), or would be willing to create a neat character sprite for me: send me pm ;)
Last edited: