DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PC×
Skip to content
VGSources
Blog

Implementing a Simple Rhythm Game in Java: A Beginner’s Guide

A hands-on beginner guide to a four-lane rhythm game in Java, using Swing painting, key bindings, a monotonic clock, chart data, and simple scoring.
WorldJava: A Beginner's Guide Length9 min Posted Quest giverVGSources Team
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Build a playable four-lane rhythm game with Java’s built-in Swing and Java 2D APIs: notes move toward a judgment line, D/F/J/K trigger lanes, and each press earns a Perfect, Good, or Miss result. The key is to keep note timing tied to elapsed time—not to how often the screen happens to update.

This is a learning project, not a production rhythm engine. It needs a JDK and a desktop environment that can open a Swing window; no third-party library is required.

As an Amazon Associate I earn from qualifying purchases.

What you’ll build

The first version has four lanes, tap notes, a fixed judgment line, a small chart, score, combo, and three judgments. Its parts map neatly to Java fundamentals:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Game feature Java concept
A note Class and fields
Chart List or array
Updates Swing timer and callbacks
Drawing Inheritance and paintComponent
Lane controls Key bindings and actions
Hit detection Conditionals and absolute differences
Score and game state Mutable state and enums
Restart Resetting object state

Keep the first build narrow. Long notes, multiple songs, BPM changes, menus, persistence, and exact music synchronization can wait until this version works.

Set up and launch the window

Use JFrame as the top-level window and a custom JPanel as the game surface. Swing and Java 2D are included in Java SE’s desktop APIs; Oracle’s desktop overview describes the available desktop technologies.

Create a file named RhythmGame.java. The launcher should create Swing components on the event-dispatching thread:

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class RhythmGame {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Simple Rhythm Game");
            GamePanel panel = new GamePanel();

            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setContentPane(panel);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setResizable(false);
            frame.setVisible(true);
            panel.startGame();
        });
    }
}

At this point, GamePanel is the panel you will implement next. A JFrame is Swing’s top-level window. Swing components should be created and updated in accordance with Swing’s threading model.

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

Compile and run from the directory containing the source file:

javac RhythmGame.java
java RhythmGame

A JDK provides the compiler as well as the runtime. If you use a named package instead, compile to an output directory and run using the fully qualified class name, for example javac -d out src/com/example/rhythm/RhythmGame.java followed by java -cp out com.example.rhythm.RhythmGame.

Represent notes and chart times

Give each note a zero-based lane, a scheduled time in milliseconds from the start of the round, and a flag that prevents it from being judged twice:

import java.util.List;

final class Note {
    final int lane;
    final long hitTimeMs;
    boolean judged;

    Note(int lane, long hitTimeMs) {
        this.lane = lane;
        this.hitTimeMs = hitTimeMs;
    }
}

List<Note> notes = List.of(
    new Note(0, 1000),
    new Note(1, 1500),
    new Note(2, 2000),
    new Note(3, 2500),
    new Note(0, 3000),
    new Note(2, 3500)
);

The times above are example chart data, not measured song timings. Keep the list sorted by hitTimeMs; the input algorithm later relies on that ordering. A chart is timing data, not screen coordinates, so the same chart can be drawn at a different panel size or note speed.

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

For the first build, hard-code the list. A later plain-text CSV chart could use rows such as lane,timeMs and 0,1000. JSON generally needs a parser library, so it adds little value to the first playable version.

Use elapsed time as the game clock

A Swing timer asks the game to update and repaint, but callbacks are not guaranteed to arrive at an exact interval. Keep those ideas separate: the timer schedules work; a monotonic clock determines where each note belongs. Java’s Swing timer documentation notes that timer actions run on the event-dispatching thread, so handlers should stay short.

Record a start time when a round begins and derive elapsed milliseconds from System.nanoTime() on each update:

long startNanos;
long songTimeMs;

private void updateGame() {
    if (state != GameState.PLAYING) {
        return;
    }

    songTimeMs = (System.nanoTime() - startNanos) / 1_000_000L;
    markMissedNotes();
}

Use long for elapsed time. Avoid moving a note with a fixed instruction such as note.y += 5: if callbacks stall or vary, movement speed varies too. Instead, calculate its screen position from its chart time:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
private static final int HIT_LINE_Y = 500;
private static final double NOTE_SPEED = 0.35; // pixels per millisecond

double distance = (note.hitTimeMs - songTimeMs) * NOTE_SPEED;
int y = (int) (HIT_LINE_Y - distance);

Before its scheduled time, a note is above the line; at that time, it reaches the line; afterward, it passes below. The 0.35 pixels-per-millisecond value is a tunable example, not a standard. A 16 ms timer requests updates roughly every 16 ms; it does not guarantee 60 updates per second.

Build the game panel and update timer

Keep the initial implementation in one file if that helps you get a result sooner. Once it works, separate responsibilities into RhythmGame (launcher), GamePanel (game state and drawing), Note (note data), Chart (song notes), and optionally SoundManager (audio).

In GamePanel, set a preferred size, make the panel focusable, and create a Swing timer. For example:

enum GameState {
    READY,
    PLAYING,
    FINISHED
}

private static final int PANEL_WIDTH = 800;
private static final int PANEL_HEIGHT = 600;
private static final int LANE_COUNT = 4;

private GameState state = GameState.READY;
private long startNanos;
private long songTimeMs;
private int score;
private int combo;
private int maxCombo;
private String lastJudgment = "";
private final Timer timer = new Timer(16, e -> {
    updateGame();
    repaint();
});

In the panel constructor, use setPreferredSize(new Dimension(PANEL_WIDTH, PANEL_HEIGHT)) and setFocusable(true). The JPanel API describes the panel as a lightweight Swing container. Keep timer work brief: do not load files, decode audio, or perform expensive calculations there.

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

Draw lanes, notes, and the judgment line

Override paintComponent, call the superclass implementation first, then draw the current game state. Oracle’s Swing painting overview explains this custom-painting approach and Swing’s built-in double buffering.

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g.create();
    try {
        g2.setColor(Color.BLACK);
        g2.fillRect(0, 0, getWidth(), getHeight());
        drawLanes(g2);
        drawNotes(g2);
        drawHud(g2);
    } finally {
        g2.dispose();
    }
}

Divide the current panel width into four lanes rather than assuming the window can never change size:

private void drawLanes(Graphics2D g2) {
    int laneWidth = getWidth() / LANE_COUNT;

    for (int lane = 0; lane < LANE_COUNT; lane++) {
        int x = lane * laneWidth;
        g2.setColor(lane % 2 == 0
            ? new Color(35, 35, 45)
            : new Color(50, 50, 60));
        g2.fillRect(x, 0, laneWidth, getHeight());
        g2.setColor(Color.DARK_GRAY);
        g2.drawRect(x, 0, laneWidth, getHeight());
    }

    g2.setColor(Color.WHITE);
    g2.fillRect(0, HIT_LINE_Y, getWidth(), 4);
}

Draw only unjudged notes that are near the visible area:

private void drawNotes(Graphics2D g2) {
    int laneWidth = getWidth() / LANE_COUNT;

    for (Note note : notes) {
        if (note.judged) {
            continue;
        }

        int y = (int) (HIT_LINE_Y
            - (note.hitTimeMs - songTimeMs) * NOTE_SPEED);
        if (y < -40 || y > getHeight() + 40) {
            continue;
        }

        int x = note.lane * laneWidth + 12;
        int width = laneWidth - 24;
        g2.setColor(Color.CYAN);
        g2.fillRoundRect(x, y - 12, width, 24, 10, 10);
    }
}

Implement drawHud to display the score, combo, most recent judgment, and the mapping D / F / J / K to lanes 1–4. Avoid creating large numbers of objects or doing slow work in painting.

Free tools Windows power users keep installed

One-click scans. No signup required.

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

Bind lane keys and judge the nearest note

Swing key bindings connect a keystroke to an action through an InputMap and ActionMap; they are usually a better fit than a raw KeyListener for this panel because they are less dependent on one component receiving keyboard focus. See the JComponent key-binding API.

private void bindKey(String key, int lane) {
    InputMap inputMap = getInputMap(
        JComponent.WHEN_IN_FOCUSED_WINDOW);
    ActionMap actionMap = getActionMap();

    inputMap.put(KeyStroke.getKeyStroke(key), "lane-" + lane);
    actionMap.put("lane-" + lane, new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
            judgeLane(lane);
        }
    });
}

// Call in the GamePanel constructor:
bindKey("D", 0);
bindKey("F", 1);
bindKey("J", 2);
bindKey("K", 3);

Use configurable judgment windows. These 60 ms and 140 ms values are example design choices, not universal rhythm-game standards:

private static final long PERFECT_WINDOW_MS = 60;
private static final long GOOD_WINDOW_MS = 140;
private static final long MISS_WINDOW_MS = 180;

On a press, find the closest unjudged note in that lane that is inside the Good window. Never select an arbitrary note: otherwise an early press might hit a later note. Since the chart is sorted, the loop can stop once it reaches a note too far in the future:

private void judgeLane(int lane) {
    if (state != GameState.PLAYING) {
        return;
    }

    for (Note note : notes) {
        if (note.judged || note.lane != lane) {
            continue;
        }

        long difference = Math.abs(note.hitTimeMs - songTimeMs);
        if (difference <= GOOD_WINDOW_MS) {
            boolean perfect = difference <= PERFECT_WINDOW_MS;
            judge(note, perfect ? "PERFECT" : "GOOD",
                  perfect ? 1000 : 500);
            return;
        }

        if (note.hitTimeMs > songTimeMs + GOOD_WINDOW_MS) {
            break;
        }
    }
}

Update the logical clock before handling presses, for example in the timer update and at the beginning of judgeLane, so input is compared against current elapsed time rather than a stale previous tick. A press with no eligible note does nothing. These example score values are deliberately simple.

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

Update score and combo once per judged note:

private void judge(Note note, String result, int points) {
    note.judged = true;
    score += points;
    combo++;
    maxCombo = Math.max(maxCombo, combo);
    lastJudgment = result;
}
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Mark misses and finish the round

A note that passes its miss window must be marked once. If it is left unjudged, every timer callback could reset combo again:

private void markMissedNotes() {
    for (Note note : notes) {
        if (!note.judged
                && songTimeMs > note.hitTimeMs + MISS_WINDOW_MS) {
            note.judged = true;
            combo = 0;
            lastJudgment = "MISS";
        }
    }
}

After marking misses, test whether every chart note is judged. If so, switch to FINISHED and stop the timer. The 180 ms miss window above is another tunable example, separate from the Perfect and Good windows.

Add a reliable start and restart

Reset all mutable round data before starting. Setting the start timestamp before switching to PLAYING avoids calculating elapsed time from an uninitialized clock.

private void startGame() {
    timer.stop();
    score = 0;
    combo = 0;
    maxCombo = 0;
    lastJudgment = "";
    songTimeMs = 0;

    for (Note note : notes) {
        note.judged = false;
    }

    startNanos = System.nanoTime();
    state = GameState.PLAYING;
    timer.start();
}

Call this method after the frame is visible for the first round, and bind a restart action when the state is FINISHED. Stopping an already-running timer before reset prevents an old round’s updates from continuing through a restart. A chart with no notes should finish immediately or be rejected as empty.

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

Debug the first playable version

Symptom Likely cause What to check
Blank window The panel is not being painted or another drawing step obscures it. Set the panel as the content pane, override paintComponent, and call super.paintComponent(g).
Keys do nothing The window lacks focus, the binding is on the wrong component, or a control has taken focus. Use WHEN_IN_FOCUSED_WINDOW; check the key names and visible mapping. If needed, call requestFocusInWindow() after the window is visible. Avoid a text field or button stealing focus during play.
Notes move inconsistently Position is based on callback count. Calculate Y from elapsed milliseconds and chart time.
Every note is missed Chart times and game clock use different units or origins. Use milliseconds from round start for both.
Notes score more than once The note is not marked judged immediately. Set judged in the same operation that records its result.
Later note hits when pressing early Candidate selection chose any note in the lane. Choose the closest eligible note and keep the chart sorted.
Window freezes Timer action does lengthy work on Swing’s event-dispatching thread. Keep updates short and move file or audio loading out of the callback.

Other edge cases to handle as you extend the game include duplicate timestamps, overlapping windows for same-lane notes, notes outside the visible play area, a long pause or computer sleep causing a time jump, and holding a key. Treat each key action as a press, not as a continuous hit; if repeated key events are a problem, track press/release state. If the window loses focus, input may stop even while the timer continues.

Add sound only after gameplay works

Java Sound is part of Java SE’s desktop technology set. Start with a short preloaded hit effect, and keep the chart clock independent of playback. A basic Clip.start() call does not make full-song playback sample-synchronized with chart timing.

If a WAV file is packaged as a classpath resource such as /hit.wav, a basic loader can look like this:

try (AudioInputStream input = AudioSystem.getAudioInputStream(
        RhythmGame.class.getResource("/hit.wav"))) {
    Clip clip = AudioSystem.getClip();
    clip.open(input);
    clip.start();
} catch (UnsupportedAudioFileException
        | IOException
        | LineUnavailableException ex) {
    ex.printStackTrace();
}

Use a packaged resource rather than a machine-specific path such as C:UsersNameDesktophit.wav. For a real project, also manage clip lifetime and loading failures deliberately. Full tracks, offset calibration, and audiovisual synchronization are a separate, more advanced problem.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

When to move beyond Swing

Swing exposes window creation, painting, input, and timing without a third-party engine or build-system setup, making it suitable for a small educational 2D prototype. It is not a dedicated game framework, and its timer should not be treated as a high-precision audio clock. A dedicated game loop can offer more simulation control, but adds thread-management and shutdown complexity; graduate to one when the project’s needs justify that cost.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

More quests from Patch Notes

  1. How to Set Up a RedM (RDR2) Server in 2026: License Key, txAdmin and server.cfg, Step by StepBlog11min
  2. Best RedM (RDR2) Server Hosting in 2026: Comparing Four Hosts on Slots, Memory and PriceBlog10min
  3. How to Host a Mindustry Server in 2026: server-release.jar, Port 6567 and the Commands That MatterBlog7min
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.