You can make a game in Java using nothing but the JDK (Java Development Kit, the full toolchain for compiling and running Java programs) and the standard library.
This guide walks you through a working game loop, a Java2D rendering pipeline using BufferStrategy, and a project structure you can actually extend, all tested against OpenJDK 21.
TL;DR
- JDK 17 or 21 LTS is the right baseline. No external engine required.
- Your game loop runs on a dedicated thread, separate from the Swing EDT.
- Use
BufferStrategyandGraphics2Dfor rendering, notrepaint().- Separate update logic and rendering from the start. Mixing them creates bugs.
- This architecture suits learning and small projects. LibGDX is the right next step for larger ones.
What You Need Before Writing Any Game Code
Start with JDK 17 or 21 LTS. Both are long-term support releases with stable APIs and good tooling support. JDK 21 adds virtual threads (Project Loom), which don’t affect a basic game loop but matter if you later add background asset loading. You don’t need JavaFX, LibGDX, or any third-party library. The java.awt and javax.swing packages in the standard library give you a window, a drawing surface, and input events.
For your IDE, IntelliJ IDEA Community Edition or VS Code with the Java Extension Pack both work well. Create a plain Java project with a src/main/java directory. No build tool is required to follow this guide, though Maven or Gradle will help once you add assets.
Understanding the Java Game Loop
A Java game loop is a continuous cycle that processes player input, updates the game state, and renders a frame to the screen, repeating until the player quits. It has three core phases: input, update, and render. Every frame your game produces comes from one pass through this cycle.
Fixed Timestep vs. Variable Timestep
A fixed timestep loop runs updates at a constant rate regardless of how long rendering takes. A variable timestep loop ties update speed to elapsed time, which sounds flexible but causes physics inconsistencies on slower machines. For your first Java 2D game, use a fixed timestep. It’s predictable, easier to debug, and keeps game speed consistent across hardware.
The reason we use a fixed timestep here is that variable timing introduces floating-point drift when you multiply velocity by delta time across hundreds of frames. You’ll feel this as jitter in movement. Fixed updates at 60 UPS (updates per second) avoid that entirely.
Why the Loop Needs Its Own Thread
Swing runs on the EDT (Event Dispatch Thread), which handles all UI repaints and input events. If you run your game loop on the EDT, it blocks all input handling and the window stops responding. We’ve seen this exact problem when developers use a javax.swing.Timer as a loop driver: frame timing becomes inconsistent and the UI freezes under load. The fix is to run the loop on a dedicated thread that calls your update and render methods directly.
Setting Up the Game Window
Create a JFrame as your application window and a JPanel subclass as the rendering surface. The panel needs three configuration calls before anything else works correctly.
public class GamePanel extends JPanel implements Runnable {
static final int WIDTH = 800, HEIGHT = 600;
public GamePanel() {
setPreferredSize(new Dimension(WIDTH, HEIGHT));
setBackground(Color.BLACK);
setDoubleBuffered(false); // We manage buffering manually via BufferStrategy
setFocusable(true);
}
}
Call setDoubleBuffered(false) because you’ll manage double buffering yourself through BufferStrategy. Letting Swing handle it and then also using BufferStrategy causes rendering conflicts. The JFrame setup is three lines: pack, center, and set visible.
Implementing the Game Loop with a Runnable Thread
- Implement
Runnableon yourGamePanelclass and add astartGameThread()method that creates and starts aThread. - Write the
run()method with a nanosecond-based timer controlling 60 updates per second. - Call
update()andrender()from inside the loop, in that order.
private Thread gameThread;
private final int TARGET_UPS = 60;
public void startGameThread() {
gameThread = new Thread(this);
gameThread.start();
}
@Override
public void run() {
double nsPerUpdate = 1_000_000_000.0 / TARGET_UPS; // nanoseconds per update
long lastTime = System.nanoTime(); // We use nanoTime for higher precision than currentTimeMillis
double delta = 0;
while (gameThread != null) {
long now = System.nanoTime();
delta += (now - lastTime) / nsPerUpdate;
lastTime = now;
if (delta >= 1) {
update(); // advance game state
render(); // draw the current frame
delta--;
}
}
}
System.nanoTime() gives nanosecond resolution and is not affected by system clock adjustments. System.currentTimeMillis() can jump forward or backward when the OS syncs the clock, which breaks your delta calculation. This connects to rendering consistency because a bad delta produces frames that appear to stutter even when your logic is correct.
Try changing TARGET_UPS from 60 to 30 or 120 and observe how update frequency changes. At 30 UPS, movement will feel noticeably less smooth. At 120, you’ll see no visual difference unless your monitor supports it, but CPU usage will rise.
Rendering Frames with BufferStrategy and Graphics2D
BufferStrategy (available since Java 1.4, consistent through Java 21) manages off-screen buffers to prevent screen tearing. It draws your frame to a hidden buffer, then flips it to the screen atomically. This is why rendering feels smooth compared to calling repaint(), which schedules a paint on the EDT and gives you no control over timing.
How to Prevent Screen Flickering in Your Java Game
The answer is BufferStrategy with two or three buffers. Call createBufferStrategy(3) after the window is visible, get a Graphics2D context from the buffer, draw your frame, dispose the context, then show the buffer. Disposing the context after each frame matters. Skipping it causes a memory leak because AWT allocates native graphics resources that don’t get freed automatically.
private void render() {
BufferStrategy bs = getBufferStrategy();
if (bs == null) {
createBufferStrategy(3); // createBufferStrategy requires the component to be visible first
return;
}
Graphics2D g2 = (Graphics2D) bs.getDrawGraphics();
g2.setColor(Color.BLACK);
g2.fillRect(0, 0, WIDTH, HEIGHT); // clear the previous frame
// Draw a placeholder rectangle to confirm the pipeline works
g2.setColor(Color.WHITE);
g2.fillRect(100, 100, 50, 50);
g2.dispose(); // release native graphics resources
bs.show(); // flip the buffer to the screen
}
Run this and you’ll see a white rectangle on a black background. That confirms your rendering pipeline is connected end to end.
Structuring Your Java Game Project for Growth
Four classes carry the initial architecture:
- GamePanel: owns the game loop thread and calls update/render each cycle
- GameState: holds and updates all game data (player position, score, entities)
- InputHandler: implements
KeyListenerwith boolean flags per key - Entity: base class for anything that moves or can be drawn
Keep update() and render() as separate methods. Mixing game logic and drawing in one method makes both harder to debug. This flat structure works for small projects. Once you add menus or multiple levels, you’ll want a state machine pattern to manage transitions between game states cleanly.
Handling Keyboard Input Without Blocking the Loop
Use KeyListener with boolean flags rather than polling key state inside the loop directly. Your InputHandler sets flags on keyPressed and clears them on keyReleased. The game loop reads those flags during update().
public class InputHandler implements KeyListener {
public boolean upPressed, downPressed, leftPressed, rightPressed;
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_UP) upPressed = true;
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_UP) upPressed = false;
}
@Override public void keyTyped(KeyEvent e) {}
}
Register it with addKeyListener(inputHandler) and call setFocusable(true) on your panel. Without setFocusable(true), the panel won’t receive keyboard events and your input handler will appear broken.
Common Mistakes in First Java Game Projects
- Calling
repaint()inside the game loop. This hands rendering control back to the Swing EDT and breaks your timing. UseBufferStrategydirectly. - Running the loop on the EDT. The UI freezes and input stops responding. Always start a dedicated thread.
- Forgetting to dispose the
Graphics2Dcontext. Each frame leaks native memory until the JVM crashes or slows to a crawl in long sessions. - Hardcoding pixel positions. A 100px offset looks fine at 1080p and breaks at 4K. Scale positions relative to your panel dimensions from the start.
Pure Java vs. LibGDX: Choosing Your Path
The architecture in this guide suits learning and small projects. You see exactly what happens at every layer, which builds real understanding. When your project needs asset management, audio, cross-platform deployment, or mobile targets, LibGDX becomes the right tool. It handles the platform layer so you can focus on game logic. The trade-off is abstraction: LibGDX hides the rendering pipeline details you’ve just learned here. Starting with pure Java first means you’ll understand what LibGDX is doing for you when you make that switch.
Key Takeaways for Your Java 2D Game
- Use JDK 17 or 21. No game engine required for a 2D starter project.
- Run your game loop on a dedicated thread, never on the Swing EDT.
- Use a nanosecond-based fixed timestep with
System.nanoTime()for reliable timing. - Manage rendering through
BufferStrategyandGraphics2D, notrepaint(). - Dispose your
Graphics2Dcontext every frame to avoid native memory leaks. - Separate update and render logic from day one. Your future self will thank you.
- Move to LibGDX when your project outgrows manual buffer management and asset handling.
Frequently Asked Questions
Do I need a game engine to make a Java game?
No. The JDK’s standard library includes everything you need for a 2D game: a window, a drawing surface, input events, and timing APIs. A game engine adds convenience for larger projects but introduces abstraction that hides how things work. Starting without one is a better learning path.
What is the difference between a fixed and variable timestep game loop?
A fixed timestep runs game updates at a constant rate (60 per second in this guide) regardless of rendering time. A variable timestep scales updates by elapsed time, which can cause physics drift on slower hardware. Fixed timestep is safer for beginners and most 2D games.
How do I know if my game loop is running at the right speed?
Add a frame counter to your loop and print the UPS to the console once per second. If you’re targeting 60 UPS and the console shows consistent 60s, your timing is correct. Drops below 55 indicate your update or render methods are taking too long.
Which Java rendering API should I use for a 2D game?
Use java.awt with BufferStrategy and Graphics2D for a starter project. Swing’s JPanel provides the drawing surface. JavaFX is a reasonable choice if you’re already familiar with it, but it adds module configuration complexity that’s not worth the friction for a first game project.
Jodie Bird is the founder and principal author of the Java Limit website, a dedicated platform for sharing insights, tips, and solutions related to Java and software development. With years of experience in the field, Jodie leads a team of seasoned developers who document their collective knowledge through the Java Limit journal.








