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.

Yes—you can build a small graphical game directly on a Raspberry Pi with Java. This guide creates a keyboard-controlled “Dodge the Falling Blocks” game using Swing/AWT, which comes with the JDK and avoids the additional ARM-specific setup that JavaFX can require. You will compile it with javac and run it on the Pi’s desktop.

The game targets about 60 simulation updates per second; that is a loop target, not a promise of 60 rendered frames per second on every model. It uses an 800 × 600 window, a player rectangle, and one falling block. No image assets, game engine, or GPIO wiring is needed.

What you need

Use a Raspberry Pi that can run the graphical Raspberry Pi OS desktop. A Pi 4 or Pi 5 is a comfortable development choice; a Pi Zero-class board may run a very simple game, but desktop responsiveness and compilation will be less comfortable. No particular frame rate or minimum memory is guaranteed here.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • A Raspberry Pi and supported boot storage, such as a microSD card.
  • A power supply appropriate for the selected model.
  • A display and keyboard; a mouse is optional.
  • Raspberry Pi OS Desktop and a JDK. A network connection is useful for installing packages.
  • Basic familiarity with Java classes, methods, and loops.

Choose the Raspberry Pi OS edition recommended for your board in Raspberry Pi Imager. For Pi 4 or Pi 5, Raspberry Pi OS Desktop 64-bit is a practical starting point. Raspberry Pi OS is Debian-based, offers 32-bit and 64-bit editions, and uses APT; its documentation identified Debian Trixie as the current base when checked on August 18, 2026. For older hardware or compatibility issues, use Imager’s recommendation rather than forcing a 64-bit edition. See Raspberry Pi OS documentation and the Raspberry Pi Imager installation guide.

#1 Best Overall
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (8GB RAM)
  • Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (8GB RAM)
  • Includes 128GB Micro SD Card pre-loaded with 64-bit Raspberry Pi OS, USB MicroSD Card Reader
  • CanaKit Turbine Black Case for the Raspberry Pi 5
  • CanaKit Low Noise Bearing System Fan
  • Mega Heat Sink - Black Anodized

Raspberry Pi OS Lite has no desktop by default, so this windowed tutorial will not open there unless you separately configure a graphical environment or another display system. Raspberry Pi advises a clean installation for a major OS release change rather than treating it as a routine in-place package update.

Install and verify Java

In a terminal on the Pi, update the existing OS release and install a JDK, which includes both the Java runtime and compiler:

sudo apt update
sudo apt full-upgrade
sudo apt install openjdk-25-jdk

Raspberry Pi recommends full-upgrade for updating the current release because package dependencies can change. The OpenJDK 25 package may not be available on every image, architecture, or repository state. If APT cannot find it, inspect available packages and install the distribution’s default JDK instead:

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.
apt search openjdk
sudo apt install default-jdk

Debian Trixie’s package metadata includes OpenJDK 25 source packages, but that does not mean every Pi image exposes the same package. Check the Debian Trixie OpenJDK package information and the OpenJDK installation guide if needed. Verify what is actually installed:

java --version
javac --version

Record the versions printed on your system; do not assume all Raspberry Pi OS images provide the same JDK release.

Why use Swing/AWT for the first game?

Swing/AWT provides a low-dependency path for a simple 2D game: JFrame creates a desktop window, Canvas gives it a drawing surface, and BufferStrategy helps present frames without the flicker associated with direct drawing. It is an older UI toolkit, but it remains useful for learning game loops, input, rendering, and collisions without bringing in an additional graphics runtime.

Rank #2
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (4GB RAM)
  • Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (4GB RAM)
  • Includes 128GB Micro SD Card pre-loaded with 64-bit Raspberry Pi OS, USB MicroSD Card Reader
  • CanaKit Turbine Black Case for the Raspberry Pi 5
  • CanaKit Low Noise Bearing System Fan
  • CanaKit Mega Heat Sink - Black Anodized

Other choices suit different needs:

Approach Strengths Trade-off Best fit
Swing/AWT Included with a standard JDK; minimal setup for a small 2D game. Older UI toolkit with fewer built-in game features. First Raspberry Pi game and learning core mechanics.
JavaFX Scene graph, controls, animation, and media. JDK, JavaFX modules, runtime, and ARM architecture need to match. A JavaFX-focused project prepared to verify its Pi runtime.
FXGL Higher-level game abstractions and 2D game features. Adds JavaFX and dependency/runtime compatibility considerations. A larger game or JavaFX-based rapid prototyping.
Pi4J Java access to GPIO and buses such as I2C, SPI, PWM, and serial. Hardware I/O library, not a graphics or game engine. Adding physical controls or electronics after the screen game works.
libGDX Mature cross-platform framework for 2D and 3D games. More framework and setup concepts than this small tutorial needs. A broader cross-platform game project.

For JavaFX, the OpenJFX setup documentation describes Maven dependencies and plugin configuration but is not Raspberry Pi-specific. Debian’s Trixie ARM64 OpenJFX listing exposes OpenJFX 11, while the JavaFX 25 builds page and FXGL project show why version matching matters. Pi4J’s JavaFX on Raspberry Pi example uses ARM-specific runtime and module configuration; treat its commands as specific to that setup, not universal current instructions. FXGL is a credible option for a larger game, but its repository and release notes describe a JavaFX/JDK-dependent baseline. For this build, Swing keeps the tutorial centered on game logic.

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

Create the game

The game repeats three jobs: read the current input state, update the simulation (movement, collisions, score, and game state), then render the current state. Separating those jobs avoids tying movement directly to how quickly the display happens to draw.

Keyboard
   ↓
Keyboard state
   ↓
Game update loop
   ├── player movement
   ├── falling block movement
   ├── collision detection
   └── score and game state
   ↓
Canvas + BufferStrategy
   ↓
Raspberry Pi display

Make a directory and source file:

mkdir -p ~/java-games/dodge-game
cd ~/java-games/dodge-game
nano DodgeGame.java

Paste this complete program into DodgeGame.java. The player moves with the arrow keys or A/D. Each time the block falls past the screen, the score increases and its speed rises gradually. A collision ends the game; press R to restart.

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferStrategy;
import java.util.Random;
import javax.swing.JFrame;

public final class DodgeGame extends Canvas implements Runnable, KeyListener {
    private static final int WIDTH = 800;
    private static final int HEIGHT = 600;
    private static final int PLAYER_WIDTH = 50;
    private static final int PLAYER_HEIGHT = 25;
    private static final int BLOCK_SIZE = 30;

    private final Random random = new Random();
    private JFrame frame;
    private Thread gameThread;
    private volatile boolean running;
    private boolean leftPressed;
    private boolean rightPressed;
    private boolean gameOver;
    private int playerX;
    private final int playerY = HEIGHT - 60;
    private int blockX;
    private int blockY;
    private int blockSpeed;
    private int score;

    public DodgeGame() {
        setPreferredSize(new Dimension(WIDTH, HEIGHT));
        setFocusable(true);
        addKeyListener(this);
        resetGame();
    }

    private void createWindow() {
        frame = new JFrame("Dodge Game");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        frame.add(this);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
        requestFocusInWindow();
    }

    private synchronized void start() {
        if (running) {
            return;
        }
        running = true;
        gameThread = new Thread(this, "game-loop");
        gameThread.start();
    }

    @Override
    public void run() {
        final double nsPerUpdate = 1_000_000_000.0 / 60.0;
        double delta = 0;
        long previous = System.nanoTime();

        while (running) {
            long current = System.nanoTime();
            delta += (current - previous) / nsPerUpdate;
            previous = current;

            while (delta >= 1) {
                update();
                delta--;
            }

            render();

            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                running = false;
            }
        }
    }

    private void update() {
        if (gameOver) {
            return;
        }

        int playerSpeed = 6;
        if (leftPressed) {
            playerX -= playerSpeed;
        }
        if (rightPressed) {
            playerX += playerSpeed;
        }
        playerX = Math.max(0, Math.min(WIDTH - PLAYER_WIDTH, playerX));

        blockY += blockSpeed;
        if (blockY > HEIGHT) {
            blockY = -BLOCK_SIZE;
            blockX = random.nextInt(WIDTH - BLOCK_SIZE + 1);
            score++;
            blockSpeed = Math.min(blockSpeed + 1, 15);
        }

        Rectangle player = new Rectangle(playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT);
        Rectangle block = new Rectangle(blockX, blockY, BLOCK_SIZE, BLOCK_SIZE);
        if (player.intersects(block)) {
            gameOver = true;
        }
    }

    private void render() {
        BufferStrategy buffer = getBufferStrategy();
        if (buffer == null) {
            if (isDisplayable()) {
                createBufferStrategy(3);
            }
            return;
        }

        Graphics2D g = (Graphics2D) buffer.getDrawGraphics();
        try {
            g.setColor(Color.BLACK);
            g.fillRect(0, 0, WIDTH, HEIGHT);

            g.setColor(Color.BLUE);
            g.fillRect(playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT);

            g.setColor(Color.RED);
            g.fillRect(blockX, blockY, BLOCK_SIZE, BLOCK_SIZE);

            g.setColor(Color.WHITE);
            g.drawString("Score: " + score, 20, 30);
            if (gameOver) {
                g.drawString("Game Over - press R to restart", 280, 300);
            }
        } finally {
            g.dispose();
        }
        buffer.show();
    }

    private void resetGame() {
        playerX = (WIDTH - PLAYER_WIDTH) / 2;
        blockX = random.nextInt(WIDTH - BLOCK_SIZE + 1);
        blockY = -BLOCK_SIZE;
        blockSpeed = 4;
        score = 0;
        gameOver = false;
    }

    @Override
    public void keyPressed(KeyEvent event) {
        switch (event.getKeyCode()) {
            case KeyEvent.VK_LEFT:
            case KeyEvent.VK_A:
                leftPressed = true;
                break;
            case KeyEvent.VK_RIGHT:
            case KeyEvent.VK_D:
                rightPressed = true;
                break;
            case KeyEvent.VK_R:
                if (gameOver) {
                    resetGame();
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void keyReleased(KeyEvent event) {
        switch (event.getKeyCode()) {
            case KeyEvent.VK_LEFT:
            case KeyEvent.VK_A:
                leftPressed = false;
                break;
            case KeyEvent.VK_RIGHT:
            case KeyEvent.VK_D:
                rightPressed = false;
                break;
            default:
                break;
        }
    }

    @Override
    public void keyTyped(KeyEvent event) {
        // This game uses key codes from keyPressed/keyReleased instead.
    }

    public static void main(String[] args) {
        DodgeGame game = new DodgeGame();
        game.createWindow();
        game.start();
    }
}

How the window and loop work

Canvas is the drawable area inside the JFrame. The window is made visible before the game thread starts. The loop accumulates elapsed nanoseconds and runs updates at a target of 60 per second, then renders the latest state. Rendering may happen at a different pace, depending on the Pi, desktop compositor, display, and workload. The short sleep helps avoid an unnecessarily busy loop; it is coarse pacing, not the timing mechanism itself.

BufferStrategy is created only once the canvas is displayable. A null strategy on the first render is therefore expected: the code creates it and returns, then draws on a later pass. Drawing uses a black background, blue player, red block, and white score. The graphics context is disposed in finally, and show() presents the completed frame.

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.

How input, movement, and collision work

The key listener changes booleans when keys are pressed and released. The update method samples those booleans, so holding a key moves continuously while game logic stays out of the event callback. The player position is clamped to the window, preventing it from moving beyond either edge.

Rank #3
CanaKit Raspberry Pi 5 Essentials Starter Kit (4GB RAM)
  • CanaKit Raspberry Pi 5 Essentials Starter Kit

The falling block moves down by blockSpeed each update. When it passes the bottom, it returns above the screen at a random horizontal position, adds one point, and speeds up to a cap. Rectangle.intersects performs a simple axis-aligned collision check. This is sufficient for one square obstacle; a larger game could represent enemies with a class or keep them in a list. Avoid allocating many temporary objects each frame in a more performance-sensitive project.

Compile and play

From the project directory, compile and launch the source:

javac DodgeGame.java
java DodgeGame

A window should appear with the player near the bottom and the block falling from above. Use the left/right arrow keys or A/D to dodge; the score increases each time the obstacle passes. After a collision, press R to restart.

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

The public class is named DodgeGame, so the source filename must be exactly DodgeGame.java. The game uses java.awt and javax.swing classes supplied by the JDK and needs no Maven or Gradle configuration.

Fix common problems

The window does not open

AWT needs a graphical display session. Raspberry Pi OS Lite, an inactive desktop session, or an SSH terminal without display forwarding can leave Java unable to connect to a display. Run the game from a terminal inside the Pi’s desktop session. These commands help inspect the session:

echo "$DISPLAY"
echo "$XDG_SESSION_TYPE"

A blank display variable in the process environment is a clue that the process cannot access the desktop display; SSH by itself does not put a window on the Pi’s physical screen.

Rank #4
CanaKit Raspberry Pi 5 16GB Starter Kit PRO - Turbine Black (128GB Edition) (16GB RAM)
  • Includes Raspberry Pi 5 16GB with 2.4Ghz 64-bit quad-core CPU (16GB RAM)
  • Includes 128GB Micro SD Card pre-loaded with 64-bit Raspberry Pi OS, USB MicroSD Card Reader
  • CanaKit Turbine Black Case for the Raspberry Pi 5
  • CanaKit Low Noise Bearing System Fan
  • Mega Heat Sink - Black Anodized

The keyboard does not move the player

  • Click the game window so it has focus.
  • Confirm the canvas is focusable and the window calls requestFocusInWindow() after becoming visible.
  • Try both the arrow keys and A/D. This game reads key codes in keyPressed and keyReleased, not keyTyped.
  • Check that another component has not taken keyboard focus.

The buffer is null, or the screen flickers

A null buffer before the canvas becomes displayable is a normal first-run condition; this program creates the strategy once the canvas is ready. If drawing flickers, keep using the buffer strategy, dispose of the drawing context, and call show() as the program does. Do not repeatedly recreate the window.

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

The game speed varies or CPU use is high

Moving an object once per rendered frame makes behavior depend on how quickly the machine draws. This example uses elapsed time for fixed-step updates and renders separately. The target update rate does not guarantee identical display performance on every Pi. A loop with no pacing can consume a CPU core continuously; the brief sleep reduces that tendency but does not replace elapsed-time control.

JavaFX or Pi4J setup fails

JavaFX failures commonly come from mismatched JDK and JavaFX versions, the wrong ARM architecture, missing module-path settings, or instructions intended for a different runtime. Pi4J issues can involve major-version changes, plugins, numbering, permissions, or wiring. Pi4J says V2 and later are a rewrite rather than drop-in compatible with earlier versions; consult its compatibility notes and documentation for the chosen version.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Extend the game after it works

Organize a larger project

One file is reasonable for the first version because it makes the loop and state easy to see. As features grow, split responsibilities into classes, for example:

src/
  Game.java
  Player.java
  Enemy.java
  Input.java
  Renderer.java

Multiple enemies, pause and restart states, levels, sound, images, and high scores are natural next steps. Keep input sampling, simulation updates, and drawing separate so each feature has a clear home.

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

Add physical buttons safely

Only add GPIO after the keyboard game works. Pi4J is the hardware I/O layer, not the renderer: a button event should update the same input state the game loop reads.

Best Value
CanaKit Raspberry Pi 5 Essentials Starter Kit (8GB RAM)
  • Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (8GB RAM)
  • Includes 32GB EVO+ Micro SD Card pre-loaded with 64-bit Pi OS, USB MicroSD Card Reader
  • CanaKit Turbine Black Case for the Raspberry Pi 5
  • CanaKit 45W PD Power Supply for the Raspberry Pi 5
  • Display Cable - 6 foot (Supports up to 4K 60p)
GPIO button → Pi4J listener → game input state → update loop

Before wiring, check the Pi4J version and plugin against your board and OS. Use the Pi’s 3.3 V GPIO logic: do not connect a 5 V signal directly to a GPIO input. Configure a pull-up or pull-down, provide a suitable common ground, and account for switch debouncing. Do not block the rendering thread on a GPIO read. Pi4J’s current site lists V4.0.2, released June 8, 2026, built on Java 25; its API and hardware support should be checked for the selected version rather than assumed across major releases. See Pi4J and its technical documentation.

Use a Java game library or modern UI toolkit

If the project outgrows simple rectangles, FXGL provides higher-level Java/JavaFX game features, while JavaFX offers a scene graph and richer UI controls. Both introduce runtime and dependency choices that need verification for the Pi’s architecture and OS. FXGL’s project page describes its scope and setup; the release page gives version-specific release details. For serious cross-platform game development, libGDX is another framework option, though it is outside this minimal build.

Package the game as a JAR

Direct compilation is simplest while developing. To package compiled classes into an executable JAR in the same directory, run:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
javac DodgeGame.java
jar cfe DodgeGame.jar DodgeGame *.class
java -jar DodgeGame.jar

The jar cfe command records DodgeGame as the entry point—the class containing public static void main(String[] args). A JAR is not a self-contained application by default: the Raspberry Pi still needs a compatible Java runtime. Bundling a runtime or creating a native package is a separate deployment task.

Once the software-only game is stable, you can launch it from a terminal or desktop shortcut. Autostart behavior depends on the Raspberry Pi OS desktop release and session, so use instructions specific to the edition you have rather than assuming a system-wide recipe applies.

Quick Recap

Bestseller No. 1
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (8GB RAM)
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (8GB RAM)
Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (8GB RAM); CanaKit Turbine Black Case for the Raspberry Pi 5
$259.95
Bestseller No. 2
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (4GB RAM)
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (4GB RAM)
Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (4GB RAM); CanaKit Turbine Black Case for the Raspberry Pi 5
$209.99
Bestseller No. 3
CanaKit Raspberry Pi 5 Essentials Starter Kit (4GB RAM)
CanaKit Raspberry Pi 5 Essentials Starter Kit (4GB RAM)
CanaKit Raspberry Pi 5 Essentials Starter Kit
$189.99
Bestseller No. 4
CanaKit Raspberry Pi 5 16GB Starter Kit PRO - Turbine Black (128GB Edition) (16GB RAM)
CanaKit Raspberry Pi 5 16GB Starter Kit PRO - Turbine Black (128GB Edition) (16GB RAM)
Includes Raspberry Pi 5 16GB with 2.4Ghz 64-bit quad-core CPU (16GB RAM); CanaKit Turbine Black Case for the Raspberry Pi 5
$419.99
Bestseller No. 5
CanaKit Raspberry Pi 5 Essentials Starter Kit (8GB RAM)
CanaKit Raspberry Pi 5 Essentials Starter Kit (8GB RAM)
Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (8GB RAM); Includes 32GB EVO+ Micro SD Card pre-loaded with 64-bit Pi OS, USB MicroSD Card Reader
$229.99

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.