Как сделать игру на Java: libGDX, jMonkeyEngine, LWJGL и игровой цикл

Как сделать игру на Java: libGDX, jMonkeyEngine, LWJGL и игровой цикл Полезное

Игру на Java обычно делают не «с нуля», а на одном из трех уровней: на фреймворке вроде libGDX (2D и простое 3D, десктоп и Android), на 3D-движке jMonkeyEngine или прямо на LWJGL — низкоуровневой обертке над OpenGL, Vulkan и OpenAL, на которой, например, работает Minecraft: Java Edition. Ниже — как выбрать уровень, как устроен игровой цикл (с кодом, который можно запустить), первый проект на libGDX и честная граница: где Java в играх уместна, а где нет.

Версии в статье сверены на сентябрь 2026: Java 25 LTS, libGDX 1.14.2, LWJGL 3.4.3, jMonkeyEngine 3.9 (стабильная ветка).

Три слова, которые путают: библиотека, фреймворк, движок

  • Библиотека дает отдельные функции, а управление программой остается у вас. LWJGL открывает окно и дает доступ к OpenGL, но где рисовать, как считать физику и когда обновлять кадр — решает ваш код.
  • Фреймворк сам владеет главным циклом и вызывает ваши методы. В libGDX вы пишете create(), render(), dispose(), а фреймворк решает, когда их вызвать.
  • Движок — фреймворк плюс готовые подсистемы и инструменты: граф сцены, загрузка 3D-моделей, физика, часто редактор. Так устроен jMonkeyEngine.

Граница условная: libGDX тоже называют движком, а у движков есть низкоуровневый доступ. Но на вопрос «сколько мне придется написать самому» эта шкала отвечает честно.

И еще одна путаница, которая встречается в старых подборках: Java и JavaScript — разные языки. Phaser, PixiJS, Babylon.js, PlayCanvas и A-Frame — это движки и библиотеки для JavaScript в браузере, к Java они отношения не имеют.

Какой инструмент выбрать под задачу

Инструмент Уровень 2D / 3D Платформы Когда брать
libGDX фреймворк 2D, базовое 3D Windows, macOS, Linux, Android, iOS, веб первая игра, 2D, мобильные и десктоп из одного кода
jMonkeyEngine движок 3D десктоп, Android 3D-игра, граф сцены, физика из коробки
LWJGL 3 библиотека любое Windows, macOS, Linux свой движок, учеба графике, полный контроль
FXGL фреймворк поверх JavaFX 2D десктоп простые 2D-игры и учебные проекты на JavaFX

Как читать таблицу: чем ниже уровень, тем больше контроля и тем больше кода вы пишете сами. Для первого проекта почти всегда разумно начать с libGDX: он берет на себя окно, ввод, звук, загрузку текстур и сборку под платформы, а игровую логику оставляет вам.

Примеры реальных проектов: Minecraft: Java Edition написан на Java и использует LWJGL (версия Bedrock для консолей и мобильных написана на C++); на libGDX сделана Slay the Spire.

Игровой цикл: сердце любой игры

Любая игра — это цикл «обработать ввод -> обновить мир -> нарисовать кадр», который крутится десятки раз в секунду. Главная ошибка новичка — двигать объекты «на столько-то пикселей за кадр». Тогда скорость игры зависит от частоты кадров компьютера.

Сначала контрпример. Объект должен пройти 120 пикселей за секунду, считаем двумя способами при разном FPS:

public class FrameBug {
    public static void main(String[] args) {
        for (int fps : new int[]{30, 60, 144}) {
            double xWrong = 0, xRight = 0;
            double dt = 1.0 / fps;
            for (int frame = 0; frame < fps; frame++) { // ровно 1 секунда
                xWrong += 2;             // 2 пикселя за кадр
                xRight += 120 * dt;      // 120 пикселей в секунду
            }
            System.out.printf("%3d FPS: за кадр -> %.0f px, с учетом dt -> %.0f px%n", fps, xWrong, xRight);
        }
    }
}

Вывод (Java 25, java FrameBug.java):

 30 FPS: за кадр -> 60 px, с учетом dt -> 120 px
 60 FPS: за кадр -> 120 px, с учетом dt -> 120 px
144 FPS: за кадр -> 288 px, с учетом dt -> 120 px

На мониторе 144 Гц «пиксели за кадр» дают персонажа, который бегает в 2,4 раза быстрее задуманного. Исправление — умножать скорость на dt (время, прошедшее с прошлого кадра).

Для физики одного dt мало: при скачке FPS шаг становится то крупным, то мелким, и столкновения ведут себя по-разному. Поэтому логику часто обновляют фиксированным шагом, а рисуют так часто, как получается. Полный пример — мяч падает и отскакивает от пола:

public class GameLoop {
    static final double STEP = 1.0 / 60.0; // шаг логики: 60 обновлений в секунду

    // Состояние "игры": мяч падает и отскакивает от пола
    static double y = 10.0;     // высота, м
    static double vy = 0.0;     // скорость, м/с
    static final double G = -9.8;

    static void update(double dt) {
        vy += G * dt;
        y += vy * dt;
        if (y < 0) {            // столкновение с полом
            y = 0;
            vy = -vy * 0.5;     // отскок с потерей энергии
        }
    }

    public static void main(String[] args) {
        long previous = System.nanoTime();
        double accumulator = 0;
        int updates = 0;
        while (updates < 180) {                    // 3 секунды игрового времени
            long now = System.nanoTime();
            accumulator += (now - previous) / 1e9;  // сколько реального времени прошло
            previous = now;
            while (accumulator >= STEP) {           // догоняем логику фиксированными шагами
                update(STEP);
                accumulator -= STEP;
                updates++;
                if (updates % 30 == 0) {
                    System.out.printf("t=%.1f c  y=%.2f м%n", updates * STEP, y);
                }
            }
            // здесь в настоящей игре был бы render()
        }
    }
}

Запуск java GameLoop.java (Java 25) печатает высоту мяча каждые полсекунды:

t=0.5 c  y=8.73 м
t=1.0 c  y=5.02 м
t=1.5 c  y=0.44 м
t=2.0 c  y=2.36 м
t=2.5 c  y=1.83 м
t=3.0 c  y=0.37 м

Разбор по шагам:

  1. accumulator копит реальное время между итерациями.
  2. Внутренний while вызывает update(STEP) столько раз, сколько целых шагов накопилось. Медленный компьютер сделает несколько шагов за итерацию, быстрый — ни одного, но игровое время идет одинаково.
  3. Поэтому числа в выводе не зависят от скорости машины: логика всегда делает 180 одинаковых шагов.

Граница примера: это учебная модель. Цикл крутится без паузы и загружает ядро процессора на 100%; в настоящей игре кадр ждет вертикальную синхронизацию (vsync) или спит, а между шагами логики делают интерполяцию для плавной картинки. Фреймворки вроде libGDX берут этот цикл на себя.

Первый проект на libGDX

Дорожная карта до артефакта — окна, где спрайт двигается стрелками:

  1. Установите JDK (для нового проекта разумно взять Java 25 LTS или ту версию, которую поддерживает текущий шаблон libGDX) и IDE: IntelliJ IDEA или Eclipse.
  2. Сгенерируйте проект утилитой gdx-liftoff — это рекомендуемый генератор проектов libGDX. Отметьте платформы (для начала хватит Desktop), он создаст Gradle-проект с модулями core и lwjgl3.
  3. Откройте проект в IDE как Gradle-проект и запустите задачу lwjgl3:run. Появится окно с логотипом.
  4. Замените класс игры в модуле core на код ниже.
  5. Когда игра заработает, соберите дистрибутив: lwjgl3:jar дает запускаемый JAR, а утилита jpackage из JDK упаковывает приложение вместе с Java-рантаймом, чтобы игроку не нужно было ставить Java.

Класс игры в модуле core:

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.ScreenUtils;

public class Main extends ApplicationAdapter {
    private SpriteBatch batch;
    private Texture image;
    private float x = 100, y = 100;
    private static final float SPEED = 200f; // пикселей в секунду

    @Override
    public void create() {                 // один раз при старте
        batch = new SpriteBatch();
        image = new Texture("libgdx.png"); // файл из папки assets
    }

    @Override
    public void render() {                 // вызывается каждый кадр
        float dt = Gdx.graphics.getDeltaTime();
        if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) x += SPEED * dt;
        if (Gdx.input.isKeyPressed(Input.Keys.LEFT))  x -= SPEED * dt;

        ScreenUtils.clear(0.15f, 0.15f, 0.2f, 1f);
        batch.begin();
        batch.draw(image, x, y);
        batch.end();
    }

    @Override
    public void dispose() {                // освобождаем ресурсы GPU
        batch.dispose();
        image.dispose();
    }
}

Что произойдет: откроется окно с темным фоном и картинкой, стрелки влево и вправо двигают ее со скоростью 200 пикселей в секунду при любом FPS — та же идея с dt, что в контрпримере выше. Метод dispose() обязателен: текстуры и SpriteBatch держат память видеокарты, сборщик мусора Java ее не освобождает.

Десктопный запускатель в модуле lwjgl3 gdx-liftoff создает сам; его суть — три строки настройки:

import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;

public class DesktopLauncher {
    public static void main(String[] args) {
        Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration();
        config.setTitle("Моя первая игра");
        config.setWindowedMode(800, 480);
        config.useVsync(true);             // кадр синхронизируется с частотой монитора
        new Lwjgl3Application(new Main(), config);
    }
}

Дальше по нарастающей: столкновения прямоугольников (Rectangle.overlaps), звуки (Gdx.audio), экраны меню и игры (Game и Screen), физика через расширение Box2D.

Если не получилось

Симптом Частая причина Что сделать
Gradle падает с ошибкой версии класса или «Unsupported class file major version» версия JDK не совпадает с той, что ждет Gradle-обертка проекта в IDE выбрать для Gradle поддерживаемый JDK или обновить Gradle-обертку
GdxRuntimeException: Couldn't load file: libgdx.png файла нет в assets или рабочая папка запуска не та положить файл в assets, запускать через задачу Gradle, а не «голый» main
На macOS окно не открывается, ошибка про главный поток GLFW на macOS требует запуск в первом потоке запускать шаблонный lwjgl3:run: в проекте gdx-liftoff это уже учтено
Персонаж на одном компьютере быстрее, чем на другом движение «за кадр» без dt умножать скорость на getDeltaTime()
Периодические подергивания кадра много новых объектов каждый кадр, паузы сборщика мусора переиспользовать объекты (Pool в libGDX), не создавать их в render()

Где граница Java в геймдеве

Java хорошо подходит для инди-игр, 2D, учебных проектов, серверной части онлайн-игр и игр под Android. Но это не основной язык крупных AAA-проектов: там доминирует C++ (Unreal Engine 5 написан на нем), а в Unity 6 скрипты пишут на C#. Популярные коммерческие движки — Unity, Unreal, Godot — Java официально не поддерживают.

Причины прагматичные. Консольные SDK и основные инструменты индустрии ориентированы на C++ и C#. Сборщик мусора требует дисциплины с памятью в каждом кадре, хотя современные сборщики (ZGC, Shenandoah) заметно сократили паузы. Поэтому формула честная: на Java можно сделать законченную и коммерчески успешную игру, но путь в студию крупных проектов обычно лежит через C++ или C#.

Выводы

  • Игры на Java делают на трех уровнях: фреймворк libGDX, 3D-движок jMonkeyEngine, низкоуровневая библиотека LWJGL; для первой игры проще всего libGDX.
  • Phaser, PixiJS, Babylon.js и подобные — инструменты JavaScript, не Java.
  • Движение считают через dt, физику — фиксированным шагом, иначе скорость игры зависит от FPS.
  • Ресурсы видеокарты в libGDX освобождают вручную в dispose(), лишние объекты в кадре вызывают подергивания.
  • Java уместна для инди, 2D, Android и серверов игр; в AAA основные языки — C++ и C#.

Где применяется / связь с практикой

Освойте тему на практике

Игра — хороший учебный проект по Java: в ней сразу нужны классы и наследование, коллекции, работа с временем, ресурсами и исключениями, а результат видно на экране. Чтобы уверенно писать такой код и дальше расти в разработке, стоит освоить базу языка системно — например, на курсе Java Developer. Basic. Разобрать отдельные темы Java с практикующими разработчиками можно на бесплатных открытых уроках Otus.

FAQ

Можно ли писать на Kotlin вместо Java в libGDX?
Да, libGDX работает с любым JVM-языком, а gdx-liftoff умеет создавать проект с Kotlin. Документация и примеры при этом в основном на Java.

Нужно ли игроку устанавливать Java, чтобы запустить мою игру?
Не обязательно: jpackage или jlink упаковывают игру вместе с урезанным рантаймом Java, и игрок получает обычный установщик или папку с исполняемым файлом.

Работает ли игра на libGDX в браузере?
Да, через веб-бэкенд, но с ограничениями: часть библиотек Java и рефлексии там недоступна, поэтому код для веба проверяют отдельно.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)