Better moving platforms

(Probably) Not compatible with GameMaker Studio 2.3

The following article is for pre 2.3 versions of GameMaker Studio. It doesn’t take advantage (and it’s probably incompatible) with functions, chained accessors and other new features of GML/GMS2.3 IDE. An updated series is already in the works 🙂

GameMaker 2.3 Beta is out

YoYo Games announced the GameMaker Studio 2.3 Beta. There are significant changes in GML (the GameMaker Language) with the introduction of functions, chained accessors, structs and exception handling. I’m pretty sure the following code (and the previous articles) are not compatible with GameMaker Studio 2.3. Having said so, there is a way to automatically import pre 2.3 projects in version 2.3 so that it gets automatically converted (so you should be able to download this project and import it in GameMaker Studio 2.3).

This article is about better moving platforms in GameMaker. I previously covered a way of doing simple moving platforms (both vertical and horizontal). They can carry the player without glitches. Unfortunately they have a severe limitation: they can’t carry more than one instance. For example, if two entities jump on a moving platform, almost all entities won’t be able to correctly collide with it.

Multiple entities seems to be ignored by the moving platforms.

Why is this happening?

The moving platforms code is simple. Too simple, actually. Indeed it checks for one collision, and only one, with a single instance of oPlayer and then it tries to move that instance before carrying out its own movement. If there’s more than one oPlayer instance, it doesn’t work.

oPlayer should be a child of oEntity

In this article I use oPlayer a lot but if you made oPlayer a child of oEntity, feel free to use oEntity in the coll_x_list and coll_y_list functions. Usually I tend to make enemies as children of oEntity as well so targeting oEntity is preferred if you need your platform to carry them as well.

How to fix the moving platforms

Fixing the problem is simple, in principles, since GameMaker Studio 2 introduced the collision_*_list functions. These functions return a list of multiple instances colliding with the calling instance. Then we will cycle through the found instances and move them one by one.

coll_x_list and coll_y_list

I created a couple of utility scripts for the GMS2 collision_rectangle_list function. I know I will pass an xdir or a ydir, a ds_list reference and the object_index for which we’re checking the collision. The function will automatically fill the ds_list and return the number of instances found, just like the collision_rectangle_list. I made these scripts simply because I didn’t want to type all the bbox_* and collision parameters each time. Sometimes being lazy helps you being organized.

coll_x_list

///@func coll_x_list(xdir, instances_ds, obj)

var xdir            = argument[0]
var instances_ds    = argument[1]
var obj             = argument[2]

var side_to_check = xdir ? bbox_right + 1 : bbox_left - 1

return collision_rectangle_list(side_to_check, bbox_top, side_to_check, bbox_bottom, obj, false, true, instances_ds, false)

coll_y_list

///@func coll_y_list(ydir, instances_ds, obj)

var ydir            = argument[0]
var instances_ds    = argument[1]
var obj             = argument[2]

var side_to_check = ydir ? bbox_bottom + 1 : bbox_top - 1

return collision_rectangle_list(bbox_left, side_to_check, bbox_right, side_to_check, obj, false, true, instances_ds, false)

These scripts should be called from the move_platform_x and move_platform_y scripts.

Copy paste code from images

I know you cannot copy & paste code form these images but there’s a link to the project download at the end of the article. I’m sorry for this; It’s not that I want you to grasp the concepts first, I just couldn’t paste the code, annotate it and make it look well on this blog.

Fixing the horizontal platform

We will start from the horizontal platform so open up the move_platform_x script and let’s take a look at it.

Less than 30 lines of code. Too easy.

In point 1 we’re concerned with the player eventually colliding on the sides.

In point 2 we’re actually carrying the player. As you notice this is quite simple. We’re going to add more complexity as we’re now dealing with a ds_list and with for loops.

Told ya it was slightly more complex

In point 1 we create a instances_ds (_ds stands for data structure as we’re creating a ds_list). This list will hold the colliding instances found.

In point 2 we pass the aforementioned instances_ds (along with other required parameters) to the actual collision check script. This will fill the list and return the number of colliding instances (which we store in the num_found variable).

In point 3 we iterate through the found instances and we move them one by one. Note that there is no need to test if num_found > 0 because for already tests that in the i < num_found condition.

Please note that if we cannot move one of the found instances, we return false as we did before. We just ensure to delete the instances_ds to avoid memory leaks. Always delete useless data structures before returning from scripts.

In point 4 we clear the instances_ds because we need a clear ds_list (otherwise we’d create a mess in the next points).

In point 5 we basically do the same thing as in point 2 but this time we check for collisions above.

In point 6 we cycle and try to move each one of the found instances. Just like the previous version, we don’t need to return anything as we don’t really care if the instance above can or cannot move. It doesn’t influence the moving platform.

In point 7 we cleanup the ds_list to avoid memory leaks and then, finally, move the platform horizontally.

Fixing the vertical platform

This is a little bit trickier but it’s totally the same thing.

In point 1 and 2 we’re inside a platform that is going downward (because the ydir is positive). We can collide with what’s below us but we also need to carry the player with us. In point 3 we’re going upward so we can only collide with what’s above us.

To adapt this code, the principle is the same. We prepare a list, we use our coll_x_list and coll_y_list scripts to check for collisions and then we cycle the colliding instances. Due to the length of the code, I’ve split the screenshots in two.

Going downward

The gist is very similar to the horizontal movement. Notice that we still apply the instance_deactivate_object(self) trick in point 7 to avoid collision during the player’s movement downward. It’s pretty much the previous code, adapted with for loops and a list. Again: remember to destroy your data structures if you need to return from this script or when you don’t use them anymore.

Going upward

Same process. Get the instances, loop, destroy the lists whenever we return or don’t need them anymore. Finally move the platform.

Apparently they work as expected. Or… do they?

Even better moving platforms (there’s a bug)

There’s something off with this implementation. Fact is that we return false during a for loop in which we might have already moved some instances.

The issue’s simple: after we detected the colliding instances sitting above the platform, we proceed moving them one by one. When we find one of these instances cannot move, we stop and return false without moving the platform. Fact is that we might have already moved some instances.

The first idea is to move them back to their previous position. And this could work. We’d need to keep track of every instance that we moved and then simply move those instances to their xprevious or yprevious if something fail.

xprevious and yprevious

These built-in variables returns the previous x and y positions for the instance. These variable will be (automatically) set just before the start of the begin step event but they can also be set through code at any time, meaning you can give them your own custom value (should that be necessary).

Another approach would be to split the movement and the collision checks in two parts. We temporarily store the instances that could be moved inside another list. After we finish our checks, ensuring each instance could move, we move them all. Otherwise we destroy the lists and avoid any movement at all (both players and platforms).

Either of these approaches should work. I tried both and it really depends on your engine. If you use another approach, let me know in the comments. I’d like to know about it. We’ll go with the first approach.

Reset the positions with instances_reset_position()

Let’s create another little utility script called instances_reset_position(). This script will accept a ds_list. This list is the one we use to keep track of the instances moved.

///@func instances_reset_position(instances_to_move)

var instances_to_move    = argument[0]
var num_of_instances     = ds_list_size(instances_to_move)
var index                = 0

repeat(num_of_instances)
{
    var instance      = instances_to_move[| index]
    instance.x        = instance.xprevious
    instance.y        = instance.yprevious
    index++
}

As you can see, we cycle through the list and simply reset the instance.x to its instance.xprevious (same for the y position, of course).

move_platform_x – fixed

  1. Create the list to keep track of the moved instances
  2. Add the instance to this list
  3. In case we cannot move any of the instance in the current loop, pass the above list to our function (to reset their positions). Just before returning, destroy the list to avoid memory leaks.
  4. As usual, cleanup the lists.

move_platform_y – fixed

  1. Create the list
  2. Add the instance
  3. Reset position if we fail to move
  4. Add the instance (do not clear the list, we need to keep track of the previously moved instances as well)
  5. Reset position of all the instances if we fail to move any of the instances
  6. Cleanup

The End (not so fast!)

Be sure to read the latest article about fixing a bug in this very project (yes, there’s still a little bug). It affects high speed (i.e. > 1) moving platforms.

As you might have read, YoYo Games released the beta of GameMaker Studio 2.3. While you could import the project into 2.3, this blog post cannot be replicated step by step, writing these scripts as they are, without any modification.

For this reason, I am now rewriting this whole guide for the 2.3 version. I’ll take into account the new features of GameMaker Studio 2.3. I don’t know how much time will pass before the 2.3 gets a public release (as I still have to get my beta access), but I already got a good look at how functions and structs work so I am planning ahead the complete rewrite.

Hope this article was useful to someone. Stay safe.

Buy Me a Coffee at ko-fi.com

7 thoughts on “Better moving platforms”

  1. Another excellent tutorial! As clean and understandable as anything.

    I’m excited to see how it’ll all be changed for the 2.3 update – plus I’ll be able to test it on the ol Mac to see if they’ve fixed that nasty recursion problem.

    Reply
    • Fact is that we don’t really know how long it will take for the 2.3 update to go public. It could be months. In the meantime I think I’ll fix the Mac bug (which is actually due to a return from a loop inside a with statement). As I learned from the YoYo bug tracker, it’s not really a recursion bug. It is fixable and I’ll work with a friend (who owns a Mac) to test out the solution. I’ll write a new article on how to fix that specific bug so you can keep on working on your project in 2.2.x. Bottom line: I wouldn’t hold my breath for 2.3 (unfortunately).

      Reply
  2. Another excellent tutorial; thanks for all your hard work.

    I prefer to avoid adding member xprevious and yprevious variables to the objects, and I hope this doesn’t bite me if I continue to follow your tutorials. As I add more movement scripts (if it ends up being necessary), I’ll feel the need to always preserve their previous x/y coordinates, or else I won’t be able to trust those member variables. If they exist, I might be tempted to use them in other circumstances, which could lead to difficult to debug scenarios if I’m not careful.

    My first instinct was to make can_move_x() and can_move_y() and then looping twice on the instances, but that felt a bit messy since I’d also need can_move_on_slope().

    I decided to include the previous x/y coordinates in the list:

    ds_list_add(
    instances_that_have_moved_with_previous_xy_list,
    instance_to_move,
    instance_to_move.x,
    instance_to_move.y);

    My reset_instance_with_xy_list_position() does this:

    /// @param instances_with_previous_xy_list
    var instances_with_previous_xy_list = argument0;

    var size = ds_list_size(instances_with_previous_xy_list);

    for (var i = 0; i < size; i += 3) {

    var instance = instances_with_previous_xy_list[| i + 0];
    var X = instances_with_previous_xy_list[| i + 1];
    var Y = instances_with_previous_xy_list[| i + 2];

    instance.x = X;
    instance.y = Y;

    }

    Reply
  3. I like to create data structures at the beginning of the script and delete them at the end. Limiting where they can be created/destroyed helps me mitigate memory leaks.

    Here’s what my move_platform_y() script looks like; I use some different variable names and I also have functions for creating/destroying data structures that help me track memory leaks:

    /// @param yvel
    var _yvel = argument0;
    var _ydir = sign(_yvel);

    var has_hit_wall = false;

    // Create collision list
    var colliding_instance_list = create_list(“move_platform_y().colliding_instance_list”);
    var instances_that_have_moved_with_previous_xy_list = create_list(“move_platform_y().instances_that_have_moved_with_previous_xy_list”);

    repeat abs(_yvel) {

    // Colliding with solid
    if is_colliding_y(_ydir) {
    has_hit_wall = true;
    break;
    }

    // Going downward
    if _ydir > 0 {

    if !move_platform_y_downwards(
    colliding_instance_list,
    instances_that_have_moved_with_previous_xy_list ) {

    has_hit_wall = true;
    break;

    }

    }

    // Going upward
    else if _ydir < 0 {

    if !move_platform_y_upwards(
    colliding_instance_list,
    instances_that_have_moved_with_previous_xy_list ) {

    has_hit_wall = true;
    break;
    }

    }

    // If everything went good, move.
    y += _ydir;
    }

    unregister_and_destroy_list(colliding_instance_list);
    unregister_and_destroy_list(instances_that_have_moved_with_previous_xy_list);

    if has_hit_wall {
    return false;
    }

    return true;

    Reply
      • Hey David! Thanks for sharing your gists. I really dig exploring how others approach the same problems 🙂

        I tend to return a lot during cycles so I can’t really afford to destroy data structures at the end of the scripts and yes, I need to take extra care for memory leaks. Interestingly your approach (of not returning during cycles) should have spared you the MacOS bug I encounter when returning from a loop inside a with statement. You simply break from the loop and return the result at the end. Which is basically the fix I’m writing for my move_x and move_y scripts (having said that, I still prefer to return but it’s a personal preference).

        Anyway I used this very same approach (checking if movement is possible, moving and then resetting positions) in my moving boxes prototype. I still have very similar code in that project (I can send it to you if you like). Admittedly I still have to fix moving stacked boxes on slopes but that’s a whole different story 😛

        I remember having scripts like

        • can_push
        • can_carry
        • can_move_x
        • can_move_y

        and a bunch of lists oddly named like items_marked_for_movement_x, items_marked_for_movement_y and items_that_cannot_move or something like that. I also went down the rabbit hole for slopes because I had to move x and y separately and I remember making a bit of a mess when refactoring slope_move to actually avoid moving items inside that script.

        I still feel that marking items for movement and then iterating is the way to go for complex problems (like stacked boxes on slopes) but I have to find a clean way to write code for that. I don’t like the mess I wrote for those prototypes.

        Going back to your xprevious and yprevious considerations, you’d be absolutely right to be wary of using them. Especially using them like I did in this article 😛

        In fact if I were to increase the yvel of the vertical platform to, say, 13, and let’s say that the platform stops at the 6th iteration of the movement loop, my instances’ positions would be set back to the wrong previous values, because I don’t manually update them at the end of each successful cycle (platform’s cycle, not single instance move_x/move_y cycles). In my case I’ll write how to fix this in the next article. I can’t see where you update the xprev/yprev (X/Y in your case) but you might or might not suffer from the same issue. Let me know if you do (try increasing the vertical speed and put more objects on the vertical platform, for example, with something blocking only one of them).

        You might find this code useful to visually debug at very high speeds (I have this in the begin step of a controller)

        var spd = keyboard_check(vk_shift) ? 1 : 60
        game_set_speed(spd, gamespeed_fps)

        Again, thanks for your thoughts and code, really appreciated!

        Reply

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.