Skip to content

How to make a game like Mario: start with the jump

4 min readBy Eduardo Orellana ·

A Mario game is a jump that feels right and levels that teach you how to use it. The jump is four small rules, none of them physics, and each one is a number you can measure. We built them in a Godot 4.7.2 CharacterBody2D and ran them headless, so this post gives the numbers, not adjectives.

Leer en español

Mario's jump is not physics

Super Mario Bros. shipped in 1985, and the first thing everyone who tries to clone it learns is that a realistic jump feels wrong. A ball thrown upward rises and falls along the same curve. Mario does not: he rises, hangs, and drops faster than he went up, and how high he goes depends on how long you hold the button.

Those are design decisions, and Godot leaves every one of them to you. A CharacterBody2D with gravity and move_and_slide() gives you the ball. The four rules below give you the plumber.

Hold to jump higher, let go to cut it short

Variable jump height is the rule that makes a platformer feel responsive: tap for a hop, hold for a full jump. The simplest version cuts the upward velocity when the button is released early. In our run, with a jump velocity of 520 pixels a second and gravity of 1,600, a held jump rose 80 pixels and a tap released after three frames rose 34.

That difference is the whole point. A player who can choose between 34 and 80 pixels with one button can thread a gap under a ceiling and clear a pit with the same input, and every level you design can ask for either.

GDScript
const JUMP_VELOCITY := -520.0
const GRAVITY := 1600.0
const FALL_GRAVITY := 2400.0  # falling is heavier than rising
const JUMP_CUT := 0.45        # releasing early keeps 45% of the rise

func _physics_process(delta: float) -> void:
	if velocity.y < 0.0 and not Input.is_action_pressed("jump"):
		if velocity.y < JUMP_VELOCITY * JUMP_CUT:
			velocity.y *= JUMP_CUT
	velocity.y += (GRAVITY if velocity.y < 0.0 else FALL_GRAVITY) * delta
	move_and_slide()

Fall faster than you rise

The second rule is a heavier gravity on the way down, 2,400 against 1,600 in our numbers. It makes the jump snappy instead of floaty, and it shortens the time a player spends watching instead of steering. Most platformers people describe as tight do some version of it.

It is also where clones overshoot. Too heavy a fall and landings feel like being yanked; too light and the character drifts. Tune it by playing, with the numbers as constants at the top of the script so you can change one and press Play again.

Coyote time forgives the edge

Players press jump slightly after running off a ledge, because their eyes are on the gap, not the edge. Coyote time keeps a jump legal for a few frames after the floor is gone. We set it to 0.1 seconds and ran a character off a ledge, pressing jump late on purpose: pressed 3 frames after leaving the ledge (0.05 s), it jumped; 5 frames (0.083 s), it jumped; 8 frames (0.133 s), it fell.

Nobody notices coyote time when it is there. Everybody notices its absence, as a game that eats jumps. The run is the way to check it: count frames, because you cannot feel the difference between 0.08 and 0.13 seconds, but your players' thumbs can.

Jump buffering remembers an early press

The mirror image of coyote time: a player who presses jump just before landing expects to jump on landing, not to have the press swallowed. A jump buffer keeps the press for a short window. With the window at 0.1 seconds, we found the exact landing frame of a fall and pressed jump before it: 4 frames early (0.067 s), the character jumped on landing; 6 frames early, at the window's edge, it did not; 9 frames early, it did not.

Coyote time and buffering together are why a good platformer feels like it reads your mind. Both are a timer and an if statement.

GDScript
const COYOTE_TIME := 0.10
const JUMP_BUFFER := 0.10
var coyote := 0.0
var buffer := 0.0

func _physics_process(delta: float) -> void:
	coyote = COYOTE_TIME if is_on_floor() else maxf(coyote - delta, 0.0)
	buffer = JUMP_BUFFER if Input.is_action_just_pressed("jump") else maxf(buffer - delta, 0.0)
	if buffer > 0.0 and coyote > 0.0:
		velocity.y = JUMP_VELOCITY
		buffer = 0.0
		coyote = 0.0

World 1-1 teaches without a word

Once the jump is right, the levels teach it. The first screen of Super Mario Bros. puts a Goomba in front of you before you can do anything else, with a block above it: you learn that enemies hurt and that jumping helps in the first five seconds, without a line of text.

Design your first level the same way: introduce one idea in a place where failing is cheap, test it once where failing costs a little, then combine it with something you already taught. A pit you can only clear with a held jump, after a gap you can clear with a tap, teaches variable height better than any tutorial prompt.

What one person should not attempt

Mario is Nintendo's, and so is everything that makes it look like Mario: the character, the enemies, the sounds, the pipes. Make the feel and the level design yours and the look entirely your own. The jump rules above are not anyone's property; they are in almost every good platformer since.

Scope the way the original did: one character, one move set, short levels. Super Mario Bros. fitted in 40 kilobytes. Its depth came from levels built around a few moves, not from many moves.

2D, and a template to start from

A Mario-like is 2D: a TileMapLayer for the ground, a CharacterBody2D for the player, Area2D nodes for coins and enemies. In the Flockbay app, New from template includes a 2D platformer to start from, and the AI can add these four rules and measure them the way this post did, while you play and judge the jump.

A platformer is a jump that feels right and levels that teach it.

Read more game design notes

Flockbay

The free AI game maker for Windows, macOS and Linux.