[Перевод] [LibGDX] Создаем клон Flappy Bird — Zombie Bird

0d8a7f6e45cf481bac6c2a057877916b.png Доброго времени суток %username%. Не давно наткнулся на хороший туториал по созданию клона игры Flappy Bird используя LibGDX и этот туториал мне понравился своей простотой и детализацией.Я отдаю себе отчет, что тема создания клонов данной игрушки изъела себя, но возможно кому-то пригодится еще один хороший туториал.

Туториал разбит на 12 дней, содержит множество картинок, полотен кода и исходный код разбит по дням. Кому интересно, добро пожаловать под кат.

День 1 — Flappy Bird — Глубокий анализ День 2 — Подготавливаем и настраиваем libGDX День 3 — Разбираемся с чем едят libGDX День 4 — GameWorld, GameRenderer и Orthographic камера День 5 — Полет мертвеца — Добавляем птицу День 6 — Добавляем графические элементы — Добро пожаловать в Некрополис День 7 — Трава, Птица и Труба с черепом День 8 — Обнаружение коллизий и звуковые эффекты День 9 — Завершаем Игровой процесс и базовый UI День 10 — GameStates и Лучший результат День 11 — Добавляем поддержку iOS/Android + SplashScreen, Меню и Tweening День 12 — Конечный вариант UI и Исходный код Чтобы скопировать игру, мы должны отлично понимать ее логику, поведение. В этой секции мы разберем различные игровые механики и процессы Flappy Bird, чтобы мы могли сэмулировать игровой процесс достаточно точно.Я собираюсь определить и расписать каждый элемент игрового процесса. Конечно, это все довольно приблизительно и в целом я могу полностью ошибаться, но опять-таки, мы должны очень точно описать игровой процесс, чтобы наша эмуляция удалась. Если будут происходить какие-то серьезные изменения по ходу пъесы — я уведомлю о них.

Все об игровом процессе — GamePlay Чтобы повторить Flappy Bird или сделать даже лучше — нам стоит сфокусировать на геймплее. Два главных элемента геймплея, с которыми нам предстоит разобраться, это Птица и Трубы. Наша птица должна двигаться как Flappy Bird, а трубы должны генериться и двигаться как их зеленые «прародители».Птиц 4a2925ca64c148baa7b3c9c82a5d3f0f.png После быстрого анализа птицы, видно, что размеры оной 17 пикселей (ширина) х 12 пикселей (высота). Так же птичка использует всего 7 цветов и занимает всего 1/8 от ширины игрового экрана (на глаз ширина экрана примерно 135–136 пикселей). Так же птичка масштабируется, чтобы хорошо выглядеть на девайсах с различной шириной экрано. Еще у птицы есть три разные цветовые схемы, которые используется случайным образом.Физика птицы Сложновато было поэксперементировать с физикой в этой игре без множества смертей, но в итоге я выяснил следующее: Во время падения — птица ускоряется. Но есть ограничение — птица не может падать быстрее выставленного ограничителя. Если тыкнуть по экрану — птица подпрыгнет на одно и тоже значение по высоте, вне зависимости от скорости падения. Птица повернута в соответствующую сторону движения, т.е. падая — птица смотрит внизу, взлетая — вверх. Анимация (взмах крыльями) присутствует только когда птица летит вверх. Нашей главной целью будет создание всего, и вся как можно близким к оригинальной игре. Весь геймплей зависит в основном от физики.Обнаружение коллизий Какие условия смерти нашей птицы? Я без понятия как это было реализовао в ориганальной игре. Но из того что я вижу, проверка коллизий по пикселям — это наш вариант. Мы создадим «hit box» для нашей птицы, и будем использовать его для определения коллизий с трубами.Если сделать хит бокс слишком маленьким — игра будет очень легкой, а если большим, то люди будут злится из-за безосновательных смертей птички.

b7eb2dd2c22541238e4db1cca28466fd.png Я сделаю хитбокс с помощью Rectangle.

Трубы 48b66be68b514538ba154763703d019f.pngТрубы, наверное, самая сложная часть которую нужно сделать должным образом, очень важно чтобы мы сделали все правильно.Большая часть привлекательности этой игрушки — ее сложность. Если сложность нашего клона каким-то образом будет не такой же как в ориганальной игре, не правильно расчитанная скорость или непоследовательно сгенерированы трубы, у игрока будет отрицательные эмоции от игры. Не будет эффекта: разочарование-награда-зависимость.

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

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

Анимации Это невероятно простая игра. Статические элементы в ней — это задний фон и песок. Они никогда не меняются. Птица зафиксирована горизонтально, примерно ⅓ от ширины экрана. Трава (?) и трубы — это единственные элементы в игре которые необходимо скролить горизонтально, и они скролятся с одинаковой скоростью. Создать траву будет самым легким этапом, мы не будем обсуждать это тут.Проблема с разными размерами экрана 52c4f43458da4ca0832db733bca91fbe.pngНа моем устройстве, птица отцентрирована вертикально (обратите внимание на красную линию на картинке слева). Смотря на это, я предположил, что игра растягивается равномерно вверх и вниз, так что размер (или соотношение) игрового пространства остается одним и тем же.Я протестировал игру на iPhone с экраном 3,5 дюйма, я полагаю, что игра была изначально сделана под этот размер, и размер игровой зоны был такой же как на картинке слева. Так что, мы реализуем поддержку различных размеров экрана по следующим принципам:

Как стандарт для нашего приложения, мы будем использовать Retina iPhone с экраном 3,6 дюйма Весь игровой процесс будет происходить в прямоугольнике, полученном из расчета используемого экрана Размер Птицы — 17 пикселей (масштабируется пропорционально) Ширина игры ~135 пикселей, пропорционально масштабируется (на коэффициент 4,75х на iPhone) Высота игры будет изменятся взависимости от устройства, но высота игрового поля (где происходит весь игровой процесс) будет (960/640) * 135 = 203 пикселей. 4f355491bc81468cbcb05427325b49ca.pngК Оглавлению

0d8a7f6e45cf481bac6c2a057877916b.png В этой секции, мы настроим фреймворк libGDX, который в целом будет выполнять за нас кучу низко-уровненых задач, так что мы сможешь больше сфокусироваться на игровом процессе.Перед тем как продолжить, посмотрите на Zombie Bird слева, творчество отдела художников Kilobolt. Zombie Bird — главный персонаж нашей игры. Как всегда, установка/настройка — самая скучная часть любого руководства. Спасибо команде libGDX, этот процесс быстрый и легкий!

Установите Java, скачайте ADT Если у васне установлена Java и у вас нет Eclipse с Android Development Tools, заходим сюда и устанавливаем их (прим. пер.: в рамках данного перевода — статья по указанной ссылке не будет переведена).Скачиваем libGDX и создаем проекты LibGDX предоставляет кросс-платформенную разработку, так что мы пишем код единожды, а используем на множестве платформ. Это возможно благодаря архитектуре libGDX, у вас в наличии один главный Java проект, в котором вы пишите весь ваш первоклассный код (в частности используя разного рода интерфейсы).Чтобы настроить главный Java проект и вспомогательные проекты для каждоый из платформ — выполним действия по списку:

Намите сюда, чтобы скачать установку libGDX. Как скачается, нужно будет установить одним из следующих способов: На Mac — попробуйте дважду кликнуть на jar файл. На PC, скопируйте скачанный файл на рабочий стол и открой Терминал/Консоль. Наберите следующее: cd path_to_desktop java -jar gdx-setup.jar Как только вы это проделаете, появится следующее окошко: 17b1ba1a61e64f4abc88ef8da7942f00.png Введите информацию, указанную ниже (так она присутствует и на картинке выше), вы можете сменить путь к папке проекта на любой другой: Name: ZombieBirdPackage: com.kilobolt.zombiebirdGame class: ZBGameDestination: Ваш выбор. Запомните только этот путь.Android SDK: Расположение Android SDK. Если вы используете ADT Bundle (Android Developer Tools: Eclipse + Android SDK) то sdk расположено внутри adt-bundle папки.

Убедитесь, что проекты Desktop, Android, iOS и HTML выбраны, и отмените выбор всех Extensions (дополнительные классы с различной вспомогательной функкциональностью для libGDX).

Данная инсталяция автоматически создаст 5 Java проектов в папке, путь к которой вы указали в параметре Destination. Главный проект (core project) — это проект где мы будет писать весь код для нашей игры. Android, iOS и HTML проекты получат доступ к нашему главному проекту и выполнять его со специфической для каждой платформы имплементацией, это нужно, чтобы наша игра работала на всех платформах.

Мы сгенерим Eclipse проект, нажав на Advanced и выбрав Eclipse77fca9f9709943f5b923a805df52e9de.pngНа заметку: libGDX использует сборщик который называется Gradle. Этот сборщик автоматизирует сборку вашего проекта, управление .JAR зависимостями, а также упрощает совместную работу с другими людьми над проектом. Grandle — это отдельная большая тема, вам необходимо иметь опыт работы с такими сборщиками как Ant и Maven. Как-нибудь позже, мы возможно опубликуем статью о работе с Gradle, но не в рамках этой статьи. Как только вы будете готовы, со словами «Понеслось!», нажмите кнопку Generate Установщик скачает все необходимые файлы и сконфигурирует ваши проекты. Как только вы увидите следующее сообщение, вы можете закрыть Установщик.f874e744c875497c9d85dde657ff6759.png Теперь в папке, которую вы указали в настройках Установщика, появились 5 проектов, мы можем импортировать их в Eclipse. Откройте ваш Eclipse.0b7dde848fad4629846c63a07721d204.png Кликните правой кнопкой мышки в Package Explorer и выберите Import, как показано ниже.d7dfd75f8cac4e4bbd8ce06060a0765c.png Выберите General > Existing Projects into Workspaceef85c2d4ceb6481e812df85e5af20e41.png Кликните на Browse правее от «select root directory»: d86a847758e644e2a16d2fd69470d17e.png Перейдите в папку проекта (путь который указывали на шестом шаге) и нажмите Open.1dc71b9006564a699e18fe0fd456400c.png Выберите все 5 проектов и нажмите «Finish»8dab606b108d473c87cca0e7a317cc2d.png Ну все, мы импортировали наши проекты в Eclipse и теперь мы готовы начать писать код.Появились сообщения об ошибках? Если у вас Eclipse ругается на ошибки в ANDROID проекте, кликните правой кнопкой мышки на этом проекте, выберите Properties, кликниет Android и убедитесь, что у вас есть установленная версия Android. Если нет, кликните сюда и перейдите на шаг в котором написано: II. Installing the Bundle: Eclipse/Android SDK/Eclipse ADT Plugin, перед тем как продолжить с текущим уроком.

Чтобы убедиться, что у нас все верно настроено, откройте ZombieBird — desktop проект и перейдите в класс DesktopLauncher.java. Обновите его следующим образом: package com.kilobolt.zombiebird.desktop;

import com.badlogic.gdx.backends.lwjgl.LwjglApplication; import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; import com.kilobolt.zombiebird.ZBGame;

public class DesktopLauncher { public static void main (String[] arg) { LwjglApplicationConfiguration config = new LwjglApplicationConfiguration (); config.title = «Zombie Bird»; config.width = 480; config.height = 320; new LwjglApplication (new ZBGame (), config); } } Кликаем правой кнопкой мышки по desktop проекту, выбираем Run и выбираем класс DesktopLauncher, 8c5ee78404274c83a9bca51047f2908b.pngесли все верно, вы увидите следующее: 2bbb6908b55b44e1876f82cd57699dc9.png Если вы добрались до этого пункта — значит libGDX работает у вас корректно и мы можем продолжить дальше. 4f355491bc81468cbcb05427325b49ca.pngК Оглавлению

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

Но на всякий случай ознакомьтесь с лицензией тут (данный файлик присутствует в проекте): http://www.apache.org/licenses/LICENSE-2.0.html

Базовая структура (как мы будем проектировать, а так же создавать нашу игру) Давайте потратим не много времени и обсудим, как мы будем создавать нашу игру. Ниже приведена диаграмма, которая в целом отображает наш проект.66c58bce828f4f33904ade5fb7b914c0.png

Мы начнем работать с веткой ZBGame на диаграмме. Создадим Framework Helpers и Screen Classes (на диаграмме GameScreen).

GameScreen зависит от двух вспомогательных классов: World и Renderer. World будет взаимодействовать с Gameplay классами и по ходу пъесы будет создать объекты нашей игры.

Если все выше сказанное понятно — поехали дальше.

Внимание! Далее будет самая концептуально сложная часть во всем уроке.

Но… вы можете ее пропустить.

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

Если вас что-то озадачивает, пролистывайте смело далее к «Пишем код» части. Мы все равно сможем создать нашу Zombie Bird игру.

Чувствуете себя уверенным? Продолжайте читать. Нервничаете? Пролистайте далее.

Расширяем и имплементируем (Можно пропустить) Если вам необходимо освежить в памяти, что такое наследование, переходим сюда.Напомню, что Interface — это список requirements, название методов без имплементации. Interface перечисляет список всех методов, которые какой то класс должен реализовать (предоставить описание метода, тело метода), в случае если этот класс должен стать такого же типа как Interface. Java библиотека содержит интерфейс с именем List, который сам по себе не предоставляет ни какой функциональности. Интерфейс List — это файл, в котором перечислены методы, которые другой класс должен реализовать, с целью быть причисленным к категории List объектов.Например, среди всех методов интерфейса List есть следующие:

list.get (int index), который возвращает item с указанным index. list.add (), который добавляет item в конец List. list.isEmpty (), который возвращает true, если List пустой. Давайте создадим новый класс, который назовем ArrayList. Данный класс имплементирует интерфейс List, т.е. он должен реализовать все методы интерфейса List, такие как list.add () и list.get (int index).

После того как мы добавим реализацию методов интерфейса List в класс ArrayList, наш класс может образаться к себе как-будто бы он класс List, как показано ниже:

List strings = new ArrayList(); Заметьте, что переменная strings типа List, была созданна как ArrayList. Т.е. данная переменная может вести себя как List или как ArrayList взависимости от ваших нужд.Так как мы знаем, что strings — это реализация интерфейса List, мы можем быть уверены, что strings содержит все методы данного интерфейса. Благодаря этому, если нам когда-то понадобится передать в какой-то метод объект типа List, мы смело сможем передать наш объект типа ArrayList с именем strings (полиморфизм).

public void printLastWordFrom (List someList) { if (someList.isEmpty ()) { System.out.println («Your list is empty.»); return; }

String lastWord = someList.get (someList.size () — 1)); System.out.println («Your last word is» + lastWord); } Эти принципы необходимо знать и понимать, так как далее мы будем использовать их в процессе разработки.Соглашения используемые в данном туториале (Важно прочитать!) В уроке будет несколько раз встречаться упоминание встроенных классов в бибилиотеку libGDX, к примеру класс Game приведенный ниже. Данные классы встроенны в библиотеку и вам НЕ НАДО писаь их самостоятельно. Просто ссылайтесь на класс который я упомяну. Все эти классы под лицензией Apache 2.0, все авторы перечислены тут: https://github.com/libgdx/libgdx/blob/master/gdx/AUTHORS.Для встроенных классов я буду в заголовке кода, в комментарии писать Built-in.

Просмотрите класс Game ниже, не надо его копировать или перенабирать, просто вскольз просмотрите его.

Game Class //Built-in

public abstract class Game implements ApplicationListener { private Screen screen;

@Override public void dispose () { if (screen!= null) screen.hide (); }

@Override public void pause () { if (screen!= null) screen.pause (); }

@Override public void resume () { if (screen!= null) screen.resume (); }

@Override public void render () { if (screen!= null) screen.render (Gdx.graphics.getDeltaTime ()); }

@Override public void resize (int width, int height) { if (screen!= null) screen.resize (width, height); }

/** Sets the current screen. {@link Screen#hide ()} is called on any old screen, and {@link Screen#show ()} is called on the new * screen, if any. * @param screen may be {@code null} */ public void setScreen (Screen screen) { if (this.screen!= null) this.screen.hide (); this.screen = screen; if (this.screen!= null) { this.screen.show (); this.screen.resize (Gdx.graphics.getWidth (), Gdx.graphics.getHeight ()); } }

/** @return the currently active {@link Screen}. */ public Screen getScreen () { return screen; } } Исследуем класс Game (Можно пропустить) Класс Game — это реализация интерфейса ApplicationListener, данный класс будет интерфейсом между нашим кодом и платформо-зависимым приложением, которое будет запускаться непосредственно на устройстве.Например, когда Android запускает наш app, он будет проверит на наличие ApplicationListener. Мы со своей стороны может предоставить Game объект который реализует необходимый интерфейс.

Есть только маленькая особенность. Заметьте, что класс Game — абстрактный. Это значит, что класс Game реализует далеко не все методы из интерфейса ApplicationListener и нам придется сами это сделать.

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

Наследование куда проще вещь, чем реализация интерфейсов. Мы просто берем абстрактный класс Game и создаем саб-класс, который наследует все публичные методы и переменные из класса Game, как будто бы они часть нашего саб-класса. И далее мы можем добавить наши собственные методы в наш саб-класс.

Давайте создадим наш класс.

Пишем код! (Наконецто!) 77acc96dbdd9447387baebedf2268ab0.png Откройте ZBGame.java который мы создали во время второго дня. Удалите все методы и все переменные внутри класса. Ваш код теперь должен выглядеть так: package com.kilobolt.zombiebird;

public class ZBGame {

} Расширим класс Game Мы собираемся расширить базовый класс Game, который будет мостом между нашим кодом и плафтормо-независимым кодом (на iOS, Android и т.д.).Добавьте extends Game Добавьте следующий импорт: import com.badlogic.gdx.Game; Импорт — означает следующее: «Эй, Компилятор, вот тебе полный адрес к классу Game на который я ссылаюсь». Необходимо это сделать, потому как может присутствовать множество классов с именем Game, и мы хотим указать какой именно класс с именем Game использовать.

package com.kilobolt.ZombieBird; import com.badlogic.gdx.Game;

public class ZBGame extends Game {

} Eclipse выдаст следующее предупреждение: 58b76c4a75a64d76af6f9eff870ae1fc.pngЭто означает, что для того, чтобы наш класс ZBGame смог стать классом Game, существует требование: наш класс должен реализовать метод create (). Просто кликните на «Add unimplemented methods,» и этот методо автоматически добавится в наш класс. Давайте добавим строчку кода в наш новый метод:

(На заметку, мы будем использовать Gdx.app.log вместо System.out.println (). Метод Gdx.app.log используется для вывода значений в консоль, и данный метод реализован под каждую платформу по-своему (на Android, этот метод будет использовать Log класс. На Java, он использует System.out.println (). В роли параметров для этого метода могут быть имя класса и тело сообщения)).

package com.kilobolt.zombiebird;

import com.badlogic.gdx.Game; import com.badlogic.gdx.Gdx;

public class ZBGame extends Game { @Override public void create () { Gdx.app.log («ZBGame», «created»); } } Давайте не много сбавим обороты на пару минут… Для чего нам надо, чтобы наш класс ZBGame был объектом типа Game? Причина #1: Как я ранее упоминал, libGDX прячет от нас реализацию платформо-зависимого кода. Весь код, который мы должны были бы написать для iOS/Android/HTML/Windows/Mac уже написан за нас. Как разработчики игры, мы должны позаботится о нашей бизнес логике, и мы делаем это создав ApplicationInterface.

Расширяя класс Game (саб-класс ApplicationInterface), ZBGame становится интерфейсом между нашим кодом и платформой на которой будет работать наше приложение. Теперь весь код за сценой для Android, iOS, HTML и т.д. может общаться с нашим классом ZBGame и творить чудеса вместе.

Причина #2: В дополнение к выше сказанному, ZBGame получает доступ ко всем полезным методам из класса Game (пролистайте выше, если запамятовали).В общем то, это относится к первой причине. Эти методы будут дергаться кроссплатформенным кодом.

Теперь, когда мы запустим наше приложение на одной из платформ, кроссплатформенный код запустит метод create (), и «created.» выведется в консоли.

Давайте разберемся, что все это значит.Мы собираемся создать наш первый Screen (который в дальнейшем станет нашим GameScreen из диаграммы) и используем его в ZBGame.

Саздание GameScreen Кликните правой кнопкой мышки на папке src внутри главного (CORE) проекта ZombieBird и создайте новый Java пакет с именем com.kilobolt.screens.Внутри него, создайте новый класс и импортируйте Screen класс:

package com.kilobolt.screens;

import com.badlogic.gdx.Screen;

public class GameScreen implements Screen {

} Мы дожлны реализовать методы из интерфейса Screen. Вы можете использовать авто-генерацию (как мы уже делали в ZBGame) нажав на «Add unimplemented methods,» или добавить методы как я это сделал ниже. Добавьте в каждый метод Gdx.app.log (): GameScreen package com.kilobolt.screens;

import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.GL20;

public class GameScreen implements Screen { public GameScreen () { Gdx.app.log («GameScreen», «Attached»); }

@Override public void render (float delta) { // Sets a Color to Fill the Screen with (RGB = 10, 15, 230), Opacity of 1 (100%) Gdx.gl.glClearColor (10/255.0f, 15/255.0f, 230/255.0f, 1f); // Fills the screen with the selected color Gdx.gl.glClear (GL20.GL_COLOR_BUFFER_BIT); }

@Override public void resize (int width, int height) { Gdx.app.log («GameScreen», «resizing»); }

@Override public void show () { Gdx.app.log («GameScreen», «show called»); }

@Override public void hide () { Gdx.app.log («GameScreen», «hide called»); }

@Override public void pause () { Gdx.app.log («GameScreen», «pause called»); }

@Override public void resume () { Gdx.app.log («GameScreen», «resume called»); }

@Override public void dispose () { // Leave blank }

} Добавим использование GameScreen в наш ZBGame класс Сделаем наш текущий screen в классе ZBGame объектом класса GameScreen, который мы только, что создали. Для этого вернемся в файл ZBGame.java.Добавим следующее в метод create (): setScreen (new GameScreen ()); На заметку: Метод setScreen () доступен благодаря наследованию! Импортируем класс GameScreen: import com.kilobolt.screens.GameScreen; package com.kilobolt.zombiebird;

import com.badlogic.gdx.Game; import com.badlogic.gdx.Gdx; import com.kilobolt.screens.GameScreen;

public class ZBGame extends Game { @Override public void create () { Gdx.app.log («ZBGame», «created»); setScreen (new GameScreen ()); } } Теперь мы можем запустить нашу игру (для этого, как всегда, перейдем в проект ZombieBird — desktop и выполним DesktopLauncher). Вы увидите красивое синее окно.

Гляньте, что у нас вывелось в консоль: 9e42f8401a8d4e608625810171c21f71.png

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

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

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

Если вы готовы, поехали дальше. В следующей части, мы начнем создавать игровой процесс.

Исходный код за день Если вы вне настроения писать код самостоятельно, скачайте его отсюда: e273ed1a4ed342abb148c53ad824d8f3.pngzombiebird_day_3.zip4f355491bc81468cbcb05427325b49ca.pngК Оглавлению

Добро пожаловать в Четвертый День! В данной секции, мы создадим два вспомогательных класса для нашего GameScreen, так, что мы сможем приступить к созданию игрового процесса. После мы добавим orthographic камеру и несколько фигур в нашу игру! Быстрая напоминалка У нас пять Java проектов, которые мы сгенерировали используя libGDX установщик. Но в целом мы будем использовать только три из них во время создания нашей игры: Если я прошу вас открыть класс или создать новый пакет, сделайте это в рамках проекта ZombieBird. Если я прошу запустить проект, вы откроете ZombieBird-desktop проект и выполните класс DesktopLauncher Если нам надо добавить картинки или звуки, мы добавим их в ZombieBird-android проект в папку assets. Все остальные проекты получат копию содержимого этой папки. Исследуем класс GameScreen Запустите Eclipse и откройте класс GameScreen. В третьем дне, мы обсуждали, как и когда каждый из методов этого класса запускается. Давайте внесем мелкие изменения в этот класс. Посмотрите на метод render (). У него один аргумент delta, тип float. Чтобы понять зачем он нужен, добавим в метод следующую строчку: Gdx.app.log («GameScreen FPS», (1 / delta) + » »);: @Override public void render (float delta) { // установим цвет бэкграцнда нашего экрана (RGB = 10, 15, 230), с прозрачностью 1 (100%) Gdx.gl.glClearColor (10/255.0f, 15/255.0f, 230/255.0f, 1f);

// заполним экран указанным цветом Gdx.gl.glClear (GL20.GL_COLOR_BUFFER_BIT);

// выведем в консоль количество кадров в секунду Gdx.app.log («GameScreen FPS», (1/delta) + »); } Попробуйте запустить игру (DesktopLauncher.java внутри вашего desktop проекта). Вы увидите следующее: f280fe99ad1845568a91de83b8ddb23a.png Float delta это количество секунд (обычно очень маленькое значение) которое прошло после последнего запуска метода render. Когда я попросил вас вывести в консоль значение 1/delta, это означало вывести сколько раз метод render будет вызван в течении одной секунды. Это значение равнозначно нашему FPS.

Такс, думаю теперь ясно, что наш метод render можно расценивать как наш игровой цикл. А в игровом цикле, мы будем делать две вещи:

Во-первых, мы будем обновлять все наши игровые объекты. Во-вторых, мы будет отрисовывать эти объекты.

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

GameScreen должен делать одну вещь, так что… Обновление игровых объектов должно лежать на плечах вспомогательного класса. Отрисовка игровых объектов должно входить в обязанности другого вспомогательного класса. Хорошо! Нам нужны два вспомогательных класса. Мы дадим им наглядные имена: GameWorld и GameRenderer.Создайте новый пакет с именем com.kilobolt.gameworld и в нем создайте эти два класса. Оставим на время их пустыми:

GameWorld.java package com.kilobolt.gameworld;

public class GameWorld {

} GameRenderer.java package com.kilobolt.gameworld;

public class GameRenderer {

} В нашем GameScreen, мы делегируем обновление и отрисовку нашим классам GameWorld и GameRenderer, соответственно. Чтобы это провернуть, сделаем следующее:

Во время создания GameScreen, мы должны создать два новых объекта типа GameWorld и GameRenderer. Внутри render метода класса GameScreen, мы должны запросить выполнить обновление и отрисовку у классов GameWorld и GameRenderer соответственно. Я попрошу вас сейчас сделать это самостоятельно, если вы застрянете на этом — пролистайте ниже.

1. Создание GameWorld и GameRenderer Откройте GameScreen. Мы создадим объекты GameWorld и GameRenderer в конструкторе класса. Их методы мы будем вызывать в методе render (). Чтобы это сделать: Нам нужны две переменные для экземляров объектов (эти переменные должны быть доступны где угодно внутри нашего класса). Объявите следующее в заголовке нашего класса: private GameWorld world; private GameRenderer renderer; GameScreen создан в конструкторе. Добавьте следующие строчки внутрь нашего конструктора, чтобы инициализировать наши переменные: world = new GameWorld (); // initialize world renderer = new GameRenderer (); // initialize renderer Добавьте необходимые иморты: import com.kilobolt.gameworld.GameRenderer; import com.kilobolt.gameworld.GameWorld; 2. Запросим GameWorld обновиться и GameRenderer отрисовать Вся суть наличия классов GameWorld и GameRenderer в том, что GameScreen не должен делать обновления и отрисовку самолично. Он может запросить наши вспомогательные классы сделать это за него.Замените весь код в методе render на следующий:

// Мы передаем delta в update метод, для того, чтобы мы могли сделать фреймо-зависимые вычисления world.update (delta); // GameWorld updates renderer.render (); // GameRenderer renders Ваш GameScreen должен теперь выглядеть так:

GameScreen.java package com.kilobolt.screens;

import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.GL20; import com.kilobolt.gameworld.GameRenderer; import com.kilobolt.gameworld.GameWorld;

public class GameScreen implements Screen { private GameWorld world; private GameRenderer renderer; public GameScreen () { Gdx.app.log («GameScreen», «Attached»); world = new GameWorld (); renderer = new GameRenderer (); }

@Override public void render (float delta) { world.update (delta); renderer.render (); }

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

@Override public void show () { Gdx.app.log («GameScreen», «show called»); }

@Override public void hide () { Gdx.app.log («GameScreen», «hide called»); }

@Override public void pause () { Gdx.app.log («GameScreen», «pause called»); }

@Override public void resume () { Gdx.app.log («GameScreen», «resume called»); }

@Override public void dispose () { // Оставьте пустым }

} Eclipse ругнется что у вас не объявлены методы update в GameWorld и render в GameRenderer. Давайте сделаем это: GameWorld package com.kilobolt.gameworld;

import com.badlogic.gdx.Gdx;

public class GameWorld { public void update (float delta) { Gdx.app.log («GameWorld», «update»); } } GameRenderer package com.kilobolt.gameworld;

import com.badlogic.gdx.Gdx;

public class GameRenderer {

public void render () { Gdx.app.log («GameRenderer», «render»); } } Попробуйте запустить игру (DesktopLauncher класс в desktop проекте).Внимание: ваша игра может мерцать (мы ведь ничего не рисуем).

В консоли мы увидим следующее: e4b7f5818c1748e1a2cbf8eb0924c35e.png

Великолепно. Подведем итоги, что мы сделали: мы делегировали два задания (обновление и отрисовку игры), так что наш GameScreen не должен беспокоиться об этом. Давайте глянем опять на нашу диаграмму (вы видите, где мы сейчас?):

0fa0f767c2b94d0dbd15bfb821c96adc.png Нам необходимо внести мелкие изменения. Наш GameRenderer должен иметь доступ к GameWorld, который он будет отрисовывать. Чтобы это сделать, спросим себя, «Кто имеет доступ к обоим: GameRenderer и GameWorld?». Если посмотреть на диаграмму, то видно, что это GameScreen. Давайте его откроем и внесем следующие изменения в его констуктор:

// Это конструктор, не объявление класса public GameScreen () { Gdx.app.log («GameScreen», «Attached»); world = new GameWorld (); renderer = new GameRenderer (world); } Упс, Eclipse ругается на не верное использование конструктора класса GameRenderer. Давайте его изменим.67912e24bcb444919d45fc5829a3e9fe.png

Откройте GameRenderer класс. Нам необходимо сохранить world как переменную внутри нашего GameRenderer класса, так что в будущем, когда нам понадобится объект из GameWorld мы сможем использовать переменную world.

Создайте переменную: private GameWorld myWorld; Внутри конструктора, добавьте новый аргумент и его значение присвойте нашей переменной myWorld: package com.kilobolt.gameworld;

import com.badlogic.gdx.Gdx;

public class GameRenderer {

private GameWorld myWorld; public GameRenderer (GameWorld world) { myWorld = world; } public void render () { Gdx.app.log («GameRenderer», «render»); } } Отвлечемся от написания код на минутку Потратьте не много времени, чтобы понять, что мы только, что сделали. Просмотрите ваш код и убедитесь, что вы видите 3-х сторонюю взаимосвязь между тремя классами, с которыми работали. Я надеюсь, вы поняли роли классов GameScreen, GameWorld и GameRenderer и как они вместе работают.Готовы продолжить? Мы сделаем еще кое, что в этом уроке, чтобы показать вам как мы можем создавать GameObjects и как их реализовать. Но прежде всего мы поговорим об Orthographic Camera.Orthographic Camera libGDX — это фреймворк для создания 3D игр. Но, наша игра будет в 2D. Что это все значит для нас? В целом ничего, потому что мы сможем задействать одну вещь, которая называется orthographic camera.Множество 2D игр, которые вы могли видеть, в реальности сделаны в 3D. Множество современных платформеров (даже те которые используют pixel art) отрисованы с помозью 3D движка, на котором разработчики создают сцены больше в 3D пространстве, чем в 2D.

На пример, посмотрите на Mario сделанную ее фанатом, в этой игре весь мир был построен используя 3D модели.e991ea9951e14eccaf60b27cb3e421dd.jpg

Играясь с Mario 2.5D выше, становится ясно, что игра в 3D. У персонажей есть «глубина».

Чтобы сделать эту игру в 2D, мы, возможно, должны повернуть камеру таким образом, чтобы мы смотрели на игру с лицевой стороны. Верьте мне или нет, но игра все равно останется в 3D. Попробуйте поиграться сами.d3f3a67f2bf8432688095504f50a2386.png

Почему так? Потому что в 3D окружении (осмотритесь вокруг), объекты которые отдалены выглядят маленькими для наблюдателя. Не смотря на это, в Mario мы смотрим с перпендикулярного угла, некоторые объекты в этом 3D мире, к примеру кирпичи/блоки, будут меньше чем те блоки, что ближе к нам (к камере).

Вот как раз в таких случаях и появляется на сцене orthographic camera. Когда мы используем ортографическую проекцию, все объекты на сцене, не зависимо от их отдаленности, спроекцированы под одну планку. Представьте себе большое полотно которым накрыли все объекты на сцене, и это объекты от соприкосновения с полотном станут плоскими, с зафиксированным размером изображения. Это то, что предоставляет для нас orthographic camera, и это то как мы можем создавать 2D игру в 3D пространстве.

А вот как игра выглядела, если бы использовали orthographic camera: a9fea445aed74cd0864f78e21fef41cf.pngИспользуя orthographic camera, мы можем проецировать 3D в единую для просмотра плоскость.

Я надеюсь не испугал вас этими разговорами про 3D пространство и проекцию камеры. Вы все поймете, когда мы будем писать код, на самом деле все очень просто. Так что давайте добавим камеру в нашу игру.

Давайте добавим еще одно изменение в наш DesktopLauncher.java в Desktop Project (который мы используем, чтобы запускать игру). Мы изменим разрешение экрана:

package com.kilobolt.zombiebird.desktop;

import com.badlogic.gdx.backends.lwjgl.LwjglApplication; import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; import com.kilobolt.zombiebird.ZBGame;

public class DesktopLauncher { public static void main (String[] arg) { LwjglApplicationConfiguration config = new LwjglApplicationConfiguration (); config.title = «Zombie Bird»; config.width = 272; config.height = 408; new LwjglApplication (new ZBGame (), config); } } Создание нашей Камеры Откройте наш класс GameRenderer. В нем мы создадим новый объект Orthographic Camera.Объявите переменную в классе: private OrthographicCamera cam; Добавьте импорт: import com.badlogic.gdx.graphics.OrthographicCamera; Создайте экземляр объекта внутри конструктора: cam = new OrthographicCamera (); cam.setToOrtho (true, 136, 204); Три аргумента означают следующее:

Хотим ли использовать Orthographic проекцию (мы хотим) Какая должна быть ширина Какой должна быть высота Это размер на

© Habrahabr.ru