Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

A reliable platformer jump needs more than moving a character upward: track vertical velocity, apply gravity over time, and resolve collisions so the player lands cleanly. This guide builds that controller for a Java 2D game using a simple player rectangle and platforms. It covers edge-triggered jump input, frame-rate-independent motion, directional collision handling, and practical tuning.

How a platformer jump works

Java 2D gives you drawing and geometry tools, but it does not include a platformer character controller. You supply the movement rules, update timing, input state, and collision resolution. Oracle’s Java 2D overview describes the API’s rendering and imaging role.

In the usual Java screen coordinate system, the origin is at the upper-left and y increases downward. That means an upward jump has a negative vertical velocity. The player rises while that velocity is negative, slows as gravity brings it toward zero, reaches the apex, then falls as velocity becomes positive. See Oracle’s Java 2D coordinate-system explanation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep position and velocity separate: position says where the player is; velocity says how quickly it moves. A minimal controller needs floating-point x and y, horizontal and vertical velocities, player dimensions, and a grounded flag. Keep physics in double or float; round only when drawing or constructing temporary integer bounds.

Choose the jump arc

Use a time-based model, with distance measured in pixels, time in seconds, velocity in pixels per second, and gravity in pixels per second squared. Pick a desired jump height H and time to apex T:

jumpSpeed = 2 * H / T
gravity   = 2 * H / (T * T)

For a jump about 120 pixels high that reaches its apex in 0.45 seconds, those values are approximately 533.33 px/s and 1185.19 px/s². They are examples, not universal constants: character scale and desired feel matter. Increase jump speed for a taller arc; increase gravity for a heavier, faster fall; reduce gravity for a floatier arc.

Make jump input a press, not a held command

A common bug is resetting the jump velocity every update while the key remains down:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
if (jumpPressed) {
    velocityY = -jumpSpeed;
}

This can make the player hover or repeatedly restart the jump. Keep keyboard state in booleans and let the update method decide whether a jump is legal. Detect the transition from released to pressed, and require the player to be grounded:

if (jumpPressed && !jumpWasPressed && grounded) {
    velocityY = -jumpSpeed;
    grounded = false;
}
jumpWasPressed = jumpPressed;

Separate input collection from physics changes. A key listener or key binding can update leftPressed, rightPressed, and jumpPressed; the game update consumes those values consistently. In Swing, make sure the intended component can receive focus, attach input handling to the correct component, and clear or resynchronize held-key state when the window loses focus.

Move and collide one axis at a time

Rectangle intersection tells you that two bounds overlap; it does not tell you whether the player landed, hit a wall, or struck a platform from underneath. Resolve horizontal motion and vertical motion separately. On a vertical collision, use the direction of travel: a falling player can land, while a rising player can hit the underside.

For a landing, the player must be moving downward, overlap the platform horizontally, and cross its top from above. Then place the player exactly on the surface, stop downward movement, and set grounded:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
y = platform.y - playerHeight;
velocityY = 0.0;
grounded = true;

Without the position correction, a player can remain partly inside a floor, jitter, or sink on the next update. For a ceiling hit while rising, place the player immediately below the platform and set vertical velocity to zero. Java’s Rectangle is adequate for simple axis-aligned platforms; the Java 2D geometry documentation covers the relevant shape model.

Example player controller

This controller uses separate horizontal and vertical passes. It assumes the platform rectangles do not overlap in ways that create conflicting corrections; for a more complex level, use a deliberate collision ordering and consider smaller physics steps or swept collision tests.

import java.awt.Rectangle;
import java.util.List;

public final class Player {
    private double x, y;
    private double velocityX, velocityY;
    private final int width, height;
    private boolean grounded;

    private static final double MOVE_SPEED = 220.0;
    private static final double AIR_ACCELERATION = 1800.0;
    private static final double GROUND_ACCELERATION = 2400.0;
    private static final double MAX_FALL_SPEED = 1200.0;

    private final double gravity;
    private final double jumpSpeed;

    public Player(double x, double y, int width, int height,
                  double gravity, double jumpSpeed) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.gravity = gravity;
        this.jumpSpeed = jumpSpeed;
    }

    public void update(double dt, boolean leftPressed,
                       boolean rightPressed, boolean jumpPressed,
                       boolean jumpWasPressed, List<Rectangle> platforms) {
        double input = (rightPressed ? 1.0 : 0.0)
                     - (leftPressed ? 1.0 : 0.0);
        double acceleration = grounded
                ? GROUND_ACCELERATION : AIR_ACCELERATION;
        velocityX = approach(velocityX, input * MOVE_SPEED,
                             acceleration * dt);

        if (jumpPressed && !jumpWasPressed && grounded) {
            velocityY = -jumpSpeed;
            grounded = false;
        }

        velocityY = Math.min(velocityY + gravity * dt, MAX_FALL_SPEED);
        moveHorizontally(velocityX * dt, platforms);
        moveVertically(velocityY * dt, platforms);
    }

    private void moveHorizontally(double amount, List<Rectangle> platforms) {
        x += amount;
        Rectangle bounds = bounds();
        for (Rectangle p : platforms) {
            if (!bounds.intersects(p)) continue;
            if (amount > 0) x = p.x - width;
            else if (amount < 0) x = p.x + p.width;
            bounds = bounds();
        }
    }

    private void moveVertically(double amount, List<Rectangle> platforms) {
        grounded = false;
        y += amount;
        Rectangle bounds = bounds();
        for (Rectangle p : platforms) {
            if (!bounds.intersects(p)) continue;
            if (amount > 0) {
                y = p.y - height;       // landing while falling
                velocityY = 0.0;
                grounded = true;
            } else if (amount < 0) {
                y = p.y + p.height;     // hit underside while rising
                velocityY = 0.0;
            }
            bounds = bounds();
        }
    }

    private Rectangle bounds() {
        return new Rectangle((int) Math.round(x), (int) Math.round(y),
                             width, height);
    }

    private static double approach(double current, double target,
                                   double amount) {
        if (current < target) return Math.min(current + amount, target);
        return Math.max(current - amount, target);
    }

    public Rectangle getBoundsForRendering() { return bounds(); }
    public double getX() { return x; }
    public double getY() { return y; }
    public boolean isGrounded() { return grounded; }
}

Call update with the same jump-press history the input layer uses, then update that history after the player has consumed it. A typical panel keeps the platform list and input booleans, calls the player update from its game loop, and repaints. In paintComponent, call super.paintComponent(g), cast to Graphics2D, draw the platforms, and draw the player bounds or sprite. The Java 2D rendering tutorial and Graphics2D API documentation cover drawing shapes and images.

For a simplified floor-only test, compare the old and proposed player bottom against the floor top and check horizontal overlap. For multiple platforms, track previous position or use axis-separated movement as above. A player moving upward should never be snapped onto a platform top merely because its rectangle intersects it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep motion consistent over time

Frame-based code such as velocityY += 1; y += velocityY; runs differently at different update rates. Scale acceleration and displacement by elapsed seconds:

velocityY += gravity * deltaTime;
y += velocityY * deltaTime;

Clamp a variable timestep after pauses, debugger stops, or system stalls so one delayed frame does not advance the player through a level:

double deltaTime = (now - previousTime) / 1_000_000_000.0;
deltaTime = Math.min(deltaTime, 0.05);

A fixed physics step is often more predictable for collision-sensitive games. Accumulate elapsed time and update in steps of 1/60 second:

final double FIXED_STEP = 1.0 / 60.0;
accumulator += elapsedSeconds;
accumulator = Math.min(accumulator, 0.25);
while (accumulator >= FIXED_STEP) {
    updateGame(FIXED_STEP);
    accumulator -= FIXED_STEP;
}

The cap prevents a long stall from demanding an unbounded catch-up workload. Variable timesteps are simpler to add to an existing loop; fixed steps are easier to reproduce and tune. Neither makes collision infallible: a very fast player can still cross a thin platform between steps, so reduce the step, use substeps, or check the path between previous and proposed bounds.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Tune horizontal movement and polish

The example approaches a target horizontal speed rather than snapping instantly. It uses different acceleration on the ground and in the air; lower air acceleration gives less airborne control. If you prefer immediate movement, set horizontal velocity directly from input. Keep that choice independent from vertical jump tuning.

  • Coyote time: Allow a jump for a brief interval after leaving an edge. Set a timer while grounded, count it down while airborne, and permit a jump while it remains positive.
  • Jump buffering: Remember a fresh jump press briefly before landing. If the player becomes grounded before the timer expires, launch immediately.
  • Variable jump height: If the jump key is released while the player is still rising, reduce upward velocity, for example by multiplying it by 0.5. Tune the factor; a large cut can feel abrupt.
  • Double jump: Track jumps remaining and restore the count on landing. This is a game rule, not something required by the physics model.

Derive animation from physics: negative vertical velocity while airborne means rising, positive means falling, grounded movement means running, and grounded stillness means idle. Keep a collision rectangle or feet sensor separate from the sprite image; transparent padding can make artwork appear to land early or catch on walls. Start with colored rectangles, then add images once movement and collisions work.

Debug the collision, not just the animation

Temporarily draw the player’s collision bounds and display key values:

g.draw(player.getBoundsForRendering());
g.drawString("velocityY: " + velocityY, 10, 20);
g.drawString("grounded: " + grounded, 10, 40);

When a landing fails, inspect the previous and current y, player bottom, vertical velocity, platform top, and grounded state. This quickly distinguishes a missed crossing from a bad input transition or a rectangle that does not match the visible sprite.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Falls through a platform: Check that vertical collision runs, that the player crossed the platform top while falling, and that the time step is not too large. Resolve to platform.y - height.
  • Jitters or sinks on the floor: Reset grounded before each vertical pass, snap exactly to the surface, set vertical velocity to zero, and retain floating-point physics state.
  • Hits the underside but lands on top: Branch on vertical movement direction; do not treat every overlap as a landing.
  • Collides with platform sides incorrectly: Resolve axes separately and use the movement direction. A single intersection check cannot classify the contact.
  • Jump changes with frame rate: Use seconds-based delta time consistently or a fixed timestep; do not mix per-frame horizontal and per-second vertical values.
  • Jump key appears broken: Verify focus, listener attachment, and key-release handling before changing physics.

When plain Java 2D is enough

Native Java 2D with Swing/AWT is a reasonable choice for a small desktop game or a project focused on learning movement and collision fundamentals. It leaves you responsible for the loop, input, camera, asset handling, sound, and other game systems. If you need a broader cross-platform game framework, libGDX provides Java-oriented tooling and documentation for setup, lifecycle, rendering, input, and game logic; see its simple game guide and project import guide. It adds framework concepts, and you still choose the platformer’s movement rules. Neither option is universally better: use the one that fits the project’s scope.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.