← View other resources
resource

Expandable Collision System (with slopes)

Create a robust collision system for your platformer (or other types of games).
· 30 min read · 303 views
Welcome! It looks like you're visiting from outside our community.
This free resource was submitted by a member of our Game Maker community. If you're interested in finding out more about game development, game jams or GameMaker Studio 2, check out these links below.

Hello, GameMakers.

This is a tutorial for one of many ways to code your collision system for your platformers or top-down games or whatever, that you can easily expand with different types of tiles if you need, and after the initial setup it should be really quick and easy to build your rooms. I’m also going to show you how I did slopes in my platformer. It’s a pretty long tutorial, but after you’re done you should have a great system that will pay off.

If you want to download the project and test it, here’s a link to it.

So how does it work? This method uses the same concept of Pope’s precise collision tutorial, but in a different way. In my way we’ll be reading the room for which tiles represent collision and spawning instances of the collision object with the correct image_index. That means we’ll be creating an instance for each tile that represents a collision, which might bring you a few questions. Why use instances for each tile, why not use tilemap_get_at_pixel and what if my room is huge? We are using instances for each tile because it makes the setup and world building easier, but most importantly because that allows us to code different behavior for different tiles, it gives us flexibility. We are not using tilemap_get_at_pixel because of two reasons: that function still checks for a rounded coordinate in the room, it doesn’t work with the new collision change in the January update where bounding boxes are floats; and again because collision with instances gives us more flexibility. And to answer the third question, don’t worry in case your room is huge, there are ways to optimize this.

In summary, we’ll be:

  • Setting up a tileset for collision
  • Setting up an object to represent the tiles
  • Creating an array for the behavior of each tile
  • Creating a character for testing
  • Coding the collision
  • Reading the room and creating collision instances
  • Expanding the collision system with slopes
  • Conclusion

1

Setting up a tileset for collision

Let’s start simple, for now I’m going to create a sprite that is 320x160 with a 32x32 grid that I called sprCollisionTileset, to serve as a sprite for a tileset of 10x5 tiles, probably more tiles than needed for different types of collision. This is what I painted:

For now, I just made one tile represent a solid block and one that will represent pass-through platforms.

Note: you don’t really need a tileset to specifically represent the collisions, you can set the system up to whatever tileset you have for your game, thus making painting your rooms much easier. But since I’m making a tutorial for every use case, I’m making a tileset just for the collision. I’ll show you how to set up the system to your specific tileset later.

2

Setting up an object to represent the tiles

Now, I’m going to create a sprite that will just serve as masks for the tiles. To make it easier I’ll make each frame of the sprite the same as the tiles in the sprCollisionTileset. I named it sprCollisionTiles:

Also, you should set the fps of the sprite asset to 0, and set the collision mask to precise per frame, and leave the origin point at Top Left:

3

Creating an array for the behavior of each tile

Ok, let’s get to coding. Now you’ll get an idea how my system mostly works. I’m going to create a script asset called collision_system for this. For starter, I’ll write an enum to use as reference to the different types of collision tiles which will be the same as the index of the array containing these different tiles, like so:

enum COLLISION_TILE {
    NONE,
    SOLID,
    PLATFORM,
    LAST
}

I’m putting an enum for LAST just so I can reference it when creating my array and I can add more tiles in the middle without worrying about it. Let’s create the array:

global.collisions = array_create(COLLISION_TILE.LAST);

Ok now that I have an array with a size of 3, I’ll code the behavior of each tile individually by creating a struct in the index of each tile with a function for each movement type my character has, which in the case of this tutorial is only moving horizontally on the ground, moving up in the air and moving down in the air. The collision for the solid block will be very straightforward, but the platform one will need some explaining. This is how I do it, still in the same script:

#region Collision structs

global.collisions[COLLISION_TILE.SOLID] = {
    move_h : function(_tile) {
        return 1;
    },
    move_air_up : function(_tile) {
        return 1;
    },
    move_air_down : function(_tile) {
        return 1;
    }
}
global.collisions[COLLISION_TILE.PLATFORM] = {
    move_h : function(_tile) {
        return 0;
    },
    move_air_up : function(_tile) {
        return 0;
    },
    move_air_down : function(_tile) {
        with (other) {
            if (yprevious <= _tile.y) {
                return 1;
            }
            return 0;
        }
    }
}

#endregion

What is that?! There’s a lot there that won’t make sense until we get to the collision code, which we are about to. But for now let me just explain some details about that code:

  • You may realize that the functions are receiving a _tile argument, that’s the id of the instance that I’m colliding with.
  • I’m returning the number 1 to represent a collision that makes the character have a full stop, and returning 0 when I want the character to continue moving as if there was no collision.
  • I’m using with (other) to enter the scope of the instance calling the function, instead of being in the scope of the struct. If you’re not familiar with structs yet, you should really look into them.
  • You may also notice that I’m using built-in variables like vspeed. I personally like using the built-in movement variables, they work for me, but maybe in your project you prefer to use a created variable vsp and hsp or something, most people do.
  • The line if (yprevious <= _tile.y) { is to make sure that I’m only stopping on the platform if the character’s feet was above the platform before detecting the collision, if the character doesn’t cross that threshold, it continues to fall as if it didn’t collide with the platform. Also keep in mind that yprevious is not different from y in the step event, I’m manually doing yprevious = y - vspeed in the collision code.
  • The tile’s origin point is its top-left corner, but the character’s origin point is in the bottom-middle. That’s why I can check the character’s y against the tile’s y.

Alright, let’s get to the collision code now.

4

Coding the collision

This is going to be a big section. But before anything, let’s create our collision object, since we haven’t done that yet. Just create an object, I called it oCollision, then set its sprite to sprCollisionTiles. That's it for that.

For the collision code, I will make 2 different functions, one for moving on the ground and one for moving in the air. Let’s start with the one for moving on the ground. I’m going to throw the whole code here first and then I’ll explain why I did things from the top:

function move_floor() {

    //Always check at least 0.5 pixels ahead, or the sprite can jitter
    if (abs(hspeed) < 0.5) var extra = (0.55 - abs(hspeed)) * sign(hspeed);
    else var extra = 0;

    var collided = ds_list_create();
    var collide_num = instance_place_list(x+hspeed+extra,y,oCollision,collided,false);
    var wall = -1, index = 0;

    #region Horizontal Collision
    for (var i = 0; i < collide_num; i++) {
        if (collided[| i].object_index == oCollision) {
            index = global.collisions[collided[| i].image_index].move_h(collided[| i])
            if (index == 1) wall = collided[| i];
        }
        // Other Objects
        else {
            if (variable_instance_exists(collided[| i],"move_h")) collided[| i].move_h();
            else {
                wall = collided[| i];
            }
        }
    }

    if (wall != -1) {
        if (x >= wall.bbox_right) {
            x = wall.bbox_right + (bbox_right - bbox_left)/2;
            hspeed = 0;
        }
        else if (x <= wall.bbox_left) {
            x = wall.bbox_left - (bbox_right - bbox_left)/2;
            hspeed = 0;
        }
    }
    #endregion

#region Checking floor
    wall = -1;
    ds_list_clear(collided);
    collide_num = collision_rectangle_list(bbox_left+0.5,y,bbox_right-0.5,y+abs(hspeed + extra)+1,oCollision,true,true,collided,false);

    for (i = 0; i < collide_num; i++) {

        if (collided[| i].object_index == oCollision) {
            index = global.collisions[collided[| i].image_index].move_h(collided[| i]);
            if (index == 1) wall = collided[| i];
        }
        else {
            wall = collided[| i];
        }
    }
    //if there is a block underneath, stay on top of it
    if (wall != -1) {
        y = wall.bbox_top;
    }
    else if (collide_num == 0){
        //Set character to a falling state here

        //I'm just going to set the state of my player to air for my tutorial
        //but you should probably do something particular to your game
        state = playerState.air; //This is an enum I created
    }
    #endregion

    ds_list_destroy(collided);
}

We are going to expand on this function later, but for now this is all we need to collide with walls horizontally. So, what’s going on with this function?

  • First of all, since our walls are instances and we get those instances’ ids, we can use their bbox values to set our character’s position. We don’t need to do the traditional while (not colliding) move closer to the wall;.
  • What’s that extra var doing there? With the collision changes in January, bounding boxes can have float values. For example, your character’s bbox_right value could be 40.0, and the wall’s bbox_left value can also be 40.0, and those 2 instances won’t be colliding. I’m using the wall’s bbox values to set the position of the player so it snugs to the wall when you collide with it. The problem is that you only collide with the wall in this example when your bbox_riight is 40.5, it doesn’t collide if the bbox_right is 40.4. So in case you make characters that could be moving at speeds lower than 0.5, for example in case you add acceleration, it could cause the instance to jitter as it moves back and forth in the 0.5 pixel range. Therefore, when speed is lower than 0.5 we make sure to check for a number that is at least higher than that.
  • Why are you creating a ds_list and using instance_place_list? Different tiles can have different behavior. Our solid tile tells the player to stop but the platform tile tells the player they can continue moving horizontally if your head is colliding with it, for example. If we don’t get a list of everything we collide, we could have conflicts and enter a wall if the instance returned is a platform and not the wall.
  • After getting the ids of everything we collided with, we loop through the list and if the instance we collided with is from an object oCollision, we run the function move_h in one of the structs in the global.collisions array in which the index of the array is the same as the image_index of the sprCollisionTiles sprite, and we store the value returned to the index variable. Then we check if that index is 1 and store it to a wall variable. Why do I do that? For now for no great reason, but later because we want to code some exceptions in case a character is on a slope.
  • What about the part of other objects and that variable_instance_exists? That’s because if we want to create other objects to collide with, they could be children of oCollision, and we could code something different to happen when a character collides with that object in case a move_h method variable exists in that instance object. If it doesn’t we just treat that object as a normal wall that stops you.
  • What’s up with the region Checking floor? For now it’s really just checking if we are standing on something and setting fall state if not, but this region will be important to slopes later. This region is also where you could code special behaviors of stepping on things, for example stepping on a button, or slippery ground. I’m not going to cover that in this tutorial, though.
  • Notice that we are resetting variables and clearing the ds_list before using them again.
  • Why am I using collision_rectangle_list instead of instace_place_list for checking the ground? Good question! I don’t remember why. I used to use instance_place_list before but I changed it for some good reason, I believe.

Ok. Now for the function for movement in the air. This function is going to be a lot longer than the one for ground movement, feel free to copy it and then I’ll try to explain why I did some things. I divided it in two sections, one for going up and one for going down. But before that, I advise you to create 2 macros in the beginning of the collision script:

#macro grav 0.25
#macro MAX_FALL_SPEED 6

You can set the values to whatever you like. Now the function:

function move_air() {

    yprevious = y - vspeed;
    vspeed = min(MAX_FALL_SPEED,vspeed + grav);

    //Always check at least 0.5 pixels ahead, or the sprite can jitter
    if (abs(hspeed) < 0.5) var extra = (0.55 - abs(hspeed)) * sign(hspeed);
    else var extra = 0;

    if (!place_meeting(x+hspeed+extra,y+vspeed,oCollision)) {
        return; //if No collision was found, no more code needs to be run
    }

    var collided = ds_list_create();
    var collide_num;

    #region Check going up
    if (vspeed < 0) {

        collide_num = instance_place_list(x+hspeed+extra,y+vspeed,oCollision,collided,false);
        var wall = array_create(0), index = 0;

        for (var i = 0; i < collide_num; i++) {
            index = collided[| i].image_index;

            if (collided[| i].object_index == oCollision) {
                index = global.collisions[collided[| i].image_index].move_air_up(collided[| i]);
                if (index == 1) array_push(wall,collided[| i]);

            }else {
                // This is a good place to code custom behavior for other objects
                array_push(wall, collided[| i]);
            }
        }

        // Collision with solid stuff
        var wall_num = array_length(wall);
        if (wall_num > 0) {

            // check if there was a collision vertically first
            // this is a good place to set corner correction to nudge the character further
            // to not hit the ceiling if they hit just the edge of the bounding box
            for (i = 0; i < wall_num; i++) {
                if (place_meeting(x,y+vspeed,wall[i])) {
                    y = wall[i].bbox_bottom + bbox_bottom - bbox_top;
                    vspeed = 0;
                }
if (place_meeting(x+hspeed+extra,y+vspeed,wall[i]) and !place_meeting(x,y,wall[i])) {
                if (x >= wall[i].bbox_right) {
                    x = wall[i].bbox_right + (bbox_right - bbox_left)/2;
                    hspeed = 0;
                }
                else if (x <= wall[i].bbox_left) {
                    x = wall[i].bbox_left - (bbox_right - bbox_left)/2;
                    hspeed = 0;
                }
            }
            }
        }

    #endregion

    }else {

    #region Check Going down

        collide_num = instance_place_list(x+hspeed+extra,y+vspeed,oCollision,collided,false);
        var wall = array_create(0), index = -1;

        for (var i = 0; i < collide_num; i++) {
            index = collided[| i].image_index;

            if (collided[| i].object_index == oCollision) {

                index = global.collisions[collided[| i].image_index].move_air_down(collided[| i]);
                if (index == 1) array_push(wall,collided[| i]);

            }
            else {
                array_push(wall,collided[| i]);
            }
        }

        var wall_num = array_length(wall);
        if (wall_num > 0) {
            for (i = 0; i < wall_num; i++) {
                if (place_meeting(x,y+vspeed,wall[i]) and y <= wall[i].bbox_top) {
                    y = wall[i].bbox_top;
                    vspeed = 0;
                }

            if (place_meeting(x+hspeed+extra,y+vspeed,wall[i])) {
                if (x >= wall[i].bbox_right) {
                    x = wall[i].bbox_right + (bbox_right - bbox_left)/2;
                    hspeed = 0;
                }
                else if (x <= wall[i].bbox_left) {
                    x = wall[i].bbox_left - (bbox_right - bbox_left)/2;
                    hspeed = 0;
                }
            }
            }
        }
    }
    #endregion

    ds_list_destroy(collided);
}

Alrighty, what’s going on there?

  • We’re setting the built-in variable yprevious to y - vspeed because GameMaker updates that variable before Begin Step, making it the same value as y, so I set it back to the previous value in order to use it.
  • We’re also limiting our fall speed to the value in the macro we created. The rest of the code is pretty similar to my explanations for moving horizontally, except that in the air we’re checking for every wall we collide with instead of any wall. That way we can prevent some conflicts.

Phew! We got our system, we got the functions, can we finally get to testing? You can, but that would require you to place oCollision instances in the room, and that takes too much work for my taste. We want a system to make the game do things for us and make level building as simple as possible. We also need a playable character, so I’m going to show a simple one next.

5

Creating a character for testing

You can skip this part if you already have a character that you created, but in case you want to know where I put the functions or how to make a simple one, here’s one:

//Create event

state = 0;

enum playerState {
    ground,
    air
}

// Step event
var move = keyboard_check(vk_right) - keyboard_check(vk_left);

switch (state) {

    case playerState.ground:
        if (move != 0) {
            hspeed = move * 2.32;
        }else {
            hspeed = 0;
        }

        move_floor();

        if (keyboard_check_pressed(vk_space)) {
            vspeed = -6;
            state = playerState.air;
        }
    break;

    case playerState.air:
        if (move != 0) {
            hspeed = move * 2.32;
        }else {
            hspeed = 0;
        }

        move_air();
    break;
}

My character in my tutorial is just a rectangle with a rectangular mask and an origin point at the Bottom Center. I put an hspeed of 2.32 just to show you that with the collision changes in January, you can have a value with fractions for movement and it will still work. On to the next step.

6

Reading the room and creating collision instances

First, let’s create a macro for the size of your tiles, we’ll need it in multiple places. I created a macro in the collision script called TILE_SIZE with a value of 32. Now let’s create an object for reading the room. I named mine oSystem, you can call yours whatever you want. You should make it a persistent object and put it only in the first room of your game, or make it get created by code in the first room of the game by coding that in a script asset. Then, create a Room Start event. This is the code you want to put in it, or actually an example of code to put in it:

var tilemap = layer_tilemap_get_id(layer_get_id("Collisions"));
var tileS = 32; //This is the size of the tiles in our tileset
layer_set_visible("Collisions",false);
var w = room_width div tileS,
    h = room_height div tileS,
    inst, tile, index;
if (!layer_exists("Collision_Objects")) {var col_layer = layer_create(0,"Collision_Objects");}

for (var i = 0; i < w; i++) {
    for (var o = 0; o < h; o++) {
        tile = tilemap_get(tilemap,i,o);
        index = tile_get_index(tile);
        if (index > 0) {
            inst = instance_create_layer(i*tileS,o*tileS,col_layer,oCollision);
            inst.image_index = index;
        }
    }
}

Not a very big event, though if you want you could really expand this event to your game’s needs, in the case of creating special tiles or objects that are represented by tiles in the room. So what’s happening in this event?

  • We are reading a layer named “Collisions”, which is just an example for this tutorial, you can name it however you want, but make sure the name is the same across all your levels.
  • We make this layer invisible and we create a new layer that will be just for the collision instances, which normally would be invisible, but aren’t in this tutorial.
  • Then we read the whole room checking every tile and if there is a tile there, we create an instance of oCollision with an image_index of the same index of the tile.

This is a very simplified version of this code. You could set this event up to whatever tilesets your game is using and completely not need a collision tile layer and not need to place the collision tiles. This makes level building much faster than placing collision instances yourself.

If your game doesn’t have slopes, this is all you need in the tutorial to get to adapting to your game. You can skip to the Conclusion for some further tips. But if you want slopes in the game (or maybe that’s the only reason you’re here), I’m also going to show you how to do it. I’m going to expand on the collision array and the movement functions and explain what’s going on with them. On to the next section.

7

Expanding the collision system with slopes

First, let’s create the mask for the slopes, both in sprCollisionTileset, and in sprCollisionTiles. It looks like this in my project: For sprCollisionTiles.

For sprCollisionTileset.

Now, we need to add these new tile types to our enum, this is how mine looks now:

enum COLLISION_TILE {
    NONE,
    SOLID,
    PLATFORM,
    SLOPE_STEEP_UP,
    SLOPE_STEEP_DOWN,
    SLOPE_SOFT_UP_1,
    SLOPE_SOFT_UP_2,
    SLOPE_SOFT_DOWN_2,
    SLOPE_SOFT_DOWN_1,
    LAST
}

And now we add their behavior in our collision array of structs:

global.collisions[COLLISION_TILE.SLOPE_STEEP_UP] = {
    move_h : function(_tile) {
        var _x = other.x mod _tile.x;
        if (_x <= TILE_SIZE) {
            other.y = _tile.bbox_bottom - _x;
            return 2;
        }
        return 0;
    },
    move_air_up : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_bottom - _x;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    },
    move_air_down : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_bottom - _x;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    }
}
global.collisions[COLLISION_TILE.SLOPE_STEEP_DOWN] = {
    move_h : function(_tile) {
        var _x = other.x mod _tile.x;
        if (_x <= TILE_SIZE) {
            other.y = _tile.bbox_top + _x;
            return 2;
        }
        return 0;
    },
    move_air_up : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_top + _x;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    },
    move_air_down : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_top + _x;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    }
}
global.collisions[COLLISION_TILE.SLOPE_SOFT_UP_1] = {
    move_h : function(_tile) {
        var _x = other.x mod _tile.x;
        if (_x <= TILE_SIZE) {
            other.y = _tile.bbox_bottom - 0.5*_x;
            return 2;
        }
        return 0;
    },
    move_air_up : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_bottom - 0.5*_x;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    },
    move_air_down : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_bottom - 0.5*_x;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    }
}
global.collisions[COLLISION_TILE.SLOPE_SOFT_UP_2] = {
    move_h : function(_tile) {
        var _x = other.x mod _tile.x;
        if (_x <= TILE_SIZE) {
            other.y = _tile.bbox_bottom - 0.5*_x - TILE_SIZE/2;
            return 2;
        }
        return 0;
    },
    move_air_up : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_bottom - 0.5*_x - TILE_SIZE/2;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    },
    move_air_down : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_bottom - 0.5*_x - TILE_SIZE/2;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    }
}
global.collisions[COLLISION_TILE.SLOPE_SOFT_DOWN_1] = {
    move_h : function(_tile) {
        var _x = other.x mod _tile.x;
        if (_x <= TILE_SIZE) {
            other.y = _tile.bbox_top + 0.5*_x + TILE_SIZE/2;
            return 2;
        }
        return 0;
    },
    move_air_up : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_top + 0.5*_x + TILE_SIZE/2;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    },
    move_air_down : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_top + 0.5*_x + TILE_SIZE/2;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    }
}
global.collisions[COLLISION_TILE.SLOPE_SOFT_DOWN_2] = {
    move_h : function(_tile) {
        var _x = other.x mod _tile.x;
        if (_x <= TILE_SIZE) {
            other.y = _tile.bbox_top + 0.5*_x;
            return 2;
        }
        return 0;
    },
    move_air_up : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_top + 0.5*_x;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    },
    move_air_down : function(_tile) {
        with (other) {
            var _x = (x+hspeed) mod _tile.x,
                _y = _tile.bbox_top + 0.5*_x;
            if (_x <= TILE_SIZE) {
                if (y+vspeed >= _y) {
                    y = _y;
                    vspeed = 0;
                    state = playerState.ground;
                }
                return 2;
            }
            return 0;
        }
    }
}

What’s going on there?

  • Just to remind you, TILE_SIZE is a macro I created with the width or height value of my tiles, which is 32.
  • We’re calculating the x position of the character along the slope tile, or in other words how far the character is on the tile. Since we want the character’s bottom center point to determine how high the character needs to be in the tile, we don’t consider the character to be on a slope unless that point is already inside the slope tile.
  • Then, we use that _x value to determine what is the y the character should be in the slope. That y is hard coded according to the type of slope the character is in. Also, the character only stops falling and lands on the slope tile if it was about to reach the intended height it should be on the tile, and not any sooner.

Now the revisioned collision functions:

function move_floor() {

    //Always check at least 0.5 pixels ahead, or the sprite can jitter
    if (abs(hspeed) < 0.5) var extra = (0.55 - abs(hspeed)) * sign(hspeed);
    else var extra = 0;

    var collided = ds_list_create();
    var collide_num = instance_place_list(x+hspeed+extra,y,oCollision,collided,false);
    var wall = -1, index = 0, slope = -1;

    #region Horizontal Collision
    for (var i = 0; i < collide_num; i++) {
        if (collided[| i].object_index == oCollision) {
            index = global.collisions[collided[| i].image_index].move_h(collided[| i])
            if (index == 1) wall = collided[| i];
            if (index == 2) slope = collided[| i];
        }
        // Other Objects
        else {
            if (variable_instance_exists(collided[| i],"move_h")) collided[| i].move_h();
            else {
                wall = collided[| i];
            }
        }
    }

    //If colliding with wall and there are no slopes
    if (slope == -1 and wall != -1) {
        if (x >= wall.bbox_right) {
            x = wall.bbox_right + (bbox_right - bbox_left)/2;
            hspeed = 0;
        }
        else if (x <= wall.bbox_left) {
            x = wall.bbox_left - (bbox_right - bbox_left)/2;
            hspeed = 0;
        }
    }

    //If on a slope and colliding with a wall while going up
    //Can't have a wall right next to the slope and another above that
    else if (slope != -1 and wall != -1) { 
        if (place_meeting(x+hspeed+extra, slope.bbox_top, wall)) {
            if (x >= wall.bbox_right) {
                x = wall.bbox_right + (bbox_right - bbox_left)/2;
                hspeed = 0;
            }
            else if (x <= wall.bbox_left) {
                x = wall.bbox_left - (bbox_right - bbox_left)/2;
                hspeed = 0;
            }
        }
    }
    #endregion

    #region Checking floor
    wall = -1;
    slope = -1;
    ds_list_clear(collided);
    collide_num = collision_rectangle_list(bbox_left+0.5,y,bbox_right-0.5,y+abs(hspeed + extra)+1,oCollision,true,true,collided,false);

    for (i = 0; i < collide_num; i++) {

        if (collided[| i].object_index == oCollision) {
            index = global.collisions[collided[| i].image_index].move_h(collided[| i]);
            if (index == 1) wall = collided[| i];
            if (index == 2) slope = collided[| i];
        }
        else {
            wall = collided[| i];
        }
    }
    //if there is a block and not on a slope, stay on top of it
    if (slope == -1 and wall != -1) {
        y = wall.bbox_top;
    }
    else if (collide_num == 0) {
        //Set character to a falling state here

        //I'm just going to set the state of my player to air for my tutorial
        //but you should probably do something particular to your game
        state = playerState.air;
    }
    #endregion

    ds_list_destroy(collided);
}

function move_air() {

    yprevious = y - vspeed;
    vspeed = min(MAX_FALL_SPEED,vspeed + grav);

    //Always check at least 0.5 pixels ahead, or the sprite can jitter
    if (abs(hspeed) < 0.5) var extra = (0.55 - abs(hspeed)) * sign(hspeed);
    else var extra = 0;

    if (!place_meeting(x+hspeed+extra,y+vspeed,oCollision)) {
        return; //if No collision was found, no more code needs to be run
    }

    var collided = ds_list_create();
    var collide_num;

    #region Check going up
    if (vspeed < 0) {

        collide_num = instance_place_list(x+hspeed+extra,y+vspeed,oCollision,collided,false);
        var wall = array_create(0), index = 0, slope = -1;

        for (var i = 0; i < collide_num; i++) {
            index = collided[| i].image_index;

            if (collided[| i].object_index == oCollision) {
                index = global.collisions[collided[| i].image_index].move_air_up(collided[| i]);
                if (index == 1) array_push(wall,collided[| i]);
                if (index == 2) slope = collided[| i];

            }else {
                // This is a good place to code custom behavior for other objects
                array_push(wall, collided[| i]);
            }
        }

        // Collision with solid stuff and while not on a slope
        var wall_num = array_length(wall);
        if (wall_num > 0 and slope == -1) {

            // check if there was a collision vertically first
            // this is a good place to set corner correction to nudge the character further
            // to not hit the ceiling if they hit just the edge of the bounding box
            for (i = 0; i < wall_num; i++) {
                if (place_meeting(x,y+vspeed,wall[i])) {
                    y = wall[i].bbox_bottom + bbox_bottom - bbox_top;
                    vspeed = 0;
                }

                if (place_meeting(x+hspeed+extra,y+vspeed,wall[i]) and !place_meeting(x,y,wall[i])) {
                    if (x >= wall[i].bbox_right) {
                        x = wall[i].bbox_right + (bbox_right - bbox_left)/2;
                        hspeed = 0;
                    }
                    else if (x <= wall[i].bbox_left) {
                        x = wall[i].bbox_left - (bbox_right - bbox_left)/2;
                        hspeed = 0;
                    }
                }
            }
        }

        //Don't collide with wall next to slope if jumping from a slope
        else if (slope != -1 and wall_num > 0) {
            if (slope.y != wall[0].y) {
                if (x >= wall[0].bbox_right) {
                    x = wall[0].bbox_right + (bbox_right - bbox_left)/2;
                    hspeed = 0;
                }
                else if (x <= wall[0].bbox_left) {
                    x = wall[0].bbox_left - (bbox_right - bbox_left)/2;
                    hspeed = 0;
                }
            }
        }

    #endregion

    }else {

    #region ============== Check Going down =========

        collide_num = instance_place_list(x+hspeed+extra,y+vspeed,oCollision,collided,false);
        var wall = array_create(0), index = -1, slope = -1;

        for (var i = 0; i < collide_num; i++) {
            index = collided[| i].image_index;

            if (collided[| i].object_index == oCollision) {

                index = global.collisions[collided[| i].image_index].move_air_down(collided[| i]);
                if (index == 1) array_push(wall,collided[| i]);
                if (index == 2) slope = collided[| i];
            }
            else { //Other Objects, Might need to copy from tile code
                array_push(wall,collided[| i]);
            }
        }

        // Collision with a wall and no slopes
        var wall_num = array_length(wall);
        if (wall_num > 0 and slope == -1) {
            for (i = 0; i < wall_num; i++) {
                if (place_meeting(x,y+vspeed,wall[i]) and y <= wall[i].bbox_top) {
                    y = wall[i].bbox_top;
                    vspeed = 0;
                    //Set your character state back to ground
                    state = playerState.ground;
                }

                if (place_meeting(x+hspeed+extra,y+vspeed,wall[i])) {
                    if (x >= wall[i].bbox_right) {
                        x = wall[i].bbox_right + (bbox_right - bbox_left)/2;
                        hspeed = 0;
                    }
                    else if (x <= wall[i].bbox_left) {
                        x = wall[i].bbox_left - (bbox_right - bbox_left)/2;
                        hspeed = 0;
                    }
                }
            }
        }
        //Collide with wall if not at the same height as the slope
        else if (slope != -1 and wall_num > 0) {
            if (slope.y != wall[0].y) {
                if (x >= wall[0].bbox_right) {
                    x = wall[0].bbox_right + (bbox_right - bbox_left)/2;
                    hspeed = 0;
                }
                else if (x <= wall[0].bbox_left) {
                    x = wall[0].bbox_left - (bbox_right - bbox_left)/2;
                    hspeed = 0;
                }
            }
        }
    }
    #endregion

    ds_list_destroy(collided);
}

There are new comments in the code for some specifics, but in general what changed?

  • We made new vars to check for slope.
  • We check the index returned and if it’s 2, it’s a slope. Then we store the id of the slope in the variable, or set slope to true. There are better ways to do this, but this works.
  • Then we check if we’re only on a solid block or both a solid block and a slope at the same time. If it’s both, we need to code some exceptions. This is because when going up and down slopes, we’ll be colliding with solid blocks, but we don’t want those solid blocks to stop us in that situation.

Now, just to prove the versatility of this system, I’m going to show you more examples of what you can do.

8

Creating additional examples

Let’s create a spring tile that we can bounce from, and a stone that we can push around. Let’s start with the spring. First create a tile for it in sprCollisionTileset:

Then add it to the sprCollisionTiles:

Then add it to the enum and the collision array:

global.collisions[COLLISION_TILE.SPRING] = {
    move_h : function(_tile) {
        return 0;
    },
    move_air_up : function(_tile) {
        return 0;
    },
    move_air_down : function(_tile) {
        other.vspeed = -10;
        return 0;
    }
}

Done. That barely took a minute. Now for a stone we can push. Create a sprite and an object for it. I called it oStone. Make it a child of oCollision. Then add a create event and an alarm and put this:

//Create event
frame = 0;
spd = 1;

move_h = function() {
    alarm[0] = 2;
    if (frame++ >= 15) {
        var dir = sign(other.hspeed);
        if (!place_meeting(x + (spd*dir),y,oCollision)) {
            other.x += spd*dir;
            x += spd*dir;
        }
        other.hspeed = 0;
    }
    else {
        if (other.x >= bbox_right) {
            other.x = bbox_right + (other.bbox_right - other.bbox_left)/2;
            other.hspeed = 0;
        }
        else if (other.x <= bbox_left) {
            other.x = bbox_left - (other.bbox_right - other.bbox_left)/2;
            other.hspeed = 0;
        }
    }
}

//Alarm 0
frame = 0;

And done again. Easy peasy. And that’s the versatility of the system.

9

Conclusion

Depending on your game’s needs, you might want to expand on this system and the functions further, but the building blocks are there. You can customize the event to create the collision instances to whatever your tilesets need. You can adapt this code to a top-down game, make tiles that represent spikes or pitfalls or walls that are dangerous. You can make objects with their own behavior when you touch them. It’s really easy to expand this system. If you have huge rooms and want to optimize performance, you can deactivate instances outside of the camera, like in the Forager tutorial.

I personally did some extra things for my game, like: using bitmasking to change the behavior of my player when stepping on different types of ground, instead of just checking if index is equal 1 for solid or 2 for slopes; I added corner correction for both going up in the air and when falling and missing the ground by a few pixels; I also coded some special cases for going up and down and stepping on different objects, but that’s specific to my game. Feel free to ping @Ailwuful in the GameMaker discord if you have questions about something or want to expand the system. Good luck with your game!