Pull to refresh

Как я физическую головоломку на Libgdx писал

Reading time12 min
Views23K
Здравствуйте!

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

Кто-то еще читает подсказки к картинкам?

Как-то бесцельно сидя в интернете я наткнулся на игру «Цепи, шары и зомби». Не знаю почему, но она меня сильно зацепила. Простой и в то же время интересный геймплей и некоторая нелинейность — уровни можно пройти несколькими способами. Чем-то она напомнила мне небезызвестную 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). Из возможностей моего движка:
— визуальное редактирование экранов (очень ускорило разработку);
— удобный доступ к ресурсам (звуки, графика) — достаточно положить их в нужные папки, дальше они грузятся сами. Причем подгрузка ресурсов «ленивая» — если мы обращаемся к ресурсу, а он еще не загружен — он загружается, кэшируется, и возвращается;
— поддержка расширений (собственно, визуальный редактор экранов — это и есть расширение)
— удобная стартовая панель, где можно выбрать разрешение игры для запуска — удобно тестировать мультиэкранность;
— возможность экспорта из стартовой панели apk и десктоп приложения — один раз настроив все параметры, дальше жмем лишь «Собрать». Кривовато работает, поэтому не юзал.
— … и еще много плюшек. Черновую версию я выкладывал на 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.05f).setTime(0.1f));
		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.05f).setTime(0.1f));
		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.4f), Actions.color(Color.BLACK, 0.4f)))); 


Анимированные диалоги. В игре есть несколько диалоговых окон — выигрыш, покупка уровней и т. д. Просто показывать их было некрасиво — двумя строчками кода делаем анимированное появление\закрытие. Код, опять же, максимально простой (показ\закрытие окна покупки) —
       private void hideBuyDialog() {
		Actor dialogContent = findByName("dialog-content");
		
		if (dialogContent != null) {
			findByName("dialog-content").addAction(Actions.scaleTo(0f, 0f, 0.1f));
		}
	}
	
	private void showBuyDialog() {
		Actor dialogContent = findByName("dialog-content");
		if (dialogContent != null) {
			dialogContent.addAction(Actions.scaleTo(1f, 1f, 0.1f));
		}
	}


Для хранения настроек я выделил отдельный класс — 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 — очень хорошее подспорье. Сюда же входит владение аудио- и видеоредакторами.

Эта игра — мой пробный проект. Интересно, будет ли из нее какой-то выхлоп, какая-то популярность. Так сказать, осторожно пробую пяткой воду — не холодная ли. С удовольствием выслушаю ваши комментарии, приветствую дискуссию по этому поводу.

Вроде как все. Жду отзывов!

UPD: Добавил видео из игры в начале
Tags:
Hubs:
+1
Comments12

Articles