[ libGDX ] Опыт разработки игры с использованием Box2D
Здравствуй, Хабр! Ух! Давно же я не писал здесь. Итак, начну пожалуй с небольшой предыстории и заодно приведу скриншот получившейся игры.
Скриншот игрового процесса
Примечание: статья для новичков, однако, если вы гуру и увидели ошибку в суждениях/коде автора, то просьба писать об этом в ЛС или в комментариях. А я их добавлю в статью.
Предыстория
Весь последний год я работал веб-программистом на языке Python. Не скажу, что мне это не нравилось, однако душа желала творчества/независимости/перемен/успеха (подчеркните своё, инди разработчики игр). И было время писать что-то своё по вечерам, но до чего-то хоть немного значимого это так и не дорастало (ввиду банальной усталости и желания провести время отдельно от компьютера).
Но вдруг, на горизонте замаячила возможность. Я и ещё пара друзей решили сделать свой проект. Идея была расписана, делались презентации, рисовались графики, среда разработки была запущена, но не хватало только одного — инвестора. Денег надо было не очень много (по обычным меркам), но мы так и не смогли найти заинтересованного человека «при деньгах». Что ж, подумал я, раз так, тогда пора бы освободить своё воображение и пустить его в разработку какой-нибудь маленькой, но гордой игры. Об этом и дальнейшая статья.
Идея
Я намеренно старался не брать чью-то идею или просто копировать уже готовое. Изначальная идея была в том, чтобы просто бросать шарики разного цвета на стенки того же цвета и вести счёт. Но сделав первый вариант игры я понял, что она скучная и ни разу не затягивает. Тогда-то мне пришла мысль сделать цифры с разными знаками (плюс, минус), вместо шариков и добавить «ненавистный нуль» ©, обнуляющий все очки игрока.
Потом я понял, что игрока нужно как-то ограничивать и добавил таймер. Затем, добавил уровни (хотя, изначально хотел сделать просто таблицу рекордов). Уровни должны были как-то усложняться, поэтому я добавил препятствия в виде стенок и платформ. Уже ближе к концу разработки я подумал, что нулю нужно дать особые права и сделал его неуязвимым для стенок и платформ, что очень злило моих «домашних тестеров» и крайне забавляло меня.
Скриншот главного меню игры
Сроки
Поскольку, у меня в любой момент могло «кончиться» свободное время, я решил поставить себе цель — написать игру за неделю. Цель эту я достиг. Игра, безусловно, ещё сыровата, но всё же уже в маркете и меня это радует.
Реализация
Для реализации своих идей я выбрал свой любимый фреймворк libGDX, который хорошо интегрирован с физическим движком box2D. Вообще, libGDX прекрасен. У него действительно хорошая документация, которая выручала меня в 80% случаев, а в 20% помогал stackoverflow. Также, он сейчас прекрасно интегрируется с Android Studio (раньше не мог), что опять же добавляет ему плюс. Получившиеся приложения получаются весьма и весьма лёгкими (в отличие о того же Unity3D) и довольно стабильными (если руки не кривые, как у меня :)).
Проблемы
Проблемы — это конечно громкое слово, однако напишу здесь о нескольких возникших проблемках, которые отняли у меня кучу нервов и времени.
- Физический мир. Честно, я не знаю, как я мог пропустить этот момент в документации (он описан в коде), но долго не мог понять, почему мои физические тела такие медленные. Проблема была в том, что я указал размеры камеры равные размеру экрана:
// Я писал так box2DCamera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); // А надо было например так. 14 -- это число, которое меня устроило. scrWidth и scrHeight -- это размеры экрана. Такая формула позволяет запускать данный код на любом экране и всё растянется пропорционально box2dCamera = new OrthographicCamera(14 * (scrWidth / scrHeight), 14);
- Спрайты/Изображения. Если работаешь с физическим движком, то размеры изображений и их позиции нужно задавать исходя из нескольких параметров:
sprite = new Sprite("your_sprite"); sprite.setBounds(body.getPosition().x / 2f, body.getPosition().y / 2f, radius, radius); // в моём примере я тело было круглым, поэтому здесь два раза повторяется radius. sprite.setOrigin(sprite.getWidth() / 2f, sprite.getHeight() / 2f);
- Вращение объектов и спрайтов. Если вы хотите вращать физическое тело, то просто передав значение угла спрайту вы ничего хорошего не получите. Необходимо перевести значение из радианов в градусы:
sprite.setRotation(MathUtils.radiansToDegrees * body.getAngle());
- Ещё были моменты с dispose () у объектов текстур и прочей графики. Дело в том, что (и это описано в документации, но я её плохо читал) если вы пользуетесь AssetManager, то ни в коем случае не делайте dispose () отдельной текстуры, полученной из менеджера, так как он её удалит и впоследствии вы увидите чёрное пятно вместо объекта графики.
- Ещё было много матов с удалением объекта из физического мира. Решение гуглится, но я всё таки приведу пример здесь:
Iterator
i = gameObjects.iterator(); while (i.hasNext()) { GameObject object = i.next(); if (!world.isLocked()) { // ВАЖНО! Если не сделать эту проверку, то получите ошибку. Так мы проверяем, можно ли удалить объект или он как-то используется в мире (коллизии и т.п.) object.getBody().setActive(false); if (mouseJoint == null) { // Это проверка на касание объекта. mouseJoint -- это захваченное тело при событии TouchDown world.destroyBody(object.getBody()); i.remove(); } } }
Вроде бы, это все проблемы, с которыми я столкнулся при разработке. Пишите в комментариях о ваших проблемах, может чем-нибудь помогу.
Немного советов
- Обфускация и минификация. До этого, я никогда не обфусцировал код, а тут решил попробовать. И знаете, понравилось! До обфускации игра весила 4,8 мб, а после её вес уменьшился до 3,7 мб. Делайте выводы сами. Тем более, делается это просто, ведь сейчас ProGuard поставляется по умолчанию с Android SDK и включается парой строк кода:
Файл android/build.gradle
android { ... buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt' } } }
Файл android/project.properties// Раскомментируйте эту строчку proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
- Не используйте фильтр Nearest (это я про текстуры). По умолчанию, TexturePacker применяет фильтр Nearest, Nearest. Лучше заменить его на Linear, Linear или MipMap, MipMap. Так всё будет плавнее, даже если с размерами картинок не угадаете.
- Ещё про текстуры. Не нужно делать их максимально большими, а потом уменьшать с помощью setSize () или setScale (). На выходе может получится смазанное изображение.
Графика и звук
Основной шрифт я взял в Google Fonts, остальные элементы либо рисовал сам в Inkscape, либо брал бесплатные иконки и немного их правил.
Что же касается звуков, то что-то я взял с freesound, что-то сделал сам в LMMS. Получилось вполне сносно. Кстати, в следующем обновлении хочу добавить фоновую музыку, которую как раз намерился делать в LMMS. Программа понравилась. Сильно напомнила Fruity Loops Studio.
Скриншот экрана уровней
Монетизация
Я пока решил выложить одну бесплатную версию с «межстраничными» объявлениями Admob. Потом, если игра понравится публике, введу возможность для отключения рекламы, а также дополнительные уровни за «коины».
Продвижение
Это самое скучное и не интересное занятие из всех, поэтому я решил, что опубликую на 5–10 ресурсах и хватит. Может, если продукт интересный, он сам найдёт свою аудиторию? (мечты…). Хотел бы почитать в комментариях, как лучше (и желательно недорого) продвигать свой продукт. А то большинство статей на эту тему либо заказные, либо попросту устарели.
Итог
Игру я выложил совсем недавно, поэтому рано ещё говорить о чём бы то ни было. Однако, я многому научился за время разработки игры и полученный опыт можно будет применить на будущих проектах.
Через неделю-две обновлю статью и приложу графики скачиваний/прибыли от рекламы. Спасибо за прочтение!