Як я фізичну головоломку на Libgdx писав

Привіт!

Скріншот для затравки:

Хтось ще читає підказки до картинок?

Як-то безцільно сидячи в інтернеті я натрапив на гру «Ланцюга, кульки і зомбі». Не знаю чому, але вона мене сильно зачепила. Простий і в той же час цікавий геймплей і деяка нелінійність — рівні можна пройти кількома способами. Чимось вона нагадала мені відому Crazy Machines, якої я теж колись хворів.

Вбиваючи зомбі, я загорівся ідеєю написати свою гру — поетесами та преферансом кулями і зомбі, тільки краще (можна грабувати коровани). Сказано — зроблено. За підсумком пари тижнів гра була зроблена, і викладена в Google Play. Якщо вам цікаво дізнатися детальніше — прошу під кат.

Вже кілька років я пишу ігри для мобільних пристроїв на Android. Використовую движок libGDX. До цього я пробував AndEngine, і навіть написав на ньому прототип невеликий іграшки — але потім пересів на libGDX, про що не шкодую і зараз. Основна фіча — можливість писати і налагоджувати код на десктопі, а потім з мінімальними правками переносити гру на Android. Виходить швидко і приємно, не потрібно чекати запуску глючного і повільного емулятора. Тому досить логічно, що для написання гри я вибрав саме цей движок.

Ідея

Як відомо, все починається з ідеї. В моєму випадку ідейними натхненниками були ігри «Ланцюга, кульки і зомбі», «Crazy machines» (дуже посереднє до цієї гри), «Stupid zombies». Мета гри в двох словах — використовуючи всі підручні засоби, знищити всіх зомбі на рівні. У ролі підручних засобів виступали важкі металеві кульки, підвішені на ланцюгах, бомби, ящики, дошки, міни, автомобільні колеса. Перерізавши ланцюг, ви скидаєте кулю на зомбі — профіт, зелена тварюка мертва. У більш ускладнених варіантах вам потрібно підривати бомби і різати ланцюга в потрібний час — є аркадний елемент. Щоб було веселіше, в грі цокає час — чим швидше ви завершите рівень, тим більше зірок отримаєте.

Назвати гру я вирішив Ugly Zombies — такий собі референс і спроба зіграти на Stupid Zombies.

image
Типовий геймплей — перезаем ланцюга, котимо бомби до скриньок, підриваємо, вибухи, ящики вбивають зомбі.

Структура гри
Структурно гра розділена на «Меню» та «Гру». З «Меню» ми можемо потрапити в «Екран коробок», а звідти — в «Екран рівнів». Також через меню можна потрапити в «Магазин». Тобто структура традиційна — в грі є 4 коробки, в кожній коробці по 18 рівнів (сітка 6*3). Рівень стає доступним лише після проходження попереднього. У магазині ми можемо придбати додаткові рівні (далі буде детальніше) і відключити рекламу.

image
Приклад вибору коробок. Кожна коробка — унікальна (своя картинка і анімація)

Управління
Власне все управління в грі зводиться до перерізанню ланцюгів і нажиманию на бомби. У першій версії, коли я отлаживал гру на десктопі, я видаляв ланцюга і підривав бомби по кліку. Потім я запустив гру на телефоні — і це було погано. Ланцюги різалися через раз, а бомби не підривали. Я почухав голову, і змінив поведінку з кліка, на дотик (тобто, торкнулися, ще не відірвали палець від екрану, а дія сталося). Допомогло, але несильно. На десктопі було нормально, а от на телефоні все ж було погано. Маленький екран робив управління незручним. Потрібно було щось робити.

Тут мені в голову прийшла ідея — а якщо різати, то можна різати! У сенсі, свайп по екрану — це воно, робимо, як Fruit Ninja. Я зробив, спробував — так, це було воно! Правда, підривали бомби все так же по тапу. Трохи згодом, я зробив і вибух бомб свайпу теж — так було зручніше. Деяким свидетельсвом зручності управління і зрозумілості гри є наступне — я дав погратися в чорнову версію гри мою десятирічному племіннику, він за годину пройшов півтори коробки. Мені особливо не з чим порівнювати, але як мінімум, дорослий повинен розібратися в грі без проблем.

Арт
Арт у грі був збірний. Інтерфейс для меню я взяв з безкоштовного набору, який нарив в інтернеті. Я ще здивувався, що такий непоганий казуальненький набір, і халявний. Як і годиться безкоштовної графіку, її не вистачало. Тому використовуючи Gimp і строго цензурні слова, я домалював на базі скачаного пака іншу графіком для меню. Ось головне меню гри:
image

Графіком для ігрового екрана я робив частково сам, частково брав з інтернету. Дошки намальовані в Gimp, ящики взяті з OpenGameArt.org, бомби, колеса, зомбі — благополучно скачати з інету. Це ж стосується і ігрового фону. Всі картинки пройшли обробку — приміщення на прозорий фон, обрізка, зміна розміру.

Що ж стосується зомбі, з ним було складніше. Я скачав цільну картинку, і нарізав її на шматки — адже мені ще потрібно було робити Ragdoll з цієї картинки. Зомбі був найбільш складною і болючою частиною, і я вважаю, що це найменш опрацьована частина гри. Відсутність художника сильно позначилося на графічній складової гри. Моя дівчина сказала, що гра виглядає симпатично, але вона не могла сказати інакше :)

Графіком для Google Play (іконку і промо-картинку) я теж малював сам, в Gimp. Методом мук і проб було створено ось це:
image

Відео для Youtube я записував програмою vokoscreen, потім редагував (вирізав потрібне і вставляв музику) програмою OpenShot Video Editor. До цього я пробував програми Avidemux (я користувався раніше), і LiVES — але обидві крашились при спробі експорту відео. А ось OpenShot впорався, незважаючи на його криве управління.

Мабуть, питання з графікою можна закрити. Графіка — це болісний процес для мене, відсутність художника навчило мене базовим основам роботи в Gimp, і усвідомлення власної недосконалості.

Звуки
Звуки були взяті частково з OpenGameArt.org частково видерто з вищевказаної флеш-ігри. Для розпакування флеша я використовував якусь написану на Java програму, назви не пам'ятаю, якщо комусь буде цікаво — пошукаю. Звук виграшу (гонг) був знайдений на просторах Інтернету, обрізаний в Audacity. Звуки великого напрягу не викликали, тим більше їх було відносно небагато, десяток.

Музика
Музику я знайшов на OpenGameArt.org. Я підшукав бадьоренький рок-трек, який, як на мене, добре вписався в гру (я повалявся на дивані, граючи в іграшку під цю музику). Загалом, з музикою великої проблеми теж не було. Треба було б ще зациклити її, але тут моїх пізнань в Audacity не вистачило, і я вирішив не морочитися — тим більше, що це сильно не вплине на ігровий процес.

Код
Перейдемо до найцікавішого для програмістів — кодом.

Гра писалася в Eclipse, я юзал власний движок-надбудову над libGDX — DDE (Dark Dream Engine). З можливостей мого движка:
— візуальне редагування екранів (дуже прискорило розробку);
— зручний доступ до ресурсів (звуки, графіка) — досить покласти їх у потрібні папки, далі вони вантажаться самі. Причому завантаження ресурсів «лінива» — якщо ми звертаємося до ресурсу, а він ще не завантажений — він завантажується, кешується і повертається;
— підтримка розширень (власне, візуальний редактор екранів — це і є розширення)
— зручна стартова панель, де можна вибрати дозвіл ігри для запуску — зручно тестувати мультиэкранность;
— можливість експорту з стартової панелі апк і десктоп програми — один раз налаштувавши параметри, далі тиснемо лише «Зібрати». Кривувато працює, тому не юзал.
— … і ще багато плюшок. Чорнову версію я викладав на GitHub, але то було давно і неправда. Як з'явиться час, хочу допилити DDE до людського виду, і написати серію туториалов — я використав його у кількох проектах, воно таки зручно.

Для фізики, як і годиться, я використовував Box2D — він дуже добре інтегрований в libGDX, питань щодо нього не виникало. Для налагодження використовував клас Box2dDebugRenderer — він дозволяє візуалізувати фізичні тіла лініями на екрані.

Структурно гра розділена на такі частини:
— Головний клас-контролер — я назвав його Zombie. Це одинак, який зберігає в собі посилання на всі ресурси. Він доступний з будь-якої точки. Можна подумати, це God Object — але це не так. Це простий клас у 135 рядків, 90% з яких — геттери і сетери. Наводжу код класу цілком, щоб ви розуміли, про що я:

Zombie.java
package ua.com.integer.labs.zombie;

import ua.com.integer.dde.kernel.DDKernel;
import ua.com.integer.dde.res.screen.Abstractscreen;
import ua.com.integer.dde.res.sound.Soundmanager;
import ua.com.integer.labs.zombie.screen.loadingscreen;
import ua.com.integer.labs.zombie.screen.game.gamecontroller;
import ua.com.integer.labs.zombie.screen.game.level;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.Spritebatch;
import com.badlogic.gdx.graphics.glutils.shaperenderer;
import com.badlogic.gdx.utils.Logger;

public class Zombie extends DDKernel {
private static Zombie instance = new Zombie();
private SpriteBatch spriteBatch;
private ShapeRenderer sRenderer;
private Settings sets;
private Level level;
private Sounds sounds;

private GameController gameController;

private PlatformInterface platformInterface;

private Zombie() {
getConfig().relativeDirectory = "../zombie-android/assets/";
}

public static Zombie getInstance() {
return instance;
}

@Override
public void create() {
super.create();

getResourceManager().getManager(SoundManager.class).loadAll();

Gdx.app.setLogLevel(Logger.DEBUG);

gameController = new GameController();
sets = new Settings();
sounds = new Sounds();

spriteBatch = new SpriteBatch();
AbstractScreen.batch = spriteBatch;

sRenderer = new ShapeRenderer();

showScreen(LoadingScreen.class);
}

@Override
public void dispose() {
super.dispose();
getResourceManager().dispose();

sRenderer.dispose();
}

public void setPlayLevel(Level level) {
this.level = level;
}

public Level getLevel() {
return level;
}

public ShapeRenderer getShapeRenderer() {
return sRenderer;
}

public Settings getSettings() {
return sets;
}

public void setPlatformInterface(PlatformInterface platformInterface) {
this.platformInterface = platformInterface;
}

public PlatformInterface getPlatformInterface() {
if (platformInterface == null) {
platformInterface = new PlatformInterface() {
@Override
public void showAds() {
System.out.println("Show ads.");
}
@Override
public void hideAds() {
System.out.println("Hide ads.");
}
@Override
public void buyUnlockAds() {
System.out.println("Buy unlock ads.");
}
@Override
public void buyLevels() {
sets.setBoxesOpened(true);
System.out.println("Buy levels.");
}
@Override
public void showInterstitial() {
System.out.println("Show interstitial!");
}
@Override
public void rateForGame() {
System.out.println("Rate for game");
}
@Override
public void shareViaFacebook() {
System.out.println("Share via facebook");
}
@Override
public void shareViaTwitter() {
System.out.println("Share via twitter");
}
@Override
public void updatePurchases() {
System.out.println("Update purchase state");
}
};
}
return platformInterface;
}

public GameController getGameController() {
return gameController;
}

public Sounds getSounds() {
return sounds;
}
}


Далі йшло поділ на менеджери ресурсів. Ресурс — це майже все в грі. Це звук, музика, текстури, екрани — все, що можна розділити на частини і редагувати. Таким чином, є менеджер звуків, менеджер текстур, менеджер екранів.

Трошки подробиць про екран. Один екран — це один клас.

Скріншот екрану магазину:
image
А ось код екрану для класу магазину покупок:
StoreScreen.java
package ua.com.integer.labs.zombie.screen.menu;

public class StoreScreen extends AbstractScreen {
public StoreScreen() {
addScreenEventListener(new ScreenListener() {
@Override
public void eventHappened(AbstractScreen screen, ScreenEvent event) {
switch(event) {
case SHOW:
findByName("all-boxes").setVisible(!Zombie.getInstance().getSettings().isBoxesOpened());
findByName("no-ads").setVisible(!Zombie.getInstance().getSettings().isAdwareDisabled());
break;
case BACK_OR_ESCAPE_PRESSED:
getKernel().showScreen(MenuScreen.class);
break;
default:
break;
}
}
});

ActorUtils.deployConfigToScreen(this, Actors.getInstance().getConfig("store-screen"));

findByName("back-button").addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
Zombie.getInstance().getSounds().playClick();
getKernel().showScreen(MenuScreen.class);
}
});

findByName("all-boxes").addListener(new ScaleActorListener(0.05 f).setTime(0.1 f));
findByName("all-boxes").addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
Zombie.getInstance().getSounds().playClick();
Zombie.getInstance().getPlatformInterface().buyLevels();
}
});

findByName("no-ads").addListener(new ScaleActorListener(0.05 f).setTime(0.1 f));
findByName("no-ads").addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
Zombie.getInstance().getSounds().playClick();
Zombie.getInstance().getPlatformInterface().buyUnlockAds();
}
});
}
}


Як бачимо, код досить короткий. Не буду пояснювати його детально, лише пару моментів.
1)
ActorUtils.deployConfigToScreen(this, Actors.getInstance().getConfig("store-screen")); 
— розгорнути конфіг екрану (у мене свій редактор екранів, пам'ятаєте?). Фон, кнопочки — це все тягається візуально, ця строчка робить все інше.
2)
findByName("all-boxes").addListener(new ClickListener()... 
— знайти об'єкт із зазначеним ім'ям і призначити йому обробник. Ім'я об'єкта (актора, в термінології libGDX) ми вказуємо в редакторі інтерфейсу.

Тобто, створення таких рутинних операцій у мене було автоматизовано, що дозволило мені зосередитися в більшій мірі на грі.

Вкусняшки
Що ж це за гра без ласощів! Під ними я розумію дрібні прикраси, що дозволяють візуально «оживити» гру. Я зробив кілька таких штук, їх опис нижче.

Анімовані коробки. Кожна коробка повинна бути «живою» — це створить відчуття опрацьованості гри. Тому на кожній є унікальний малюнок за простій анімацією — рука збільшується\зменшується, череп моргає, бомба обертається, а зірка крутиться дзигою. Це дуже прості ефекти, робити які з допомогою libGDX одне задоволення, завдяки його розвиненій системі Actions. Наприклад, код для додавання анімації моргання черепу —
findByName("head-item").addAction(Actions.forever(Actions.sequence(Actions.color(Color.WHITE, 0.4 f), Actions.color(Color.BLACK, 0.4 f)))); 


Анімовані діалоги. В грі є кілька діалогових вікон — виграш, купівля рівнів і т. д. Просто показувати їх було негарно — двома рядками коду робимо анімоване поява\закриття. Код, знову ж таки, максимально простий (показ\закриття вікна покупки) —
private void hideBuyDialog() {
Actor dialogContent = findByName("dialog-content");

if (dialogContent != null) {
findByName("dialog-content").addAction(Actions.scaleTo(0f, 0f, 0.1 f));
}
}

private void showBuyDialog() {
Actor dialogContent = findByName("dialog-content");
if (dialogContent != null) {
dialogContent.addAction(Actions.scaleTo(1f, 1f, 0.1 f));
}
}


Для зберігання налаштувань я виділив окремий клас — Settings. Доступ до цього класу можна отримати через клас Zombie. Включення\виключення музики, купівля рівнів, відключення реклами — цим займається він.

Для взаємодії з Android кодом з головного проекту я створив інтерфейс (в термінах Java), реалізував його в Android-частини, і передав його в головний проект. Код інтерфейсу —
package ua.com.integer.labs.zombie;

public interface PlatformInterface {
public void buyLevels();
public void buyUnlockAds();
public void showAds();
public void hideAds();
public void showInterstitial();
public void rateForGame();
public void shareViaFacebook();
public void shareViaTwitter();
public void updatePurchases();
}


Як бачимо, просто набір методів без параметрів. Всі деталі реалізовані в Android-частини, в десктоп проекті лише заглушки.

Екран гри
Ігровий екран був складніший за інші. Його я розділив на HUD і фіз. модель. Фізичну модель так і назвав — Model.java. Була введена абстракція — GameObject. Цей самий GameObject містив фізичне тіло, спрайт для відтворення та метод update() для відтворення спрайту. Модель керувала об'єктами — додавання, видалення, виклик update(), старт\зупинка фіз. світу — це все вона. Такий менеджер вийшов.

Для реалізації різних об'єктів — кулі, ящики тощо — були створені субклассы GameObject. Був створений ObjectLoader, який умів вантажити GameObject-и. Таким чином, якщо я хотів додати новий об'єкт — я робив субкласс від GameObject, і субкласс від ObjectLoader. Можливо, це кілька оверхед, але це дозволило зберегти ігровий екран і модель відносно чистим — розрослося лише кількість відносно невеликих класів.

Для створення рівнів був створений редактор рівнів. Написаний на Java, старий-добрий Swing. Рівні зберігаються в JSON, вага одного рівня — близько 2 кілобайт. Використовуючи вбудований в libGDX JSON-worker, я завантажував\зберігав рівні з файлу одним рядком.

Якби я робив все 72 рівня вручну — я б зійшов з розуму, підозрюю. Тим, хто робить гри з рівнями — є сенс задуматися про створення редактора якомога раніше, потім менше гемороя буде. Напевно, це прописна істина, але все ж.

За бортом залишилося досить багато — опис обробки фізичних зіткнень, створення рэгдолла, адаптація до різними дозволами екранів — але це потягне на статтю, і навіть не на одну. Якщо комусь буде цікаво — пишіть в коментарях, що зможу розписати. А ми рухаємося далі.

Монетизація
Як і будь-якій людині, мені хочеться їсти. Тому я задумався про хоч якийсь дохід від гри. Почитавши в інтернеті статті, я вирішив монетизувати рекламою і внутриигровыми покупками.

Реклама. Банер внизу (не видно в ігровому екрані), і проміжні оголошення на екрані виграшу (interstitial ad). Рекламу можна відключити за один долар.

Купівля рівнів. Спочатку в грі доступно дві коробки — це половина рівнів. Решта стоять 1.99$. Ціни я брав «зі стелі», був би радий почути в коментарях відгуки з цього приводу від знаючих людей.

В принципі, ось і вся монетизація. При кліці на закриту коробку з'являється пропозиція купівлі коробок, і також є окремий екран магазину, який доступний з головного меню. У мене немає великого досвіду в цій сфері, знову ж таки був би радий почитати коментарі людей, які цим займаються професійно.

Локалізація
Ніякої локалізації я не робив. У грі мінімум тексту, і я подумав, що це невиправдані витрати. Тому спочатку гра була на англійською, так і залишилася.

Просування
Особливого просування я не робив. Пост тут (без посилання, кому цікаво — пишіть в приват, скину). Пост на 4pda, на сайті libGDX, на сайті gcup.ru. Планую ще поразмножать на форумах. Звичайно ж розміщу на своєму блозі (не знаю, чи можна вказати тут посилання. Мій блог некомерційний, реклами там немає. Блог игрострою, досить багато є по libGDX). Буду радий порадам від знаючих людей.

Гру я виклав на Google Play. В принципі, немає великої складності зібрати десктоп версію — тільки куди її викладати і навіщо — я не знаю. Якщо хтось зможе підказати, чи варто цим займатися — буду дуже вдячний

Висновки
Гра зайняла кілька тижнів. Основна робота була зроблена за тиждень — час від восьмої вечора до третьої ранку. Дуже велике значення має мотивація. Якщо ви налаштовані на роботу, ви дуже швидко все робите.

Дуже добре, якщо в команді є художник. В іншому випадку сайти з безкоштовними ресурсами для ігор — ваше все. Володіння Gimp або Photoshop — дуже хороша підмога. Сюди ж входить володіння аудіо — і відеоредакторами.

Ця гра — мій пробний проект. Цікаво, чи буде з неї якийсь вихлоп, якась популярність. Так сказати, обережно пробую п'ятою воду — чи не холодна. Із задоволенням вислухаю ваші коментарі, вітаю дискусію з цього приводу.

Ніби як все. Чекаю відгуків!

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

0 коментарів

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