How to code a platformer engine
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.
In this series, we’ll see how to code a platformer engine in GameMaker Studio 2. It’s mainly for low resolution games but it can be tweaked to either simulate fluid, hi-res movement or to accomodate optimized collision code for hi-res games.
This engine It will include
- pixel perfect movement
- slopes
- moving platforms
- jump through platforms
- other neat features such as smooth sub-pixel movement
Starting from basic movement and collision code with slopes, we’ll build up the player object with rudimentary state machines. In the next articles we’ll build upon this and add enemies, hazards and other engine features.
Project Setup
We start by creating some placeholder sprites. These are square sprites (for blocks) and a triangle (for slopes). This is because we want a functioning platformer engine in the fastest time possible. We don’t need to work with the final graphics right now.
Let’s create a new project in GameMaker Studio 2 and populate it with some sprites. My own sprites are organized in folders so I don’t overcrowd the navigation panel.
Keep in mind some properties when you design these placeholder sprites.
- Size: 16×16
- Origin: top left
- Collision mask: automatic / rectangle
Slope sprites and their collision mask
Slopes are a different beast. They need a precise collision mask to work correctly with our engine. When you transform an object’s image_xscale
or image_yscale
, the collision mask stretches and adapts itself to it so there’s no need to create different sprites or objects for those slopes.
We’ll use just one image for slopes but this doesn’t mean we won’t be able to use different slope angles. This is because we’ll rely on image_xscale
for various slope inclinations.
Player placeholder sprite
The placeholder for the player is a rectangle with the following dimensions.
- Width: 14 pixels
- Height: 30 pixels
Pay attention to the sprite’s origin; since we’ll flip the player using the image_xscale
, just like for slopes, we risk colliding the player’s collision mask with near walls when flipping left/right sides in-game.
That’s why we position the origin in the very middle of the sprite.
You may have noticed that the origin is pushed below the player by 1 pixel. This is for easy placement in the room editor.
This little change to the sprite’s origin will influence the x/y position of the instance in the game so you need to take this into account when coding. I rely on proper bounding boxes for collision checks and changing the origin doesn’t change the bounding box of the sprite but it may lead to odd results if you interchange place_meeting
and collision_*
functions without considering that the origin position is pushed down below by 1px.
Object Hierarchy
The most important part of this whole engine, is to get the object hierarchy right. You can do so much stuff in GameMaker Studio 2 when you plan the correct hierarchy; it can really simplify your code.
To avoid using the oSolid
object in the game, I left it without a sprite. We shall never use it in the room editor.
As you can see from the image below, we differentiate slopes from walls. I say walls but I mean to use walls for floors and ceilings as well. I know this is not proper naming, but I couldn’t come up with a sound nomenclature.
Please note that there’s a oSlopeWall
object that inherits from slopes. It’s a special block but a very important one for our system. We’ll see its uses later on.
Why are slopes separated?
I used to code slopes in a very different way. They were simple solids and the character would collide with them with its bounding box. Honestly, this looks terrible. The new system allows for some more sensible positioning of the objects moving up or down the slopes. There might be some trade-offs but I haven’t encountered any issue while coding the new system. It works and it looks way better than the previous one.
Coding the Engine
Is the object on the ground?
This question needs answering in a lot of different contexts. You need to know how to behave based on the answer to this question so it’s essential to get this script right. Because of that, we need a way to tell if an object is on the ground be it on an oWall
or a oSlope
. For this reason we code a on_ground
script like this one.
///@func on_ground()
///@desc return instance_id of the colliding ground object or noone if not colliding
return on_wall() || on_slope()
It’s a simple script but you can already tell it’s doing a lot of things for us. It relies on two other scripts to return the colliding instance below (or noone
if none found). In the future, it will signal us even when the player is standing on a jump-though platform.
///@func on_wall()
///@desc return instance_id of the colliding instance or noone if none found
return collision_rectangle(bbox_left, bbox_bottom + 1, bbox_right, bbox_bottom + 1, oWall, false, true)
///@func on_slope()
///@desc return the colliding slope instance or noone if none
return collision_point(x, bbox_bottom + 1, oSlope, true, true)
It’s important to note that we’re targeting oWall
with bounding box collision checks (a collision_line
would have provided the same result but I read somewhere it’s slower than a collision_rectangle
) while we’re targeting oSlope
with a collision_point
, centered in the origin (still using the bounding box for vertical point positioning though, just in case you use different bounding box sizes). Because of this distinction, we’re able to let objects go a little bit “inside” slopes.
Code the Player
Create Event
In the create event we setup some variables for movement (and a very basic state machine which we’ll expand in later posts).
/// @description Basic player object
xaxis = 0
yaxis = 0
xvel = 0
yvel = 0
xvel_fract = 0
yvel_fract = 0
state_current = playerStateGround
state_is_new = true
state_timer = 0
Step Event
In the step event of the player we do 4 things
- Grab the input from the keys
- Apply some arbitrary speed to the
xvel
and flip the sprite accordingly (or stop the player from moving if no input is detected) - Run a simple state machine
- Finally we run the
move_and_collide
script which will take those manipulatedxvel
andyvel
speeds and move the player accordingly while managing collisions.
/// @description Basic Player actions
kright = keyboard_check(vk_right)
kleft = keyboard_check(vk_left)
kdown = keyboard_check(vk_down)
kup = keyboard_check(vk_up)
xaxis = kright - kleft
yaxis = kdown - kup
// Calculate speeds
if abs(xaxis)
{
xvel = 1.23 * xaxis
image_xscale = xaxis
}
else
xvel = 0
// Simple State Machine
var _state_current = state_current
script_execute(state_current)
state_is_new = _state_current != state_current
state_timer = state_is_new ? 0 : state_timer + 1
// Movement after all state calculations
move_and_collide()
Please note that in a decently coded project, you’d manage the horizontal speed in the states themselves and not in the step event like I did in this example. This is because you’d probably want different physics for ground and air movements.
Player States
Unless you code the player states for ground and air, your character won’t be able to do much, so here they are. We will expand these states and add many other interesting things inside these scripts.
playerStateGround
// Player just entered this state. These actions will be executed only once.
if state_is_new
{
state_is_new = !state_is_new
}
// We're not on ground so let's change state to air
if !on_ground()
{
state_current = playerStateAir
exit
}
// If we intentionally jump with X key, let's change state
if keyboard_check_pressed(ord("X"))
{
state_current = playerStateAir
yvel = -3 // this is the jump speed
exit
}
playerStateAir
// Player just entered this state. These actions will be executed only once.
if state_is_new
{
state_is_new = !state_is_new
}
// We're on ground. Let's change the state to ground (unless we're going upwards)
if on_ground() && yvel >= 0
{
state_current = playerStateGround
exit
}
// Keep adding speed to simulate gravity acceleration
yvel += 0.17
// If we fall faster than this, let's slow down
if yvel > 3
yvel = 3
As you can see from the code above, I used so-called magic numbers for gravity acceleration, maximum vertical speed (terminal velocity?) and jump speed. You’d be better off defining some variables in the create event of the player so you could easily edit those values (and know the meaning of those numbers by reading the variable’s names).
Move and Collide script
Finally, we’re going to code movement and collisions! If the player has an horizontal speed of 7.43 pixels, we round it to 7, keep the remainder for the next frame, and for 7 times we check for collisions before moving it by one pixel after each check. This ensures we get pixel perfect collisions.
Granted, this might get expensive for very high resolution games where the player needs to travel hundreds of pixels per step (with hundreds of collision checks per step) but for low resolution games, it works pretty damn well. You could come up with different movement code if you wish. Let me know in the comments what you came up with then.
Notice the slope code inside the horizontal movement. This is what lets us stick the player to slopes when going up or down, without changing player states or messing with vertical speeds.
It basically say “if the object is inside a slope, get it outside by pushing it upwards” (going up a slope) and also “if there’s no ground below but there’s a slope just 1px below, stick to it” (going down a slope).
// Movement/Collision X
xvel_fract += xvel;
xvel = round(xvel_fract);
xvel_fract -= xvel;
xdir = sign(xvel)
repeat(abs(xvel))
{
if !place_meeting(x + xdir, y, oWall)
{
x += xdir
// Inside a slope (must go up)
while collision_point(x, y - 1, oSlope, true, true)
{
if can_go_up()
y--
else
{
// Let's go back (outside slope) and stop
x -= xdir
xvel = 0
xvel_fract = 0
break
}
}
// On a slope going down
while !on_ground() && collision_point(x, y + 1, oSlope, true, true)
{
y++
}
}
else
{
xvel = 0
xvel_fract = 0
break
}
}
// Movement/Collision Y
yvel_fract += yvel;
yvel = round(yvel_fract);
yvel_fract -= yvel;
ydir = sign(yvel)
repeat(abs(yvel))
{
// Going down
if ydir
{
if !on_ground()
{
y += ydir
}
else
{
yvel = 0
yvel_fract = 0
break
}
}
// Going up
else
{
if can_go_up() && !collision_rectangle(bbox_left, bbox_top, bbox_right, bbox_top, oSolid, true, true)
{
y += ydir
}
else
{
yvel = 0
yvel_fract = 0
break
}
}
}
And this is the can_go_up
script
///@func can_go_up()
///@desc return true if player's not colliding with walls/ceilings
return !collision_rectangle(bbox_left, bbox_top - 1, bbox_right, bbox_top - 1, oWall, false, true)
There’s plenty of space for optimizing this script and we’ll see how in the next articles.
On Slopes and the oSlopeWall object
Because the player must be able to go inside slopes, we treat them separately from the usual solids/walls. Due to this difference in collision checking, we need to ensure we place the oSlopeWall
at the start, in the middle and at te end of our ramps.
In the next article
In the next article we’ll talk about jump-through platforms and we’ll refactor the code to solve some minor issues. Keep in mind that some limitations of this system can be either avoided altogether with sensible level design or manually fixed via code, case per case. This engine, as of now, isn’t really advanced, yet we briefly touched topics such as state machines and slopes.
Please leave a comment
If you notice any oddity, please let me know in the comments. I’m curious about what would you want me to fix. I’m already planning on some fixes and improvements but maybe you noticed something that I missed.
Nice! Thanks for putting this together
Very well written tutorial. In the info box at the top you mention that an updated 2.3+ compatible update s in the works. Is this still in process?
Thank you, Chris. Unfortunately I am unable to update the tutorials for the foreseeable future. I’ve been unable to update this blog for months and I put all my gamedev project on hiatus. In GMS 2.3+ each script should become a function and then they work in a very similar way; someone in the comments confirmed that little work is needed to make them work. Importing the final project in the latest post of the series should trigger a conversion, from there you should do a little cleanup and refactor (to your liking) and you shuold be ready to go. As of now I don’t know when I will be able to come back to game dev or blogging 🙁
I’ve updated the warning box. Downloading and importing the project will trigger a conversion in the new IDE. The project should run just fine from there.
Hi! Sorry to bother you but I have a question?
I followed the tutorial exactly but I’m running into a problem where the player character gets stuck on the right side of walls? But not the left side of walls? It’ll just get stuck on the wall and stop falling, and I can’t jump either until I come off of the wall. And again, this only happens on the right side of walls, so I’m a little confused. Again, sorry for the bother! Thank you in advance.
If you updated to the recent 2022.1 version, you might encounter issues with the new collision system. Read this for more information: Collision Compatibility Mode. Please, let me know if this fixes your issues. Thank you!