Простоя игра

Перед погружением в API libGDX, давайте создадим очень простую игру, которая будет затрагивать все модули. Будет представлено несколько концепций, не вдаваясь в подробные детали.

Будет рассмотрено:

  • Простейший доступ к файлам
  • Очистка экрана
  • Отрисовка изображений
  • Использование камеры
  • Основы обработки ввода
  • Воспроизведение звуковых эффектов

Настройка проекта

Следуйте инструкциям по настройке, запуску и отладке проекта. Будут использованы следующие имена:

  • Имя приложения: drop
  • Имя пакета: com.badlogic.drop
  • Game класс: Drop

После импорта в Eclipse вы должны иметь 4 проекта: drop, drop-android, drop-desktop и drop-html5.

Игра

Идея игры очень простая:

  • Нужно ведром ловить дождевые капли.
  • Ведро расположено в нижней части экрана.
  • Капли случайным образом появляются вверху экрана каждую секунду и устремляются вниз.
  • Игрок с помощью мыши, клавиш клавиатуры или нажатия на сенсорный экран может горизонтально передвигать ведро.
  • Игра не имеет условий окончания.

Assets

Для того чтобы игра выглядела хорошо, нужно несколько изображений и звуковых эффектов. Для графики будет использовано разрешение 800x480 пикселей (ландшафтная ориентация на Android). Если игра запуститься на устройстве с другим разрешением экрана, то все просто смасштабируется по границам экрана.

Игры высокого профиля

Для игр высокого профиля, возможно, придется иметь различные assets ресурсы для экранов с разным разрешением. Это довольно большая тема и здесь она не будет рассматриваться.

Изображения капли и ведра должны занимать небольшую часть экрана по вертикали, поэтому пусть они имеют размер 64x64 пикселей.

Assets взяты из следующих источников:

Для того чтобы assets были доступны в игре их нужно поместить в assets директорию Android проекта. Имена файлов следующие: drop.wav, rain.mp3, droplet.png и bucket.png. Поместите их в drop-android/assets/ директорию. Desktop и HTML5 проекты должны ссылаться на эту директорию, поэтому можно хранить только одни assets.

Настройка Starter классов

Выполнив необходимые требования можно настроить Starter классы. Начнем с Desktop проекта. Откройте Main.java класс в drop-desktop/ директории. Мы хотим, чтобы размер окна был 800x480 пикселей и заголовок был "Drop". Код должен выглядеть следующим образом:

package com.badlogic.drop;

import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;

public class Main {
   public static void main(String[] args) {
      LwjglApplicationConfiguration cfg = new LwjglApplicationConfiguration();
      cfg.title = "Drop";
      cfg.width = 800;
      cfg.height = 480;
      new LwjglApplication(new Drop(), cfg);
   }
}

Перейти в Android проект, так как мы хотим, чтобы приложение запускалось в ландшафтном режиме. Для этого нам нужно изменить AndroidManifest.xml файл в корневой директории Android проекта, который выглядит следующим образом:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.badlogic.drop"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="5" android:targetSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="landscape"
            android:configChanges="keyboard|keyboardHidden|orientation">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Программа установи уже заполнила для нас правильные значения, атрибут android:screenOrientation выставлен в "landscape". Если бы мы хотели, чтобы игра запускалась в портретном режиме, то нужно этот атрибут установить в "portrait".

Мы также хотим сохранить заряд батареи и отключить акселерометр и компас. Мы делаем это в MainActivity.java файле Android проекта, который выглядит примерно так:

package com.badlogic.drop;

import android.os.Bundle;

import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;

public class MainActivity extends AndroidApplication {
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
        
      AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration();
      cfg.useAccelerometer = false;
      cfg.useCompass = false;
        
      initialize(new Drop(), cfg);
   }
}

Мы не может определить разрешение экрана Activity, так как оно устанавливается операционной системой Android. Как мы определили ранее, мы просто масштабируем разрешение 800x480 до размеров экрана устройства.

Наконец, мы хотим убедиться, что HTML5 проект тоже использует область рисования размером 800x480 пикселей. Для этого нужно изменить GwtLauncher.java файл в HTML5 проекте.

package com.badlogic.drop.client;

import com.badlogic.drop.Drop;

public class GwtLauncher extends GwtApplication {
   @Override
   public GwtApplicationConfiguration getConfig () {
      GwtApplicationConfiguration cfg = new GwtApplicationConfiguration(800, 480);
      return cfg;
   }

   @Override
   public ApplicationListener getApplicationListener () {
      return new Drop();
   }
}
HTML5 и версия OpenGL

Нам не нужно указывать какие версии OpenGL использовать для этой платформы, так как она поддерживает только OpenGL 2.0.

Теперь все Starter классы правильно настроены, поэтому можно переходить к реализации игры.

Код

Мы хотим разделить наш код на несколько частей. Для этого мы просто будет держать все в Drop.java файле основного проекта.

Загрузка Assets

Нашей первой задачей будет загрузить assets и сохранить ссылки на них. Assets обычно загружаются в ApplicationListener.create() методе:

public class Drop implements ApplicationListener {
   Texture dropImage;
   Texture bucketImage;
   Sound dropSound;
   Music rainMusic;
   
   @Override
   public void create() {
      // Загрузка изображений капли и ведра, каждое размером 64x64 пикселей
      dropImage = new Texture(Gdx.files.internal("droplet.png"));
      bucketImage = new Texture(Gdx.files.internal("bucket.png"));
      
      // Загрузка звукового эффекта падающей капли и фоновой "музыки" дождя 
      dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
      rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));
      
      // Сразу же воспроизводиться музыка для фона
      rainMusic.setLooping(true);
      rainMusic.play();

      ... еще не все ...
   }

   // Остальная часть опущена для ясности

Для каждого asset есть поля в Drop классе, чтобы мы могли позже ссылаться на них. Первые две строки в create методе загружают изображения капли и ведра. Texture представляет загруженное изображения, которое храниться в видео памяти. Texture загружается передачей в конструктор FileHandle соответствующего asset файла. Такие FileHandle экземпляры можно получить с помощью одного из методов Gdx.files. Существуют различные типы файлов, мы использует внутренний (internal) тип файла для ссылки на asset. Внутренние файлы располагаются в assets директории Android проекта. Desktop и HTML5 проекты ссылаются на ту же директорию через связь в Eclipse.

В следующем шаге мы загружает звуковой эффект и музыку для фона. libGDX различает звуковые эффекты, которые хранятся в памяти, и музыку, которая воспроизводиться как поток из места ее расположения. Музыка обычно слишком большая, чтобы полностью хранить ее в памяти, отсюда вытекают различия. Как правило, вы должны использовать экземпляр Sound класса, если продолжительность меньше чем 10 секунд, и экземпляр Music класса для более долгих аудио частей.

Загрузка Sound и Music экземпляров осуществляется через методы Gdx.app.newSound() и Gdx.app.newMusic(). Оба метода принимают FileHandle, так же как и Texture конструктор.

В конце create() метода мы говорим Music экземпляру, чтобы музыка повторялась, и сразу же запускалось ее воспроизведение. Если вы запустите это приложение, то увидите розовый фон и услышите звук дождя.

Камера и SpriteBatch

Далее мы хотим создать камеру и SpriteBatch. Мы использует такое же разрешение, чтобы убедиться в том, что может делать визуализацию с разрешением 800x480 независимо от фактического разрешения экрана. SpriteBatch это специальный класс, который используется для рисования 2D изображений, таких как текстуры, которые мы загрузили.

Мы добавим два новых поля в класс и назовем их camera и batch

OrthographicCamera camera;
SpriteBatch batch;

В create() методе мы сначала создаем камеру следующем образом:

camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);

Метод setToOrtho() позволяет убедиться в том, что камера всегда показывает область мира игры, которая размером 800x480 единиц. Думайте об этом как о виртуальном окне в наш мир. В настоящее время мы интерпретировали единицы как пиксели для облегчения жизни. Камера является очень мощным механизмом и позволяет делать очень много разных вещей, которые мы не будет рассматривать в этой базовой статье. Смотрите остальное руководство libGDX разработчика для больше информации.

Замет необходимо в том же create() методе создать SpriteBatch:

batch = new SpriteBatch();

Мы почти завершили создание всех вещей необходимых для запуска этой простой игры.

Добавляем ведро

Пока еще отсутствует представление ведра и капли. Давайте подумаем о том, что нам нужно, чтобы представить их в коде.

  • Ведро и капля имеют x и y координаты в 800x480 мире.
  • Ведро и капля имеют ширину и высоту, выраженные в единицах нашего мира.
  • Ведро и капля имеют графическое представление, у нас оно уже есть в форме Texture экземпляров.

Для описания ведра и капли необходимо сохранить их позицию и размер. libGDX предоставляет класс Rectangle, который можно использовать для этой цели. Давайте начнем с создания Rectangle, который будет представлять ведро. Добавим новое поле:

Rectangle bucket;

В create() методе создается Rectangle и указываются начальные значения. Мы хотим, чтобы ведро было на 20 пикселей выше нижней границы экрана и центрировалось по горизонтали.

bucket = new Rectangle();
bucket.x = 800 / 2 - 64 / 2;
bucket.y = 20;
bucket.width = 64;
bucket.height = 64;

Мы центрируем ведро по горизонтали и размещаем на 20 пикселей выше нижней границы экрана. Возникает вопрос - почему bucket.y координата установлена в 20, разве она не должна рассчитываться как 480-20? По умолчанию вся визуализация в libGDX (и в OpenGL) осуществляет вверх по y-оси. Координаты x/y bucket определяют нижний левый угол ведра, нулевые координаты находятся в нижнем левом углу. Ширина и высота прямоугольника устанавливается в 64x64.

Начальные координаты

Можно изменить эти установки, чтобы y-ось уходила вниз и начальные координаты были в верхнем левом углу экрана. OpenGL и камера настолько гибки, что можно иметь угол обзора какой вы захотите, в 2D и в 3D.

Визуализация ведра

Время нарисовать ведро. Первое, что мы хотим сделать, это очистить экран темно-синим цветом. Просто измените render() метод, чтобы он выглядел следующим образом:

@Override
public void render() {
   Gdx.gl.glClearColor(0, 0, 0.2f, 1);
   Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

   ... еще не все ...
}

Эти две строки кода единственное, что вам нужно знать о OpenGL, если вы используете классы высокого уровня, такие как Texture и SpriteBatch. Первый вызов установит цвет очистки в синий цвет. Аргументами являются красный, зеленый, синий и альфа-компонент цвета, каждый в диапазоне [0, 1]. Следующий вызов говорит OpenGL очистить экран.

Затем мы должны сказать камере, чтобы убедиться в том, что она обновляется. Камеры используют математическую сущность, называемую матрицу, которая отвечает за создание системы координат для визуализации. Эти матрицы нужно пересчитывать каждый раз, когда мы меняем свойства камеры, как и ее положение. Мы не делаем этого в нашем простом примере, но как правило, хорошей практикой является обновление камеры один раз за кадр:

camera.update();

Теперь можно нарисовать ведро:

batch.setProjectionMatrix(camera.combined);
batch.begin();
batch.draw(bucketImage, bucket.x, bucket.y);
batch.end();

В первой строке сообщается SpriteBatch использовать систему координат камеры. Как отмечалось ранее, это делается с помощью, так называемой матрицы, если быть более конкретным, то матрицы проекции. Поле camera.combined является такой матрицей. SpriteBatch нарисует все, что будет находиться в заданных координатах.

Следующим мы говорим SpriteBatch начать новую batch серию. Зачем нам это и что такое серия? OpenGL не любит когда ему говорят только об одном изображении. OpenGL хочет знать обо всех изображениях, чтобы нарисовать за один раз как можно больше.

OpenGL в этом помогает класс SpriteBatch. Он будет записывать все команды рисования между SpriteBatch.begin() и SpriteBatch.end(). Как только мы вызываем SpriteBatch.end() метод, он предоставит все запросы рисования, повышая скорость визуализации. Все это может выглядеть непонятным в начале, но это то, что дает разницу между рисованием 500 спрайтов при 60 кадрах в секунду и рисованием 100 спрайтов при 20 кадрах в секунду.

Делаем ведро подвижным (прикосновение/мышь)

Время, чтобы позволить пользователю управлять ведром. Ранее мы сказали, что мы будем позволять пользователю перетаскивать ведро. Давайте сделаем задачу немного легче. Если пользователь прикасается к экрану (или нажимает кнопку мыши), то ведро центрируется по горизонтали относительно точки прикосновения. Добавление следующего кода вниз render() метода будет делать это:

if(Gdx.input.isTouched()) {
   Vector3 touchPos = new Vector3();
   touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
   camera.unproject(touchPos);
   bucket.x = touchPos.x - 64 / 2;
}

Сначала вызывается Gdx.input.isTouched() метод, который спрашивает модуль ввода о том, есть ли на данный момент прикосновение к экрану (или нажатие кнопки мыши). Далее идет преобразование координат прикосновения/мыши в систему координат камеры. Это необходимо, поскольку система координат прикосновения/мыши может отличаться от используемой нами системы для представления объектов в мире.

Gdx.input.getX() и Gdx.input.getY() возвращают текущую позицию прикосновения/мыши (libGDX поддерживает multi-touch). Для преобразования этих координат в систему координат нашей камеры, мы должны вызвать camera.unproject() метод, в который передается трехмерный вектор Vector3. Мы создаем вектор, устанавливаем текущие координаты прикосновения/мыши и вызываем соответствующий метод. Теперь вектор содержит координаты прикосновения/мыши в такой же системе координат, как и ведро. Далее изменяется позиция ведра так, чтобы центр находился в координатах прикосновения/мыши.

Постоянное создание экземпляра нового объекта

Очень и очень плохо, когда постоянно создается экземпляр нового объекта, в нашем случае это экземпляр Vector3 класса. Причиной этого является сборщик мусора, который должен часто освобождать память этих недолго живущих объектов. На Desktop это не так уж и важно, но сборщик мусора на Android может вызвать паузу на несколько миллисекунд, что приведет к затормаживанию. Чтобы решить эту проблему, в данном случае можно сделать touchPos полем в Drop классе, вместо постоянного создания экземпляра.

touchPos является трехмерным вектором. Вы можете удивиться, почему это так, если мы работаем только в 2D. OrthographicCamera на самом деле 3D камера, которая также принимает во внимание Z-координату.

Делаем ведро подвижным (клавиатура)

На Desktop и в браузере можно также обрабатывать ввод с клавиатуры. Давайте заставим ведро двигаться по нажатию на клавиши влево и вправо.

Мы хотим, чтобы ведро передвигалось без ускорения, на двести пикселей в секунду влево или вправо. Для реализации такого зависящего от времени движения, мы должны знать время, прошедшее между последним и текущем кадром визуализация. Вот как можно сделать это:

if(Gdx.input.isKeyPressed(Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime();
if(Gdx.input.isKeyPressed(Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime();

Метод Gdx.input.isKeyPressed() сообщает о нажатии определенной клавиши. Перечисление Keys содержит все коды клавиш, которые поддерживает libGDX. Метод Gdx.graphics.getDeltaTime() возвращает время, прошедшее между последним и текущим кадром в секундах. Все что нам нужно сделать – изменить x-координаты ведра путем добавления/удаления 200 единиц умноженных на дельту времени в секундах.

Мы также должны убедиться в том, что ведро остается в пределах экрана:

if(bucket.x < 0) bucket.x = 0;
if(bucket.x > 800 - 64) bucket.x = 800 - 64;

Добавляем каплю

Для хранения капель используется список Rectangle экземпляров, каждый из которых хранит позицию и размер капли. Добавим список в качестве поля:

Array<Rectangle> raindrops;

Класс Array – специальный класс для использования вместо стандартных Java коллекций, таких как ArrayList. Проблема с ними в том, что они производят мусор различными способами. Класс Array пытается минимизировать мусор в максимально возможной степени. libGDX предлагает другие сборщики мусора для коллекций хэш-таблиц и множеств.

Так же необходимо отслеживать последнее появление капли, так что добавим еще одно поле.

long lastDropTime;

Мы будет хранить время в наносекундах, поэтому мы используем long.

Для облегчения создания капли мы напишем метод, называемый spawnRaindrop(), который создает новый Rectangle, устанавливает его в случайной позиции в верхней части экрана и добавляет его в raindrops массив.

private void spawnRaindrop() {
   Rectangle raindrop = new Rectangle();
   raindrop.x = MathUtils.random(0, 800-64);
   raindrop.y = 480;
   raindrop.width = 64;
   raindrop.height = 64;
   raindrops.add(raindrop);
   lastDropTime = TimeUtils.nanoTime();
}

Метод должен быть довольно очевидным. Класс MathUtils – libGDX класс, предлагающий статические методы, связанные с математикой. В этом случае возвращается случайное число между нулем и 800 - 64. Класс TimeUtils – другой libGDX класс, который предоставляет очень простые статические методы, связанные со временем. В этом случае мы записываем текущее время в наносекундах, основываясь на том, следует ли порождать новую каплю или нет.

В методе create() сейчас создается экземпляр массива капель и порождается первая капля.

raindrops = new Array<Rectangle>();
    spawnRaindrop();

Затем добавляем несколько строк в render() метод, который будет проверять, сколько времени прошло, с тех пор как была создана новая капля и если необходимо, создавать еще одну новую каплю.

if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop();

Также нужно сделать так, чтобы капли двигались. Давайте пойдем легким путем, и пусть они двигаются с постоянной скоростью 200 пикселей в секунду. Если капля находится ниже нижнего края экрана, мы удаляем ее из массива.

Iterator<Rectangle> iter = raindrops.iterator();
    while(iter.hasNext()) {
    Rectangle raindrop = iter.next();
    raindrop.y -= 200 * Gdx.graphics.getDeltaTime();
    if(raindrop.y + 64 < 0) iter.remove();
    }

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

batch.begin();
batch.draw(bucketImage, bucket.x, bucket.y);
for(Rectangle raindrop: raindrops) {
   batch.draw(dropImage, raindrop.x, raindrop.y);
}
batch.end();

Одна последняя корректировка: если капля попала в ведро, то нужно воспроизвести соответствующий звук и удалить каплю из массива. Просто добавьте следующие строки в цикл обновления капли:

if(raindrop.overlaps(bucket)) {
   dropSound.play();
   iter.remove();
}

Метод Rectangle.overlaps() проверяет, если прямоугольник пересекается с другим прямоугольником. В нашем случае воспроизводится звук, и капля удаляется из массива.

Очистка

Пользователь может закрыть приложение в любой момент. Для этого простого примера ничего особенного делать не нужно. Тем не менее, хорошей идей является немного помочь операционной системе и навести порядок в созданном нами беспорядке.

Любые libGDX классы, которые реализуют Disposable интерфейс и имеют dispose() метод, должны быть освобождены вручную, если они больше не используются. В нашем примере это относится к текстурам, звукам, музыки и SpriteBatch. Будучи хорошими гражданами, мы реализуем ApplicationListener.dispose() метод следующим образом:

@Override
public void dispose() {
   dropImage.dispose();
   bucketImage.dispose();
   dropSound.dispose();
   rainMusic.dispose();
   batch.dispose();
}

После освобождения ресурса вы не должны больше обращаться к нему.

Ресурсы, реализующие Disposable интерфейс, обычно являются нативными ресурсами и не обрабатываются сборщиком мусора Java. Это причина того, что мы должны вручную освобождать их. libGDX предоставляет различные способы помощи в управлении asset ресурсами. Читайте остальные части руководства.

Обработка паузы и возобновления

Android имеет особенность приостанавливать и возобновлять приложения всякий раз, когда пользователю звонят или при нажатии кнопки home. libGDX для таких случаев делает много вещей автоматически, например, перезагружает изображения, которые могут быть потеряны (потеря OpenGL контекста), останавливает и возобновляет потоковую музыку и так далее.

В нашей игре нет реальной необходимости в обработки паузы и возобновления. Как только пользователь заходит обратно в приложение, игра продолжается с того момента где она и была. Обычно можно реализовать экран паузы и просить пользователя прикоснуться к экрану, чтобы продолжить игру. Это останется в качестве упражнения для читателя. Смотрите ApplicationListener.pause() и ApplicationListener.resume() методы.

Полный исходный код игры

Вот крошечный исходный код нашей простой игры:

package com.badlogic.drop;

import java.util.Iterator;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.TimeUtils;

public class Drop implements ApplicationListener {
    Texture dropImage;
    Texture bucketImage;
    Sound dropSound;
    Music rainMusic;
    SpriteBatch batch;
    OrthographicCamera camera;
    Rectangle bucket;
    Array<Rectangle> raindrops;
    long lastDropTime;

    @Override
    public void create() {
        // загрузка изображений для капли и ведра, 64x64 пикселей каждый
        dropImage = new Texture(Gdx.files.internal("droplet.png"));
        bucketImage = new Texture(Gdx.files.internal("bucket.png"));

        // загрузка звукового эффекта падающей капли и фоновой "музыки" дождя
        dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
        rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));

        // сразу же воспроизводиться музыка для фона
        rainMusic.setLooping(true);
        rainMusic.play();

        // создается камера и SpriteBatch
        camera = new OrthographicCamera();
        camera.setToOrtho(false, 800, 480);
        batch = new SpriteBatch();

        // создается Rectangle для представления ведра
        bucket = new Rectangle();
        // центрируем ведро по горизонтали
        bucket.x = 800 / 2 - 64 / 2;
        // размещаем на 20 пикселей выше нижней границы экрана.
        bucket.y = 20;
        bucket.width = 64;
        bucket.height = 64;

        // создает массив капель и возрождает первую
        raindrops = new Array<Rectangle>();
        spawnRaindrop();
    }

    private void spawnRaindrop() {
        Rectangle raindrop = new Rectangle();
        raindrop.x = MathUtils.random(0, 800-64);
        raindrop.y = 480;
        raindrop.width = 64;
        raindrop.height = 64;
        raindrops.add(raindrop);
        lastDropTime = TimeUtils.nanoTime();
    }

    @Override
    public void render() {
        // очищаем экран темно-синим цветом.
        // Аргументы для glClearColor красный, зеленый
        // синий и альфа компонент в диапазоне [0,1]
        // цвета используемого для очистки экрана.
        Gdx.gl.glClearColor(0, 0, 0.2f, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

        // сообщает камере, что нужно обновить матрицы
        camera.update();

        // сообщаем SpriteBatch о системе координат
        // визуализации указанной для камеры.
        batch.setProjectionMatrix(camera.combined);

        // начинаем новую серию, рисуем ведро и
        // все капли
        batch.begin();
        batch.draw(bucketImage, bucket.x, bucket.y);
        for(Rectangle raindrop: raindrops) {
            batch.draw(dropImage, raindrop.x, raindrop.y);
        }
        batch.end();

        // обработка пользовательского ввода
        if(Gdx.input.isTouched()) {
            Vector3 touchPos = new Vector3();
            touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
            camera.unproject(touchPos);
            bucket.x = touchPos.x - 64 / 2;
        }
        if(Gdx.input.isKeyPressed(Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime();
        if(Gdx.input.isKeyPressed(Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime();

        // убедитесь что ведро остается в пределах экрана
        if(bucket.x < 0) bucket.x = 0;
        if(bucket.x > 800 - 64) bucket.x = 800 - 64;

        // проверка, нужно ли создавать новую каплю
        if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop();

        // движение капли, удаляем все капли выходящие за границы экрана
        // или те, что попали в ведро. Воспроизведение звукового эффекта
        // при попадании.
        Iterator<Rectangle> iter = raindrops.iterator();
        while(iter.hasNext()) {
            Rectangle raindrop = iter.next();
            raindrop.y -= 200 * Gdx.graphics.getDeltaTime();
            if(raindrop.y + 64 < 0) iter.remove();
            if(raindrop.overlaps(bucket)) {
                dropSound.play();
                iter.remove();
            }
        }
    }

    @Override
    public void dispose() {
        // высвобождение всех нативных ресурсов
        dropImage.dispose();
        bucketImage.dispose();
        dropSound.dispose();
        rainMusic.dispose();
        batch.dispose();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }
}

Куда двигаться дальше

Это был очень простой пример того, как использовать libGDX для создания мини игры. Некоторые вещи могут быть улучшены, такие как класс управления памяти, для повторного использования экземпляра Rectangle, который высвобождает сборщик мусора каждый раз, когда мы удаляем каплю. OpenGL не любит когда ему дают много различных изображений. В нашем случае это нормально, так как у нас было только два изображения. Обычно все изображения помещаются в один Texture, так же известный как TextureAtlas. Screen и Game экземпляры тоже могут быть использованы для повышения взаимодействия; узнайте больше в следующей части руководства по libGDX основанной на этой статье.

Рекомендует прочитать остальные части руководства разработчика и посмотреть демонстрационные примеры и тесты в Git репозитории.