Начало начал
Предполагается, что вы уже знаете как создавать проекты для этой библиотеки. Перевод и сокращение оффициальной статьи.
В данной статье мы создадим примитивнейшую игру и познакомимся с азами библиотеки на простом и понятном примере. Ну, я надеюсь. С чем конкретно мы столкнёмся:
- Доступ к файлам (любой игре нужны картинки, звук и прочие прелести, которые не хранятся в коде)
- Выводить картинки на экран (базовая составляющая любой нетекстовой игры)
- Познакомимся с игровой камерой (хотя пример несколько неудачный для этого)
- Сделаем управление (как-то же мы должны в игру играть)
- И добавим звуковых эффектов
При создании проекта были использованы следующие данные:
- Application name: drop
- Package name: com.badlogic.drop
- Game class: Drop
Ок, проект создали. Переходим к игре, точнее, к её сути: сверху капают капли, а по нижней части экрана игрок перемещает ведро и ловит эти капли. Вроде всё просто.
Соответственно, для игры нам потребуются изображения ведра и капли. А заодно фоновая музыка и звук капли, упавшей в ведро.
Скачали, что делать дальше? Заходим в папку с проектом. Нам нужна папка assets. Если проект расчитан на андроид, то эта папка находится в подпроекте "android". Если же вы плюнули на андроид, то эта папка лежит в подпроекте "core". Нашли? Теперь кидаем картинки со звуками в папку assets.
Drop\android\assets
Почему ресурсы хранятся именно в паке assets? Потому что в андроид-приложениях основные ресурсы хранятся в этой папке. Все остальные проекты (десктопы, иос...) получают ссылку на эту папку и обращаются к ней, т.е. игра создаётся мультиплатформенная, но ресурсы хранятся для всех платформ в одном месте.)
Файлы запуска
Итак, у нас есть идея и необходимые ресурсы. Осталось написать код (ага, как всё "просто").
Первым делом рассмотрим точки входа в программу, т.е. классы, запускающие игру. У каждой платформы свои особенности архитектуры, поэтому они находятся в отдельных подпроектах.
Файл для запуска на ПК:
Drop\desktop\src\com\badlogic\drop\desktop\DesktopLauncher.java
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 config = new LwjglApplicationConfiguration(); config.title = "Drop"; // Задаем название окну игры config.width = 800; // Задаем размеры окна config.height = 480; // Вообще, в конфигурации есть ещё полезные вещи, но о них не в этот раз new LwjglApplication(new Drop(), config); } }
Как вы могли заметить, мы используем альбомную ориентацию для игры. Для ПК мы изменили всего один класс, но с андроидом у нас чуть больше возни: помимо запускающего класса, у него есть конфигурационный xml-файл, описывающий то, как должна запускаться программа.
Drop\android\src\com\badlogic\drop\android\AndroidLauncher.java
package com.badlogic.drop; import android.os.Bundle; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; public class AndroidLauncher extends AndroidApplication { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration config= new AndroidApplicationConfiguration(); config.useAccelerometer = false; // Просто отключаем ненужные нам штуки config.useCompass = false; initialize(new Drop(), config); } } Для андроида нельзя указать конкретное разрешение - оно зависит от устройства, поэтому мы просто будем масштабировать игру, изначально сделанную для разрешения 800х480.
Drop\android\AndroidManifest.xml
<?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="8" android:targetSdkVersion="19" /> <uses-feature android:glEsVersion="0x00020000" android:required="true" /> <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>
В нашем примере суть данного файла в том, что он включает использование OpenGL и устанавливает альбомную ориентацию при запуске приложения.
Файлами запуска для HTML и ios являются только классы-лаунчеры, как в случае с ПК.
Класс с игрой
С файлами запуска разобрались, остался класс с самой игрой. Для простоты примера мы сделаем только геймплей.
Структура класса нашей игры выглядит следующим образом:
public class Drop implements ApplicationListener { public void create () { // Вызывается при запуске игры. } public void render () { // Главный цикл, в котором происходит обновление данных игрового мира, ловится управление и рисуется графика. } public void resize (int width, int height) { // Вызывается при изменении размеров игры. } public void pause () { // На андроиде вызывается при сворачивании игры, на остальных платформах - непосредственно перед dispose(). } public void resume () { // Вызывается на андроиде при разворачивании игры. } public void dispose () { // Вызывается при закрытии игры. } }
Использование файлов
Первое, что нам нужно сделать в этом классе - загрузить картинки и звуки. Объявим для них переменные и в методе create() загрузим их.
public class Drop implements ApplicationListener { // Блок переменных. Вы ведь знаете, что это значит? Здесь создаём все "глобальные" переменные, к которым будем обращаться по ходу дела. private Texture dropImage; private Texture bucketImage; private Sound dropSound; private 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(); ... }
Библиотека GDX состоит из нескольких статичных модулей, методы которых вызываются из любой точки приложения. Это модули Gdx.files, Gdx.audio и т.д. Каждый модуль имеет методы для работы с соответствующими сущностями, например, files - с файлами, audio - со звуками.
Как вы помните, все наши ресурсы лежат в папке assets. Это корневая папка для внутренних ресурсов игры, поэтому для метода
Gdx.files.internal("bucket.png")
в качестве аргумента идёт относительный путь к файлу, т.е. только само название. Если бы мы положили картинки в папку "images", а звуки - в "sounds", то в качестве аргумента брали "images\bucket.png" и "sounds\rain.mp3".
Разница между "Sound" и "Music" в том, что звуки - короткие аудиофайлы, предназначенные для проигрывания при определённом действии, а музыка - длинный аудиофайл, например, фоновый трек, играющий всё время.
Camera и SpriteBatch
Камера в LibGDX отображает видимую ею область (Viewport) на весь игровой экран. Даже если мы сделаем обзор камеры квадратом в один единственный пиксель, то этот пиксель будет растянут на весь экран.
SpriteBatch отвечает за рисование картинок и связан с OpenGL. OpenGL штука ленивая, вследствие чего картинки стоит загружать не по одной, а одним файлом, который потом уже самостоятельно в коде разбивать на регионы и распихивать по переменным. В нашем случае этого делать не обязательно, у нас всего-то 2 изображения.
Создаём переменные для камеры и батча в блоке переменных:
private OrthographicCamera camera; private SpriteBatch batch;
И сразу же инициализируем их в методе create():
public void create() { ... camera = new OrthographicCamera(); camera.setToOrtho(false, 800, 480); batch = new SpriteBatch(); ... }
Создание ведра
Что из себя представляет ведро (да и капля тоже)? В нашем случае это прямоугольник, который имеет координаты (х,у), ширину/высоту и, разумеется, картинку, которую мы уже загрузили и готовы использовать.
Создаём переменную для нашего единственного и неповторимого ведёрка:
private Rectangle bucket;
и инициализируем его:
public void create() { ... bucket = new Rectangle(); bucket.x = 800 / 2 - 64 / 2; bucket.y = 20; bucket.width = 64; bucket.height = 64; ... }
Надеюсь, вы знаете что такое конструктор и внутренние переменные объекта.
Заметьте одну важную особенность LibGDX - ось Y идёт снизу вверх, т.е. начало координат находится в левом нижнем углу. // Можно, конечно, включить для камеры стандартную систему отсчета, но сейчас не об этом.
Рисование ведра
Мы создали ведро и загрузили для него картинку, теперь его нужно вывести на экран. Сперва очистим экран и обновим состояние камеры:
public void render() { Gdx.gl.glClearColor(0, 0, 0.2f, 1); // Устанавливает цвет в формате RGBA (от 0 до 1) Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); // Очищает экран выбранным цветом camera.update(); // Обновляем обзор камеры // Теперь нарисуем само ведро. Для этого используется созданный ранее SpriteBatch. batch.setProjectionMatrix(camera.combined); // Связывает батч с камерой. Теперь то, что рисует батч, видно только этой камере. batch.begin(); batch.draw(bucketImage, bucket.x, bucket.y); // Рисуем ведро в соответсвующих координатах. batch.end(); ... }
Прицнип работы батча таков, что он накапливает действия после метода begin(), а после вызова end() OpenGL отрисовывает сразу всё, а не по отдельности. Это даёт выигрыш в скорости отрисовки, позволяя нарисовать много спрайтов при большом фпс.
Примечание: во многих официальных и не очень примерах и статьях, написанных давно, используется класс GL10, который в текущей версии LibGDX вообще отсутствует. А вместе с ним и неюзабельны некоторые методы других классов. Будьте внимательнее с кодом.
Перемещение ведра
Для простоты эксперимента пусть наше ведро мгновенно перемещается туда, где мы кликнем мышью (или прикоснёмся пальцем).
public void 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. Передвижение курсора, нажатие клавиш, использование джостика - за всем этим следит input.
Точку создаём именно трёхмерную в виду того, что камера у нас тоже трёхмерная. Не заморачивайтесь над этим, главное, что мы показываем игроку двумерное пространство, а трётью ось не трогаем и координаты по ней всегда будут равны 0.
Что значит "Переводим точку на плоскость камеры"? У каждого устройства своя архитектура не только в плане электроники, но и железа. Пиксели на экранах устройств тоже иногда отличаются: где-то они квадратные, где-то прямоугольные, где-то шире, где-то уже. Чтобы на всех устройствах приложение работало одинаково, у камеры есть своя разметка пространства, в которую и переводится полученная с экрана точка.
Примечание: плодить локальные переменные в методе, который отрабатывает десятки раз в секунду крайне нехорошо, поэтому вот вам первое "домашнее задание" - перенесите объявление точки в блок переменных и инициализируйте её в методе create().
Как ловить мышку - мы узнали, теперь узнаем, как ловить клавиатуру.
public void render() { ... 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; // выйти за границы экрана ... }
Тут всё просто:
Если нажата "стрелочка влево" - двигаем ведро левее. Двигаем со скоростью 200 единиц в секунду. Поскольку у нас игра отрисовывается несколько раз в секунду, то каждый кадр мы перемещаем ведро не на 200 единиц, а на часть от 200. Метод getDeltaTime() возвращает время, прошедшее с прошлого выполнения метода. Аналогично двигаем вправо.
Создаём капли
В отличие от ведра, капля у нас будет не одна. Они будут падать, мы будем их ловить. Сперва объявим динамический массив для капель.
private Array<Rectangle> raindrops;
Необходимо заметить, что LibGDX имеет свои виды коллекций. Они аналогичны стандартным, но "легче и быстрее". Лежат они в пакете "com.badlogic.gdx.utils".
Также нам необходимо знать, когда было предыдущее падение капли.
private long lastDropTime;
Тип long используется по той причине, что будут сохраняться наносекунды.
Теперь нам нужно создать каплю и заставить её падать вниз. Для этих целей мы, пожалуй, выделим целый метод.
public class Drop implements ApplicationListener { public void create () { ... } public void render () { ... } ... 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(); // Засекаем время появления этой капли } }
Теперь мы можем инициализировать массив и создать первую каплю.
public void create () { ... raindrops = new Array<Rectangle>(); spawnRaindrop(); }
Теперь сделаем так, чтобы капли периодически появлялись.
public void render () { ... if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop(); ... }
И сразу же опишем процесс их падения:
public void render () { ... 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(); } }
Напоминаю, что итератор позволяет нам работать с объектами как с элементами динамического массива.
Данный цикл устанавливает положение каждой капли всё ниже и ниже, т.е. создаёт падение капель. И уничтожает их, когда капля касается нижней части экрана.
Но капли тоже нужно отрисовывать, иначе как же мы будем ловить их в ведро?
Помните, где мы рисовали ведро? Добавляем туда же отрисовку капель:
public void render() { ... // Для отрисовки объектов используется один и тот же 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(); ... }
И последнее, что нам осталось сделать - это отловить тот момент, когда капля попадает в ведро, т.е. их прямоугольники (вы ведь помните, что наши объекты имеют форму прямоугольника?) соприкосаются. У Rectangle для определения этого есть метод overlaps(Rectangle r).
public void render() { ... 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(); // И удаляем каплю } } }
Очистка памяти
Пользователь в любой момент может выключить игру и нам надо будет освободить всё, что мы взяли у операционной системы. Любой класс данной библиотеки, расширенный интерфейсом Disposable должен быть уничтожен методом dispose(). В нашем случае это текстуры и звуки.
public class Drop implements ApplicationListener { ... public void dispose() { dropImage.dispose(); bucketImage.dispose(); dropSound.dispose(); rainMusic.dispose(); batch.dispose(); } }
Всё потому, что такие объекты мало связаны с Java и зависят от системы, и сборщик мусора их просто не видит.
Вот и всё, можете немножко поиграться.
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.GL20; 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 { // Блок переменных. Вы ведь знаете, что это значит? Здесь создаём все "глобальные" переменные, к которым будем обращаться по ходу дела. private Texture dropImage; private Texture bucketImage; private Sound dropSound; private Music rainMusic; private OrthographicCamera camera; private SpriteBatch batch; private Rectangle bucket; private Array<Rectangle> raindrops; private 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(); // Создание камеры camera = new OrthographicCamera(); camera.setToOrtho(false, 800, 480); // и батча batch = new SpriteBatch(); bucket = new Rectangle(); bucket.x = 800 / 2 - 64 / 2; bucket.y = 20; bucket.width = 64; bucket.height = 64; raindrops = new Array<Rectangle>(); spawnRaindrop(); } @Override public void resize(int width, int height) { // TODO Auto-generated method stub } @Override public void render() { Gdx.gl.glClearColor(0, 0, 0.2f, 1); // Устанавливает цвет в формате RGBA (от 0 до 1) Gdx.gl.glClear(GL20.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 pause() { // TODO Auto-generated method stub } @Override public void resume() { // TODO Auto-generated method stub } @Override public void dispose() { dropImage.dispose(); bucketImage.dispose(); dropSound.dispose(); rainMusic.dispose(); batch.dispose(); } 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(); // Засекаем время появления этой капли } }
Смотрите также:
Комментарии
Здесь еще никто не оставил комментарий
CollectableItemData.cs
[CreateMenuItem(fileName = "newItem", menuName = "Data/Items/Collectable", order = 51]