I wanted the character in my platform game to feel better when jumping: a bit floaty on the way up, but quick and responsive on the way down.

The problem

A single gravity value gives you one of two bad options:

  • High gravity → snappy landing, but the jump feels stiff (no hang time at the apex).
  • Low gravity → nice floaty peak, but the descent is sluggish and the landing feels like moonwalking.

Mario, Celeste, and most good platformers solve this with asymmetric gravity: gentle pull while rising, stronger pull while falling.

The idea

Instead of exposing raw gravity numbers (hard to reason about), expose three intuitive targets:

ParameterWhat it means
jump_heightHow high the jump peaks (metres)
jump_rise_timeSeconds to reach the apex (up phase)
jump_fall_timeSeconds to fall back down (down phase)

Then derive gravity and launch speed from kinematics:

1
2
gravity  = 2 x height / time²
velocity = 2 x height / time

Since jump_fall_time < jump_rise_time, the fall gravity is automatically stronger.

Setup

  1. Create a Resource script for tuning values.
  2. Use it in your CharacterBody3D player script.

The tuning resource

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# player_tuning.gd
extends Resource
class_name PlayerTuning

@export_group("Jump")
## Peak height of a jump (m).
@export var jump_height := 2.6
## Seconds to reach the apex. Bigger = floatier rise.
@export var jump_rise_time := 0.34
## Seconds to fall back down. Smaller than rise_time = snappy landing.
@export var jump_fall_time := 0.26

Create a .tres file: right-click in the FileSystem -> New Resource -> PlayerTuning, then set:

  • jump_height = 2.6
  • jump_rise_time = 0.34
  • jump_fall_time = 0.26

These values give a jump that peaks around 2.6m with a noticeable hang at the top and a quick drop back down.

The player script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# player.gd
extends CharacterBody3D

@export var tuning: PlayerTuning

var _vy := 0.0


func _ready() -> void:
	if tuning == null:
		tuning = PlayerTuning.new()


func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("jump") and is_on_floor():
		# Launch speed derived from jump_height and jump_rise_time
		_vy = 2.0 * tuning.jump_height / tuning.jump_rise_time


func _physics_process(delta: float) -> void:
	# Pick the right time constant based on direction
	var t: float = tuning.jump_rise_time if _vy > 0.0 else tuning.jump_fall_time
	var g: float = 2.0 * tuning.jump_height / (t * t)

	_vy -= g * delta
	velocity.y = _vy

	move_and_slide()

	if is_on_floor() and _vy < 0.0:
		_vy = 0.0

That’s the whole technique: 6 lines of physics.

How it works

1
2
3
4
5
6
7
8
9
                jump_rise_time
                (gentle gravity)
           /""""\
          /      \  <- jump_height
         /        \
        /          \
   ----/            \------ <- ground
                jump_fall_time
                (stronger gravity)

When _vy > 0 (rising), we use jump_rise_time to compute gravity:

1
g_rise = 2 x 2.6 / 0.34² ~= 45 m/s²

When _vy <= 0 (falling), we use jump_fall_time:

1
g_fall = 2 x 2.6 / 0.26² ~= 77 m/s²

The fall gravity is about 1.7x stronger, so the descent is noticeably quicker than the ascent. The apex feels floaty because the upward gravity is gentle: the player lingers near the peak before dropping.

Tuning tips

Want…Change
Higher jumpIncrease jump_height (keeps timing the same)
More hang time at apexIncrease jump_rise_time
Snappier landingDecrease jump_fall_time
Moon jumpjump_height = 5.0, jump_rise_time = 0.6
Tight, responsive hopjump_height = 1.5, jump_rise_time = 0.2, jump_fall_time = 0.18