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

Стаття буде корисна як початківцям, так і досвідченим розробникам, тому що вона охоплює і базові моменти розробки ігор, і нетривіальні проблеми, які доводилося вирішувати. Якщо вас зацікавило, прошу під кат. Так само розробникам на 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.

Потік: граємо зі складністю
Один з основних моментів при розробці гри подібного роду — це захист від неправильного використання, в нашому випадку це була божевільна тряска телефону. Цю проблему вирішували обмеженням максимальної швидкості по осі 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/

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.