Better moving platforms
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.
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.
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.
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
.
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.
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
- Create the list to keep track of the moved instances
- Add the instance to this list
- 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.
- As usual, cleanup the lists.
move_platform_y – fixed
- Create the list
- Add the instance
- Reset position if we fail to move
- Add the instance (do not clear the list, we need to keep track of the previously moved instances as well)
- Reset position of all the instances if we fail to move any of the instances
- 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.
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.
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 awith
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).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;
}
Better formatting can be found in this gist:
https://gist.github.com/bitlather/3c114cee9359264977250f10ee0f7fdc
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;
Better formatting for this code can be found here:
https://gist.github.com/bitlather/ca6f8481e29153b17aaf2c186f68858c
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 awith
statement. You simplybreak
from the loop and return the result at the end. Which is basically the fix I’m writing for mymove_x
andmove_y
scripts (having said that, I still prefer toreturn
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
anditems_that_cannot_move
or something like that. I also went down the rabbit hole for slopes because I had to movex
andy
separately and I remember making a bit of a mess when refactoringslope_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
andyprevious
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 instancemove_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!