v0.1 ⌊ALPHA⌉
On this page
  1. How it works
  2. Implementations
  3. Comparing the arcs
  4. History
  5. Variants and combinations
  6. Related
  7. References
  8. Sources

Tap jump for a hop; hold it for a full jump. Super Mario Bros., Sonic the Hedgehog, and Celeste all do this, by three different mechanisms.

Mario falls under lighter gravity while the button is held1. Releasing it caps Sonic’s upward speed2. Celeste holds Madeline’s take-off speed on a timer, then halves gravity near the apex3,4.

#How it works

All three games run one loop per update (one frame): gravity reduces the upward speed, and the speed moves the position. The button changes a different term in each.

Super Mario Bros.Sonic the HedgehogCeleste
Holdinglighter gravity, 0.117–0.156 px/frame² ($1E–$28)5nothing; one gravity, 0.21875 px/frame² ($38)6keeps the take-off speed for up to 0.2 s7; half gravity near the apex4
Releasingheavier gravity, 0.375–0.5625 px/frame² ($60–$90)8, for the rest of the jump1clamps upward speed to −4 px/frame (-$400)2ends the hold; the current speed carries on3
Apex, held vs tapped66–83 px held, 24–29 px tapped9100 px held, 39 px tapped1027 px held, 7 px tapped11

Heights are native pixels at the character’s feet, from flat ground, simulated frame by frame in each game’s own number format and update order. An independent re-implementation reproduces every figure (probes/jump-physics/jump-heights.mjs).

Interactive: jump-arc (needs JavaScript)

Jump arcs simulated from the cited constants. The slider sets how long jump is held; hover or tap for per-frame heights.

#Implementations

#Super Mario Bros. (1985)

A jump starts on the frame A goes down while Mario is on the ground12. His horizontal speed picks a row of four small tables13. The rows give three distinct jumps, for standing, walking, and running14. Each sets a take-off speed of −4 px/frame (−5 running)15,16, a held gravity, and a falling gravity.

on the frame A goes down while standing:             # CheckForJumping → InitJS
    row      = jump row for |x speed|
    speed    = TAKEOFF[row]                          # px/frame, fraction zero
    gravity  = HELD[row]
    fallGrav = FALL[row]

every frame in the air:                              # JumpSwimSub
    if speed >= 0:                                   # at or past the apex
        gravity = fallGrav
    else if not (A held now and A held last frame) and risen >= HALT:
        gravity = fallGrav                           # released; never switched back
    position += speed                                # ImposeGravity, 8.8 fixed point
    speed    += gravity
    if speed has reached MAX_FALL + 1/2: speed = MAX_FALL

The button never pushes Mario up; it picks only the gravity. Once JumpSwimSub swaps in the falling gravity, nothing swaps it back that jump1. The swap waits until Mario has risen 1 px17, which the press frame already covers18.

Held gravity is 0.117–0.156 px/frame² ($1E–$28)5 across the rows; falling gravity is 0.375–0.5625 px/frame² ($60–$90)8. For a standing jump, they are 0.125 px/frame²19 and 0.4375 px/frame²20.

Vertical speeds are 8.8 fixed point, with a third byte for the subpixel position21. Falling speed is capped at 4 px/frame22, but the clamp fires only once the speed’s fraction byte reaches $80, as at 4.5 px/frame23.

Jdaster64’s chart of SMB’s physics engine agrees on the take-off speeds and gravities15,5,8. It starts the fast jump at a higher horizontal speed than the code; the ledger follows the code13.

#Sonic the Hedgehog (1991)

Sonic jumps along the ground’s normal, so on flat ground all 6.5 px/frame ($680)24 goes upward25. The press frame doesn’t move him: Sonic_Jump drops its return address, skipping the rest of the grounded routine26. Curling into a ball lowers his center 5 px27, keeping his feet on the same row27.

on the frame a jump button goes down on the ground:  # Sonic_Jump
    ySpeed -= JUMP                                   # along the ground normal
    curl into a ball                                 # height radius 19 → 14, y += 5
    skip the rest of this frame                      # no movement yet

every frame in the air:                              # Sonic_MdJump2
    if jumping and no jump button held and ySpeed < -CAP:
        ySpeed = -CAP                                # Sonic_JumpHeight
    y      += ySpeed                                 # ObjectFall: old speed first,
    ySpeed += GRAVITY                                # then gravity

One gravity, 0.21875 px/frame² ($38)6, applies held or not. The release check runs every airborne frame, before the move28. With no jump button held, it clamps any faster rise to −4 px/frame (-$400)2. Gravity brings the take-off speed under the cap in 12 updates29, so later releases change nothing.

Speeds are 8.8 and positions 16.16 fixed point30, and nothing limits the fall31. The Sonic Retro community’s Sonic Physics Guide matches the disassembly, down to the release check2 and the motionless press frame26.

#Celeste (2018)

Celeste works in pixels per second and multiplies by Engine.DeltaTime every update32. A jump sets the vertical speed to -105 px/s33 and starts a 0.2 s7 timer34.

every update:
    if varJumpTimer > 0: varJumpTimer -= dt          # Player.Update, before the state runs
    # NormalUpdate:
    if airborne:
        mult  = 1/2 if |speed| < HALF_GRAV and jump held else 1
        speed = approach(speed, MAX_FALL, GRAVITY * mult * dt)
    if varJumpTimer > 0:
        if jump held: speed = min(speed, varJumpSpeed)   # up is negative: keep take-off speed
        else:         varJumpTimer = 0                   # release ends the hold, cuts nothing
    if jump pressed and jumpGraceTimer > 0:              # Jump()
        speed = JUMP_SPEED; varJumpSpeed = speed; varJumpTimer = VAR_JUMP_TIME
    moveV(speed * dt)                                    # whole pixels, fraction kept

Holding jump while the timer runs cancels each update’s gravity, so Madeline rises at a constant -105 px/s33,3. Gravity, 900 px/s²35,36, applies once the hold ends. Unlike Sonic’s clamp, an early release cuts no speed3.

A separate rule halves gravity while jump is held and her vertical speed is under 40 px/s37,4. The fall limit is 160 px/s38. Positions are whole pixels, with the fraction kept in a remainder39.

At 60 Hz40, the hold nominally spans 12 updates7, but the timer is a float and its last update rests on rounding. A 0.0166667 s step gives 11 updates; an exact 1/60 s step gives 1241, and 2 px more height11. The released code doesn’t set the step. The widget defaults to 0.0166667 s42, the likelier value (Coyote time explains why).

#Comparing the arcs

  • Super Mario Bros. is lopsided: a long, gentle rise, then a fall under 3.5×43 the gravity (standing). A release bends the rise into an early, steep descent1. Held jumps peak at 66 px standing, 71 px walking, and 83 px running44.
  • Sonic the Hedgehog is a symmetric parabola under one gravity6. A release cuts the rise to −4 px/frame (-$400)2 at once, starting a smaller parabola.
  • Celeste rises in a straight ramp while the hold lasts3, then follows a parabola with a flattened top4.

#History

#Variants and combinations

Celeste also has coyote time, a grace window for late jumps45. Super Mario Bros. and Sonic have none46,47.

References

  1. ^ abcdJumpSwimSub swaps in falling gravity, never back, once speed is zero or downward, or once A is not held both this frame and last after a DiffToHaltJump rise. T3Xkeeper0/smb1 · src/prg.asm L4756–4769Jdaster64 n.d. · JUMPING/VERTICAL PHYSICS
  2. ^ abcdeDuring a jump, with no jump button held, Sonic_JumpHeight clamps any rise faster than 4 px/frame to exactly -$400. T3sonicretro/s1disasm · _incObj/01 Sonic.asm L1273–1288Sonic Retro, “Sonic Physics Guide: Jumping” · Variable Jump Height
  3. ^ abcdeWhile varJumpTimer runs, holding jump keeps Madeline rising no slower than varJumpSpeed, canceling gravity; releasing zeroes the timer and cuts no speed. T3NoelFB/Celeste · Source/Player/Player.cs L2960–2966
  4. ^ abcdGravity halves while |Speed.Y| is under HalfGravThreshold and jump is held (or auto-jump is on), so a held jump hangs at the top. T3NoelFB/Celeste · Source/Player/Player.cs L2952
  5. ^ abcWhile a jump rises with A held, gravity is JumpMForceData's $20, $20, $1E, $28, $28 subpixels/frame² for rows 0–4. T3Xkeeper0/smb1 · src/prg.asm L4858–4859, L4966–4967Jdaster64 n.d. · JUMPING/VERTICAL PHYSICS, Mario table, Holding “A” Gravity (+Y)
  6. ^ abcObjectFall's gravity equate adds $38 subpixels/frame to an airborne object's downward speed each frame, Sonic's included. T3sonicretro/s1disasm · _incObj/sub ObjectFall & SpeedToPos.asm L5, _incObj/01 Sonic.asm L363Sonic Retro, “Sonic Physics Guide: Air State” · Constants
  7. ^ abcVarJumpTime, how long holding jump can keep the take-off speed, is 0.2 s. T3NoelFB/Celeste · Source/Player/Player.cs L51
  8. ^ abcAfter the switch, gravity is FallMForceData's $70, $70, $60, $90, $90 subpixels/frame² for rows 0–4. T3Xkeeper0/smb1 · src/prg.asm L4861–4862, L4968–4969Jdaster64 n.d. · JUMPING/VERTICAL PHYSICS, Mario table, Falling Gravity (+Y)
  9. ^Simulating the jump code on flat ground gives apexes, tapped/held, of 24/66 px standing, 27/71 px walking (row 2), and 29/83 px running (row 4). T4Xkeeper0/smb1 · src/prg.asm L4756–4769, L6664–6696probe probes/jump-physics/jump-heights.mjs
  10. ^Simulating the jump code (flat ground, out of water) gives apexes of 39 px tapped and 100 px held, airtimes 39 and 62 frames. T4sonicretro/s1disasm · _incObj/01 Sonic.asm L1273–1288, _incObj/sub ObjectFall & SpeedToPos.asm L16–20probe probes/jump-physics/jump-heights.mjs
  11. ^ abSimulating the jump code (flat ground, no lift boost, (float)0.0166667 step) gives apexes of 7 px tapped and 27 px held, airtimes 15 and 39 updates; an exact 1/60 s step gives 29 px held in 40. T4NoelFB/Celeste · Source/Player/Player.cs L2952–2966, L1660–1673probe probes/jump-physics/jump-heights.mjs
  12. ^A jump starts only on the frame A goes down while Mario is grounded; InitJS zeroes the vertical subpixel and sets the jumping state. T3Xkeeper0/smb1 · src/prg.asm L4913–4923, L4931–4942, L4139–4141
  13. ^ abInitJS picks one of five jump rows by take-off horizontal speed, stepping up at $09, $10, $19, and $1C. T3Xkeeper0/smb1 · src/prg.asm L4943–4955Jdaster64 n.d. · JUMPING/VERTICAL PHYSICS, Mario table
  14. ^Mario's top ground speed is $18 walking and $28 running (B held). T3Xkeeper0/smb1 · src/prg.asm L4873–4874, L5000–5014Jdaster64 n.d. · SMB PHYSICS 101 / GROUND PHYSICS, Mario table
  15. ^ abPlayerYSpdData starts a jump at −4 px/frame (rows 0–2) or −5 px/frame (rows 3–4). T3Xkeeper0/smb1 · src/prg.asm L4864–4865, L4972–4973Jdaster64 n.d. · JUMPING/VERTICAL PHYSICS, Mario table, Initial Velocity (−Y)
  16. ^InitMForceData gives jump rows 0–4 a zero speed fraction, so take-off is exactly −4.00 or −5.00 px/frame. T3Xkeeper0/smb1 · src/prg.asm L4867–4868, L4970–4971
  17. ^InitJS sets DiffToHaltJump to 1, so a release switches gravity only once Mario has risen at least 1 pixel. T3Xkeeper0/smb1 · src/prg.asm L4957–4958
  18. ^The press frame already moves Mario, since PlayerPhysicsSub starts the jump before the state dispatch runs JumpSwimSub and MovePlayerVertically. T3Xkeeper0/smb1 · src/prg.asm L4713–4725, L4796
  19. ^In rows 0–1, the standing and slow jumps, held gravity is $20/256 = 0.125 px/frame². T3Xkeeper0/smb1 · src/prg.asm L4858–4859, L6681–6687
  20. ^In rows 0–1, falling gravity is $70/256 = 0.4375 px/frame². T3Xkeeper0/smb1 · src/prg.asm L4861–4862, L6681–6687
  21. ^Vertical motion is 8.8 fixed point; each frame, ImposeGravity adds speed to position, the fraction via the YMF_Dummy subpixel byte, then gravity to speed. T3Xkeeper0/smb1 · src/prg.asm L6666–6687
  22. ^MovePlayerVertically gives ImposeGravity a maximum fall speed of 4 px/frame for Mario. T3Xkeeper0/smb1 · src/prg.asm L6547–6550, L6688–6696Jdaster64 n.d. · JUMPING/VERTICAL PHYSICS
  23. ^ImposeGravity clamps a fall of 4 px/frame or more only when the fraction byte is at least $80. T3Xkeeper0/smb1 · src/prg.asm L6688–6696Jdaster64 n.d. · JUMPING/VERTICAL PHYSICS
  24. ^Sonic's jump speed, son_jumpspeed, is $680 subpixels per frame (6.5 px/frame) out of water. T3sonicretro/s1disasm · _incObj/01 Sonic.asm L8, L1218Sonic Retro, “Sonic Physics Guide: Jumping” · Constants
  25. ^Sonic_Jump adds the jump speed along the ground normal (CalcSine of angle − $40); on flat ground that is exactly -$680 to Y speed, nothing to X. T3sonicretro/s1disasm · _incObj/01 Sonic.asm L1224–1233, _incObj/sub CalcSine.asm L53Sonic Retro, “Sonic Physics Guide: Jumping” · Jump Velocity
  26. ^ abSonic doesn't move on the press frame; Sonic_Jump drops its return address, skipping the rest of the grounded routine, SpeedToPos included. T3sonicretro/s1disasm · _incObj/01 Sonic.asm L1236, L317–323Sonic Retro, “Sonic Physics Guide: Jumping” · Bugs when Jumping
  27. ^ abA standing jump curls Sonic up, shrinking his height radius from 19 to 14 and adding 5 to Y, so his feet stay put. T3sonicretro/s1disasm · _incObj/01 Sonic.asm L1250–1254, _Constants.asm L312–314Sonic Retro, “Sonic Physics Guide: Characters” · Sonic
  28. ^In the airborne-rolling mode (Sonic_MdJump2), the release check runs before ObjectFall moves Sonic and adds gravity. T3sonicretro/s1disasm · _incObj/01 Sonic.asm L359–363Sonic Retro, “Sonic Physics Guide: Jumping” · Variable Jump Height
  29. ^Gravity slows Sonic's 6.5 px/frame take-off below the 4 px/frame release cap within 12 airborne updates. T4sonicretro/s1disasm · _incObj/01 Sonic.asm L8, _incObj/sub ObjectFall & SpeedToPos.asm L5, _incObj/01 Sonic.asm L1276–1284probe probes/jump-physics/jump-heights.mjs
  30. ^ObjectFall adds the pre-gravity speed, shifted left 8 bits, to the 32-bit position, so positions are 16.16 and speeds 8.8 fixed point. T3sonicretro/s1disasm · _incObj/sub ObjectFall & SpeedToPos.asm L16–20Sonic Retro, “Sonic Physics Guide: Calculations (Pixel and Subpixel)” · Pixel and Subpixel
  31. ^Sonic 1 never caps falling speed in the air; with FixBugs = 0, Sonic_JumpHeight's only cap is upward (-$FC0) and skipped after jumps. T3sonicretro/s1disasm · _incObj/01 Sonic.asm L1291–1302Sonic Retro, “Sonic Physics Guide: Air State” · Top Y Speed
  32. ^In each update, Player.Update ticks both jump timers, the state machine runs NormalUpdate (gravity, hold, and new jump), and then MoveV applies Speed.Y * Engine.DeltaTime. T3NoelFB/Celeste · Source/Player/Player.cs L742–744, L908, L934–935, L322–323JamesMcMahon/monocle-engine · Monocle/Entity.cs L95–98
  33. ^ abA normal jump sets Madeline's vertical speed to JumpSpeed, −105 px/s. T3NoelFB/Celeste · Source/Player/Player.cs L49
  34. ^Jump() sets vertical speed to JumpSpeed plus any platform lift boost, saves it as varJumpSpeed, starts varJumpTimer at VarJumpTime, and zeroes the grace timer. T3NoelFB/Celeste · Source/Player/Player.cs L1660–1673
  35. ^Celeste's gravity is 900 px/s²; each airborne update moves vertical speed toward the fall limit by Gravity * mult * Engine.DeltaTime. T3NoelFB/Celeste · Source/Player/Player.cs L24, L2957
  36. ^In the public Monocle source, Calc.Approach(val, target, maxMove) moves val toward target by at most maxMove without overshooting. T3JamesMcMahon/monocle-engine · Monocle/Util/Calc.cs L852–855
  37. ^HalfGravThreshold is 40 px/s, the vertical speed below which (in either direction) holding jump halves gravity. T3NoelFB/Celeste · Source/Player/Player.cs L25
  38. ^MaxFall, the normal limit on falling speed, is 160 px/s; NormalBegin starts maxFall there and gravity approaches it. T3NoelFB/Celeste · Source/Player/Player.cs L23, L2760–2763
  39. ^Celeste's actors sit on whole pixels; MoveV adds the float distance to a remainder, moves by its rounded value, and keeps the fraction. T2Thorson 2020 · Actor BasicsNoelFB/Celeste · Source/Player/Player.cs L935
  40. ^Celeste's developers describe the game as locked to 60 updates per second, so the bundled PICO-8 port skips every second frame. T2NoelFB/Celeste · Source/PICO-8/Emulator.cs L268–270
  41. ^The 0.1 s grace and 0.2 s variable-jump timers end on float rounding; a (float)0.0166667 step gives 5 and 11 updates, and (float)(1/60) gives 6 and 12. T4NoelFB/Celeste · Source/Player/Player.cs L713–720, L742–744WPI DGMI wiki, “Coyote Time” · Examplesprobe probes/jump-physics/jump-heights.mjs
  42. ^At normal speed, Celeste's Engine.DeltaTime is most likely (float)0.0166667 s, the XNA and FNA default 166,667-tick step, not an exact 1/60 s. T1FNA-XNA/FNA · src/Game.cs L247NoelFB/Celeste · Source/Player/Readme.md L28JamesMcMahon/monocle-engine · Monocle/Engine.cs L198–199WPI DGMI wiki, “Coyote Time” · Examples
  43. ^In rows 0–1, falling gravity ($70) is 3.5 times held gravity ($20). T4Xkeeper0/smb1 · src/prg.asm L4858–4862probe probes/jump-physics/jump-heights.mjs
  44. ^Simulated with A held, Mario's jumps peak at 66 px standing, 71 px walking, and 83 px running. T4Xkeeper0/smb1 · src/prg.asm L4756–4769, L6664–6696probe probes/jump-physics/jump-heights.mjs
  45. ^Grounded updates refill jumpGraceTimer; airborne ones subtract Engine.DeltaTime before NormalUpdate, which jumps on a press while it is above zero. T3NoelFB/Celeste · Source/Player/Player.cs L713–720, L2970–2975
  46. ^Mario gets no grace window after leaving a ledge; ProcJumping refuses airborne jumps unless he is swimming. T3Xkeeper0/smb1 · src/prg.asm L4921–4925
  47. ^Sonic gets no grace window after leaving a ledge: only the grounded modes, Sonic_MdNormal and Sonic_MdRoll, call Sonic_Jump. T3sonicretro/s1disasm · _incObj/01 Sonic.asm L113–116, L318, L348
  48. ^Swimming jumps use row 5 of the jump tables, or row 6 in a whirlpool. T3Xkeeper0/smb1 · src/prg.asm L4959–4964

Sources

Code

Primary

Secondary