Moving Platforms – Horizontal and Vertical

(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 🙂

MacOS Crash Update – 2020-04-19

A reader kindly reported this bug to YoYo Games (attaching the previous version of the project). You can read the bug report here: 0031657: macOS: Runner crashes during collision check in the attached project

It’s due to “bailing from a repeat within a with” and it’s marked as fixed for the upcoming version 2.3.0.

MacOS Crash Warning (see update above)

The following code will probably break on MacOS. Along with YoYo Games we are currently investigating the culprit. If I had to make a wild guess, I’d say that the MacOS runner has some weird bug about nested context and recursion that makes it lose track of who’s doing what, eventually crashing without any warning. I’m confident that YoYo will get back to me with some good news but in the meantime I’m publishing this article anyway… because this is how I make moving platforms 🤷‍♂️

I’m not saying this code is bug free. It might very well be a sneaky bug in my code. But to this date, I still haven’t found it. If you spot it, please point it out.

I’ve seen a lot of bad implementations of moving platforms in GameMaker Studio, especially the vertical moving platforms. Some rely on the fact that, due to gravity, the player will naturally follow a downward moving platform (which is odd, and wrong), whilst others don’t even try to explain the vertical moving platforms at all. Like they don’t exist.

I’m not sure what’s going on here. Maybe my code is super naive and I’m not doing it right either, but moving platforms don’t have to be difficult. They’re objects that live in the begin_step event; they do their collision checks, they can push or carry other objects (or squish them or whatever you decide) and… and that’s it, really.

The player will naturally collide with these platforms but only in the step event, when the platforms have already completed their movements so there won’t be any odd behavior. To entities, moving platforms are… not really moving at all. I’d say that entities are, to moving platforms, also not moving at all. Remember that they live in two separate events.

Basic platforms vs Advanced platforms

The following article will describe basic horizontal and vertical platforms which can push and carry the player. These are not advanced moving platforms, able to carry and push more than one item. Those kind of platforms need slightly more work in the collision detection area and the colliding instances must be handled differently. Don’t worry though, they will be covered in the next article. If you need moving platforms for the player though, the following article is what you need!

Fixes and cleanups

Some fixes before we begin

Before we begin I need to fix my movement code. Let’s create a platformer_init script and put these lines inside it. I introduce a couple of new variables here for clarity purposes. I like to know that xvel_int and yvel_int will always hold an integer number no matter what.

Also remember to call the platformer_init() script in the create event of the player.

///@func platformer_init()

xvel		= 0
yvel		= 0
xvel_int	= 0 // Integer x speed
yvel_int	= 0 // Integer y speed
xvel_fract      = 0
yvel_fract      = 0
This is the platformer_init script
From now on we use this platformer_init script on our moving objects

Also open the oPlayer object and slightly alter the movement lines in the step event to accommodate these changes.

// Use the xvel_int here
if !move_x(xvel_int, true)
{
    xvel       = 0
    xvel_fract = 0
}

// Use the yvel_int here
if !move_y(yvel_int)
{
    yvel       = 0
    yvel_fract = 0
}

There’s one last piece of code I need to alter: round_vel()

///@func round_vel()
///@desc Round the xvel/yvel while keeping track of fractions

xvel_fract += xvel;
xvel_int    = floor(xvel_fract);   // use xvel_int here
xvel_fract -= xvel_int;            // and here

yvel_fract += yvel;
yvel_int    = floor(yvel_fract);   // Use yvel_int here
yvel_fract -= yvel_int;            // and here

Other than using the new variables, I decided to go with flooring, instead of rounding. It should work either way but I prefer to know we always round down the velocities.

New collision scripts

coll_x and coll_y to simplify collision checks

I’ve introduced a couple of new scripts to avoid having to write collision checks over and over. Let’s see how they work. Passing a direction, will automatically select the correct side to check. Also the obj parameter is optional.

///@func coll_x(xdir, [obj])

var xdir = argument[0]
var obj = argument_count == 2 ? argument[1] : oWall

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

return collision_rectangle(side_to_check, bbox_top, side_to_check, bbox_bottom, obj, false, true)
///@func coll_y(ydir, [obj])

var ydir = argument[0]
var obj = argument_count == 2 ? argument[1] : oWall

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

return collision_rectangle(bbox_left, side_to_check, bbox_right, side_to_check, obj, false, true)

Ternary operator

The ternary operator is just a shorthand to write an if / else statement. It’s usually used in assignment statements and it works like this:

some_var = (condition) ? value_if_true : value_if_false

Moving Platforms

Let’s see how to implement uncomplicated moving platforms

oMoving object is a child of oWall. Object hierarchy is important for collisions.
A new object with an oWall parent

First of all create a new sprite sMoving (maybe duplicate the wall and give it another color) and assign it to a new object named oMoving. Let’s make this object a child of oWall. And voilà!

With this single action you’ve already taken care of every possible collision that might happen in the player’s step event. Indeed if you were to run the game now with a couple of these oMoving objects in the game, they’d be sitting still, of course, but the player cannot go through them. He can stand on them just like any other oWall. And from the player’s side of things, it’s going to stay this way.

“moving platforms” that the player cannot traverse. As solid as they can get…

Moving on

Let’s add some movement

Now let’s see what happens when we begin to move the moving platforms. Spoiler alert: they do not collide with the player.

Open the create event of the oMoving platforms and initialize the platformer by calling platformer_init(). Then open the begin step event and place the movement code inside it.

/// @desc move

round_vel()

// Let the instance decide what to do when it can't move
if !move_x(xvel_int, false)
{
    xvel       = -xvel    // Reverse the speed in case of collision
    xvel_fract = 0
}

if !move_y(yvel_int)
{
    yvel       = -yvel    // Reverse the speed in case of collision
    yvel_fract = 0
}

This is the very same code we have inside the oPlayer object slightly altered to let the moving platforms invert their speed when colliding with walls. Now, for the moving platforms to actually move we need to define an initial speed. We can do so in the room editor with the creation code.

Initial moving platforms speed is defined in the creation code of the room editor.
Double click the instance and open the creation code. Place the initial speed here (here we’ll set the vertical speed)

Creation code

Creation code is code that is run after the create event for each instance. Meaning that, as an instance is created, its create event runs, then its creation code runs and then GameMaker go on creating another instance and so on. This is a good place to override values initialized in the Create event.

If you run the game now, you end up with this. Disappointed? That’s exactly the result we expected though.

The moving platforms can go inside the player.

This is what might get some people confused. It’s true that the player cannot go inside the platforms but the platforms can go inside the player. While the player’s collisions are already set correctly (since the moving platforms are children of oWall), the oMoving platforms need some more work.

We can’t use the same movement and collision code that we use in the oPlayer object. Indeed those scripts don’t take into account collisions with the oPlayer itself (of course) so to the moving platforms, there is no collision at all. We need to define movement and collision detection specific for the moving platforms.

Complexity

If those move_x and move_y scripts were complex enough to take into account each object’s own characteristics, they would work. For instance, if every object defined a list of what if could collide with and what it could carry or push, those script could read the list and then behave accordingly. But for the sake of this article, they’re not that complex. And I don’t want to complicate this too much. We’ll explore such generalized solutions in more advanced engines.

Platforms’ own movement scripts

move_platform_x and move_platform_y to the rescue

So let’s change the platform’s begin step event code into this, introducing move_platform_x and move_platform_y scripts.

In these scripts we take into account collisions with oPlayer from the sides and from above.

Let’s create them and let’s see how they work. First is the horizontal movement. The idea is to return false whenever the platform encounters and unmovable obstacle (such as walls or a stuck player).

/// @desc move

round_vel()

if !move_platform_x(xvel_int)
{
    xvel       = -xvel
    xvel_fract = 0
}

if !move_platform_y(yvel_int)
{
    yvel       = -yvel
    yvel_fract = 0
}

The collision from the sides may return false in case the player cannot move at all (e.g. squashed between a moving platform and a solid). Here is where you decide what to do in your game (I just return false and let the platform reverse its speed but you might as well kill the player). It’s also where the MacOS crashes, btw.

The collision with the player standing on the platform, on the other hand, does not return anything. This is because the platform doesn’t care about the player being unable to move. It will simply slide off its feet and continue its movement.

///@func move_platform_x(xvel_int)
///@arg xvel

var _xvel    = argument[0]
var _xdir    = sign(_xvel)

// Movement/Collision X
repeat(abs(_xvel))
{
    // Colliding with solid
    if coll_x(_xdir)
        return false
    
    // Pushing the player
    var player_on_sides = coll_x(_xdir, oPlayer)
    if player_on_sides && false == move_x(_xdir, true, player_on_sides)
        return false // Squashed between solids
            
    // Carrying the player
    // Notice how we don't care if the player
    // can't move in this case. The underlying platform will
    // simply slide off its feet (hence we don't return false)
    var player_on_top = coll_y(-1, oPlayer)
    if player_on_top
        move_x(_xdir, false, player_on_top)

    // Finally move        
    x += _xdir
}

return true

Taming the vertical movement

With a trick

The following script is for vertical movement instead. If the platform is going upward, in case of player collision, we simply try to push it upward as well. No big deal. As usual we return false if the player is stuck (or you might kill it). Note: this is where the macOS crashes.

Here comes what might confuse someone. If we’re going downward, we move the player downward as well via its own move_y script. But if we’re carrying it on top of the platform, we need deactivate the platform during player’s collision checking.

If we don’t do this, the player would collide with the very platform that is trying to move it, before being able to move downward. This collision will leave the player behind in mid-air, making it bounce on the platform on its way down. That’s why we deactivate and reactivate the moving platform really quickly, just to remove the instance from the possible collisions.

It’s like saying “Not considering this very platform, try moving the player down 1px”.

///@func move_platform_y(yvel_int)
///@arg yvel

var _yvel    = argument[0]
var _ydir    = sign(_yvel)

repeat(abs(_yvel))
{
    // Colliding with solid
    if coll_y(_ydir)
        return false

    // Going upward
    if !_ydir
    {
        // Carry the player upward (lift it)
        var player_above = coll_y(_ydir, oPlayer)
        if player_above && false == move_y(_ydir, player_above)
            return false
    }

    // Going downward
    if _ydir
    {
        // Push the player downward if it's colliding from below
        var player_below = coll_y(_ydir, oPlayer)
        if player_below && false == move_y(_ydir, player_below)
            return false

        // Carry the player downward with the platform if it's standing on top of the platform
        var player_above = coll_y(-1, oPlayer)
        instance_deactivate_object(self)       // Dirty trick begins
        if player_above
            move_y(_ydir, player_above)
        instance_activate_object(self)         // Dirty trick ends
    }

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

return true

And just like that, believe it or not, the moving platforms are done. There’s nothing else to do to have horizontal and vertical moving platforms capable of carrying and pushing the player around without odd, jerky movements.

If you are curious about more advanced platforms, able to carry more than one item, simply stay tuned for the next iteration about advanced moving platforms.

On returning false

As a reader pointed out, some of my scripts return 0 instead of return false. Most of the time it doesn’t make a difference but for clarity, I decided to replace all instances of return 0 with return false in scripts such as move_x, move_y and others. You can do a project-wide search and replace if you want to do so as well (or just download my project).

Buy Me a Coffee at ko-fi.com

3 thoughts on “Moving Platforms – Horizontal and Vertical”

  1. Great tutorial! Instead of setting the moving platform’s x/y velocity in the instance’s creation code inside the room editor, you could set it in the “variables” menu.

    You can define an object’s “variables” in the object editor, then overwrite those values in the room editor per instance.

    Reply
    • You’ll need to remove their declaration inside of platformer_init(), though, because it seems like the Create event runs after variables are assigned. Or, write the code so the velocity variables are only set if they don’t already exist, using something like:

      /// platformer_init()
      if !variable_instance_exists(id, “x_velocity”) {
      x_velocity = 0;
      }
      if !variable_instance_exists(id, “y_velocity”) {
      y_velocity = 0;
      }
      x_velocity_fraction = 0;
      y_velocity_fraction = 0;
      x_velocity_int = 0; // Integer x speed
      y_velocity_int = 0; // Integer y speed

      Reply
      • Yep! I don’t use the variables GUI panel at all but I guess some might be more comfortable using it. I prefer to have init code in the create event and override vars in the creation code only because of habit, I guess, but it could be done 🙂

        Reply

Leave a Comment

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