How to code a platformer engine

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

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.

My sprites organized in groups
Keep your resource panel organized with folders for maximum sanity.

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.

GameMaker Studio 2 Sprite Editor
  • 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.

Precise collision check for slopes is essential
Use a precise collision mask for slopes or our engine won’t work correctly.
Different slopes don't need different sprites.
One sprites for differently inclined 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.

Player origin position
Pay attention to the dimensions and the origin position.

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.

Object hierarchy in GameMaker Studio 2

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.

Platformer engine object hierarchy

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.

Odd slope mechanics.
The old system made the objects look like they were merely touching the slope.
Nicer slope mechanics.
This new system lets the objects go a little “inside” the slopes and it doesn’t look that weird anymore.

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 manipulated xvel and yvel 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.

Incorrect slope placement in the room editor.
The player will hit these oWall blocks and will jump off the first slope on the left when going down to the left.
Correct slope placement in the room editor.
The player will correctly travel upward and downward without ever changing state, jumping or getting stuck.

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.

Buy Me a Coffee at ko-fi.com

Leave a Comment

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