Skip to main content
Coding Quests
The Scroll Library
Tutorials

Godot 4 Platformer Movement: Jump, Gravity, and Coyote Time

June 16, 20266 min read

Platformer movement lives or dies on feel. The difference between a jump that feels great and one that feels stiff isn't the engine, it's a handful of small touches most tutorials skip. Let's build movement that feels good, not just movement that works.

The basic setup

Use a CharacterBody2D with a Sprite2D and a CollisionShape2D, same as any 2D character. Add input actions for move_left, move_right, and jump in Project Settings, Input Map. Then the bones of it:

GDScript
extends CharacterBody2D
​
@export var speed: float = 300.0
@export var jump_velocity: float = -400.0
​
func _physics_process(delta: float) -> void:
# Gravity pulls you down every frame you're in the air
if not is_on_floor():
velocity += get_gravity() * delta
​
# Jump
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
​
# Left and right
var direction := Input.get_axis("move_left", "move_right")
velocity.x = direction * speed
​
move_and_slide()

That's a working platformer character. get_gravity() pulls the project's gravity setting, so you tune it in one place. is_on_floor() only works because of move_and_slide(), which figures out what you're standing on. And note jump_velocity is negative, because in 2D, up is negative Y.

This works. But it doesn't feel good yet. Here's why.

Coyote time

Real players press jump a few frames after walking off a ledge, then get annoyed when nothing happens. Coyote time gives them a tiny grace window where a jump still counts even though they've technically left the floor. Named after a certain cartoon coyote who hangs in the air a moment too long.

GDScript
@export var coyote_time: float = 0.1
var coyote_timer: float = 0.0
​
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity += get_gravity() * delta
coyote_timer -= delta
else:
coyote_timer = coyote_time
​
if Input.is_action_just_pressed("jump") and coyote_timer > 0.0:
velocity.y = jump_velocity
coyote_timer = 0.0
​
var direction := Input.get_axis("move_left", "move_right")
velocity.x = direction * speed
move_and_slide()

One hundred milliseconds is invisible to the player but makes the controls feel forgiving instead of punishing.

You can feel that window in the coyote time lab. It puts the grace period on a slider and times every late press.

Jump buffering

The flip side: players hit jump just before they land. A jump buffer remembers that press for a few frames and fires it the instant they touch ground:

GDScript
@export var jump_buffer_time: float = 0.1
var jump_buffer_timer: float = 0.0
​
func _physics_process(delta: float) -> void:
if Input.is_action_just_pressed("jump"):
jump_buffer_timer = jump_buffer_time
else:
jump_buffer_timer -= delta
​
# ... gravity and coyote logic ...
​
if jump_buffer_timer > 0.0 and coyote_timer > 0.0:
velocity.y = jump_velocity
jump_buffer_timer = 0.0
coyote_timer = 0.0

To feel it before you code it, try the jump buffering lab. Press early, switch the buffer off and on, and watch each press land on a timing strip.

Coyote time and jump buffering together are the single biggest upgrade you can make to a platformer's feel, and they're maybe ten lines.

A snappier fall

Floaty jumps feel bad. A cheap fix is to fall faster than you rise. Multiply gravity when the player is moving downward:

GDScript
if velocity.y > 0:
velocity += get_gravity() * delta * 1.5

Small number, big difference. The character snaps back to the ground instead of drifting.

Where movement becomes a game

Tight movement is the price of entry for a platformer, but it's just the start. The moment you add running, dashing, wall jumps, and attacks, you'll feel the code turn into a mess of booleans fighting each other. That's the problem a state machine solves, and it's worth reading before your player script gets out of hand.

If you want a structured path that builds movement fundamentals the right way, the free 2D Top-Down Movement quest covers the same CharacterBody2D base these platformer mechanics sit on.

FAQ

How do I add gravity to a CharacterBody2D in Godot 4?

Call get_gravity() and add it to velocity each frame while the body is in the air: velocity += get_gravity() * delta inside an if not is_on_floor() check. get_gravity() reads the project's default gravity, so you tune it in Project Settings.

What is coyote time and why do I need it?

Coyote time is a short grace period after walking off a ledge where a jump still registers. Players naturally press jump a few frames late, and without this window the controls feel unfair. A tenth of a second is enough to feel right without looking like a bug.

Why is my jump velocity negative?

In Godot's 2D coordinate system, the Y axis points down, so negative Y is up. To launch the character upward you set velocity.y to a negative number like -400. A positive value would push them into the floor.

godottutorial2dplatformer

Stop reading. Start building.

Write real Godot code in your browser, then build this for real in the Move & Jump Platformer Quest.

Free, and no card needed. Built by a real person, with new quests every month.

Get the next Godot build in your inbox

New quests, project breakdowns, and game-dev tips.

You'll get emails from CodingQuests with Godot tips, new tutorials, course updates, and occasional offers. Unsubscribe anytime. Privacy Policy

Written by Omar

We teach Godot 4 by making you build complete systems: inventories, save systems, action roguelike controllers, enemy AI. The scrolls are free. The quests are where it sticks.