Playing with Collision Masks


Playing with Collision Masks

The latest GameMaker Studio 2 update (2.1.5) has added a new collision mask kind to the Sprite Editor. This addition is the rotated rectangle collision mask and in this short tech blog we're going to show you how it works and at the same time revise the already existing collision masks, showing their differences through the use of a small test project that you can then build on and play with later. So, to get started, you'll want to create a new project in GameMaker Studio 2 (it can be GML or DnD™, as the article will cover both)...


SETTING UP SPRITES

For the sake of this article, we'll be using some very simple sprites of different shapes:

Sprite SHapes

You can download those (each one is 64x64 px) or make your own, but it's important to note that if you make your own then they should be appropriate for the different mask kinds that we'll be using. In total we want to create six new sprites:

  • spr_Rectangle - Uses the square sprite
  • spr_Rotated_Rectangle - Uses the rectangular sprite
  • spr_Ellipse - Uses the circular or elliptical sprite
  • spr_Diamond - Uses the diamond sprite
  • spr_Precise - Uses the "blob" sprite
  • spr_Precise_Animated - Uses each of the above mentioned single sprites as different sub-images

Go ahead and make these now, and for each sprite added, you will need to set the origin to the middle/center and then apply the correct collision mask, as illustrated in the GIF below:

Add Sprite Mask

Once you have done that your resource tree should look something like this:

Sprites In The Resource Tree


ADDING OBJECTS

To match our sprites, we'll also need some objects, so now add 6 new objects into the project, and assign a separate sprite to each of them. Keep in mind, that for an instance of an object to detect a collision, it must have a sprite assigned to it, as the collision events and most functions will work off of the collision mask of the assigned sprite. When you've done that, and named each of the objects appropriately, you'll need to add another object to be used as the Parent object for collision detection. Call this object obj_Collision_Parent or something similar and then assign it as the parent to all 6 of the shape objects (if you are unsure of how to do this, please see the manual).

We need to add one more object now, which will act as the "controller" object for the project, so add another object now and call it something like obj_Control. Your resource tree should now look like this:

Objects In The Resource Tree

We'll come back to the controller object in a bit but for now, we need to set some things in the shapes. To start with open the obj_Rectangle and add the following into the Create Event:

image_angle = irandom(360);

And using DnD™:

obj_Rectangle Create Event

Still in obj_Rectangle, add a Right Mouse Down event. Here we will make the instance rotate when clicked with the right mouse button (so that you can compare what happens when using a regular rectangle mask with a rotated rectangle mask). To rotate the sprite we need:

image_angle += 5;

And the DnD™ would be:

obj_Rectangle Mouse Event

Close the rectangle object now and open obj_Rotated_Rectangle. Here we want to add a Step Event with the following to rotate the rectangle automatically:

image_angle += 1;

The DnD™ would be:

obj_Rotated_Rectangle Step Event

Finally, we want to slow down the animation of the object obj_Precise_Animated, so open that now and add a Create Event with this:

image_speed = 0.1;

And using DnD™ it would be:

Set Animation Speed


ADDING A SCRIPT

For this example, we're going to use a script resource to check for collisions. We want to try and get an exact position for where a line intersects the mask of the shape objects, and that's easiest to do using a small script. So, create a new Script resource, call it collision_line_first, and in it add this:

/// @function               collision_line_first(x1, y1, x2, y2, obj, prec, notme)
/// @param  {real}  x1      The X coordinate to start the line check from
/// @param  {real}  y1      The Y coordinate to start the line check from
/// @param  {real}  x2      The X coordinate to end the line check at
/// @param  {real}  y2      The Y coordinate to end the line check at
/// @param  {id}    obj     The object index to check for a collision with
/// @param  {bool}  prec    Whether to use precise collision checking or not
/// @param  {bool}  notme   Whether to exclude the calling instance from the check or not

/// @description                This script works the same as the collision_line function/action   
///                         only it will return the ID of the first instance found to be in 
///                         collision as well as the X/Y position of the actual collision point.
///                         This information is returned as an array where:
///
///                             [0] = Instance ID of the found instance, or -1 if none are found
///                             [1] = The x position of the collision
///                             [2] = The y position of the collision
///

// Declare arguments
var x1 = argument0;
var y1 = argument1;
var x2 = argument2;
var y2 = argument3;
var obj = argument4;
var prec = argument5;
var notme = argument6;

// Declare internals
var dx = 0;
var dy = 0;
var return_array = array_create(3, -1);

// Get the first hit
var first_instance = collision_line(x1, y1, x2, y2, obj, prec, notme);

// If hit find the exact hit
if instance_exists(first_instance)
    {
    // Get x and y segment lengths
    dx = x2 - x1;
    dy = y2 - y1;
    // Perform check while distances are greater or equal to 1
    while (abs(dx) >= 1 or abs(dy) >= 1)
        {
        // Divide the modifier distance by 2 every iteration
        dx /= 2;
        dy /= 2;
        // Check the new collision line modified by pulling back the end of the hit line by half the distance each loop.
        var new_instance = collision_line(x1, y1, x2 - dx, y2 - dy, obj, prec, notme);
        // If we still hit the instance we didn't move back far enough to get outside of it.
        if (new_instance != noone)
            {
            //set the found instance to what we hit, and pull back the line end by the current modifier
            first_instance = new_instance;
            x2 -= dx;
            y2 -= dy;
            }
        }
    }
else first_instance = -1;

// Set return array
return_array[0] = first_instance;
return_array[1] = x2 - dx;
return_array[2] = y2 - (dy * 2);

return return_array;

If you are using DnD™ then simply create the new script resource and then drag an Execute Code action in and copy/paste the above.

All this script does is check for a collision along a line in "chunks" and then when one is found, try to find the precise point where the collision line intersects the mask of the instance in the collision. The script will return an array of three values: the ID of the instance in the collision, and the x/y coordinates of the mask/line intersection.

With that done, we can set up the controller object.


THE CONTROLLER OBJECT

Open the controller object now and add a Draw Event to it. In this event we'll be using the following code:

draw_set_colour(c_blue);
with (obj_Collision_Parent)
    {
    draw_line(bbox_left, bbox_top, bbox_left, bbox_bottom);
    draw_line(bbox_left, bbox_top, bbox_right, bbox_top);
    draw_line(bbox_right, bbox_top, bbox_right, bbox_bottom);
    draw_line(bbox_left, bbox_bottom, bbox_right, bbox_bottom);
    image_blend = c_white;
    }
draw_set_colour(c_white);

var _array = collision_line_first(x, y, mouse_x, mouse_y, obj_Collision_Parent, true, true);
if _array[0] != -1
    {
    draw_line(x, y, _array[1], _array[2]);
    _array[0].image_blend = c_red;
    }
else
    {
    draw_line(x, y, mouse_x, mouse_y);
    }

For DnD™ users you'd need to have this:

Draw Event DnD™

That's our controller all set up and all that's left is to add a few of instances of each shape object (except the parent object!) into a room along with an instance of the controller object. So, open the default room that was added when the project was created, and drag a number of each instance into the room on the "Instances" layer. 2 or 3 of each will be fine, and then add in an instance of the controller object, placing it in the center of the room:

The Room Editor


START PLAYING!

You should run the project now and move the mouse around the screen, checking to see that each collision mask kind behaves as it should. This means that the instances should only turn red when the mouse is over the collision mask, and the line being drawn should stop at the intersection. However, if you put the mouse over one of the instances with the regular rectangular mask and then click the right mouse button to rotate it, what happens? You can see that the mask does not rotate but instead expands to "fit" within the bounding box (the blue lines):

Rectangle Collision Masks

In the GIF above, on the left we have the rotated rectangular mask and on the right the normal rectangular mask. As you can see, the regular mask does not give the same results when rotated, since the mask is expanded to fit the bounding box rather than rotating. One thing about all this that's important to note, is that when using instance/instance collision events and functions (ie: those things that require both instances to have a collision mask), then if both instances do NOT have the same mask type, the collisions will default to precise checking,

You can now start to edit the project and make changes to see what happens. Here are some suggestions for you to start playing with:

  • change the shape of the collision mask (EG: give the ellipse sprite a diamond mask) and see how that affects things.
  • change the script being used for one of the other collision functions or actions and see how they work.
  • change the bounding box settings in the sprite editor to "clip" the collision masks and see how that changes things.
  • change the image X/Y scale and see how that affects the collision detection for the different masks.
  • switch off the "precise" argument for the script being used (set it to false) and see how the collisions are now all based on the bounding box rather than the mask.

The important thing here is to have fun experimenting and learn how the different collision masks interact with the different collision functions and with each other!



Written by Mark Alexander

Mark Alexander is the former GameMaker technical writer. He’s moved on to pastures new, but still hangs around as admin on the GameMaker Community forum and makes his own indie games as Nocturne Games.