Lesson 20

Jump Feel

Same jump as lesson 2, but it stops feeling broken. Coyote time, jump buffering and variable height. Flip them off to feel why they exist.

Loading the game engine. This takes a moment the first time.

Platformer only

THIS LESSON ADDS NO NEW FEATURES. IT ONLY CHANGES HOW THE JUMP FEELS.

Every other lesson gives you something you could not do before. This one takes a jump you already have and makes it stop feeling broken. That is most of what game "polish" actually is, and it is worth knowing that the difference between a game that feels cheap and one that feels good is usually about fifteen lines like these.

THE THREE FIXES, AND THE COMPLAINT EACH ONE ANSWERS

"I pressed jump but nothing happened!" → COYOTE TIME

You ran off the ledge and pressed jump a few hundredths of a second later. The strict rule says you must be touching the floor, and you were not any more. Coyote time keeps the jump available for a moment after you leave the ground. Players never notice it is there. They only notice when it is missing.

"I pressed jump the instant I landed and it ignored me!" → JUMP BUFFER

You pressed just BEFORE touching down. Same rule, opposite side. Buffering remembers the press for a moment and fires it on landing.

"Every jump is the same giant leap." → VARIABLE HEIGHT

Holding and tapping did the same thing, so you could never make a small hop. Letting go early now cuts the jump short.

HOW TO ACTUALLY FEEL IT

Press the button to turn forgiveness OFF, then try the course below. The two brown platforms with the gap are built to catch you out: run off the left one and jump late. With forgiveness ON you make it. With it OFF you drop in the pit, and it feels like the game ignored you, even though the game did exactly what you told it to.

New code in this lesson

None of the earlier lessons use these. Each one shows the line where it appears here.

Input.is_action_just_released()

True only on the one frame the key is let go.

player.gd
if Input.is_action_just_released("jump") and velocity.y < 0.0:

*=

Multiplies what a variable already holds.

player.gd
velocity.y *= tap_jump_cut

The scenes

■ UI node · ■ 2D node · % unique name · .tscn instanced scene

game.tscn
  • Game Node2D game.gd
  • Floor StaticBody2D block.tscn
  • LeftWall StaticBody2D hidden block.tscn
  • RightWall StaticBody2D hidden block.tscn
  • Step StaticBody2D block.tscn
  • LeftPlatform StaticBody2D block.tscn
  • RightPlatform StaticBody2D block.tscn
  • TallPlatform StaticBody2D block.tscn
  • Player CharacterBody2D % player.tscn
  • HUD CanvasLayer % hud.tscn
hud.tscn
  • HUD CanvasLayer hud.gd
  • MarginContainer
  • Rows VBoxContainer
  • ForgivingButton Button %
  • ExplainLabel Label %
  • HintLabel Label
player.tscn
  • Player CharacterBody2D player.gd
  • Sprite2D
  • CollisionShape2D
block.tscn
  • Block StaticBody2D
  • Polygon2D
  • CollisionPolygon2D

FileSystem

  • game.tscn
  • game.gd
  • hud.tscn
  • hud.gd
  • player.tscn
  • player.gd
  • block.tscn

The code

extends Node2D

@onready var player: CharacterBody2D = %Player
@onready var hud: JumpHUD = %HUD

func _ready() -> void:
	hud.forgiving_toggled.connect(_on_forgiving_toggled)
	# The player starts forgiving. The HUD's button label starts matching it.
	player.forgiving = true

func _on_forgiving_toggled(enabled: bool) -> void:
	player.forgiving = enabled