Добавлен SomeFire,
опубликован
Примитивная игра на LibGDX
Содержание:
Предполагается, что вы уже знаете как создавать проекты для этой библиотеки. Перевод и сокращение оффициальной статьи.
В данной статье мы создадим примитивнейшую игру и познакомимся с азами библиотеки на простом и понятном примере. Ну, я надеюсь. С чем конкретно мы столкнёмся:
- Доступ к файлам (любой игре нужны картинки, звук и прочие прелести, которые не хранятся в коде)
- Выводить картинки на экран (базовая составляющая любой нетекстовой игры)
- Познакомимся с игровой камерой (хотя пример несколько неудачный для этого)
- Сделаем управление (как-то же мы должны в игру играть)
- И добавим звуковых эффектов
При создании проекта были использованы следующие данные:
- Application name: drop
- Package name: com.badlogic.drop
- Game class: Drop
Ок, проект создали. Переходим к игре, точнее, к её сути: сверху капают капли, а по нижней части экрана игрок перемещает ведро и ловит эти капли. Вроде всё просто.
Соответственно, для игры нам потребуются изображения ведра и капли. А заодно фоновая музыка и звук капли, упавшей в ведро.
Скачали, что делать дальше? Заходим в папку с проектом. Нам нужна папка assets. Если проект расчитан на андроид, то эта папка находится в подпроекте "android". Если же вы плюнули на андроид, то эта папка лежит в подпроекте "core". Нашли? Теперь кидаем картинки со звуками в папку assets.
Почему ресурсы хранятся именно в паке assets? Потому что в андроид-приложениях основные ресурсы хранятся в этой папке. Все остальные проекты (десктопы, иос...) получают ссылку на эту папку и обращаются к ней, т.е. игра создаётся мультиплатформенная, но ресурсы хранятся для всех платформ в одном месте.)Drop\android\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. Это корневая папка для внутренних ресурсов игры, поэтому для метода
в качестве аргумента идёт относительный путь к файлу, т.е. только само название. Если бы мы положили картинки в папку "images", а звуки - в "sounds", то в качестве аргумента брали "images\bucket.png" и "sounds\rain.mp3".Gdx.files.internal("bucket.png")
Разница между "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() возвращает время, прошедшее с прошлого выполнения метода. Аналогично двигаем вправо.
Если нажата "стрелочка влево" - двигаем ведро левее. Двигаем со скоростью 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 и зависят от системы, и сборщик мусора их просто не видит.
Вот и всё, можете немножко поиграться.
Весь код файла Drop.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(); // Засекаем время появления этой капли
}
}
Содержание
Комментарии пока отсутcтвуют.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.