Услуги Портфолио Контакты БлогНовости +7 (800) 511‑32‑27 Обсудить проект
Блог

Как написать 2048 на Java за полчаса

5 мая 2023·7 мин чтения
Логотип YuSMP Group
Автор материалаРедакция компании YuSMP GroupБлог YuSMP Group
Профиль редакции
Как написать 2048 на Java за полчаса

Игра 2048 — это поле 4×4, по которому нужно сдвигать плитки с цифрами: одинаковые числа при столкновении суммируются, а цель — собрать плитку со значением 2048. Создать 2048 на Java можно самостоятельно всего за три класса — ниже разберём рабочий код и логику движения плиток. Такая мини-игра — хорошая тренировка навыков Java и ООП, а если нужен не учебный проект, а полноценный продукт — в YuSMP Group есть команда мобильной и игровой разработки, которая доведёт идею до релиза.

TL;DR. 2048 на Java пишется тремя классами: Board (поле 4×4, счёт, спавн новых плиток), Tile (одна ячейка — значение и цвет) и Game (окно, обработка клавиш, отрисовка). Вся логика игры — это один метод сдвига ряда shiftRow(), который убирает нули и сливает соседние одинаковые значения; для вертикальных ходов тот же метод применяют к транспонированной матрице. Минимальная рабочая версия на Swing — 30–60 минут для разработчика с опытом Java.

Как создать игру на Java на примере 2048

Что понадобится:

  • Ноутбук.
  • Eclipse, IntelliJ IDEA или любая другая IDE на ваш выбор.
  • JDK 8 или новее — Swing входит в стандартную библиотеку, ставить ничего дополнительно не нужно.

Разделим игру на три класса: Board, Tile и Game. Ниже — рабочий код каждого класса и объяснение, как они взаимодействуют.

КлассЧто делаетКлючевые методы
BoardХранит матрицу 4×4 из объектов Tile, двигает и объединяет плитки, отслеживает счётspawn(), isGameOver(), getScore()
TileОдна ячейка поля: числовое значение и цвет, который от него зависитgetValue(), setValue(), getColor()
GameЗапускает окно, слушает нажатия клавиш, перерисовывает поле после каждого ходаmain(), keyPressed(), paintComponent()

Шаг 1: класс Board — игровое поле

Board работает с игровым полем: хранит матрицу объектов Tile, считает текущий счёт и находит самую крупную плитку. Здесь же живёт логика добавления новых плиток и проверки конца игры.

public class Board {
    private final int size = 4;
    private Tile[][] tiles;
    private int score;

    public Board() {
        tiles = new Tile[size][size];
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                tiles[i][j] = new Tile();
            }
        }
    }

    public Tile[][] getBoard() {
        return tiles;
    }

    public int getScore() {
        return score;
    }

    public int getHighTile() {
        int max = 0;
        for (Tile[] row : tiles) {
            for (Tile t : row) {
                max = Math.max(max, t.getValue());
            }
        }
        return max;
    }

    // Добавляет 2 (90% случаев) или 4 (10% случаев) в случайную пустую клетку
    public void spawn() {
        List<int[]> empty = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (tiles[i][j].getValue() == 0) empty.add(new int[]{i, j});
            }
        }
        if (empty.isEmpty()) return;
        int[] cell = empty.get(new Random().nextInt(empty.size()));
        int value = Math.random() < 0.9 ? 2 : 4;
        tiles[cell[0]][cell[1]] = new Tile(value);
    }

    // Игра окончена, если нет пустых клеток и нет соседних клеток с одинаковым значением
    public boolean isGameOver() {
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (tiles[i][j].getValue() == 0) return false;
                if (j < size - 1 && tiles[i][j].getValue() == tiles[i][j + 1].getValue()) return false;
                if (i < size - 1 && tiles[i][j].getValue() == tiles[i + 1][j].getValue()) return false;
            }
        }
        return true;
    }
}
Окно Swing-приложения с игровым полем 2048 4 на 4 с разноцветными числовыми плитками

Шаг 2: класс Tile — одна плитка

Tile — самый маленький класс: одна ячейка поля с числовым значением и цветом, который зависит от этого значения. Конструктор по умолчанию создаёт пустую плитку со значением 0.

public class Tile {
    private int value;

    public Tile() {
        this.value = 0;
    }

    public Tile(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    // Цвет плитки зависит от значения — чем больше число, тем ярче цвет
    public Color getColor() {
        return switch (value) {
            case 2 -> new Color(0xEEE4DA);
            case 4 -> new Color(0xEDE0C8);
            case 8 -> new Color(0xF2B179);
            case 16 -> new Color(0xF59563);
            default -> new Color(0xCDC1B4);
        };
    }
}

Шаг 3: класс Game — окно и запуск

Game принимает классы Tile и Board и связывает их друг с другом: настраивает окно JFrame, слушает нажатия клавиш и перерисовывает поле методом paintComponent после каждого хода.

public class Game extends JPanel implements KeyListener {
    private final Board board = new Board();

    public static void main(String[] args) {
        Game game = new Game();
        JFrame frame = new JFrame("2048");
        frame.add(game);
        frame.addKeyListener(game);
        frame.setSize(420, 420);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
        game.board.spawn();
        game.board.spawn();
    }

    @Override
    public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_UP -> board.up();
            case KeyEvent.VK_DOWN -> board.down();
            case KeyEvent.VK_LEFT -> board.left();
            case KeyEvent.VK_RIGHT -> board.right();
        }
        board.spawn();
        if (board.isGameOver()) {
            JOptionPane.showMessageDialog(this, "Игра окончена! Счёт: " + board.getScore());
        }
        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Tile[][] tiles = board.getBoard();
        for (int i = 0; i < tiles.length; i++) {
            for (int j = 0; j < tiles[i].length; j++) {
                g.setColor(tiles[i][j].getColor());
                g.fillRect(j * 100 + 10, i * 100 + 10, 90, 90);
                if (tiles[i][j].getValue() != 0) {
                    g.setColor(Color.DARK_GRAY);
                    g.drawString(String.valueOf(tiles[i][j].getValue()), j * 100 + 45, i * 100 + 55);
                }
            }
        }
    }

    @Override public void keyTyped(KeyEvent e) {}
    @Override public void keyReleased(KeyEvent e) {}
}
Услуги мобильной разработки
Узнайте цену

Шаг 4: логика движения и слияния плиток

Самая важная часть — сдвиг и слияние плиток. Как только вы поймёте движение в одну сторону, остальные три направления — то же самое, применённое к развёрнутой или транспонированной матрице. Весь сдвиг одного ряда влево укладывается в один метод: уберите нули, слейте соседние одинаковые значения слева направо.

// Сдвигает и сливает один ряд влево; для других направлений ряд/столбец
// предварительно разворачивают или транспонируют и применяют тот же метод
private int[] shiftRow(int[] row) {
    int[] result = new int[row.length];
    int pointer = 0;
    for (int value : row) {
        if (value == 0) continue;
        if (pointer > 0 && result[pointer - 1] == value) {
            result[pointer - 1] *= 2;
            score += result[pointer - 1];
        } else {
            result[pointer++] = value;
        }
    }
    return result;
}

Для движения вправо массив разворачивают перед сдвигом и обратно после него. Для движения вверх и вниз матрицу транспонируют (меняют строки и столбцы местами), применяют shiftRow к каждой строке, а затем транспонируют обратно — отдельный код для вертикальных ходов не нужен.

Схема слияния числовых плиток на сетке 4 на 4 со стрелками направления движения влево вправо вверх вниз

Шаг 5: когда игра окончена

Игра заканчивается, когда одновременно выполняются два условия: на поле нет пустых клеток и ни одна пара соседних по горизонтали или вертикали плиток не имеет одинакового значения — то есть ни один ход больше невозможен. Метод isGameOver() из класса Board (см. Шаг 1) проверяет это после каждого хода.

Шаг 6: обработка нажатий клавиш

Метод keyPressed в классе Game проверяет код нажатой клавиши и вызывает соответствующий метод Board — up(), down(), left() или right(). После каждого хода добавляется новая плитка через spawn() и вызывается repaint(), чтобы перерисовать поле с учётом изменений. Если хотите управление на WASD вместо стрелок — добавьте в switch коды KeyEvent.VK_W, VK_A, VK_S, VK_D как синонимы стрелок.

Шаг 7: запускаем и дорабатываем

Надеемся, это руководство дало представление о том, как написать игру на Java, в частности — 2048. Из очевидных улучшений: анимация движения плиток, сохранение рекорда между запусками (через файл или SharedPreferences на Android-версии) и отмена последнего хода. Если вам нужно создать более сложный продукт или систему для бизнеса — веб-услуги и разработка в YuSMP Group станут лучшим решением. Проекты, которые мы создали, уже приносят высокие показатели своим владельцам.

Частые вопросы о создании 2048 на Java

Сколько времени занимает написать 2048 на Java?

Минимальную консольную или Swing-версию с тремя классами (Board, Tile, Game) опытный разработчик пишет за 30–60 минут. Если добавить анимации движения, сохранение рекорда и адаптивный интерфейс, уйдёт день-два.

Какая библиотека нужна для графического интерфейса 2048 на Java?

Для простого десктопного прототипа достаточно встроенного Swing (JFrame, JPanel, Graphics) — он уже входит в JDK, ничего дополнительно ставить не нужно. Для более современного интерфейса используют JavaFX, а для Android-версии — Jetpack Compose или классические View.

Как реализовать слияние одинаковых плиток в коде?

Пройдите ряд или столбец от края к центру: если встретили две соседние ненулевые плитки с одинаковым значением, замените их на одну с удвоенным значением и прибавьте его к счёту, а вторую ячейку обнулите. Это и есть метод shiftRow() из Шага 4.

Почему новая плитка 2 или 4 появляется в случайном месте?

Так устроена оригинальная механика 2048: после каждого хода в случайную пустую клетку добавляется плитка — в 90% случаев со значением 2, в 10% — со значением 4. Это создаёт баланс сложности и непредсказуемость партии.

Как понять, что игра окончена?

Игра заканчивается, когда на поле нет пустых клеток и ни одна пара соседних по горизонтали или вертикали клеток не имеет одинакового значения — то есть ни один ход больше не возможен.

Можно ли сделать 2048 на Java для Android, а не только для десктопа?

Да, логика классов Board и Tile из этой статьи переносится на Android почти без изменений — меняется только слой отрисовки: вместо Swing используется View/Canvas или Jetpack Compose, а обработка касаний — вместо KeyListener.

Что делать, если плитки сдвигаются, но не сливаются?

Чаще всего причина в том, что сравнение значений идёт до сдвига нулей, а не после. Сначала уберите из ряда все нули (сожмите плитки к краю), и только затем сравнивайте соседние пары на равенство — иначе логика слияния пропускает часть ходов.

Нужны разработчики в команду?

Подберём инженеров нужного стека под ваш проект — быстрый старт.

Обсудить команду