Мышь, сенсорный экран и клавиатура

Основные устройства, которые поддерживает libGDX – мышь для компьютера/браузера, сенсорный экран для Android и клавиатура. Рассмотрим, как libGDX абстрагирует эти устройства.

Клавиатура

Клавиатура сообщает о пользовательском вводе, генерируя события нажатия и отпускания клавиши. Каждое событие несет в себе код клавиши, который идентифицирует нажатую/отпущенную клавишу. Эти коды отличаются друг от друга на разных платформах. libGDX пытается скрыть этот факт, предоставляя свою собственную таблицу кодов клавиш, смотрите класс Key. Вы можете запросить, какие клавиши сейчас нажаты с помощью опроса (Polling).

Сами по себе коды клавиш не несут информации о введенном пользователем символе. Часто эта информация зависит от состояния нескольких клавиш, например символ 'A' генерируется одновременным нажатием 'A' и 'Shift'. В общем, получение зависящих символов (которые были нажаты) из состояния клавиатуры нетривиально. К счастью, операционная система обычно имеет средство для обработки событий, которые не только сообщают о коде клавиши, но и о символе. libGDX использует этот механизм, чтобы предоставить вам информацию о символе. Смотрите обработку событий.

Мышь и сенсорный экран

Мышь и сенсорный экран позволяют пользователю указывать на что-либо на экране. Оба механизма ввода сообщают о местонахождении взаимодействия, передавая 2D координаты относительно левого верхнего угла экрана. Ось X указывает направление вправо, а ось Y указывает вниз.

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

Сенсорный экран не имеет такого понятия как кнопки и сложный из-за возможности аппаратного оборудования отслеживать несколько прикосновений руки одновременно. Первое поколение Android телефонов поддерживало только одно прикосновение. Начиная с телефонов как Motorola Droid, мультитач экраны стали стандартной особенностью большинства Android телефонов.

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

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

Система координат на Android

Система координат на Android относительна портретного или ландшафтного режима, в зависимости от установок вашего приложения.

События мыши и сенсорного экрана могут быть опрошены или обработаны при помощи обработчика событий.

Конфигурация и запрос устройств ввода

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

Отключение акселерометра и компаса

В данный момент настройка устройств ввода имеет смысл только для Android платформы.

Класс AndroidApplicationConfiguration имеет несколько публичных полей, которые можно установить прежде передачи в AndroidApplication.initialize() метод.

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

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

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

Запрос доступных устройств ввода

Чтобы проверить наличие конкретного устройства на платформе для работающего в настоящий момент libGDX приложения, вы можете воспользоваться Input.isPeripheralAvailable() методом.

boolean hardwareKeyboard = Gdx.input.isPeripheralAvailable(Peripheral.HardwareKeyboard);
boolean multiTouch = Gdx.input.isPeripheralAvailable(Peripheral. MultitouchScreen);

Чтобы увидеть остальные доступные в libGDX константы, смотрите Peripheral перечисление.

Отметим, что всего в нескольких Android устройствах имеется аппаратная клавиатура. Даже если клавиатура физически присутствует, пользователь может ее не выдвинуть. В этом случае вышеописанный метод вернет false.

Устройства ввода и манипуляторы

Различные платформы имеют различные средства ввода. На компьютере пользователи могут общаться с приложением через использование клавиатуры и мыши. То же самое верно для игр в браузере. Мышь на Android мыши заменяет сенсорный экран, а аппаратная клавиатура часто отсутствует. Все (совместимые) Android устройства также имеют акселерометр и иногда даже компас (датчик магнитного поля).

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

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

Все устройства ввода в libGDX доступны с помощью Input модуля.

// Проверка нажата ли клавиша 'A'
boolean isPressed = Gdx.input.isKeyPressed(Keys.A);

Настройки предпочтения

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

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

Получение экземпляра Preferences класса

Preferences можно получить с помощью следующего фрагмента кода:

Preferences prefs = Gdx.app.getPreferences("My Preferences");

Ваше приложение может иметь несколько предпочтений, просто дайте им разные имена.

Запись и чтение значений

Изменение настроек предпочтений так же просто, как изменение Java Map:

prefs.putString("name", "Donald Duck");
String name = prefs.getString("name", "No name stored");

prefs.putBoolean("soundOn", true);
prefs.putInteger("highscore", 10);

В libGDX методы получения значений идут в двух вариантах: с учетом и без учета значения по умолчанию. Значение по умолчанию будет возвращено, если в настройках предпочтений нет значения для указанного ключа.

Сохранение значений

Изменения в Preferences экземпляре будут сохранены только при явном вызове flush() метода.

// изменение ваших настроек предпочтений
prefs.flush();

Хранилище настроек

В Windows, Linux и OS X, настройки сохраняются в XML файл в домашней директории пользователя.

OS Местоположение хранилища предпочтений
Windows %Профиль_Пользователя%/.prefs/My Preferences
Linux и OS X ~/.prefs/My Preferences

Этот файл называется тем, что вы передали в Gdx.app.getPreferences() метод.

Это полезно знать, если вы хотите для тестирования изменить или удалить их вручную.

На платформе Android используется системный SharedPreferences класс. Это означает, что настройки предпочтений останутся при обновлении приложения, но удаляются при удалении приложения.

Файлы

libGDX приложения работают на различных платформах: Desktop системах (Windows, Linux, Mac OS X), Android и на JavaScript/WebGL возможных браузерах. Каждая из этих платформ работает с файлами немного по-разному.

В libGDX Files (код) модуль предоставляет следующие возможности:

  • Чтение из файла
  • Запись в файл
  • Копирование файла
  • Перемещение файла
  • Удаление файла
  • Выдача списка файлов и директорий
  • Проверка существования файла/директории

Прежде чем погрузиться в аспекты libGDX, мы должны сначала рассмотреть различные понятия файловых систем для всех поддерживаемых платформ.

Файловые системы платформ

Здесь мы рассмотрим обзор парадигм файловых систем платформ, поддерживающихся в libGDX.

Desktop (Windows, Linux, Mac OS X)

На Desktop ОС, файловая система является одним большим куском памяти. Файлы могут быть доступны по относительному пути к рабочей директории (директории в которой запущено приложение) или абсолютному пути. При этом игнорируются права доступа, файлы и директории всегда доступны на чтение и запись для всех приложений.

Android

На Android ситуация немного сложнее. Файлы можно хранить внутри APK приложения, либо как ресурсы или asset. Эти файлы доступны только для чтения. libGDX использует только asset механизм, так как он предоставляет прямой доступ к байтовым потокам и более напоминает традиционную файловую систему. Ресурсы отлично предоставляются для обычных Android приложений, но вызывают проблемы при использовании в играх. Android совершает манипуляции во время их загрузки, например, автоматически изменяет размер.

Asset хранятся в assets директории Android проекта и затем автоматически упаковываются в ваш APK файл при развертывании приложения. Ни одно другое приложение не может получить доступ к этим файлам.

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

Наконец, файлы можно хранить на внешнем накопителе данных, таком как SD карта. Внешние накопители не всегда могут быть доступны, например пользователь может вытащить SD карту. Файлы в этом месте хранения следует считать не всегда доступными. Чтобы иметь возможность записи на внешние накопители, вам нужно будет добавить разрешения в AndroidManifest.xml файл, смотрите разрешения ниже.

iOS

На iOS доступны все типы файлов.

Javascript/WebGL

Обычное Javascript/WebGL приложение не имеет традиционной концепции файловой системы. Вместо этого, assets подобны изображениям, ссылающимся на URL адреса, которые указывают на конкретные фалы одного или нескольких серверов. Современные браузеры поддерживают локальное хранилище данных, которое приближается к традиционным файловым системам с чтением и записью.

libGDX предоставляет абстракцию файловой системы, доступной только для чтения.

Типы (хранения) файлов

Файл в libGDX представлен экземпляром FileHandle класса. FileHandle имеет тип, который определяет где находится файл. Следующая таблица иллюстрирует наличие и местоположение каждого типа файла для каждой платформы.

Тип Описание, путь файла и особенности Desktop Android HTML5 iOS
Classpath Classpath файлы хранятся непосредственно в директории исходников. Они упакованы вместе с jar файлами и всегда доступны только для чтения. Они имеют свое назначение, но по возможности нужно избегать их использования. Да Да Нет Нет
Внутренний Внутренние файлы относятся к корневой или рабочей директории приложения на Desktop, к assets директории на Android, и к war/assets/ директории GWT проекта. Эти файлы доступны только для чтения. Если файл не может быть найдет во внутреннем хранилище, то файловый модуль возвращается к поиску файла в classpath. Это необходимо, если в Eclipse используется механизм ссылки на asset директорию, смотрите настройку проекта. Да Да Да Да
Локальный Локальные файлы хранятся относительно корневой или рабочей директории на Desktop и относятся к внутреннему (частному) хранилищу приложений на Android. Обратите внимание, что локальный и внутренний тип в основном одно и то же для Dekstop. Да Да Нет Да
Внешний Пути к внешним файлом являются относительными корня SD карты на Android и домашней директории текущего пользователя на Desktop системах. Да Да Нет Да
Абсолютный Абсолютные файлы должны иметь полностью указанный путь.
Примечание: Ради портативности, эта опция должна использоваться только в случае крайней необходимости
Да Да Нет Да

Абсолютные и classpath файлы в основном используются для таких инструментов, как редакторы на Desktop, которые имеют более сложные требования файлового ввода/вывода. Для игр их можно спокойно игнорировать. Порядок, в котором вы должны использовать типы файлов выглядит следующим образом:

  • Внутренние файлы: все assets (изображения, аудио файлы и так далее), которые упакованные вместе с приложением являются внутренними файлами. Если вы используете Setup UI, просто положите их в assets директорию Android проекта.
  • Локальные файлы: Если необходимо записывать небольшие файлы, например, сохранить состояние игры, используйте локальные файлы. В общем, они имеют частный доступ для вашего приложения. Если вы хотите использовать ключ/значение хранилище, то вам следует посмотреть настройки предпочтения.
  • Внешние файлы: Если необходимо записывать большие файлы, например снимки экрана или скачивать файлы из интернета, то следует использовать внешние накопители. Обратите внимание, что внешний накопитель является не всегда доступным, пользователь может вынуть его или удалить файлы, которые вы записали.

Проверка наличия хранилища и путей

Различные типы хранилищ могут быть недоступны в зависимости от платформы, на которой запущено приложение. Вы можете запросить такую информацию через Files модуль:

boolean isExtAvailable = Gdx.files.isExternalStorageAvailable();
boolean isLocAvailable = Gdx.files.isLocalStorageAvailable();

Можно также запросить корневой путь для внешнего и локального хранилища:

String extRoot = Gdx.files.getExternalStoragePath();
String locRoot = Gdx.files.getLocalStoragePath();

Получение дескриптора файла (FileHandles)

Получение FileHandle используется одним из вышеупомянутых типов напрямую из Files модуля. Следующий код получает дескриптор для внутреннего myfile.txt файла.

FileHandle handle = Gdx.files.internal("data/myfile.txt");

Если вы использовали Setup UI, то этот файл будет содержаться в asset директории Android проекта, если точно, то в $ANDROID_PROJECT/assets/data. Ваши Desktop и HTML проекты в Eclipse ссылаются на эту директорию, и автоматически имеют к ней доступ.

FileHandle handle = Gdx.files.classpath("myfile.txt");

Файл "Myfile.txt" находится в каталоге, там же где скомпилированные классы или включенные jar файлы.

FileHandle handle = Gdx.files.external("myfile.txt");

В этом случае необходимо чтобы myfile.txt файл был в домашней директории пользователя (/home/<пользователь>/myfile.txt на Linux или \Users\<пользователь>\myfile.txt на Windows и Mac OS) на Desktop и в корне SD-карты на Android.

FileHandle handle = Gdx.files.absolute("/some_dir/subdir/myfile.txt");

В случае абсолютного дескриптора файла, файл должен быть именно там, куда указывает путь. В /некоторая_директория/поддиректория/ текущего диска в Windows или точный путь для Linux, Mac OS и Android.

Экземпляры FileHandle передаются методам соответствующих классов отвечающих за чтение и запись данных. Например, нужно указать FileHandle для загрузки изображение с помощью Texture класса или для загрузки аудио файла с помощью Audio модуля.

Список файлов и проверка свойств

Иногда необходимо проверить конкретный файл на его наличие или просмотреть содержимое директории. FileHandle предоставляет методы, чтобы сделать этот в простой форме.

Вот пример, который проверяет существование конкретного файла и проверяет, является ли он директорией или нет:

boolean exists = Gdx.files.external("doitexist.txt").exists();
boolean isDirectory = Gdx.files.external("test/").isDirectory();

Просмотр списка директории столь же прост:

FileHandle[] files = Gdx.files.local("mylocaldir/").list();
for(FileHandle file: files) {
   // делаем что-нибудь интересное с файлом
}
Просмотр внутренних каталогов

Просмотр списка внутренних каталогов не поддерживается на Desktop.

Можно также запросить родительскую директорию файла или создать FileHandle для файла в родительской директории.

FileHandle parent = Gdx.files.internal("data/graphics/myimage.png").parent();
FileHandle child = Gdx.files.internal("data/sounds/").child("myaudiofile.mp3");

parent будет указывать на data/graphics/, child будет указывать на data/sounds/myaudiofile.mp3.

В FileHandle есть множество методов позволяющих проверять определенные атрибуты файла. Пожалуйста, для деталей обратитесь к Javadoc.

В HTML5 функции в основном нереализованные

На текущий момент функции в основном нереализованные на HTML5 back-end. Постарайтесь не полагаться на них, если целевым приложением будет HTML5.

Обработка ошибок

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

Чтение из файла

После получения FileHandle, мы можем либо передать его в класс, который знает, как загружать содержимое из файла (например, изображение) либо читать его сами. Последнее делается через любой из методов ввода в FileHandle классе. В следующем примере показано, как загрузить текст из внутреннего файла:

FileHandle file = Gdx.files.internal("myfile.txt");
String text = file.readString();

Если у вас есть двоичные данные, вы можете легко загрузить файл в байтовый массив:

FileHandle file = Gdx.files.internal("myblob.bin");
byte[] bytes = file.readBytes();

Класс FileHandle имеет гораздо больше методов чтения. Смотрите дополнительные сведения в Javadoc.

Запись в файл

Как и для чтения файлов, FileHandle предоставляет методы для записи в файл. Заметьте, что только локальные, внешние и абсолютные типы файлов поддерживаются для записи в файл. Запись строки в файл работает следующим образом:

FileHandle file = Gdx.files.local("myfile.txt");
file.writeString("My god, it's full of stars", false);

Второй параметр метода FileHandle.WriteString() указывает, должно ли содержимое быть добавлено к файлу. Если установлено значение false, то текущее содержимое файла будут перезаписано.

Можно, конечно и записывать двоичные данные в файл:

FileHandle file = Gdx.files.local("myblob.bin");
file.writeBytes(new byte[] { 20, 3, -2, 10 }, false);

Есть много других методов в FileHandle, которые облегчают запись различными способами, например использование OutputStream. Смотрите дополнительные сведения в Javadoc.

Удаление, копирование, переименование и перемещение файлов/директорий

Эти операции возможны только для типов файлов доступных для записи (локальных, внешних и абсолютных). Однако заметьте, что источником операции копирования может быть FileHandle доступный только для чтения. Несколько примеров:

FileHandle from = Gdx.files.internal("myresource.txt");
from.copyTo(Gdx.files.external("myexternalcopy.txt");

Gdx.files.external("myexternalcopy.txt").rename("mycopy.txt");
Gdx.files.external("mycopy.txt").moveTo(Gdx.files.local("mylocalcopy.txt"));

Gdx.files.local("mylocalcopy.txt").delete();

Обратите внимание, что источником и приемником могут быть файлы или директории.

Более подробную информацию о доступных методах смотрите в FileHandle Javadoc.

Расширение простой игры

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

Давайте начнем со знакомства с более продвинутыми libGDX классами в игре.

Интерфейс Screen

Экраны имеют основополагающее значение для любой игры с несколькими компонентами. Экраны содержат много методов, которые вы использовали в ApplicationListener объектах, и включают в себя пару новых методов show и hide, которые вызываются при получении и потери фокуса соответственно.

Класс Game

Класс Game в libGDX является абстрактным и предоставляет для использования реализацию ApplicationListener, наряду со вспомогательными методами для обработки визуализации экрана.

Вместе объекты Screen и Game в libGDX используются для создания простой и мощной структуры для игр.

Мы начнем с создания Game объекта, который будет входной точкой нашей libGDX игры.

Давайте посмотрим и пройдемся по коду:

package com.badlogic.drop;

import com.badlogic.gdx.Game;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;


public class Drop extends Game {

    SpriteBatch batch;
    BitmapFont font;

    public void create() {
        batch = new SpriteBatch();
        // libGDX по умолчанию использует Arial шрифт.
        font = new BitmapFont();
        this.setScreen(new MainMenuScreen(this));
    }

    public void render() {
        super.render(); // важно!
    }

    public void dispose() {
        batch.dispose();
        font.dispose();
    }

}

Приложение начинается с создания экземпляра SpriteBatch и BitmapFont. Создание множества экземпляров, которые можно использовать совместно является плохой практикой. Объект SpriteBatch используется для отображения объектов на экране, таких как текстуры; BitmapFont используется для отображения текста на экране. Мы коснемся этого подробнее в Screen классах.

Затем мы устанавливаем Screen экран игры как MainMenuScreen, единственный параметр экземпляр Drop класса.

Общей ошибкой является забывчивость вызова super.render(). Без этого вызова экран, который вы установили в Create() методе не будет отображен!

Наконец напоминание об освобождении объектов.

Главное меню

Рассмотрим MainMenuScreen класс.

package com.badlogic.drop;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;

public class MainMenuScreen implements Screen {

    final Drop game;

    OrthographicCamera camera;

    public MainMenuScreen(final Drop gam) {
        game = gam;

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

    // остальное опущено для краткости...

}
ф

В этом фрагменте кода, мы делаем конструктор для MainMenuScreen класса, который реализует интерфейс Screen. Интерфейс Screen не предоставляет какой-либо create() метод, поэтому вместо этого используется конструктор. Единственным необходимым параметром конструктора для игры является экземпляр Drop, так что если необходимо, то можно вызывать его методы и поля.

Последний метод в MainMenuScreen классе: render(float)

public class MainMenuScreen implements Screen {

    //public MainMenuScreen(final Drop gam)....

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

        camera.update();
        game.batch.setProjectionMatrix(camera.combined);

        game.batch.begin();
        game.font.draw(game.batch, "Welcome to Drop!!! ", 100, 150);
        game.font.draw(game.batch, "Tap anywhere to begin!", 100, 100);
        game.batch.end();

        if (Gdx.input.isTouched()) {
            game.setScreen(new GameScreen(game));
            dispose();
        }
    }

    // остальное опущено для краткости...

}

Этот код довольно прост, за исключением того, что нам нужно вызывать SpriteBatch и BitmapFont экземпляры из game поля, вместо создания собственных. Метод game.font.draw(SpriteBatch, String, float,float) отбражает текст на экране. libGDX поставляется с предварительно установленным шрифтом, Arial, так что можно использовать конструктор по умолчанию и все равно получить шрифт.

Замет проверяется, было ли прикосновение к экрану, если это так, то мы устанавливаем GameScreen экземпляр и освобождаем ресурсы MainMenuScreen экземпляра. Остальные методы, которые необходимы для реализации в MainMenuScreen остаются пустыми.

Экран игры

Теперь, когда главное меню закончено, время сделать игру. Мы будем использовать большую часть кода из предыдущего урока простой libGDX игры, чтобы избежать избыточности и предположения того, что это новая игра, так как это все таже Drop игра.

package com.badlogic.drop;

import java.util.Iterator;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.Screen;
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.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 GameScreen implements Screen {
    final Drop game;

    Texture dropImage;
    Texture bucketImage;
    Sound dropSound;
    Music rainMusic;
    OrthographicCamera camera;
    Rectangle bucket;
    Array<Rectangle> raindrops;
    long lastDropTime;
    int dropsGathered;

    public GameScreen(final Drop gam) {
        this.game = gam;

        // загрузка изображений для капли и ведра, 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);

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

        // создается 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(float delta) {
        // очищаем экран темно-синим цветом.
        // Аргументы для glClearColor красный, зеленый
        // синий и альфа компонент в диапазоне [0,1]
        // цвета используемого для очистки экрана.
        Gdx.gl.glClearColor(0, 0, 0.2f, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

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

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

        // начитаем новую серию, рисуем ведро и
        // все капли
        game.batch.begin();
        game.font.draw(game.batch, "Drops Collected: " + dropsGathered, 0, 480);
        game.batch.draw(bucketImage, bucket.x, bucket.y);
        for (Rectangle raindrop : raindrops) {
            game.batch.draw(dropImage, raindrop.x, raindrop.y);
        }
        game.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)) {
                dropsGathered++;
                dropSound.play();
                iter.remove();
            }
        }
    }

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

    @Override
    public void show() {
        // воспроизведение фоновой музыки
        // когда отображается экрана
        rainMusic.play();
    }

    @Override
    public void hide() {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }

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

}

Это код состоит почти на 95% из оригинальной реализации, только теперь мы используем конструктор вместо Create() метода ApplicatonListener и передаем объект Drop как в MainMenuScreen классе. Мы также воспроизводим музыку, как только экран установлен в GameScreen.

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

Теперь у вас есть полностью законченная игра. Вот и все, что нужно знать о Screen интерфейсе и абстрактном Game классе, и все это создает многогранную игру с несколькими состояниями.

Пожалуйста, посетите Github для получения полного исходного кода.

Будущее

Теперь, когда у вас есть понимание о нескольких экранах, настало время ими воспользоваться. Изучите Scene2d, Scene2D.ui и Skin, чтобы сделать более красивым ваше главное меню.

Если вы также уже прочитали шаги из предыдущего урока Drop, то вы должны быть готовы сделать вашу собственную игру. Лучшей практикой будет сделать это, так что идите и создавайте собственную игру.

Простоя игра

Перед погружением в 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 репозитории.

Взаимодействие с кодом платформы

Иногда бывает необходимо получить доступ к API конкретной платформы, например добавление рекламных услуг или функциональность leaderboard, предоставляемая таким фреймворком как Swarm.

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

Следующий пример не является реальным и предполагает, что мы хотим использовать в libGDX приложении очень простой leaderboard API, который доступен только на Android. Для других платформы мы просто логируем вызов или возвращаем тестовое значение.

Android API выглядит следующим образом:

/** Давайте предположим, что это API предоставлено Swarm **/
public class LeaderboardServiceApi {
   public void submitScore(String user, int score) { ... }
}

Первым шагом является создание абстракции API в виде интерфейса.

Интерфейс нужно поместить в основной проект (смотрите настройку проекта)

public interface Leaderboard {
   public void submitScore(String user, int score);
}

Далее мы создадим конкретные реализации для каждой платформы и поместим их в соответствующие проекты.

Следующий код для Android проекта:

/** Android реализация, можно иметь доступ к LeaderboardServiceApi напрямую **/
public class AndroidLeaderboard implements Leaderboard {
   private final LeaderboardServiceApi service;

   public AndroidLeaderboard() {
      // Предположим можно создать экземпляр так:
      service = new LeaderboardServiceApi();
   }

   public void submitScore(String user, int score) {
      service.submitScore(user, score);
   }
}

Следующий код для Desktop проекта:

/** Desktop реализация, мы просто пишем сообщение в лог **/
public class DesktopLeaderboard implements Leaderboard {
   public void submitScore(String user, int score) {
      Gdx.app.log("DesktopLeaderboard", "would have submitted score for user " + user + ": " + score");
   }
}

Следующий код для HTML5 проекта:

/** Html5 реализация, какая же для DesktopLeaderboard **/
public class Html5Leaderboard implements Leaderboard {
   public void submitScore(String user, int score) {
      Gdx.app.log("DesktopLeaderboard", "would have submitted score for user " + user + ": " + score");
   }
}

Далее ApplicationListener получает конструктор, в который мы может передать конкретную реализацию Leaderboard:

public class MyGame implements ApplicationListener {
   private final Leaderboard leaderboard;

   public MyGame(Leaderboard leaderboard) {
      this.leaderboard = leaderboard;
   }

   // остальное опущены для ясности
}

В каждом Starter классе мы просто создаем экземпляр MyGame, передавая как агрумент соответствующею Leaderboard реализацию, например для Desktop проекта:

public static void main(String[] argv) {
   LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
   new LwjglApplication(new MyGame(new DesktopLoaderboard()), config);
}

Потоки

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

Любые операции связанные с графикой и прямым участием OpenGL, должны быть выполнены в потоке визуализации. Делая это в другом потоке, вы можете вызвать не предсказуемое поведение приложения. Это связано с тем, что контекст OpenGL находится в активном состоянии только в потоке визуализации. Смена текущего контекста в других потоках имеет проблемы на множестве Android устройств, поэтому визуализации в другом потоке не поддерживается.

Для передачи данных в поток визуализации из другого потока рекомендуется использовать postRunnable() метод из Application. Этим самым выполнение кода будет в Runnable и в потоке визуализации при следующем кадре, но перед вызовом render() метода ApplicationListener интерфейса.

new Thread(new Runnable() {
   @Override
   public void run() {
      // выполнение чего-либо важного асинхронно потока визуализации
      final Result result = createResult();
      // передача Runnable в поток визуализации, который обработает результат
      Gdx.app.postRunnable(new Runnable() {
         @Override
         public void run() {
            // обработка результата, например добавление в Array<Result> поле ApplicationListener.
            results.add(result);
         }
      });
   }
}).start();

HTML5

По своей сути Javascript является однопоточным. Таким образом, многопоточность не возможна. В будущем, одним из вариантов может быть Web Workers, однако данные между потоками передаются через сообщения. Java использует другие примитивы и механизмы, поэтому перенос кода для Web Workers будет не столь прямым.

Логирование

Интерфейс Application предоставляет возможность простого и четкого логирования.

Сообщение может быть информационным, сообщением об ошибке или исключении и сообщением отладки:

dx.app.log("MyTag", "my informative message");
Gdx.app.error("MyTag", "my error message", exception);
Gdx.app.debug("MyTag", "my error message");

В зависимости от платформы лог сообщения выводятся в консоль (Desktop), LogCat (Android) или в GWT элемент TextArea (HTML5), который создается автоматически или предоставляется GwtApplicationConfiguration.

Логирование можно ограничить до определенного уровня:

Gdx.app.setLogLevel(logLevel);

Значение logLevel может быть одним из следующих значений:

  • Application.LOG_NONE:
  • Application.LOG_DEBUG:
  • Application.LOG_ERROR:
  • Application.LOG_INFO:

Запросы конфигурации

Интерфейс Application предоставляет различные методы запросов свойств среды выполнения.

Определение типа приложения

Иногда в некоторых случаях определенные части libGDX приложения зависят от типа платформы. Метод getApplicationType() возвращает платформу, на которой в данный момент выполняется приложение:

switch(Gdx.app.getApplicationType()) {
   case ApplicationType.Android:
       // кода для Android
   case ApplicationType.Desktop:
       // код для Desktop
   case ApplicationType.WebGl:
       /// код для HTML5
}

На платформе Android в libGDX приложении можно так же запросить версию самого Android:

int androidVersion = Gdx.app.getVersion();

Так возвращается поддерживаемый устройством уровень SDK, например 3 для Android 1.5.

Потребление памяти

Для отладки и профилирования часто необходимо знать количество потребляемой Java и нативной heap памяти:

int javaHeap = Gdx.app.getJavaHeap();
int nativeHeap = Gdx.app.getNativeHeap();

Оба метода возвращают количество байтов используемых для Java и нативной heap памяти.

Starter классы и конфигурация

Для каждой платформы должен быть написан Starter класс. Этот класс создает экземпляр конкретной back-end реализации Application и ApplicationListener реализует логику приложения. Starter классы зависят от платформы. Далее будет рассмотрено, как создать экземпляр и настроить его для каждого back-end.

Эта статья предполагает, что вы ознакомлены с инструкциями настройки проекта и имеете импортированные Core, Desktop, Android и HTML5 проекты в Eclipse.

Desktop (LWJGL)

При открытии Main.java класса в my-gdx-game проекте можно увидеть следующее:

package com.me.mygdxgame;

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 = "my-gdx-game";
      cfg.useGL20 = false;
      cfg.width = 480;
      cfg.height = 320;
                
      new LwjglApplication(new MyGdxGame(), cfg);
   }
}

Сначала создается экземпляр LwjglApplicationConfiguration. Этот класс позволяет задать различные параметры конфигурации, такие как начальный размер экрана, использование OpenGL ES 1.x или 2.0 и так далее.

Как только указан объект конфигурации, создается экземпляр LwjglApplication. MyGdxGame класс представляет реализацию ApplicationListener и логики приложения.

Начиная с этого момента, будет создано окно приложения и будут вызываться методы ApplicationListener, как описано в жизненном цикле приложения.

Android

Android приложение не использует main() метод как точку входа, но вместо этого требуется Activity. Откройте MainActivity.java класс в my-gdx-game-android проекте.

package com.me.mygdxgame;

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.useGL20 = false;
        
        initialize(new MyGdxGame(), cfg);
    }
}

Главная точка входа - onCreate() метод Activity класса. Обратите внимание, что MainActivity наследуется от AndroidApplication класса, который сам наследуется от Activity. Как и для Desktop starter класса задается конфигурация через создание экземпляра класса AndroidApplicationConfiguration. После настройки вызывается initialize() метод класса AndroidApplication и ему передается ApplicationListener(MyGdxGame).

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

Файл AndroidManifest.xml

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

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.me.mygdxgame"
    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>

Целевая версия SDK

Крайне важно задать targetSdkVersion больше или равной 6, если приложение должно работать на версии выше, чем 1.5. Если этот атрибут не задан, то приложение запуститься с высшей версией Android в режиме совместимости. Размеры области рисования будут меньше чем фактическое разрешение экрана.

Ориентация экрана и изменение конфигурации

В дополнение к targetSdkVersion должны быть установлены атрибуты screenOrientation и configChanges.

ScreenOrientation атрибут определяет фиксированную ориентацию экрана приложения. Можно не задавать, если приложение может работать в landscape и portrait режиме.

ConfigChanges атрибут имеет важное значение и всегда должен быть задан как указано выше. Отсутствие этого атрибута означает, что приложение будет перезагружаться каждый раз, когда показывается или скрывается физическая клавиатура или изменяется ориентация устройства. Если ScreenOrientation атрибут не задан, то libGDX приложение будет принимать вызовы ApplicationListener.resize() метода, чтобы уведомить об изменении ориентации.

Права и разрешения

Если приложение должно иметь возможность записи во внешнее хранилище устройства (например, на SD карту), иметь доступ в интернет, использовать вибратор, не давать экрану входить в спящий режим, записывать звук, то нужно добавить следующие разрешения в AndroidManifest.xml файл:

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>  

Обычно пользователи относятся с подозрением к приложению с множеством разрешений.

Для того, чтобы работал экран блокировки, нужно установить AndroidApplicationConfiguration.useWakeLock в true значение.

Если игра не нуждается в доступе к акселерометру или компасу, то рекомендуется отключить их, установив useAccelerometer и useCompass поля AndroidApplicationConfiguration в значение false.

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

Живые обои

libGDX имеет простой в использовании способ создавать живые обои для Android. Starter класс для живых обоев называется AndroidLiveWallpaperService, вот простой пример:

package com.mypackage;

// imports snipped for brevity 

public class LiveWallpaper extends AndroidLiveWallpaperService {
        @Override
        public ApplicationListener createListener () {
                return new MyApplicationListener();
        }

        @Override
        public AndroidApplicationConfiguration createConfig () {
                return new AndroidApplicationConfiguration();
        }

        @Override
        public void offsetChange (ApplicationListener listener, float xOffset, float yOffset, float xOffsetStep, float yOffsetStep,
                int xPixelOffset, int yPixelOffset) {
                Gdx.app.log("LiveWallpaper", "offset changed: " + xOffset + ", " + yOffset);
        }
}

Методы createListener() и createConfig() будут вызываться, когда живые обои показываются в просмотре или при их создании на главном экране.

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

В дополнение к Starter классу, вы также должны создать XML файл, описывающий ваши обои. Назовите файл, к примеру, livewallpaper.xml. Создайте директорию с именем XML в res директории Android проекта и поместите в нее файл (res/xml/livewallpaper.xml). Вот пример содержимого файла:

<?xml version="1.0" encoding="utf-8"?>
<wallpaper
       xmlns:android="http://schemas.android.com/apk/res/android"  
       android:thumbnail="@drawable/ic_launcher"
       android:description="@string/description"
       android:settingsActivity="com.mypackage.LivewallpaperSettings"/>

Здесь определяется миниатюра для окна выбора живых обоев, описание и Activity, которая будет показана, когда пользователь вызовет меню настроек в окне выбора обоев. Эта должна быть обычная Activity, которая имеет несколько виджетов для изменения настроек, таких как цвет заднего фона и подобные вещи. Вы можете сохранить эти настройки в SharedPreferences и загрузить их позже в ApplicationListener живых обоях через Gdx.app.getPreferences() метод.

Наконец, вы должны будете добавлять некоторые вещи в AndroidManifest.xml файл. Вот пример для приложения живых обоев с Activity простых настроек:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.mypackage"
      android:versionCode="1"
      android:versionName="1.0"
      android:installLocation="preferExternal">
        <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="14"/>     
        <uses-feature android:name="android.software.live_wallpaper" />
                
        <application android:icon="@drawable/icon" android:label="@string/app_name">
                <activity android:name=".LivewallpaperSettings" 
                                  android:label="Livewallpaper Settings"/>
                
                <service android:name=".LiveWallpaper"
            android:label="@string/app_name"
            android:icon="@drawable/icon"
            android:permission="android.permission.BIND_WALLPAPER">
            <intent-filter>
                <action android:name="android.service.wallpaper.WallpaperService" />
            </intent-filter>
            <meta-data android:name="android.service.wallpaper"
                android:resource="@xml/livewallpaper" />
        </service>                                      
        </application>
</manifest> 

Манифест определяет:

  • Использование особенности живых обоев, смотрите uses-feature.
  • Разрешения для привязки обоев, смотрите android:permission
  • Activity для настроек.
  • Сервис живых обоев, смотрите meta-data.
Живые обои в Android

Живые обои поддерживаются, только начиная с версии Android 2.1 (SDK версия 7).

Живые обои имеют некоторые ограничения относительно сенсорного ввода. В целом только tap и drop будут сообщаться. Установите AndroidApplicationConfiguration # getTouchEventsForLiveWallpaper флаг на true, для получения всех событий от сенсорного экрана.

Daydream

Начиная с Android 4.2 пользователи могут установить Daydream, который показывается когда устройство находится в режиме ожидания. Daydream схож с заставкой на экране и может отображать такие вещи как фото альбомы и так далее. libGDX позволяет с легкостью создать подобные Daydream.

Starter класс для Daydream называется AndroidDaydream. Вот пример:

package com.badlogic.gdx.tests.android;

import android.annotation.TargetApi;
import android.util.Log;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
import com.badlogic.gdx.backends.android.AndroidDaydream;
import com.badlogic.gdx.tests.MeshShaderTest;

@TargetApi(17)
public class Daydream extends AndroidDaydream {
   @Override
   public void onAttachedToWindow() {
      super.onAttachedToWindow();      
          setInteractive(false);

      AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration();
      cfg.useGL20 = true;
      ApplicationListener app = new MeshShaderTest();
      initialize(app, cfg);
   }
}

Просто наследуйтесь от AndroidDaydream класса, переопределите onAttachedToWindow метод, установите конфигурацию и ApplicationListener и затем инициализируйте Daydream.

В дополнение к Daydream, вы можете предоставить Activity для настроек, которая позволяет пользователю настроить ваш Daydream. Это может быть обычная Activity или libGDX AndroidApplication. Пример пустой Activity:

package com.badlogic.gdx.tests.android;

import android.app.Activity;

public class DaydreamSettings extends Activity {

}

Activity настроек должна быть указана в metadata сервиса Daydream. Создайте xml файл в res/xml директории Android проекта и укажите Activity:

<dream xmlns:android="http://schemas.android.com/apk/res/android"
   android:settingsActivity="com.badlogic.gdx.tests.android/.DaydreamSettings" />
}

В конце добавьте раздел для Activity настроек в AndroidManifest.xml и описание сервиса Daydream:

<service android:name=".Daydream"
   android:label="@string/app_name"
   android:icon="@drawable/icon"
   android:exported="true">
   <intent-filter>
           <action android:name="android.service.dreams.DreamService" />
           <category android:name="android.intent.category.DEFAULT" />
   </intent-filter>
   <meta-data android:name="android.service.dream"
           android:resource="@xml/daydream" />
</service>

iOS

iOS back-end опирается на использовании Xamarin's MonoDevelop среды разработки и Monotouch для развертывания. Точкой входа Monotouch приложения является AppDelegate, который можно найти в Main.cs классе проекта. Ниже приведен пример:

using System;
using System.Collections.Generic;
using System.Linq;

using MonoTouch.Foundation;
using MonoTouch.UIKit;
using com.badlogic.gdx.backends.ios;
using com.me.mygdxgame;

namespace com.me.mygdxgame
{               
    public class Application
    {
        [Register ("AppDelegate")]
        public partial class AppDelegate : IOSApplication {
            public AppDelegate(): base(new MyGdxGame(), getConfig()) {

            }

            internal static IOSApplicationConfiguration getConfig() {
                IOSApplicationConfiguration config = new IOSApplicationConfiguration();
                config.orientationLandscape = true;
                config.orientationPortrait = false;
                config.useAccelerometer = true;
                config.useMonotouchOpenTK = true;
                config.useObjectAL = true;
                return config;
            }
        }

        static void Main (string[] args)
        {
            UIApplication.Main (args, null, "AppDelegate");
        }
    }
}

Файл Info.plist

Файл Info.plist содержит информацию о конфигурации приложения: ориентация экрана, минимальная версия операционной системы, дополнительные параметры, скриншоты и так далее. Ниже приведет пример:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDisplayName</key>
    <string>my-gdx-game</string>
    <key>MinimumOSVersion</key>
    <string>3.2</string>
    <key>UIDeviceFamily</key>
    <array>
        <integer>2</integer>
        <integer>1</integer>
    </array>
    <key>UIStatusBarHidden</key>
    <true/>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string>
    </array>
    <key>UISupportedInterfaceOrientations~ipad</key>
    <array>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
</dict>
</plist>

Файл convert.properties

Процесс преобразования необходим для создания сборки Monotouch iOS платформы. Эта обработка выполнена как часть шага перед сборкой, когда выполняется операция сборки MonoDevelop. Если вы используете сторонние библиотеки или не стандартные расположение некоторых исходников, то нужно обновить convert.properties файл. Пример файла ниже:

SRC =       ../my-gdx-game/src/
CLASSPATH = ../my-gdx-game/libs/gdx.jar
EXCLUDE   =
IN        = -r:libs/ios/gdx.dll -recurse:target/*.class
OUT       = target/my-gdx-game.dll

Этот файл определяет входные файлы для сборки my-gdx-game.dll.

HTML5 GWT

Главной точкой входа HTML5/GWT приложения является GwtApplication. Откройте GwtLauncher.java в my-gdx-game-html5 проекте:

package com.me.mygdxgame.client;

import com.me.mygdxgame.MyGdxGame;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.backends.gwt.GwtApplication;
import com.badlogic.gdx.backends.gwt.GwtApplicationConfiguration;

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

   @Override
   public ApplicationListener getApplicationListener () {
      return new MyGdxGame();
   }
}

GwtApplication состоит из двух методов: getConfig() и getApplicationListener(). Первый метод должен вернуть экземпляр GwtApplicationConfiguration класса, который определяет различные параметры конфигурации HTML5 приложения. Метод getApplicatonListener для запуска приложения возвращает ApplicationListener.

Файлы модулей

GWT нужен реальный Java код каждого jar файла jar/projtect директории. Кроме того, каждый из файлов должен иметь один модуль определения с gwt.xml суффиксом.

Файл модуля HTML5 проекта может выглядеть следующим образом:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit trunk//EN" "http://google-web-toolkit.googlecode.com/svn/trunk/distro-source/core/src/gwt-module.dtd">
<module>
   <inherits name='com.badlogic.gdx.backends.gdx_backends_gwt' />
   <inherits name='MyGdxGame' />
   <entry-point class='com.me.mygdxgame.client.GwtLauncher' />
   <set-configuration-property name="gdx.assetpath" value="../my-gdx-game-android/assets" />
</module>

Этим самым описываются наследование от двух модулей (gdx-backends-gwt и основного проекта), а также точка входа (класс GwtLauncher) и пути относительно HTML5 корневой директории, указывающие на assets директорию.

Оба gdx-backend-gwt.jar и основной проект имеют аналогичный файл модуля, описывающий другие зависимости. Вы не можете использовать файлы в jars/projects, которые не содержат файла модуля и исходников.

Дополнительную информацию о модулях и зависимостях смотрите в руководстве GWT разработчика.

Поддержка Reflection

GWT не поддерживает Java reflection по различным причинам. libGDX имеет внутренний слой эмуляции, который генерирует reflection информацию для некоторых внутренних классов. Это означает, что если вы используете JSON сериализацию libGDX, то вы столкнетесь с проблемами. Вы можете решить их, указав для каких пакетов и классов необходимо сгенерировать reflection информацию. Чтобы сделать это, поместите свойства конфигурации в gwt.xml файл GWT проекта следующим образом:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<module>
    ... other elements ...
    <extend-configuration-property name="gdx.reflect.include" value="org.softmotion.explorers.model" />
    <extend-configuration-property name="gdx.reflect.exclude" value="org.softmotion.explorers.model.HexMap" />
</module>

Можно указать несколько пакетов и классов добавив еще extend-configuration-property элементов.

Это экспериментальная возможность, используйте ее на свой страх и риск.

Экран загрузки

libGDX HTML5 приложение загружает все asset находящиеся в gdx.assetpath. Во время процесса загрузки отображается экран загрузки, реализованный с помощью GWT виджета. Если нужно изменить экран загрузки, то просто переопределите метод getPreloaderCallback() в GwtApplication классе. В следующем примере рисуется очень простой экран загрузки, используя Canvas:

long loadStart = TimeUtils.nanoTime();
public PreloaderCallback getPreloaderCallback () {
  final Canvas canvas = Canvas.createIfSupported();
  canvas.setWidth("" + (int)(config.width * 0.7f) + "px");
  canvas.setHeight("70px");
  getRootPanel().add(canvas);
  final Context2d context = canvas.getContext2d();
  context.setTextAlign(TextAlign.CENTER);
  context.setTextBaseline(TextBaseline.MIDDLE);
  context.setFont("18pt Calibri");

  return new PreloaderCallback() {
     @Override
     public void done () {
            context.fillRect(0, 0, 300, 40);
     }

     @Override
     public void loaded (String file, int loaded, int total) {
            System.out.println("loaded " + file + "," + loaded + "/" + total);
            String color = Pixmap.make(30, 30, 30, 1);
            context.setFillStyle(color);
            context.setStrokeStyle(color);
            context.fillRect(0, 0, 300, 70);
            color = Pixmap.make(200, 200, 200, (((TimeUtils.nanoTime() - loadStart) % 1000000000) / 1000000000f));
            context.setFillStyle(color);
            context.setStrokeStyle(color);
            context.fillRect(0, 0, 300 * (loaded / (float)total) * 0.97f, 70);

            context.setFillStyle(Pixmap.make(50, 50, 50, 1));
            context.fillText("loading", 300 / 2, 70 / 2);
     }

     @Override
     public void error (String file) {
            System.out.println("error: " + file);
     }
  };
}

Обратите внимание, что для отображения экрана загрузки можно использовать только чистые GWT объекты, использование libGDX API будет доступно после того, как закончится загрузка.