本ページはプロモーションが含まれています

Java入門

初心者向けJavaプログラミングでテトリスを作る方法

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

Javaでゲームを作ってみたいけど、何から始めればいいのか分からない。

そんな方に向けて、この記事では「テトリス」を題材に、Javaプログラミングの基礎から実践までを丁寧に解説します。

Swingを使ったGUIの構築、ブロックの回転・落下処理、行の削除判定、ゲームオーバーの条件、さらにはスコア管理まで、初心者がつまずきやすいポイントを段階的に分かりやすく説明。

1ファイルで完結するシンプルなテトリスコードをベースに、ゲームロジックの全体像を学べる内容となっています。

この記事を読み終えるころには、Javaでオリジナルのテトリスが作れる自信がきっと芽生えるでしょう。

Javaテトリス開発の全体像

まず、Javaでテトリスを作るための要素を整理してみましょう。

ポイント

  • ブロックの表示(GUI)
  • ブロックの移動・回転
  • 衝突判定(壁・他のブロック)
  • 行が揃ったときの削除処理
  • スコア表示
  • ゲームオーバーの判定

こうして並べてみると複雑そうですが、実はひとつずつ分解していけばそれほど難しくありません。

初心者の方でも、まずは「動くブロックが表示される」だけの状態からスタートすればOKです。

JavaでGUIを作るには?

テトリスの画面を作るには、Javaの「Swing」ライブラリを使うのがおすすめです。

JFrame frame = new JFrame("Mini Tetris");
MiniTetris game = new MiniTetris();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(game);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);

上記のコードで、テトリスのベースとなるウィンドウを表示することができます。

ブロックの描画は paintComponent メソッドをオーバーライドして Graphics オブジェクトを使って行います。

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        for (int y = 0; y < ROWS; y++) {
            for (int x = 0; x < COLS; x++) {
                if (board[y][x] == 1) {
                    g.setColor(Color.CYAN);
                    g.fillRect(x * BLOCK, y * BLOCK, BLOCK, BLOCK);
                }
            }
        }
        g.setColor(Color.GREEN);
        for (int y = 0; y < currentPiece.length; y++) {
            for (int x = 0; x < currentPiece[y].length; x++) {
                if (currentPiece[y][x] == 1) {
                    g.fillRect((px + x) * BLOCK, (py + y) * BLOCK, BLOCK, BLOCK);
                }
            }
        }
    }

このようにして、1マスずつ描画していくことでテトリスのフィールドを構成していきます。

ブロックのデータ構造を考えよう

テトリスのブロックは、基本的に「4つのマスから成る図形」です。これを2次元配列で表現するのが定番です。

    private final List<int[][]> pieces = Arrays.asList(
        new int[][] {
            {0, 0, 0, 0},
            {1, 1, 1, 1},
            {0, 0, 0, 0},
            {0, 0, 0, 0}
        },
        new int[][] {
            {1, 1},
            {1, 1}
        },
        new int[][] {
            {0, 1, 0},
            {1, 1, 1},
            {0, 0, 0}
        },
        new int[][] {
            {0, 1, 1},
            {1, 1, 0},
            {0, 0, 0}
        },
        new int[][] {
            {1, 1, 0},
            {0, 1, 1},
            {0, 0, 0}
        },
        new int[][] {
            {1, 0, 0},
            {1, 1, 1},
            {0, 0, 0}
        },
        new int[][] {
            {0, 0, 1},
            {1, 1, 1},
            {0, 0, 0}
        }
    );

このような配列で、ブロックの形状を定義しておきます。ブロックの回転も、この配列を回転させることで実現できます。

キーボード操作でブロックを動かす

プレイヤーの入力に応じてブロックを左右・下に移動、もしくは回転させる必要があります。これは KeyListener を使って実装します。

    public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_LEFT:
                if (canMove(-1, 0)) px--;
                break;
            case KeyEvent.VK_RIGHT:
                if (canMove(1, 0)) px++;
                break;
            case KeyEvent.VK_DOWN:
                if (canMove(0, 1)) py++;
                break;
            case KeyEvent.VK_UP:
                rotatePiece();
                break;
        }
        repaint();
    }

この処理で、ユーザーのキーボード操作がテトリスのプレイに反映されるようになります。

行削除の処理

行が揃ったかどうかのチェックは「盤面の横一列がすべて埋まっているか?」を判定するだけです。

全て埋まっていればその行を削除し、上の行を1段ずつ下にずらす処理を行います。

private void clearLines() {
        for (int y = ROWS - 1; y >= 0; y--) {
            boolean full = true;
            for (int x = 0; x < COLS; x++) {
                if (board[y][x] == 0) full = false;
            }
            if (full) {
                // 上の行を下にずらす
                for (int row = y; row > 0; row--) {
                    board[row] = board[row - 1].clone();
                }
                board[0] = new int[COLS]; // 一番上は空に
                y++; // 同じ行をもう一度確認(複数ライン消し対応)
            }
      

ゲームオーバーとスコア管理

ブロックを固定した際に、すでに最上段に他のブロックが存在している場合はゲームオーバーとします。

        if (!canMove(0, 0)) {
            timer.stop();
            JOptionPane.showMessageDialog(this, "Game Over!");
        }

スコア管理は、削除した行数に応じて点数を加算していく形式が一般的です。行数によって倍率を変えると、ゲーム性が高まります。

完成コード:Javaで作るシンプルテトリス

以下がJavaで作成するミニマムなテトリスの全体コードです。1ファイルで動作するように構成しています。

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;

public class MiniTetris extends JPanel implements ActionListener, KeyListener {

    // フィールドサイズ
    private final int ROWS = 20, COLS = 10;

    // 1ブロックのピクセルサイズ(30x30)
    private final int BLOCK = 30;

    // タイマー(定期的に actionPerformed を呼び出す)
    private Timer timer;

    // ゲームボードの状態を表す2次元配列(0: 空, 1: 固定ブロック)
    private int[][] board = new int[ROWS][COLS];

    // 現在操作中のブロック形状(2次元配列)
    private int[][] currentPiece;

    // 現在操作中ブロックの座標(左上基準)
    private int px = 3, py = 0;

    // ランダムブロック生成用
    private Random rand = new Random();

    // ブロックパターン(I, O, T, S, Z, J, L)
    private final List<int[][]> pieces = Arrays.asList(
        new int[][] { // I
            {0, 0, 0, 0},
            {1, 1, 1, 1},
            {0, 0, 0, 0},
            {0, 0, 0, 0}
        },
        new int[][] { // O
            {1, 1},
            {1, 1}
        },
        new int[][] { // T
            {0, 1, 0},
            {1, 1, 1},
            {0, 0, 0}
        },
        new int[][] { // S
            {0, 1, 1},
            {1, 1, 0},
            {0, 0, 0}
        },
        new int[][] { // Z
            {1, 1, 0},
            {0, 1, 1},
            {0, 0, 0}
        },
        new int[][] { // J
            {1, 0, 0},
            {1, 1, 1},
            {0, 0, 0}
        },
        new int[][] { // L
            {0, 0, 1},
            {1, 1, 1},
            {0, 0, 0}
        }
    );

    // コンストラクタ(初期化処理)
    public MiniTetris() {
        setPreferredSize(new Dimension(COLS * BLOCK, ROWS * BLOCK)); // パネルサイズ指定
        setBackground(Color.BLACK); // 背景黒
        setFocusable(true); // キー入力受け付け
        addKeyListener(this); // キーイベント登録
        spawnPiece(); // 最初のブロック出現
        timer = new Timer(500, this); // 0.5秒ごとに落下処理
        timer.start(); // タイマースタート
    }

    // ランダムにブロックを出現させる
    private void spawnPiece() {
        currentPiece = deepCopy(pieces.get(rand.nextInt(pieces.size()))); // パターンからランダム取得
        px = 3;
        py = 0;

        // 初期位置で置けない場合はゲームオーバー
        if (!canMove(0, 0)) {
            timer.stop();
            JOptionPane.showMessageDialog(this, "Game Over!");
        }
    }

    // 2次元配列のディープコピー
    private int[][] deepCopy(int[][] shape) {
        int[][] copy = new int[shape.length][];
        for (int i = 0; i < shape.length; i++) {
            copy[i] = Arrays.copyOf(shape[i], shape[i].length);
        }
        return copy;
    }

    // 指定方向に移動できるかどうかチェック
    private boolean canMove(int dx, int dy) {
        for (int y = 0; y < currentPiece.length; y++) {
            for (int x = 0; x < currentPiece[y].length; x++) {
                if (currentPiece[y][x] == 1) {
                    int nx = px + x + dx;
                    int ny = py + y + dy;
                    // 画面外または既存ブロックと衝突していないか
                    if (nx < 0 || nx >= COLS || ny >= ROWS || (ny >= 0 && board[ny][nx] == 1))
                        return false;
                }
            }
        }
        return true;
    }

    // 回転後の形状で配置可能かを判定
    private boolean canRotate(int[][] rotated) {
        for (int y = 0; y < rotated.length; y++) {
            for (int x = 0; x < rotated[y].length; x++) {
                if (rotated[y][x] == 1) {
                    int nx = px + x;
                    int ny = py + y;
                    if (nx < 0 || nx >= COLS || ny >= ROWS || (ny >= 0 && board[ny][nx] == 1))
                        return false;
                }
            }
        }
        return true;
    }

    // ブロックの回転処理(90度右回転)
    private void rotatePiece() {
        int size = currentPiece.length;
        int[][] rotated = new int[size][size];
        for (int y = 0; y < size; y++) {
            for (int x = 0; x < size; x++) {
                rotated[x][size - 1 - y] = currentPiece[y][x];
            }
        }
        if (canRotate(rotated)) {
            currentPiece = rotated;
        }
    }

    // ブロックを盤面に固定(マージ)
    private void mergePiece() {
        for (int y = 0; y < currentPiece.length; y++) {
            for (int x = 0; x < currentPiece[y].length; x++) {
                if (currentPiece[y][x] == 1) {
                    board[py + y][px + x] = 1;
                }
            }
        }
        clearLines();  // ライン消去
        spawnPiece();  // 次のブロックを出現
    }

    // ラインが揃っていたら消去
    private void clearLines() {
        for (int y = ROWS - 1; y >= 0; y--) {
            boolean full = true;
            for (int x = 0; x < COLS; x++) {
                if (board[y][x] == 0) full = false;
            }
            if (full) {
                // 上の行を下にずらす
                for (int row = y; row > 0; row--) {
                    board[row] = board[row - 1].clone();
                }
                board[0] = new int[COLS]; // 一番上は空に
                y++; // 同じ行をもう一度確認(複数ライン消し対応)
            }
        }
    }

    // 描画処理(盤面と現在のブロック)
    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        // 固定されたブロックを描画
        for (int y = 0; y < ROWS; y++) {
            for (int x = 0; x < COLS; x++) {
                if (board[y][x] == 1) {
                    g.setColor(Color.CYAN);
                    g.fillRect(x * BLOCK, y * BLOCK, BLOCK, BLOCK);
                }
            }
        }

        // 現在の操作中ブロックを描画
        g.setColor(Color.GREEN);
        for (int y = 0; y < currentPiece.length; y++) {
            for (int x = 0; x < currentPiece[y].length; x++) {
                if (currentPiece[y][x] == 1) {
                    g.fillRect((px + x) * BLOCK, (py + y) * BLOCK, BLOCK, BLOCK);
                }
            }
        }
    }

    // タイマーから呼ばれる落下処理
    public void actionPerformed(ActionEvent e) {
        if (canMove(0, 1)) {
            py++; // 1マス下に移動
        } else {
            mergePiece(); // 着地 → 固定化
        }
        repaint(); // 画面更新
    }

    // キー入力処理
    public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_LEFT:
                if (canMove(-1, 0)) px--;
                break;
            case KeyEvent.VK_RIGHT:
                if (canMove(1, 0)) px++;
                break;
            case KeyEvent.VK_DOWN:
                if (canMove(0, 1)) py++;
                break;
            case KeyEvent.VK_UP:
                rotatePiece(); // 回転
                break;
        }
        repaint();
    }

    // 他のキーイベントは使用しない
    public void keyReleased(KeyEvent e) {}
    public void keyTyped(KeyEvent e) {}

    // メインメソッド:ゲーム起動処理
    public static void main(String[] args) {
        JFrame frame = new JFrame("Mini Tetris");
        MiniTetris game = new MiniTetris();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(game);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

このコードで、単純な落下型テトリスが動作します。ブロック回転やスコア表示などの機能は未実装ですが、テトリスの基本構造を学ぶには最適です。

Javaでテトリスを作る楽しさと学び

実際に手を動かしてテトリスを作っていくと、以下のようなスキルが身につきます。

  • JavaのGUIプログラミング
  • オブジェクト指向設計(クラスの分割や再利用)
  • イベントドリブン設計(ユーザー入力・タイマー処理)
  • 状態管理やロジックの分岐処理

そして何より「自分で作ったゲームが動く」という喜びは、プログラミング学習のモチベーションを大きく引き上げてくれます。

まとめ:まずは1ブロック描画から始めよう

Javaでテトリスを作成するには、GUI構築・ブロックの管理・操作処理・描画ロジック・ゲーム状態の判定など、複数の要素を組み合わせる必要があります。

ですが、本記事では1ファイル完結のシンプルな構成により、初心者でも着実に学習を進められるように工夫しています。Javaの基礎力を伸ばしたい方や、自作ゲームを通じて成長したい方に最適な内容です。

【要点まとめ】

  • JavaのSwingでウィンドウと描画処理を構築する
  • テトリスのブロック形状は2次元配列で表現する
  • paintComponentで盤面とブロックを描画する
  • KeyListenerでプレイヤー操作を実装する
  • ブロックの回転は配列の回転処理で行う
  • canMoveメソッドで移動可能かどうかを判定する
  • 一列が埋まれば削除してスコア加算する
  • ゲームオーバーは初期位置で配置できないときに発生する
  • Timerを使って自動的に落下を進行させる
  • 最終的に1ファイルで構成された実用的なコードが完成する

Javaでゲーム開発を体験することで、GUI構築やイベント処理、状態管理の理解が深まります。

まずはブロックを動かすところから始めて、少しずつ機能を加えていけば、誰でも自分だけのテトリスを作れるようになります。

  • この記事を書いた人
  • 最新記事

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

-Java入門