Moving Platforms – Horizontal and Vertical
Some adjustments required for GameMaker Studio 2.3
The following article is for pre 2.3 versions of GameMaker Studio 2. It doesn’t take advantage of functions, chained accessors, structs and other new features of GML/GMS2.3+ IDE. Unfortunately I’m unable to update the series for the foreseeable future, but downloading and importing the final project inside the new IDE should trigger the conversion.
It will run correctly. From there you could refactor and clean the code a bit to suit your taste, I guess.
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
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
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 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
.
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.
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).
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.
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
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 🙂
Hi Nikles!
Great tutorial, pretty neat ‘clean code’ vibes there 🙂
Just one thing (of course) :3
After I implemented your project to mine (because I had problems with moving platforms) and started using ‘normal’ sprites, I found out, that the movement in either directions are a little shaky. If I start to use whole numbers for velocities, the movement becomes smooth, so that was an easy fix to it, but I can’t use a whole number for gravity :/ So every time I jump with my character, there is a little bit of shakiness. Any thought about that? 🙂 Thank you, and for your cool tuts!
Great tutorial, I confess that initially I don’t like to use objects as a collision source, I think it is better to leave this task for the tilemaps to do it, but I have to admit that their method seems more stable and less subject to small bugs that always happen.
Congratulations,
I’m trying to adapt to the standard. but I still have resistance
Thank you! Overall, tilemap collisions should indeed be faster (less overhead in running lots of objects). Of course moving platforms really are objects with their own behavior and they need their own events to run and react correctly. Maybe one could try and mix some tile collisions (e.g. for static walls and slopes) with some object collision (e.g. moving platforms). Personally, I tried and failed; I gave up and once I started using object collisions for everything, I found it to be pretty versatile. Using less, bigger objects can boost performance as well (or I guess out-of-view collision objects could be disabled).
Having said that, there is no standard way of doing things. As long as it does what one intended it to do, it just works.
Btw if you find any other interesting method you feel like sharing, I’m always willing to learn new things 🙂
PS Sometimes I draw collisions with tiles in my room editor. Once the room starts, I spawn collision objects where collision tiles are, then simplify/merge collision objects with adjacent ones, then hide all the collision layers. So I still draw with tiles in the GMS room editor (it’s faster), and then use object collision at runtime.