Как набить кучу шишек и выпустить игру
ИДЕЯ ИГРЫ
Из существующих на тот момент игр нам понравилась игра, где катается шарик по лабиринту, уворачиваясь от дырок, и пытаясь добраться до финальной лузы. На тот момент таких игр уже было несколько штук, но по части графики они были не очень. Поэтому мы решили сделать свою, с упором на красочность. Хочется отметить художника, рисовал он очень и очень классно. Благодаря ему игра получилась такой как есть — красивой. Имя игре дали простое — Labirinth.
В качестве движка было решено использовать libGDX. Я хорошо знал его на тот момент, и он мне нравился, уже были завершенные проекты на нем. Я был уверен, что мы с ним дойдем до победного конца. Как оказалось — дошли.
ОСОБЕННОСТИ ГЕЙМПЛЕЯ
В принципе, геймплей получился стандартным для игр такого типа — катаем шарик с помощью акселерометра. Дополнительная фишка — у нас есть телепорты, а стены могут быть не просто скучными прямоугольниками. Но об этом дальше.
Из особенностей хочется отметить управление. Мы долго подбирали настройки акселерометра для комфортной игры, но наш тестер постоянно отмечал, что что-то не так. Как образец он приводил похожую игру с Android Market. В конце-концов, я скачал эту игру, декомпилировал (еще удивился, что она была не обфусцирована), и посмотрел момент с управлением. Я обнаружил, что код идентичный моему, отличаются лишь коэффициенты. Отмечу, что декомпилированная игра была тоже написана на libGDX — то есть, в то время на нем уже писали массовые игры.
У нас можно настроить управление
ТЕХНИЧЕСКАЯ ЧАСТЬ
Как я уже упомянул, игра написана на libGDX. В игре используется множество ресурсов — графика, звуки, карты, шрифты. Пройдемся по каждому моменту.
Графика. В игре есть несколько сотен спрайтов. С целью производительности, все спрайты были упакованы в текстурные атласы. Отдельный атлас для экрана меню, отдельный — для выбора уровней и т.д. Стандартная техника, которая экономит и видеопамять, и вытягивает производительность. Атласы упаковывались программой Texture Packer (доступная для скачивания на сайте libGDX). Если бы я делал это сейчас — я бы использовал встроенный класс TexturePacker. Упаковка атласа выглядит примерно так — TexturePacker.pack («folder-with-sprite», «output-folder», «atlas-name»). Преимущество такого подхода — не нужно переключаться из IDE на другую программу, чтобы перепаковать атласы.
Вот так выглядит уменьшенный атлас с разными шариками. Это малая часть, у нас несколько таких атласов.
Формат атласов — 32 бита, png. Некоторые картинки, что без прозрачности, упакованы в jpeg атласы. Если бы я делал сейчас — атласы без прозрачности я бы паковал в ETC1. У libGDX есть родная поддержка этого формата, а для Android это экономия видеопамяти.
Для управления графикой (загрузка\выгрузка атласов, получение нужной картинки) был написан свой самопальный класс Images. Я знал про встроенный AssetManager, но на тот момент мне нужно было более удобное решение. Например, если я хочу получить какую-то фоновую картинку, я хочу, чтобы предыдущий запрошенный фон был автоматически выгружен. Мое решение позволяло делать такие вещи.
Отходя немного в сторону — когда я пишу игру, я часто делаю свои менеджеры ресурсов. Часто на базе стандартного AssetManager. Цель — более простой доступ к ресурсам. На более поздних стадиях разработки читаемость кода играет очень важную роль. А хорошо написанный код, как книга. Проще понять строчку images.createButton («green-button»), чем assetManager.get (TextureAtlas.class, «data/atlases/buttons»).findRegion («green-button»). Из минусов такого подхода — когда возвращаешься к игре через длительное время, нужно вспомнить особенности этих менеджеров. Из плюсов — когда вспомнишь, дальше править что-то уже просто.
Звуки. Все звуки в игре в формате .ogg. Почему не mp3? На некоторых моделях телефонов на тот момент были проблемы с воспроизведением нескольких звуков подряд в формате .mp3. Я допускаю, что это были мои кривые руки, но с .ogg проблем не возникло. Благо libGDX поддерживает оба формата, замена .mp3 на .ogg была несложной.
С целью оптимизации звуки были пожаты в минимальный битрейт, формат — моно. Разработка велась на linux, для конвертации использовал Sound Converter. На мой взгляд, Sound Converter — это образец отличной утилиты, которой я пользуюсь до сих пор. Одно маленькое окошко с настройками для выходного формата, и кнопка «Convert». Отличный ответ монструозным конвертерам.
Шрифты. В игре используются два типа шрифтов.
Первый тип — это .ttf шрифты и библиотека gdx-freetype. Она позволяет во время исполнения генерировать из .ttf шрифтов растровые картинки (bitmap fonts). Преимущество библиотеки — можно задать нужный размер шрифта прямо во время запуска программы. Это гарантирует, что шрифт всегда будет именно того размера, что нужно. Из недостатков — поскольку шрифт генерируется «на лету» в текстуру, это значит, что это будет есть оперативную память. И чем больше размер шрифта, тем больше ее нужно, этой самой памяти. Еще один неочевидный момент — поскольку шрифт — это отдельная текстура, то при отрисовке любой надписи будет дополнительный draw call (за исключением момента, когда мы рисуем несколько надписей подряд). Исходя из этого, область применения .ttf была ограничена лишь экраном «About».
Второй тип шрифтов — это distance field шрифты. Идея простая — с помощью программы Hiero (доступна для загрузки на официальном сайте libGDX) генерируется специальная текстура для шрифта. В текстуре для каждой буквы выделена своя область. Но эта область не отрисовывается «как есть». Вместо этого применяется специальный шейдер, который особым образом рисует этот регион (конечно, это тоже ломает батчинг). Преимущество — с относительно маленькой текстуры можно отрисовывать шрифт очень большим размером без потери качества. Недостатки — шрифт нужно заранее сгенерировать, и для шрифта недоступны эффекты (тень, обводка, и т.д.). Также, как я упомянул, это ломает батчинг. Можно лишь во время отрисовки устанавливать цвет шрифта. Для наших потребностей этого хватало, поэтому в игре повсеместно используется именно Distance Field шрифты.
Вот так выглядит Distance Field шрифт. Фон должен быть прозрачным, я лишь для наглядности сделал его черным.
Карты. Одним из «фишек» хорошей игры является количество контента. Можно генерировать его автоматически, а можно подготовить наборы уровней. Мы пошли вторым путем — у нас больше сотни уровней. Уровни разбиты на коробки — в коробке 15 уровней. Понятно, что с таким количеством уровней возникает вопрос их удобного редактирования и хранения. Нужен какой-то редактор карт. Дело усложняется тем, что у нас есть два типа карт — простые и «оригинальные» (во время разработки мы называли их именно так).
Простые карты — это стандартные для этого типа игр лабиринты с прямоугольными препятствиями. Для редактирования таких карт я написал редактор — простенькая Java-программа с использованием Swing как графической библиотеки. Вручную можно расставить препятствия, дырки, и т.д. Препятствия можно вращать, изменять размер как угодно — в этом плане редактор получился довольно удобным. Тестер жаловался, что неудобно, что нельзя отменять действия — я его понял, и написал отмену\повторение действий, изучив паттерн Команда.
Редактор карт в действии
Простая банальная карта — ничего необычного
Фишка — чтобы побыстрее тестировать карты, в редакторе была возможность переключения в игру. Сразу (в этом же окошке) запускалась игра, где можно было проверить карту. В роли акселерометра выступала мышь. Могу сказать, что написание редактора — это как та фраза, «лучше день потерять, потом за пять минут долететь».
Оригинальные карты. Так мы называли вручную нарисованные художником картинки, где отдельные элементы картинки — это препятствия. Ниже — пример такой карты.
Футбольное поле — уже интересней
Понятно, что стандартный редактор карт не подходит в данном случае. Я подумал, и решил проблему следующим образом — берем редактор tiled. В нем создаем два слоя — один фоновый, там будет картинка оригинального уровня. Второй слой — это слой разметки. На нем примитивами (кругами, квадратами и полигонами) размечается карта. Например, дырка — это круг с именем «hole» (tiled позволяет каждому объекту назначить свое имя). На выходе мы получаем xml-файл в формате tiled. Но нам этот формат не подходит. Поэтому я написал дополнительную утилиту, которая берет сгенерированный tiled xml, и конвертирует в мой формат уровня. На этом же этапе выяснилось, что box2d не поддерживает полигоны, которые состоят более чем из 4-х вершин — проще говоря, четырехугольники (по крайней мере порт box2d для libGDX).
Размечаем оригинальную карту
Весь этот «Франкенштейн» хоть и выглядел неуклюже, но свою задачу выполнил на отлично. Почти половина игровых уровней «оригинальные», и благодаря tiled мы обошлись без написания еще одного редактора. Вывод — иногда не нужно писать свое что-то сложное и большое, достаточно оглянуться по сторонам, и взять готовое решение с минимальным допиливанием «под себя».
АНИМАЦИЯ ШАРИКА
Когда шарик движется, он должен как-то анимироваться, создавать иллюзию движения. Некоторые игры этого жанра не заморачиваются с этим, и используют простую статическую картинку. Мы решили заморочиться.
У нас есть несколько «скинов» шарика — стеклянный, огненный (лава), футбольный и т.д. На тот момент я был не в курсе возможностей 3D в libGDX, и использовать 3D-модельку не мог. Поэтому решили делать покадровой анимацией. Полный оборот шарика у нас занимал порядка 50 кадров. В зависимости от скорости движения я менял частоту смены кадров, создавая иллюзию более быстрого или медленного вращения. Ну и конечно менял поворот этих кадров. Не могу сказать, что вышло идеально, 3d модель однозначно дала бы более красивую картинку, но что есть, то есть.
ФИЗИКА
В игре используется физический движок box2d (точнее, его порт под libGDX). Из плюсов — он быстрый, и возможностей у него много. Из минусов — нужно его знать :) Шарик — это динамическое тело, препятствия — статические тела. Когда загружается уровень, создается физическая модель мира, и, собственно, начинается игра. При паузе симуляция останавливается. В принципе, это все, что можно сказать про физику — особых проблем с ней не возникало.
ДОПОЛНИТЕЛЬНЫЕ ФИШКИ
Экран меню у нас — непростой. Он тоже выполнен, как часть игры. По нем катается шарик, слушаясь акселерометра. А когда шарик ударяется о кнопки, он отскакивает. А если коснуться шариком определенных кнопок в определенном порядке, откроется портал в секретный уровень :)
Шарик катается и отскакивает от кнопок — как в пинболе
Есть много достижений — за прохождение уровней, за определенное количество выигрышей, за определенное время в игре, и т.д. Мы не использовали стандартные Android Google Play достижения, а сделали свою систему достижений.
Малая часть достижений
МОМЕНТЫ, КОТОРЫЕ Я БЫ ДЕЛАЛ ИНАЧЕ
Понятно, что не бывает идеальных решений, и с опытом приходит понимание, что та или иная часть сделана неоптимально. Я скажу, что я бы переделал, делай я игру сейчас.
Первый момент — это выбор коробок. Если вы откроете игру, и посмотрите на коробки, вы увидите, что на каждой коробке есть точки. В зависимости от сложности, этих точек больше или меньше. Так вот — для расстановки этих точек я написал класс, который загружает json-файл, и по этому конфиг-файлу расставляет точки. Мотивация, почему я так делал — сэкономить место в текстурных атласах. То есть, мы храним общую картинку-фон коробки, одну точку, и порядок расстановки этих точек для конкретной коробки. Почему это плохо — сложность модификации. Если бы каждая коробка была отдельной картинкой, достаточно подправить картинку —, а теперь нужно лезть в конфиг-файлы и разбираться что и как. Лучше бы хранить это просто картинками.
Каждая коробка — это не просто картинка. Это подложка + текстовый файл-конфиг с расположением точек.
Второй момент — я очень заморочился с оптимизацией (пример выше — тому подтверждение). Все экраны (меню, выбор коробок, игровой и т.д.) я создал в одном экземпляре, и просто переключался между ними. Это избавило от создания этих же экранов каждый раз, когда мы переходим в другой экран, но привело к проблеме очистки. Например, игровой экран хранит множество параметров для конкретного уровня (например, очки, время и т.д.). Когда мы загружаем новый уровень, нужно сначала сбросить состояние игрового экрана. Но по мере добавления новых фишек легко было забыть что-то очистить. Это «сьело» приличный кусок времени, отладка таких проблем. Когда я делаю игру сейчас, я делаю иначе. Если нужно сменить экран — я сохраняю нужные для этого экрана параметры в каком-то классе настроек, потом создаю новый экран. Тот новый экран создается, и читает нужные данные из класса настроек. При этом больше тратится ресурсов, но на фоне современных мощных телефонов это время незаметно.
Приведенный пример про экраны — лишь один из. Плюс такого подхода — игра не лагает даже на самых плохих телефонах. Минус подхода —, а много ли их, таких плохих телефонов, чтобы тратить так много времени на оптимизацию? Для себя я решил проблему так — при покупке нового смартфона я намеренно выбирал характеристики чуть ниже средних. Если игра на нем идет без тормозов, я не заморачиваюсь над оптимизацией — у большинства людей игра запустится и пойдет нормально.
ЗАКЛЮЧЕНИЕ
Идеальных игр не бывает, как и не бывает идеальной разработки. Спустя время, мы довели игру до конца, и она увидела свет в Google Play. Что сказать — несмотря на все шишки и грабли, заниматься разработкой игр интересно. Всяких моментов возникает много, некоторые моменты не решаемы, и приходится искать обходные пути. Но решение этих проблем поднимает ваш уровень, как разработчика. Мне было интересно писать эту игру, и я верю, что в нее будет интересно играть.