Друзья! А вы помните, какими были мобильные игры в 2000-х годах? Помните, как разработчики умудрялись уместить целые миры в устройство с небольшим дисплеем, аппаратной клавиатурой, весьма слабым железом и парой сотен килобайт памяти? Но задумывались ли вы, как в своё время работали эти сами игры «под капотом»? В сегодняшней статье-ретроспективе предлагаю вспомнить мобильный геймдев нулевых и узнать, как же работали 2D Java-игры, какие API были доступны и что из себя представлял средний телефон тех лет! Интересно? Тогда добро пожаловать под кат!
❯ Предисловие
Пожалуй, геймдев всегда был одной из самых интересных сфер программирования. Множество нестандартных задач, возможность применить профильные математические и алгоритмические познания, а также огромный простор для архитектов в продумывании архитектуры будущей игры, ведь, например, в больших студиях комплексные и большие игры обычно очень сложные и нередко тянут за собой целый пласт легаси-кода и наработок чуть ли не из 90-х.
Но помимо десктопных и консольных игр, существуют и мобильные игры, которые в последние 5 лет вплотную приблизились к уровню AAA на консолях (привет порту GRID, AC Mirage, RE4 на мобилки). А ведь ещё 15-20 лет назад мы играли на кнопочных телефонах с небольшими дисплейчиками, которые в свое время подарили нам множество эмоций и кайфа от прохождения этих самых игр, несмотря на простенькую графику, не особо комплексный геймплей и относительно простой левел-дизайн. Продвинутые мобильные геймеры играли уже на Symbian-смартфонах и WinMobile-коммуникаторах (да, в какой-то момент времени, устройства на WM были весьма перспективными), но чаще всего — на Java-телефонах Nokia, Sony Ericsson, Siemens и, конечно-же, Samsung с LG!
По правде сказать, игры на смартфонах — тема отдельная, например на Symbian был полноценный телефон-игровая консоль Nokia N-Gage, о которой я писал отдельный материал, а о разработке игры под Windows Mobile я относительно недавно написал отдельную статью. У смартфонов обычно было несколько больше ресурсов: шустрее процессор, значительно больше памяти доступной игре, а также возможность запуска нативного кода, но и игры для них было разрабатывать значительно сложнее.
Зато о том, как работали игры на Java-телефонах информации практически нет и этот недостаток нужно исправлять, ведь это была одна из первых попыток унифицировать формат приложений на телефонах вне зависимости от архитектуры их процессоров и ОС на борту. Недавно я писал о том как работали 3D-игры на Java-телефонах, но там затрагивалась только 3D-часть без 2D, звука, обработки ввода и иных модулей, без которых игра не может работать!
❯ Каким был телефон?
В середине 2000-х годов, обычно телефон представлял из себя девайс в корпусе моноблок/раскладушка/слайдер и «флип» с весьма большим цветным дисплеем, одним/двумя (привет Motorola E398) динамиками и несколькими аппаратными кнопками. В зависимости от ценового сегмента устройства, обычно менялся корпус, разрешение и размер дисплея, а также материалы, из которого был изготовлен девайс. При этом у многих больших вендоров были собственные программные платформы — у Nokia это был S40, у Sony Ericsson своя, у Samsung и LG тоже свои.
В среднем, характеристики телефонов были следующими:
Процессор: ARMv4/ARMv5 на частоте ~100-200МГц. Есть исключения — Siemens E-Gold работал на базе архитектуры C166, а платформа Motorola работала на 66МГц (что и объясняет небольшую тормознутось).
ОЗУ: ~8Мб SDRAM. Эта память распределялась под все нужды системы, в том числе и обработку GSM, Java и пользовательский интерфейс. Java-приложениям было доступно ~1Мб ОЗУ.
Постоянная память: в среднем ~10-30Мб, плюс возможность расширения памяти за счет MicroSD или MS Pro Duo (Sony Ericsson).
Казалось бы, не густо. На самом деле вполне достаточно, учитывая все ограничения телефонов тех лет. Но почему именно Java?
Ещё в начале нулевых, когда прогресс развития телефонов шёл семимильными шагами, перед разработчиками телефонов встал вопрос, какой формат для программ выбрать, дабы привлечь как можно больше разработчиков на рынок мобильных приложений. Очевидно, что нативные программы на C/C++ точно не подойдут (разные архитектуры, большие отличия в платформах), поэтому нужна была виртуальная машина с собственным байткодом. Вариантов было несколько: Mophun, некая корейская виртуальная машина (точного названия, увы, не помню и инфы очень мало) и, конечно-же, Java с JVM. Со временем именно J2ME стала стандартом благодаря оптимальной скорости работы, хорошему и простому API и низкому порогу входа.
❯ Какие API существовали?
Несмотря на то, что игры под кнопочные телефоны писались на Java, набор API и поддерживаемых пакетов отличался от обычной JVM на ПК, которую использует, например, Minecraft. Всего существует три профиля — J2SE (Android и ПК), J2EE (серверы и энтерпрайз) и J2ME (встраиваемая электроника и телефоны). Однако сам по себе J2ME делится ещё на два стандарта — CLDC/CDC (набор поддерживаемых фишек языком — например, ранние телефоны не поддерживали float) и MIDP (набор поддерживаемых телефоном фишек — работа с дисплеем, проигрывание звуков, доступ в сеть и обработка ввода — всё это часть MIDP). За всё время существования было две версии MIDP — 1.0, которая была весьма ограничена в возможностях (например, нельзя было развернуть игру на весь экран) и использовалась с 2001 по ~2003 год и MIDP 2.0, которая использовалась вплоть до кончины J2ME.
Теоретически, появление J2ME должно было стандартизировать игры на телефонах разных производителей… но был нюанс — ведь функционал телефонов рос как на дрожжах, разрешение дисплеев тоже, у телефонов появлялась собственная память и файловая система, возможность подключения к интернету и Bluetooth и появился целых ворох API…
Несмотря на то, что игры по большей части были одинаковыми (или почти одинаковыми) на всех кнопочных телефонах, тем не менее набор поддерживаемых API каждым устройством значительно отличался. Вероятно, вы помните как многие игры подразделялись не только на версии для разных разрешений дисплея, но и на версии для каждого производителя отдельно: Nokia, SE, Samsung и т. п. Для реализации каких-то особых фишек (например, быстрая отрисовка изображений с регулируемой прозрачностью) требовалось использовать пакеты, неподдерживаемые в базовом профиле MIDP. И подобные пакеты делились на два типа — JSR и Vendor-specific пакеты.
JSR — это расширения-спецификации (то есть просто описание классов без какого либо кода), которые вносились в специальную базу Java community process и формально стандартизировались среди всех нормальных производителей телефонов. Среди таких JSR есть и поддержка 3D-графики (JSR184 — M3G, JSR239 — OpenGLES Bindings for J2ME), и доступа к файловой системе устройства (JSR75), и возможность использования Bluetooth для реализации мультиплеера (JSR82). Говоря простыми словами, это опциональные «фишки», которые могли быть доступны на каких-то телефонах, а на каких-то не поддерживались и соответственно игры, которые их используют, в большинстве случаев просто вылетают с ошибкой (однако особенно «умные» игры используют рефлексию и определяют поддерживается ли та или иная функция с помощью метода Class.forName).
Vendor-specific пакеты обеспечивали очень крутой функционал, характерный не просто одному производителю телефонов, а зачастую даже одной линейке телефонов на определенной платформе. На SE такие пакеты практически не использовались (кроме, конечно, Mascot Capsule), а вот на Nokia постоянно (Nokia UI, Nokia S40 API), позволяя на изначально «слабеньких» s40-телефонах рисовать в буфер дисплея напрямую, а также отрисовывать треугольники, рисовать полупрозрачные картинки и выполнять некоторые другие операции, недоступные на других телефонах. У Samsung, же, например, в свое время была поддержка MMF-звуков в мобильных играх, что в начале и середине 2000х годов было просто нереально крутым, даже несмотря на другие ограничения корейских телефонов.
❯ Графика
Возможности по отрисовке графики на кнопочных телефонах были не сказать что сильно широкие, но тем не менее позволяли легко реализовать графику уровня SNES или даже PlayStation 1. Например, в отличии от современных смартфонов, мы не могли использовать шейдеры, умножить спрайт на цвет (дабы придать ему другой оттенок) и даже использовать аффинные трансформации (поворот, скейлинг) — исключительно полупрозрачные спрайты даже без возможности плавно «растворить» спрайт путем изменения его альфы! Поэтому многие разработчики шли на «хак» и предварительно рисовали в редакторе 8-16 положений одного спрайтов с разным углом поворота, дабы потом выбрать нужный в зависимости от физического угла поворота в градусах!
Для графики использовался пакет javax.microedition.lcdui, в котором были классы для построения нативного интерфейса (выглядело так себе на большинстве телефонов), а также механизм фреймов (Form, Canvas).
Для игр же предлагался Canvas и GameCanvas, которые позволяли развернуть поверхность для рисования на весь экран и предлагали инстанс объекта Graphics, который сразу предоставлял механизм двойной буферизации! В свою очередь, Graphics предоставлял методы для отрисовки спрайтов (Image и drawRGB для «сырых» картинок не в нативном-формате, может быть медленно), примитивов (линии, прямоугольники, овалы), текста и… всё! Например, картинку можно было нарисовать вот так:
getGraphics().drawImage(img, 0, 0, Graphics.LEFT | Graphics.TOP);
При этом с шрифтами вопрос был отдельный: у каждого устройства был свой набор поддерживаемых шрифтов и свои фишки, о которых клиентская программа даже могла и незнать: например поздние телефоны поддерживали сглаживание шрифтов (что дико лагало на устройствах типа Nokia Asha), но что самое забавное — шрифты не могли быть произвольного размера, лишь 3х типов (один из них — моноширинный) и 3х размеров (маленький, средний, большой). Немудрено, что многие вендоры реализовывали свои рендереры битмапных шрифтов, которые точно будут нужного разработчику размера.
Но откуда же грузить картинки? Для этого, в Java был использован встроенный механизм открытия ресурсов из JAR: никакого кэша, никаких OBB, все нужные данные сразу в пакете с игрой. Да, это накладывало некоторые ограничения: например на телефонах Samsung долгое время было ограничение ~250Кб на приложение, зато было просто и портативно. Выглядело это вот так:
InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream('/img.png");
Или в случае картинок так:
Image image = Image.createImage("/img.png");
Всё очень легко и понятно, согласитесь?
❯ А звук?
Помните диалог «включить звук» при запуске почти каждой игры? Конечно же помимо графической части, в каждой игре должен быть и звук! И с его реализацией были свои нюансы: ведь в MIDP 1.0 звук поддерживался только с помощью Vendor-specific API (то есть его вообще могло и не быть, зато на телефонах Samsung поддерживался MMF, что, как я уже и говорил раннее, было очень круто).
MIDP 2.0 уже стандартизировал нормальный протокол для общения с мультимедийной подсистемой устройства с помощью пакета javax.microedition.media, в котором было три класса: Player (собственно, сам звук или музыка), PlayerListener (прослушиватель событий от плеера) и Control для управления различными параметрами воспроизведения (громкость, тональность и, вероятно, прочие расширения от производителей типа эквалайзера).
Конечно-же набор поддерживаемых форматов был невелик, но почти все устройства хотя-бы поддерживали wav (для коротких эффектов) и midi (для музыки), на ранних телефонах ни о каком mp3 и речи не шло (именно в Java-приложениях). При этом на некоторых телефонах, насколько мне известно, не было возможности воспроизводить одновременно звуки и музыку из-за отсутствия программного или аппаратного микшера. Интерфейс для воспроизведения звуков был один: мы создаём Player с помощью метода createPlayer, которому передаём адрес нужного ресурса и проигрываем его. Это мог быть как и трек на удаленном сервере (стриминг поддерживался не везде), так и в ресурсах программы:
InputStream is = getClass().getResourceAsStream("/music.wav");
Player player = Manager.createPlayer(is, "audio/x-wav");
player.prefetch();
player.start();
Так почему-же в большинстве игр на телефонах тех лет были midi-мелодии вместо wav? Всё дело в размере и ресурсах: во первых, midi-мелодия на пару минут может весит пару десятков килобайт. Помните «бумер.mid», «europa.mid» и другие известные тогда файлы? Эти треки весили совсем немного благодаря тому, что в отличии от оцифрованных сэмплов (т.е аналоговых данных с микрофона), вес которых зависит от разрешения, наличие стерео и частоты дискретизации, midi оперировали лишь наборами инструментов: что где и когда нужно проиграть. Во вторых, в Java-телефонах был ограниченный объем памяти, а heap мог быть менее 1 мегабайта, поэтому загрузка даже небольшого wav-файла могло быть крайне проблематичным на таком устройстве. Поэтому выкручивались как могли!
Но в целом, аудио-возможности были хорошими. Java-игры славились весьма неплохим звуковым сопровождением для уровня телефонов, явно не хуже GBA.
❯ Мультиплеер! Давай про мультиплеер!
Вероятно многие читатели помнят, что локальный мультиплеер в Java-играх был зачастую Must-have: возможность игры с друзьями по «локалке» собирала все лавочки и подоконники в школах на переменах в жёстких баталиях на бипланах, или, например, в матчах CS для Java!
И для реализации мультиплеера у Java было довольно немало возможностей: в первую очередь, это наличие полноценных TCP-сокетов и Http-подключений с помощью класса Connection. Да, были некоторые ограничения (например на Nokia нельзя было установить TCP-соединение на порт 80 в обход встроенного клиента Http), но тем не менее даже через GPRS можно было создать с кем-то матч и попробовать поиграть, а чуть позже, к 2009 году, в РФ уже появился +- стабильный 3G и можно было поиграть в игры с достаточно быстрым и стабильным интернетом! Но интернет был дорогой, да и смысл ради сессионного матча подключаться к интернету, когда есть Bluetooth?
Появление Bluetooth в телефонах значительно расширяло возможности телефонов в обмене информации на короткой дистанции. Конечно и до этого уже был ИК-порт, который позволял передавать файлы на относительно низкой скорости, но у него была не самая большая стабильность, да и далеко не все можно было успеть перекинуть за время школьной переменной (и не все давали свой телефон «на урок»). Появление OBEX и возможности передачи файлов друг-другу через беспроводной канал дало возможность скидывать музыку и игры прямо на уроке, что было очень круто и позволило некоторым школьникам с флэшкой или телефоном с большим объемом встроенной памяти даже торговать контентом и скидывать, например, эротику за пирожок или школьную пиццу (я застал когда она уже стоила около 10 рублей — весьма немало!). Особо красноречивые ребята умудрялись уболтать друзей себе скидывать весь контент, что был у них на телефонах и становились центром внимания с новым крутым треком — я и сам в некоторой степени таким был (у меня была флэшка на 2 гигабайта!).
Но помимо возможности обмена файлами, Bluetooth также поддерживал некоторые профили: например, подключение к наушникам или протокол L2CAP/RFCOMM для установки соединения клиент-сервер между устройствами, которое и использовалось в Java-играх. Именно оно позволяло сделать один телефон сервером (хостом), а другому — клиентом, который подключается к серверу и они инициируют сессию игры!
❯ Проблемы мультиплатформенности
На бумаге все было хорошо: Java-машина была стандартизированной, поддерживаемые профили тоже и по идее игры и программы должны без проблем запускаться на большинстве Java-телефонов. Но как-бы не так: проблемы с кроссплатформенностью имели место быть. Начиная от упомянутых выше Vendor-specific API и версиями MIDP, заканчивая… как это ни странно, разрешениями экрана.
Да, сейчас игры не зависят от разрешения дисплея благодаря возможности скейлинга картинок до любого размера. Таким образом достаточно заранее нарисовать спрайты для, например, FHD разрешения и просто скейлить их по размеру дисплея в меньшую или большую сторону. Никто не мешает и отдалять камеру в зависимости от разрешения дисплея, впрочем, это считается не очень хорошей практикой (и зависит от игры).
Во времена Java-телефонов, зависимость от разрешения дисплея была критичной и поэтому игры для «не того» разрешения либо выходили за экран, либо наоборот — выглядели слишком маленькими и игрались в небольшом окошке. Многие вероятно вспомнят как устанавливая игру для малого разрешения дисплея, можно было заметить как шлейфом уезжают спрайты за виртуальный экран и остаются на белом фоне…
Небольшие проблемы были и с обработкой ввода. И если резистивные тачскрины поддерживались еще в MIDP 2.0 с помощью обработки определенных событий, то с мультитачем (во времена Asha и поздних телефонов Samsung) было уже сложнее. Другой вопрос что даже коды кнопок почему-то не унифицировали, из-за чего возникало деление на Samsung, Sony Ericsson и Nokia: разработчики J2ME предполагали что смартфоны будут в разных форм-факторах и предоставили лишь механизм для унификации «игровых» кнопок. Таким образом, некоторые игры, собранные под телефоны конкретного производителя могли не реагировать на нажатие кнопок клавиатуры из-за отличающихся кодов клавиш.
❯ Заключение
Друзья! Вы, вероятно, думаете что если телефоны с поддержкой J2ME больше не производятся, значит и коммьюнити уже «всё»? Как-бы не так: после моих статей мне продолжают писать читатели и спрашивать детали реализации тех или иных техник или игровых механик! Да, энтузиастов мало, но они есть, как и у ретро-компьютеров: например, спектрума, или консолей типа NES… А значит наше дело будет жить и Java-телефоны с их играми останутся в наших сердцах, а Java-телефоны останутся на скрижалях истории! Берегите своих кнопочных красавцев и восстанавливайте по возможности, благо пока-что даже корпуса на популярные модели кнопочных телефонов найти относительно легко.
Надеюсь, сегодняшний материал вам был интересен, писал его специально так, чтобы было понятно даже тем читателям, которые не пишут код или знакомы с программированием поверхностно. Подписывайтесь на мой Telegram-канал, куда я публикую различные мысли и советы по ремонту и программированию под гаджеты прошлых лет, подсъемы с новых видосов и всегда актуальные ссылки на новые статьи!
Материал подготовлен при поддержке TimeWeb Cloud. Подписывайтесь на меня и @Timeweb.Cloud, дабы не пропускать новые статьи каждую неделю!