С момента публикации игры постоянно люди просили выложить исходники. Решил всё-таки поделиться ими. Может кому-нибудь и поможет в изучении LibGDX.
Небольшое отступление
Не имел опыта работы с Java, а уж тем более с Android. Решил и то, и другое изучать на практической задаче. Писать на чистом OpenGL свой движок не очень хотелось. Долго движок выбирал, выбор пал на LibGDX. Собственно, разницы особой не было, на чём писать.
Почему Bomberman?
Собственно, всегда игрушку эту любил (: Но причина, конечно же, не в этом. Просто размер карты здесь статичен. А во-вторых, нету проблемы с различными размерами экрана. Но, обо всём по порядку.
Архитектура
Для архитектуры, как мне показалось, лучше всего подходит MVC. Я об этом уже рассказывал в статье про архитектуру.
В целом всё довольно просто и понятно. В качестве View у нас класс, который реализует интерфейсы Screen
и InputProcessor
. В классе мы обрабатываем действия пользователя и рендерим объекты. Изменение объектов и последующая отрисовка происходит в одном методе этого класса. Сначала изменяем состояние объектов, если это необходимо (грубо говоря, работаем с физикой), а потом рендерим:
@Override public void render(float delta) { controller.update(delta); renderer.render(); }
Контроллер, думаю понятно, работает с моделью. Большая часть логики как раз в нём. Не совсем был уверен в том, куда всё-таки логику запихнуть. В итоге, часть логики перенёс в сами объекты (обработка коллизий), а логику по обработке взрывов, убийств (т.е. когда объектам надо влиять на другие объекты) в контроллере. Была идея реализовать подобное с помощью сообщений. То есть, в каждом объекте определить хэндлер, который будет обрабатывать сообщения и как-то реагировать. Но тогда проблема с синхронизацией возникла. Поэтому решил всё синхронно обрабатывать. В итоге в контроллере просто последовательно работаем со всеми объектами мира:
public void update(float delta) { processInput(); checkBombsTimer(); if(!world.bonus) destroyBricks(); removeDeadNpc(); killByBoom(); removeHiddenObjects(); bomberman.update(delta); for(NpcBase npc : world.getNpcs()){ npc.update(delta); } removeBooms(); }
Для объяснения модели, думаю, проще диаграмму предоставить:
World
является контейнером, который содержит в себе все объекты: блоки, бонусы, игрока, npc. Никакой логики в нём нет. Лишь в самих объектах часть логики по ним есть (как уже выше писал). Не стал полную диаграмму классов приводить. Все типы npc наследуются от NpcBase
. Все объекты (дверь, бонусы) от HiddenObject
.
Прототип
Изначально разбирался с текстурами и атласом. Первоначально все спрайты были в отдельных файлах. После чего объединил всё в один атлас, что ускорило работу. И тут были проблемы. Тестировал на своём телефоне всё (HTC One S) с Android 4.0. Когда пробовал на более ранних версиях, то игра крашилась. Как оказалось, более старые устройства падают из-за ограничений на размер текстуры. К примеру, какие-то не поддерживали спрайты более 1024×1024. К тому же, изначально размеры у атласы были не степенью двойки, что тоже приводило к падениям на многих устройствах.
Так что: 1) Не делать слишком большие атласы. 2) Размеры должны быть степенью двойки.
Вещи довольно очевидные, но бродя по develop форумам, многие от подобных ошибок страдали). После того, как все собрал в одном атласе и разобрался, как выделять регионы и настроить анимацию, начал обрабатывать коллизии.
Так почему же всё-таки Bomberman?
Дискретная сетка
Большинство объектов имеют целочисленные координаты, да и меняются не так часто. Определить массив объектов public int[][] map;
и обращаться к нему, чтобы постоянно не прогонять списки объектов. Вы скажете, что тогда необходимо постоянно обновлять этот массив. Не совсем, в массиве я храню информацию лишь о положении статичных объектов: блоки, бонусы.
Проблема с размером экрана
Так как размер экрана у устройств разный, то приходится как-то из этого выкручиваться. Кто-то центрирует игровую область, а по краям какие-то текстурки выводить, кто-то растягивает. Я же скалирую по высоте, а затем вычисляю ширину видимой области карты.
float CAMERA_WIDTH =WorldRenderer.CAMERA_WIDTH; float CAMERA_HEIGHT = WorldRenderer.CAMERA_HEIGHT; CAMERA_WIDTH = CAMERA_HEIGHT* Gdx.graphics.getWidth()/Gdx.graphics.getHeight();
В итоге, на всех экранах будет выглядеть одинаково, не будет никаких растянутых текстур. Только видимая область карты будет различаться, но это не критично.
NPC
После того, как разобрался с текстурами и обработкой коллизий, добавил в игре и npc. Стратегий поведения, в сущности, у них всего три: 1) Двигаться в случайном направлении. По прошествии определённого времени меняют направление на рэндомное. 2) Как и первая в целом. Но, если npc видит игрока, то начинает двигаться к нему. Если игрок пропадает из поля зрения, то npc меняет направление движения на случайное. 3) В целом как вторая стратегия. Вот только, если теряет из вида игрока, всё равно идёт в ту сторону, где последний раз видел игрока.
Все виды npc наследуются от NpcBase и должны реализовать методы:
public abstract void changeDirection(float delta); public abstract void update(float delta) ;
GUI
GUI тоже реализовал средствами LibGDX. Все элементы интерфейса представлены в виде классов, экземпляры которых содержаться в классе World. У каждого элемента координаты и размеры есть в свойствах, что позволяет нам определять при тапах по экрану, на какой, собственно, элемент пользователь нажал.
Реализовал два метода управления: 1) Стрелками.
2) Джойстиком.
Реализацию джойстика уже рассматривал, если кому-то именно этот аспект интересен.
Какие-то другие аспекты уже освещал раньше. Про работу с атласом, текстурами, звуками тоже уже писал, так что, думаю, не стоит на этом останавливаться.
Исходники выложил на GitHub.
Если буду какие-то конкретные вопросы, спрашивайте в комментах (:
P.S. скачать сам .apk можно отсюда. Или же по этому QR коду: