Портирование AstroSmash на Android OS: декомпиляция Java ME MIDlet’а

История игры AstroSmash берёт своё начало из далёкого 1981 года. Именно тогда John Sohl разработал её для игровой консоли Intellivision от компании Mattel Electronics. Первоначально разработчик хотел выпустить простую аркаду Meteor!, которая являлась обычным клоном популярной в то время игры Asteroids, но юристы компании хотели избежать судебных притязаний и John’у Sohl’у пришлось в срочном порядке переделывать механику уже готовой игры. Так и появился на свет AstroSmash, который быстро завоевал расположение любителей поиграть в видеоигры и стал коммерчески успешной игрой. Всего было продано около миллиона копий Astrosmash, что сделало эту игру одной из самых продаваемых на приставке Intellivision. Огромная популярность дала свои результаты — по лицензии Mattel Electronics начали выходить похожие игры и для других консолей, например, AstroBlast для Atari 2600. В начале 2000-ых годов о старых-добрых играх вспомнили и производители программного обеспечения для мобильных телефонов.



Порт AstroSmash на Android OS, на фотографии Motorola Photon Q.

Впервые я столкнулся с игрой AstroSmash в 2002 или 2003 году на мобильном телефоне Motorola T720. Это был очень дорогой и функциональный девайс, который поддерживал возможность установки и запуска Java-приложений, что было большой редкостью в то время. Среди предустановленных игр на этом телефоне был и ремейк AstroSmash, выполненный на Java ME. Несмотря на своё примитивное оформление и простейший геймплей, игрушка очень сильно затягивала и я часто соревновался с другом в том, кто больше наберёт в ней очков и попадёт в таблицу рекордов.



Java-игра AstroSmash, запущенная на Motorola T720. Автор фотографии Никита Прокопчик (превью, увеличение по клику).

Ремейк для Motorola T720 тоже получился популярным и Motorola продолжила сотрудничество с компанией THQ, по заказу которой и была разработана Java-игра AstroSmash усилиями программистов из Lavastorm. Позже её переписали на компилируемый язык программирования и выпустили в качестве встроенной в прошивку игры для бюджетных моделей Motorola: C350 и V150.



Нативная игра AstroSmash, запущенная на Motorola V150. Автор фотографии Никита Прокопчик (превью, увеличение по клику).

Правила игры AstroSmash очень простые, игроку необходимо управлять небольшим космическим корабликом с лазерной пушкой и уничтожать пролетающие мимо астероиды и врагов, не дав упасть им на поверхность планеты. Нужно сбить как можно больше объектов и набрать максимальное количество очков. За уничтожение разных врагов выдаётся разное количество очков, а за падение объектов на поверхность и за потери жизней, очки, соответственно, снимаются.

Объект

Очки

Большой метеорит

10

Малый метеорит

20

Большой спутник

40

Пульсар

50

Малый спутник

80

НЛО

100

Таблица очков, которые дают за уничтожение определённого объекта в игре AstroSmash.

Пульсары наводятся на ваш кораблик, поэтому необходимо соблюдать осторожность при их появлении на экране. За каждые 1000 очков добавляется ещё одна жизнь, а когда счёт равен или превышает 5000 очков, на экране появляется НЛО, которое атакует кораблик, сбрасывая на него бомбы, которые можно уничтожить. Если большой или малый спутник упадёт на землю, либо метеорит, его осколки или пульсар столкнутся с корабликом, то он разрушается и в итоге сгорает одна жизнь. При расстреле большого метеорита он распадается на два малых. Если столкновение неизбежно, можно использовать прыжок в гиперпространство, это моментально переместит кораблик в случайно выбранную позицию на экране. При желании можно включить или выключить автоматический огонь.



Страница из инструкции к мобильному телефону Motorola V150, посвящённая игре AstroSmash.

Недавно мой давний друг напомнил мне об этой замечательной игре за разговором и у меня появилась идея сделать ремейк мобильной версии для современных смартфонов под управлением Android OS. Восстанавливать логику AstroSmash и писать собственный движок не очень хотелось, хоть он и был примитивным, и я решил поискать в интернете какую-либо информацию, касательно этой игры. Мои поиски увенчались успехом, на каком-то польском файлообменном сайте я нашёл архив со стандартными Java-играми для Motorola T720, внутри которого был и MIDlet интересующей меня игры, из которого теоретически можно было достать необходимую логику и ресурсы. Итак, распаковав загруженный архив, я приступил к изучению JAR-файла AstroSmash.



Оригинальная игра AstroSmash 1981 года, запущенная на эмуляторе Mattel Intellivision — Bliss.

Содержание:

1. Декомпиляция MIDlet’а с помощью программы Java Decompiler
2. Обзор декомпилированного Java-кода
3. Создание заготовки SurfaceView и сопряжение декомпилированного кода с ней
4. Логические ошибки декомпилированного Java-кода
5. Отрисовка и реализация сенсорного управления
6. Создание лаунчера с таблицей рекордов
7. Дополнительные возможности
8. Заключение, полезные ссылки и ресурсы

1. Декомпиляция MIDlet’а с помощью программы Java Decompiler

JAR-файл представляет собой обычный ZIP-архив, который можно открыть любым архиватором, чем я и воспользовался. Все игровые файлы с данными были доступны в открытом виде и для всех спрайтов использовался формат изображений PNG, так что готовые к использованию ресурсы достаточно было просто извлечь из JAR-архива. Поскольку AstroSmash является проприетарной игрой с закрытым исходным кодом, мне пришлось воспользоваться специальной программой-декомпилятором Java-кода — Java Decompiler, чтобы посмотреть, как устроена логика игры и её движок. Для декомпиляции MIDlet’а достаточно открыть JAR-файл через главное меню программы или перенести его в главное окно.



Программа Java Decompiler с открытым MIDlet’ом AstroSmash (превью, увеличение по клику).

Декомпилированный Java-код получился весьма читаемым, сохранились все названия классов и их переменных. Автоматически сгенерированными были лишь локальные аргументы методов, названия которых выглядели как paramLong, paramInt, paramString, то есть они именовались по типу или классу, которому принадлежал этот параметр-аргумент. Чтобы сохранить все декомпилированные классы в Java Decompiler необходимо выбрать опцию File -> Save All Sources, которая создаст обычный ZIP-архив.

Для изучения декомпилированного исходного кода его можно импортировать в любую IDE, поддерживающую Java, например, Eclipse IDE. Для этого просто создаём новый Java-проект и помещаем всё содержимое сохранённого архива в каталог src/.



Eclipse IDE с декомпилированным кодом игры AstroSmash (превью, увеличение по клику).

Поскольку классы Java ME в стандартной поставке JDK отсутствуют, в исходниках будет куча ошибок при импорте Java ME пакетов, которые будут мешать изучению кода. Чтобы избавиться от них я подключил к проекту библиотеку из пакета MicroEmulator. MicroEmulator позволяет эмулировать прослойку Java ME на различных устройствах, например, на десктопном компьютере. К сожалению, у меня так и не получилось добиться эмуляции AstroSmash, хотя другие MIDlet’ы с помощью MicroEmulator весьма неплохо работали. Но поскольку эмуляция мне не требовалась, я ограничился лишь использованием одной из множества библиотек эмулятора, которая реализовывала подсистему MIDP 2.0. Эту библиотеку можно подключить в настройках проекта: в разделе Java Build Path следует выбрать вкладку Libraries, нажать кнопку Add External JARs… и выбрать файл midpapi20.jar из поставки MicroEmulator, после чего все ошибки, связанные с импортом библиотек Java ME исчезнут.

Декомпиляция Java-классов прошла не совсем успешно, и несколько ошибок компиляции всё-таки всплыло. Все они были связаны с неправильным определением класса или опусканием типа объявляемой переменной и были исправлены простейшим патчем:

После которого никаких неполадок не осталось и можно было приступить к изучению декомпилированного исходного кода.

<< Перейти к содержанию

2. Обзор декомпилированного Java-кода

Любой MIDlet для мобильного телефона это набор определённых классов и игра AstroSmash не является исключением. Главный класс, из которого стартует движок игры, называется AstrosmashMidlet и наследуется от системного класса MIDlet. В этом классе реализована различная рутина, связанная с загрузкой игровых ресурсов, отображением пунктов главного меню игры и некоторые вспомогательные статические методы, связанные с генерацией псевдослучайных чисел. Приблизительным аналогом этого класса в Android OS является класс, отнаследованный от Activity. Некоторые методы и конструктор из AstrosmashMidlet:

В AstrosmashMidlet создаётся объект класса AstrosmashScreen, родителем которого является системный класс Canvas и реализует интерфейс Runnable. В этом классе в отдельном потоке создаётся главный цикл игры с объектом игрового мира и происходит обработка различных событий, к примеру, нажатий на кнопки мобильного телефона. Приблизительным аналогом этого класса в Android OS является наследник системного класса SurfaceView. Конструктор AstrosmashScreen и некоторые из его методов выглядят следующим образом:

Игровым движком является объект класса GameWorld, в нём выполняется обработка поступающих игровых событий, коллизий, поведения противников и пуль, отрисовка готового кадра на канвас. Внутри класса создаётся масса различных объектов, управлением которых занимается главный метод tick(), а метод paint() формирует игровую картинку и выводит её на экран.

Движимые игровые объекты являются экземплярами различных классов, иерархия которых выглядит следующим образом:

Формированием и уничтожением этих объектов во время игры занимаются специальные классы-фабрики: EnemyFactory и MunitionsFactory. За создание игрового фона и интерфейса отвечают классы BackgroundManager и StarManager. В большинстве подобных классов присутствуют метод tick() для управления и метод paint() для отрисовки объекта на канвас.

Некоторые классы реализуют интерфейсы IDeathListener и IGameWorldListener, которые отвечают за своевременный вызов методов doneExploding() и gameIsOver() соответственно. Имеется ещё несколько вспомогательных классов, реализующих разные игровые экраны, подсчёт количества очков и таблицу рекордов, строки с локализацией и настройки игровых параметров в зависимости от мобильного устройства, которое определяется весьма интересным образом: по разрешению экрана, на который установлена игра.

После того, как я получил небольшое общее представление о том, как работает AstroSmash, я приступил к созданию приложения для Android OS, на основе полученных знаний.

<< Перейти к содержанию

3. Создание заготовки SurfaceView и сопряжение декомпилированного кода с ней

Создав простое Android-приложение, я дополнил его двумя классами: AstroSmashActivity, наследником Activity, и AstroSmashView, отнаследованным от SurfaceView и реализующим интерфейсы SurfaceHolder.Callback и Runnable. Получилась вот такая заготовка, которую теперь можно было соединить с декомпилированными классами AstroSmash:

Я выделил движок игры и его классы в отдельный подкаталог AstroSmashEngine/. Множество декомпилированных классов были просто лишними, поэтому я решил разрешить все зависимости класса игрового мира GameWorld, добавляя ему всё необходимое. Таким образом, отпала надобность в некоторых классах AstroSmash, функциональность которых я реализую позже. AstrosmashMidlet и AstrosmashScreen были заменены на AstroSmashActivity и AstroSmashView соответственно.

Движок AstroSmash использовал стандартные API и классы Java ME, которые, конечно, были недоступны в Android OS. Поэтому мне пришлось заменить их аналогами. Так, класс Image был заменён на класс Bitmap, а класс Graphics — на связку из классов Canvas и Paint. Ресурсы для класса Bitmap, а в моём случае это спрайты в формате PNG, необходимо было разместить в каталоге res/drawable-nodpi/, чтобы они не масштабировались в зависимости от DPI экрана Android-смартфона. Помимо этого, для объектов Bitmap пришлось пробрасывать вниз по цепочке конструкторов экземпляр класса Context, чтобы иметь возможность загружать изображения из ресурсов в необходимых классах таким методом:

Класс Graphics в Java ME очень похож на таковой из JDK, но в Android OS отрисовку нужно выполнять с помощью классов Canvas и Paint. Методы Graphics и Canvas несколько различаются, но весьма похожи по смыслу, к примеру, вместо drawImage() следует использовать drawBitmap(), а вместо drawString() необходимо вызывать drawText(). Кроме этого следует помнить о координатах, они тоже немного различаются, например, если в метод drawText() передать начальные координаты X и Y равные нулю, то текст будет отрисовываться за экраном, это же поведение справедливо и для отрисовки Bitmap’ов.

После всех выполненных мной преобразований, мне удалось избавиться от ошибок и собрать APK-пакет, который я запустил на Android-эмуляторе. Примечательно, что всё заработало с первого раза и приложение отобразило мне уменьшенный игровой контекст. Я пробросил обработку аппаратных кнопок в AstrosmashScreen и попробовал немного поиграть, но ничего путного из этого не получилось: при любом игровом столкновении, даже когда я стрелял и попадал по врагу, мой кораблик разрушался и я терял одну жизнь.



Первая попытка запуска порта AstroSmash, часть скриншота из Android-эмулятора.

Стало очевидно, что просчёт коллизий в движке работает неправильно, и его необходимо поправить.

<< Перейти к содержанию

4. Логические ошибки декомпилированного Java-кода

Ещё во время изучения декомпилированного кода игры я заметил странный метод intersects() в классе Collidable:

Больше всего меня удивило длиннющее логическое выражение в нём. Почему-то оно всегда возвращало true и кораблик думал, что в него что-то врезалось и погибал. Я переписал этот код так:

И всё чудесным образом заработало! Видимо Java-декомпилятор не справился со сложным логическим условием и где-то допустил ошибку. К слову, исходный код метода intersect() класса Rect, выглядит вот так:

К сожалению, это была не единственная логическая ошибка декомпиляции, вторым моим разочарованием было отсутствие отрисовки звёзд в игре. Над этой ошибкой я долго бился, а причина оказалась банальной: неправильно задавался цвет маркера и звёзды рисовались чёрным цветом на чёрном небе. Оказалось, что декомпилятор развернул все константы в int вида 16777215 и при вызове метода:

Вместо белого цвета устанавливался чёрный, поскольку холст был формата ARGB_8888. Эту проблему я решил с помощью класса Color, который возвращал правильное значение цвета:

Подобным образом я исправил остальные проблемы со цветами и звёзды зажглись на небе! Третья отловленная ошибка была забавной: когда игру ставили на паузу, она просто вылетала. Дело оказалось вот в чём: в движке есть метод, который отрисовывает сразу две текстовых строки на экране, верхнюю и нижнюю:

Когда игра окончена, в верхней строке отображается надпись GAME OVER а в нижней — количество набранных очков. Когда игрок вызывает паузу, в верхней строке отображается GAME PAUSED, а нижняя строка остаётся пустой. Так вот декомпилятор вместо пустой строки подставил туда null и вызов метода для отрисовки сообщения паузы стал выглядеть так:

Естественно, что вызов любого метода относительно null сразу бросает исключение. Я поправил код таким образом:

После чего игра перестала падать.

<< Перейти к содержанию

5. Отрисовка и реализация сенсорного управления

Добившись нормальной работы движка игры я решил заняться отображением игрового конетекста на весь экран и сенсорным управлением. Растянуть картинку не представляет особой сложности, нужно лишь изменить аргументы метода drawBitmap() таким образом, чтобы он имел представление о параметрах двух прямоугольников: об исходном и о том, который должен получиться в итоге. Исходным прямоугольником является тот маленький отображаемый игровой контекст, а результирующим — прямоугольник с размерами экрана современного Android-устройства. Всё это можно приправить сглаживанием по желанию, чтобы растянутая картинка не так сильно резала глаз.

Модификацией метода drawBitmap() я добился необходимого и картинка растянулась так, как и следовало ожидать.



Растянутый на весь экран игровой контекст, скриншот с Motorola Photon Q (превью, увеличение по клику).

Теперь пришло время задуматься о сенсорном MultiTouch управлении. Я решил разбить экран на три части: нижняя часть должна отвечать за передвижение кораблика, верхняя часть экрана за приостановку игры на паузу, а центральная, самая большая, за стрельбу, когда отключена функция автоматического огня. Кораблик должен следовать за движением пальца по нижней части экрана и стрелять, когда нажимают в центр. Для реализации такого поведения я перегрузил метод onTouchEvent() в классе AstroSmashActivity и наполнил его следующим кодом:

Приватный метод convertCoordX() выполняет преобразования между координатами исходного и результирующего прямоугольников и выглядит следующим образом:

Получилось весьма неплохо, но обнаружилась маленькая проблемка: палец, который управлял корабликом, полностью закрывал его. Я решил в нижней части контекста отрисовать прямоугольник со стрелкой в 15% от высоты экрана. Простенькую стрелку было решено рисовать процедурно.



Стрелка управления корабликом, скриншот с Motorola Photon Q (превью, увеличение по клику).

В общем счёте, метод render() и его вспомогательные методы получились такими:

Поскольку теперь палец перестал перекрывать кораблик, прицеливаться во врагов стало гораздо удобнее.

<< Перейти к содержанию

6. Создание лаунчера с таблицей рекордов

Вместо игровых экранов с настройками и счётом, которые были в MIDlet’e, я решил создать лаунчер к игре и разместить все настройки там, так как их накопилось довольно много. Кроме того, в лаунчер я вынес таблицу с рекордами. Счёт и все параметры сохраняются во внутреннем хранилище игры с помощью класса SharedPreferences и извлекаются после запуска лаунчера AstroSmash. А когда игрок нажимает кнопку Run AstroSmash!, все параметры с формы отправляются в игровой движок. Для реализации лаунчера я создал класс AstroSmashLauncher и унаследовал его от Activity, затем добавил в него всю необходимую рутину, занимающуюся чтением настроек с формы, их сохранением и заполнением формы из настроек.



Лаунчер AstroSmash и таблица рекордов, скриншоты с Motorola Photon Q (превью, увеличение по клику).

Когда игрок в AstroSmash набирает рекордный счёт и завершает игру, появляется диалог, предлагающий ввести ему своё имя и сохранить счёт. Вариант предлагаемого имени автоматически генерируется в зависимости от производителя и модели устройства.



Диалог сохранения счёта, скриншот с Motorola Photon Q (превью, увеличение по клику).

Этот диалог реализован посредством специального класса AstroSmashHighScoreDialog, отнаследованного от Activity, который использует системную тему диалога, выставленную в файле AndroidManifest.xml. В зависимости от нажатой в диалоге кнопки, результат заносится в таблицу или игнорируется. Реализация класса следующая:

Вызов диалога и проверка того, что был получен рекордный счёт, происходит в классе AstroSmashView в методе checkHiScores():

Обмен данными между экземплярами классов-наследников Activity реализован с помощью вызовов методов putExtra() и getIntExtra().

В качестве небольшого украшения и для завершённости я вставил в лаунчер изображения обложек игры AstroSmash, которые шли в ресурсах MIDlet’a и реализовал диалоги About и Help с краткой справкой по игре и различной информацией об авторах и разработчиках, работавших над созданием AstroSmash.



Диалоги About и Help, скриншоты с Motorola Photon Q (превью, увеличение по клику).

Благодаря созданию лаунчера я отказался от использования классов MIDlet’а, которые работали с канвасом и которые было бы трудно перенести и адаптировать под Android OS.

<< Перейти к содержанию

7. Дополнительные возможности

Оригинальная игра AstroSmash, выпущенная для Intellivision имела звуковые эффекты, а её Java-версия для мобильных телефонов почему-то получилась полностью немая. Я решил исправить этот значительный недочёт и добавить в игру звуки и виброотдачу. Для этого я создал статические методы doVibrate(), playSound() и playGameOverSound() в классе AstroSmashLauncher:

В случае с playSound() проигрывается обычный звук в формате OGG из каталога res/raw/ приложения, а в случае с playGameOverSound() используется встроенный системный тоногенератор. Инициализация звука, вибромотора и тоногенератора производится в методе onCreate():

Для проигрывания звука и виброотдачи в нужный момент, я просто расставил эти методы внутри движка в местах расчёта коллизий, разрушения игрового кораблика и сигнала конца игры. Кстати, интересный момент: звуки в формате WAV выводились с ощутимой задержкой, поэтому пришлось конвертировать их в OGG.

По умолчанию звёзды в игре одного цвета — белые. Я решил сделать опцию, позволяющую генерировать звёзды случайно выбранного видимого цвета из спектральной классификация звёзд. Для этого я создал массив цветов, заполнил его таким образом, чтобы белый цвет был преобладающим и реализовал вспомогательные методы:

Теперь звёздное небо в игре стало выглядеть гораздо красивее и реалистичнее:



Цветные звёзды в AstroSmash, скриншот с Motorola Photon Q (превью, увеличение по клику).

Локализация игры уже была заложена в самом MIDlet’е, мне осталось лишь продолжить начатое. Я добавил методы, возвращающие локализованные строки на русском языке в класс InfoStrings и поправил определение текущей локали Android OS.



Локализация лаунчера AstroSmash на русский язык, скриншот с Motorola Droid 4 (превью, увеличение по клику).

Затем я скопировал файл res/values/strings.xml в каталог res/values-ru/ и перевёл в нём все строки. Игра отлично подхватила системную локаль Android OS с русским языком.

<< Перейти к содержанию

8. Заключение, полезные ссылки и ресурсы

Портирование игры AstroSmash с Java ME на Android OS позволило мне разобраться в некоторых тонкостях работы с декомпилированным Java-кодом и научиться создавать приложения для Android OS, использующие в качестве блиттера канвас класса SurfaceView. Этот процесс дал мне огромное количество ценного опыта, я смог сделать интересную реализацию сенсорного управления и научился локализовывать приложения.

Размер установочного APK-пакета получился весьма небольшим, всего лишь 114 КБ. Поскольку я использовал только язык программирования Java, приложение должно отлично работать на всех доступных архитектурах процессоров, на которых работает Android OS.

Скачать APK-пакет AstroSmash, готовый для запуска на любом Android-устройстве, можно по этим ссылкам:

[Скачать APK-пакет AstroSmash, 114 КБ | Download AstroSmash APK-package, 114 КB]

Все исходные коды и проект в целом выложен в репозиторий на ресурсе Github. Мои изменения и исходные файлы доступны под лицензией MIT. Ссылка на репозиторий:

https://github.com/EXL/AstroSmash

В этой работе я использовал огромное количество материалов, основные из них я выделю в полезных ссылках ниже. Огромное спасибо ресурсам stackoverflow.com и google.com за то, что они есть.

  1. Руководство по созданию клона игры Space Invaders с использованием отрисовки на SurfaceView;
  2. Исходники клона игры Babson Space Invaders на Github;
  3. История игры AstroSmash;
  4. Коммерческая реализация игры Intellivision Astrosmash Gen2, доступная в Google Play.

<< Перейти к содержанию

Android, Dev

Портирование AstroSmash на Android OS: декомпиляция Java ME MIDlet’а: 2 комментария

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *