Pull to refresh

Казуальные игры на Libgdx, тонкие моменты в разработке

Reading time 7 min
Views 18K
Статья будет полезна как начинающим, так и опытным разработчикам, т.к. она охватывает и базовые моменты разработки игр, и нетривиальны проблемы, которые приходилось решать. Если вас заинтересовало, прошу под кат. Так же разработчикам на libdgx будут полезны ссылки, приведенные в конце статьи.

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

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

Расскажу немного про гейм дизайн, так будет проще понять тонкие моменты в разработке.

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

Первая версия которая увидела свет


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

Герой как личность — это довольно интересный шаг, даже отношение людей к игре сильно изменилось, когда вместо фиолетового шарика начал летать угрюмый монстр в шлеме, появилась что ли душа.

Как игра выглядит сегодня




Gradle и libgdx.


Очень приятно удивила система сборки проектов в libgdx. Начиная с версии 1.0 при создании проекта через их ui утилиту создаются так же gradle скрипты для сбора проекта под необходимые платформы. Это очень удобно для начинающих разработчиков, т.к. я думаю, не каждый захочет с нуля изучать, как собрать iOS проект из Idea и запустить его на эмуляторе или на устройстве. Используя стандартные средства, это делается в один клик.

Небольшой пример того, что за вас делает gradle. Метод распаковывает библиотеки из natives-ios.jar в *.a и помещает их в build/libs/ios:

task copyNatives << {
    file("build/libs/ios/").mkdirs();
    configurations.natives.files.each { jar ->
        def outputDir = null
        if (jar.name.endsWith("natives-ios.jar")) outputDir = file("build/libs/ios")
        if (outputDir != null) {
            copy {
                from zipTree(jar)
                into outputDir
                include "*.a"
            }
        }
    }
}


Используйте атласы


Это пишут во всех книгах по разработке игр, когда речь идёт об оптимизации работы с графикой.

Плюсы использования атласов текстур.
  • Сокращает количество смен состояний до одного для атласа;
  • Позволяет уменьшить количество занятых текстурных слотов до одного для атласа;
  • Минимизируется фрагментация видеопамяти.

Для себя мы нашли gui экстеншен gdx-texturepacker-gui. В принципе, претензий нет, но, насколько мне известно, поддержка и разработка его закончилась в конце 2012 года.

Синглтоны для сцены


Стандартный подход при использовании libgdx, описанный во всех туториалах — для нового экрана делать новый screen:
habrahabr.ru/post/224175
github.com/libgdx/libgdx/wiki/Extending-the-simple-game

    game.setScreen(new GameScreen(game));

Все прекрасно, новая игра не зависит от предыдущей, новая жизнь — новая скрин.
Но мы поняли, что не все так радостно, когда реализовывали функцию возрождения после смерти и логику «начать сначала». Процесс мог быть такой: Новая игра -> возрождение -> возрождение -> начать сначала -> возрождение -> начать сначала.

Здесь начинались проблемы с памятью, начинал включаться GC, иногда встречались с тем, что некоторые объекты ссылались на уже убитый скрин.

Проблему нужно было решать.

Здесь на помощь приходят синглтоны, объект каждого экрана нам нужен только один, не нужно плодить несколько экранов игры и геймовера, можно просто управлять их состоянием.

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

Пулы для одинаковых объектов


Знающие могут пропустить этот раздел, но, когда я только начинал разрабатывать игры, я был очень рад если бы мне рассказали про пулы раньше. Подробнее можно почитать здесь.
Кто бы мог подумать, что наш старый друг GC будет вешать игру на 0.2-0.4 секунды с завидной периодичностью.

Одно из главных правил создания игр — строить архитектуру и писать код таким образом, чтобы все объекты были переиспользуемые и GC не вызывался (желательно никогда)

Например, в нашем случае это переиспользуемые блоки, монетки, частицы, которые мы создаём лишь однажды в небольшом количестве, для блоков это 25 штук, помещаем в очередь и при необходимости используем, когда объект исчез/разрушился мы кладем его обратно в очередь до следующего использования.

Реализация пула
        public class MultiPool<T> {
            private final List<Pool<T>> mPools = new ArrayList<>();
            public void registerPool(final int pID, final Pool<T> pPool) {
                this.mPools.add(pID, pPool);
            }

            public T obtainPoolItem(final int pID, float x, float y, int type) {
                final Pool<T> pool = this.mPools.get(pID);
                if (pool == null) {
                    return null;
                } else {
                    return pool.newObject(x, y, type);
               }
           }

          public void recyclePoolItem(final int pID, final T pItem) {
              final Pool<T> pool = this.mPools.get(pID);
              if (pool != null) {
                  pool.free(pItem);
              }
         }
    }

public class Pool<T> {

    public interface PoolObjectFactory<T> {
        public T createObject(float x, float y, int type);
    }

    private final Array<T> freeObjects;
    private final PoolObjectFactory<T> factory;
    private final int maxSize;

    public Pool(PoolObjectFactory<T> factory, int maxSize) {
        this.factory = factory;
        this.maxSize = maxSize;
        this.freeObjects = new Array<T>(false, maxSize);
    }

    public T newObject(float x, float y, int type) {
        T object = null;
        if (freeObjects.size == 0) {
            object = factory.createObject(x, y, type);
        } else {
            object = freeObjects.pop();
        }
        return object;
    }

    public void free(T object) {
        if (freeObjects.size < maxSize) {
            freeObjects.add(object);
        }
    }
}
    


Оптимизация объектов с использованием пулов одна из самых значительных, которые можно провести в игре. В различных модификациях это используется и в приложениях, но там немного проще, т.к. если приложение используется в более-менее пассивном режиме, пользователь может даже не замечать работы GC.

P.S полезный комментарий от пользователя 1nt3g3r

В LibGDX есть свой удобный класс для пулов (Pool), и есть пул пулов (Pools). В итоге, работа сводится к:

Monster m = Pools.obtain(Monster.class); //получить обьект из пула. Просто указываем, какого класса обьект нам нужен
… // попользовались обьектом
Pools.free(m); //вернули обьект в пул

Поток: играем со сложностью


Один из основных моментов при разработке игры подобного рода — это защита от неправильного использования, в нашем случае это была сумасшедшая тряска телефона. Эту проблему решали ограничением максимальной скорости по оси X, получается, как бы пользователь сильно не тряс телефон, быстрее он не полетит.

Сама настройка скоростей игры потребовала очень большого количества тестов, т.к. нужно было сбалансировать 3 показател: ускорение камеры, ускорение героя по x и ускорение героя по y. Математически мы вычислили граничные условия — те скорости, после которых игра становится нереальной и бессмысленной, а дальше все данные подбиралось экспериментальным путем. Логику ускорения героя мы переписывали несколько раз. Я думаю многие видели график зависимости сложности игры от прогресса.

График зависимости сложности игры от прогресса


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

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

Мы подняли планку, до 10 секунд игра шла очень спокойно, потом снова наращивала темп. Мы все равно слышали отзывы о том, что слишком сложно и играть нереально, нам советовали уменьшить уровень хардкора примерно на 20%.

Снова поиграли со сложностью и провели еще один эксперимент. Первые 20-30 секунд люди были довольны, но потом, к нашему удивлению, все говорили примерно одинаково: «Как-то просто. Медленно ускоряется. Я уже должен был умереть».
В конечном итоге мы пришли к классической диаграмме сложности, в нашем варианте она немного видоизменилась, из-за того, что сложность растет не бесконечно.

Наша зависимость сложности игры от прогресса


Так же очень важно было подобрать правильный алгоритм расстановки платформ. В нашем случае алгоритм выглядит примерно следующим образом. Всего в одну линию помещается 5 платформ, первая платформа создается с шансом 80% для каждой следующей платформы этот процент уменьшается. Чтобы вся линия не застроилась платформами, приходится считать количество сгенерированных платформ, и если 4 уже стоят, то 5-ю не ставить. Так же мы храним позицию прохода для предыдущей линии, это необходимо, чтобы строить дорожку из монет, которые должен собирать игрок.

Libgdx и 2D графика.


Разработчики libgdx пишут, что у них очень удобная система создания интерфейсов. Я думаю, многие, кто работал с libgdx, знают, как это тяжело и долго.
github.com/libgdx/libgdx/wiki/Scene2d.ui
Да, у них есть набор классов, облегчающий работу, есть лайауты, есть виджеты, но вот простое расставление их на экране — это был огромный кусок работы.

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

В итоге на гитхабе нашел очень интересное решение. Оказывается, у фреймворка Cocos2d-x существует собственная студия для создания интерфейсов, и — о, чудо! — ее ранние версии используют json в качестве экспортируемого формата.
Один умелец из Китая уже написал парсер для этой студии. Суть работы очень проста: берется json, сериализуется в объекты, а после по этим объектам создается GUI с использованием Scene2d.

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

Версии cocostudio поддерживаемые на сегодняшний день:

v1.0.0.0 Beta for Mac
v1.5.0.1 Beta for Windows

Буду рад контрибьютерам, библиотеке есть куда развиваться (кокос уже перешёл на версию 2+), надеюсь добиться хорошей совместимости этих инструментов.

Хочу заметить, что собирали все на андроид, так как решили обкатывать на этой платформе. Под iOS билд тоже собирается прекрасно. Благо gradle билд уже написан за нас. Единственная сложность с iOS — это то, что все околоигровые сервисы придётся переписывать на robovm.

Спасибо тем, кто дочитал до конца. Буду рад ответить на ваши вопросы в комментариях. Как и обещал, ссылки на ресурсы.

Coco-libgdx-gui: github.com/xPutnikx/cocostudio-ui-libgdx/tree/kotlin
Texture packer: code.google.com/p/libgdx-texturepacker-gui
Tags:
Hubs:
+12
Comments 10
Comments Comments 10

Articles