Modern technology gives us many things.

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

88

Уровень сложности Средний Время на прочтение 17 мин Количество просмотров 3.4K Блог компании Timeweb Cloud Ненормальное программирование *Работа с 3D-графикой *Разработка игр *Смартфоны Туториал

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Думаю, большинство моих читателей успела застать эру кнопочных телефонов с поддержкой Java-приложений. Помните ли вы, как мониторили разделы с загрузками на WAP-сайтах и ждали выхода новых игр, которые подойдут под ваш телефон и разрешение экрана? А какой восторг вызывали свежие 3D-игры, где графика с каждым релизом становилась всё лучше и была вполне на уровне PlayStation 1? V-Rally, Galaxy On Fire, Asphalt Urban GT, Deep3D, Sony Ericsson Tennis, Left 2 Die, Counter Terrorism 3D — думаю, хотя бы один из этих тайтлов вам знаком. Но задумывались ли вы, как работали эти игры «под капотом»? Каким образом разработчикам удавалось адаптировать полноценные 3D-шутеры и гонки под железо, где не было 3D-ускорителей (видеокарт), сопроцессора для чисел с плавающей точкой (FPU), а одноядерный процессор, работающий на частоте 100-200МГц, помимо игры обрабатывал ещё и звук, ввод, а также радиомодуль? Сегодня мы узнаем: как разрабатывали игры под J2ME, какие графические API существовали и на каких телефонах поддерживались, почему игры на Sony Ericsson шли лучше, чем на Nokia, а на «закуску» сами с нуля напишем 3D-бродилку в практической части! Интересно? Тогда добро пожаловать под кат!

❯ Предыстория

Обычно принято считать, что полноценные 3D-игры на кнопочных телефонах начали выходить примерно в 2004-2005 году, как раз с выходом графического API M3G. Однако, история мобильного трёхмерного гейминга тянется несколько дальше и уходит корнями в самое начало двухтысячных годов — как раз, когда телефоны только-только начинали обрастать мультимедийным функционалом.

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Мы с вами привыкли, что существовали Java-игры для простых кнопочных телефонов и BREW-игры для телефонов Qualcomm. Но на рубеже нулевых сразу несколько перспективных компаний боролось за право стать разработчиком основной мобильной платформы и вытеснить J2ME. Одной из самых известных была Mophun, которая представляла из себя кроссплатформенную виртуальную машину, исполняющую байткод в собственном формате. И уже в ~2002-2003 году, Mophun представили 3D API для разработки очень симпатичных мобильных игр, которые и работали шустро.

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Кстати, опробовать Mophun-библиотеку вы можете и сами: в РФ из Mophun-девайсов был как минимум Sony Ericsson T610, которые сейчас можно купить чуть ли не по «сотке» на вторичке.

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Помимо Mophun, на французских телефонах производства Alcatel и Sagem была установлена платформа In-Fusio, тоже основанная на J2ME (а может и какой-то собственный сабсет API), однако со своим специфическим набором API, ориентированном на разработку игр. Мы с EXL даже в прошивке OT535 копались, прямо в HEX-редакторе, чтобы найти информацию о встроенной 2.5D-игре-бродилке:

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Как можно заметить, спрос на 3D-игры появился практически в тот момент, когда в мобильные девайсы начали ставить достаточно мощные по тем годам процессоры: ~60-100МГц. Разработчики реально верили, что телефоны можно превратить не только в мультимедийное устройство, но и портативную хэндхэлд-консоль с графикой уровня не хуже GBA!

В сегодняшнем материале я хотел бы сосредоточиться именно на трехмерных Java-играх. Очевидно, что вручную рисовать 3D-графику без использования внешнего нативного API, написанного, например, на C, было бы проблематичным — девайс просто не вывез бы оверхеда Java-машины (хотя есть и исключение — некоторые 2.5D игры используют собственный портальный рендерер по типу того, что был в Duke Nukem 3D).

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

В процессе существования J2ME любая компания могла внести предложение по необязательному расширению мобильной платформы: это называется JCP (Java Community Process), а спецификации в ней — JSR. Таким образом, появилось множество разных расширений: AWT, GLES, M3G, PIM, Bluetooth API и примерно к 2005 году, M3G появился на большинстве кнопочных телефонов. Но мы ведь не одним M3G ограничены! Давайте глянем, какие ещё GAPI существовали на мобилках.

❯ Какие были 3D GAPI?

Под J2ME телефоны существовало сразу три стандартизированных графических API для отрисовки трёхмерной графики, которые были описаны в виде JSR. Вероятно, были ещё какие-то особенные API для телефонов из Азии (где традиционно телефоны акцентировались на играх с крутой 3D-графикой), однако они не поддерживались на обычных устройствах и информации о них очень мало.

Рассмотрим основные GAPI, которые использовались на большинстве телефонов:

  • M3G JSR184: Mobile 3D Graphics — самый популярный графический API, который поддерживался на большинстве Java-телефонов. Известен своей открытостью, функционалом и довольно неплохой производительностью. Строго говоря, M3G — это не только Immediate API для отрисовки треугольников в духе OpenGL или D3D как мы привыкли, но и уже готовый граф сцены (а-ля Unity), формат моделей, система материалов, обработки столкновений, освещения и т. д. Был достаточно хорошо оптимизирован и имел низкий порог вхождения, благодаря чему стал стандартом на мобилках.
  • Mascot Capsule: графический движок, разработанный японской компанией Hi Corp. Использовался в основном на телефонах для азиатского рынка и выдавал очень хороший на момент выхода уровень графики. Во многих аспектах лучше, чем M3G, однако порог вхождения в него несколько выше. Несколько напоминает D3D/OpenGL. Поддерживался на Sony Ericsson и на Motorola.
  • OpenGL ES JSR239: поддерживался только на некоторых моделях и вышел достаточно поздно (в контексте Java-телефонов), из-за чего популярности на простых телефонах не получил, зато активно использовался в смартфонах (стоит взглянуть на игры для iPhone 2G для сравнения). Является самым «тяжелым» и функциональным графическим API из перечисленных. Что интересно: изначально Google полностью перенесли JSR239 на Android, дабы поспособствовать портированию игр с Java-телефонов на смартфоны с зеленым роботом. По первой это, скорее всего, даже помогало.

Большинство читателей застали игры, использующие именно платформу M3G, которая, помимо отрисовки 3D-графики, предоставляла ещё много самых разных фишек: например, уже упомянутый граф сцены с собственным форматом файлов. Поскольку плагины для импорта и экспорта в 3d max были доступны каждому, а сам M3G плохо располагает к тому, чтобы использовать собственные форматы файлов, вскоре на некоторые игры начали повально появляться моды. Пожалуй, одним из рекордсменов по числу модов является игра Left 2 Die, которую как только не переделывали: и под Half-Life, и под Quake, и под обычную Left 4 Dead закос делали… чего только не было!

Другой игрой, на которую часто делали моды — это Comcraft, написанная студентом в начале 2010-х годов. Там, в основном, моды имели чисто характер ретекстура а-ля «новые типы блоков». Всё это было возможно благодаря наличию на Java-телефонах различных Zip-архиваторов, благодаря чему молодые моддеры могли перепаковывать ресурсы игр как им угодно.

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Ну и третья легендарная игра, которую расковыряли через пару лет после выхода — это, конечно-же, Galaxy on Fire 2. Изначально, она была рассчитана для запуска на мощных устройствах уровня Symbian-смартфонов и телефонов Sony Ericsson. Однако умельцы урезали звуки, музыку и игра запускалась даже на s40!

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

А вот с глобальными модами, меняющими геймплей игры, не задалось. Несмотря на то, что программы на Java легко декомпилируются, большинство разработчиков обфусцировали код при публикации игры. По каким-то причинам никто не хотел копаться и деобфусцировать чужой код (хотя это явно гораздо проще, чем копаться в нативном коде в IDA Pro) и хотя бы попытаться сделать некоторое подобие «SDK для модов». Увы 🙁

Читать на TechLife:  И термометр, и пульсоксиметр, и стетоскоп, и электрокардиограф в одном маленьком устройстве. Представлен мультископ Withings BeamO

❯ Секреты производительности

Характеристики типичного кнопочного телефона тех лет были не особо впечатляющими:

  • Процессор: 104-200МГц, ARM926EJ-S ядро, иногда с поддержкой нативного выполнения Java-байткода. Без сопроцессора для чисел с плавающей точкой (FPU), без какого-либо видеоускорителя (за некоторыми исключениями) — вся нагрузка ложилась на процессор и иногда вспомогательный DSP.
  • ОЗУ: 8-16Мб SDRAM-памяти. Java-приложениям не доставалась вся память, а лишь её небольшой кусок, называемый кучей (Heap). Обычно размер Heap был от 800Кб до 2Мб. Умельцы даже патчили Java-машины некоторых телефонах, дабы выделить программам больше памяти. От размера кучи зависела работа тяжелый игр и программ: когда heap заканчивался, приложение падало в OutOfMemoryException.
  • Память: 10Мб-8Гб Flash-памяти.
  • Дисплей: CSTN, TN или AMOLED (редко) матрица с разрешением от 128×128, до 480×320 (возможно бывало и выше).

Очевидно, что на устройстве с такими характеристиками классические техники отрисовки 3D-графики не будут работать из-за малого количества памяти. Поэтому в ход шли некоторые интересные хаки, знакомые нам со времен PlayStation 1 и даже более старых консолей!

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

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

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Пример такого эффекта можно найти в большинстве игр для PlayStation 1 — вот тут, например, лапки обезьяны видны поверх камня, чего быть не должно.

В современных приложениях, для сортировки геометрии по удаленности от наблюдателя используется screen-space техника Z-buffering, которая позволяет «дешево» отсекать невидимую глазу часть геометрии. Принцип её прост: по размерам окна приложения создаётся буфер, где каждый пиксель содержит информацию не о цветности, а о дальности фрагмента в этой конкретной точке. По итогу, во время отрисовки машинки, в Z-буфер будет записана глубина (дальность) конкретного фрагмента (участка геометрии), а когда будет нарисована домик, то видеочип сравнит значение глубины фрагмента машинки с машинкой и если значение глубины, хранящееся в буфере окажется меньше (или больше — зависит от реализации) прошлого значения — тогда часть машинки будет нарисована там, где нужно. Думаю, такое объяснение более чем понятное 🙂

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Однако, Z-буфер требует драгоценную память (минимум ширина экрана * высота экрана * 2 — число байт в half float — т. е. 153 килобайта для экрана 240х320 как минимум) и наличие FPU для достаточной точности буфера глубины. Если же попробовать использовать обычные числа для этого, то вскоре мы столкнемся с проблемами точности, из-за чего будем видеть depth-fighting и от техники будет больше проблем, чем пользы.

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

В телефонах используется точно такая же техника, как и в PS1: сортировка отдельных полигонов. На PS1 с этим помогал отдельный векторный сопроцессор, который управлял трансформацией геометрии, освещением и мог эффективно сортировать треугольники, не нагружая основной процессор. А вот на телефонах этим занят основной процессор, вместе с растеризацией, обработкой игровой логики, звука и радиомодуля. Поэтому для корректной сортировки, вся отрисовка в GAPI на телефонах делится на «батчи», где программист отсылает набор нужных ему команд (отрисовать такую-то модельку с такой-то текстурой и такой-то трансформацией), а API затем уже трансформирует и сортирует вершины наиболее эффективным способом.

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Второй интересный момент — это система координат. Как я уже говорил ранее, аппаратной поддержки float-чисел в телефонах зачастую не было. Однако j2me-машина и компилятор C (в те годы для прошивок чаще использовали ADS, чем GCC) предоставляли программную реализацию float-чисел, которая была ощутимо медленнее аппаратного FPU. Поэтому даже такие операции, как вычисление синуса и косинуса могли стать относительно тяжелыми для телефона, чего уж говорить о перемножении матриц.
Для обхода этого ограничения использовалось две техники: fixed-point числа, где число с дробной частью представлено в виде обычного целого чисел, с которым умеет работать процессор (M3G) и обычные целые числа, которые представляют нормализованные числа 0.0… 1.0 (Mascot Capsule). Оба способа имеют ограниченную точность и в некоторых моментах могут давать артефакты, но здесь всё сильно зависит от самой игры. Например, из-за низкой точности при движении персонажа мир может «трясти».

И третий момент — это текстурирование и освещение. Сама концепция идентична той, которая используется в большинстве современных игр, однако из неё исключается важный этап: перспективно-корректное наложение текстур. Если говорить простыми словами, то при линейном наложении текстуры на геометрию «как есть», если подойти к модельке домика слишком близко — мы увидим искажения текстуры на его поверхности. Другой пример — шахматная доска, которая при приближении будет не идеально ровной, а если подойти совсем близко — то мы получим серьезные артефакты, которые полностью исказят визуальное представление. Для перспективной коррекции нужен тоже нужен FPU: это не бесплатная операция, поэтому от неё обычно отказываются (исключение — M3G), потому игры под J2ME иногда выглядят весьма странно:

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Один из «универсальных» советов: желательно на этапе проектирования уровня и геймплея не ставить слишком близко друг к другу разные объекты и не давать камере игрока «смотреть» слишком близко на большие объекты.

В процессе подготовки статьи, я декомпилировал и изучил несколько 2.5D-игр из нулевых а-ля Wolfenstein 3D. Многие из них для лучшей производительности использовали пакет Nokia UI с DirectGraphics, который предоставлял некоторые плюшки для 2D-игр и возможность блиттинга произвольных изображений напрямую в экранный буфер. Там разработчики на что только не шли: и классический рейкастинг, и портальный рендерер — и всё это работало довольно шустро 🙂

Но вы ведь сюда не учебник по «матану» и не OpenGL Red Book пришли читать, верно? Поэтому давайте напишем свою 3D-бродилку под Java-телефоны с нуля! Да, это не совсем игра, но даст явное представление о том, как писали игры в те годы.

❯ Пишем свою «игру» с нуля

А напишем мы не просто что-то совсем примитивное, а основу для 3D-шутера от первого лица! Да, вот так сразу 🙂 Конечно, не уровень Crysis, графика из 90-х, но для кнопочных девайсов вполне неплохо. Более того, эту игрушку я написал за полтора дня: основной рендерер за полдня и ещё какую-то базовую часть геймплея за день.

В качестве графического API я решил использовать Mascot Capsule. Материала о нём в сети относительно мало и для многих вообще остается загадкой, как он работает «под капотом». Про M3G писал немного aNNiMON, да и некоторая информация в сети есть, а про JSR239 особо из-за плохой поддержки на реальных девайсах. Тут важно понимать, что M3G и Mascot Capsule — это не DX11 и не Vulkan, порог вхождения у них достаточно низкий и понять их довольно легко, если иметь базовые представления о том, как работает 3D-графика. Поэтому создаём проект, мидлет (приложение в терминологии J2ME) и погнали!

Рендерер

Начинаем, конечно же, с инициализации контекста.
В J2ME принято наследоваться от GameCanvas и реализовывать игровой цикл именно в нём. Для начала работы с MascotCapsule и M3G достаточно лишь создать объект Graphics3D (в случае M3G — получить ссылку на синглтон), а также выделить необходимые ресурсы — в нашем случае, это матрицы, которые здесь называются AffineTrans.

Читать на TechLife:  Daihatsu приостанавливает работу всех заводов в Японии

super(false); setFullScreenMode(true); display = Display.getDisplay(midlet); display.setCurrent(this); log(«Initializing renderer»); g3d = new Graphics3D(); g = super.getGraphics(); viewWidth = getWidth(); viewHeight = getHeight(); affineRotationY = new AffineTrans(); affineTranslate = new AffineTrans(); affineMatrix = new AffineTrans();
Самый примитивный игровой цикл будет выглядеть так. Сначала мы заливаем экран цветом для предотвращения эффекта Hall of mirrors и биндим объект Graphics к Mascot Capsule, затем рисуем сцену и освобождаем контекст, а потом вызываем flushGraphics для вывода изображения на экран:

public void startGameLoop() { while(true) { Game.current.update(); g.setColor(0, 0, 255); g.fillRect(0, 0, viewWidth, viewHeight); g3d.bind(g); Game.current.draw(); g3d.release(g); flushGraphics(); } }
Результат: синий экран.

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Давайте что-нибудь нарисуем. Mascot Capsule может использовать как собственный формат моделей mbac и формат анимаций с ActionTable, так и произвольную геометрию. Юзать готовые форматы слишком просто, да и накатывать 3ds Max с плагинами не очень хочется, поэтому мы будем генерировать геометрию сами. Начнем с отрисовки треугольника и трансформации геометрии.

Effect3D — это материал в терминологии Mascot Capsule. Данный объект позволяет задавать визуальные параметры для геометрии: освещение и источник света, Toon-shading для придания эффекта мультяшности, настройки альфа-блендинга и некоторых других эффектов.

Далее идёт трансформация геометрии: процесс преобразования треугольников из локальной системы координат в мировую, экранную и затем уже Clip-space.
Где affineMatrix — основная матрица, хранящая в себе результат перемножения viewProj матриц, а affineRotationY и affineTranslate — матрицы трансформации сначала для камеры, а затем уже для преобразования модели в мировые координаты. При этом проекция — тоже часть AffineTrans. Вот такая вот путаница.

В FOV (512) задается значение в радианах, где 0 — это 0, 4096 — это 3.14 * 2 (360 градусов). Это же касается и углов в поворотах.

affineMatrix.setIdentity(); // View transformation affineRotationY.setRotationY(CameraRotation + (2048)); affineTranslate.lookAt(new Vector3D(-CameraX, 0, -CameraZ), new Vector3D(0, 0, -4096), new Vector3D(0, -4096, 0)); affineMatrix.mul(affineRotationY); affineMatrix.mul(affineTranslate); // Model matrix calculation affineRotationY.setRotationY(ry); affineTranslate.lookAt(new Vector3D(x, y, -z), new Vector3D(0, 0, -1), new Vector3D(0, 4096, 0)); affineMatrix.mul(affineTranslate); affineMatrix.mul(affineRotationY); // Final transformation matrix.setAffineTrans(affineMatrix); matrix.setCenter(getWidth() / 2, getHeight() / 2); matrix.setPerspective(1, 4096, 512);
Обратите внимание — матрицы имеют размерность 3×3, а не 4×4, как это принято в «больших» системах. translate здесь нет — только lookAt.

Идём дальше — к фактической отрисовке треугольников. Геометрия может иметь текстурные координаты, цвета и нормали. Формат вершины можно задавать динамически — необязательно передавать сразу все аттрибуты вершин. Из-за особенностей Mascot Capsule, геометрия не будет отрисована, если не переданы текстурные координаты или цвет.

UV-координаты указываются в абсолютных координатах текстуры. т. е. не 0..255, как на 3dfx Voodoo, например, а 0… ширина текстуры и 0… высота текстуры. Учитывая, что класс текстуры не позволяет даже её размер узнать… решение так себе.

int[] verts = { 0, 0, 128, 128, 0, 128, 128, 128, 128 }; int[] uv = { 0, 255, 255, 255, 255, 0 }; int[] empty = new int[verts.length]; g3d.renderPrimitives(tex, 0, 0, matrix, ef, vertFormat, 1, verts, empty, uv, empty); g3d.flush();
Результат:

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Добавляем второй треугольник, дабы нарисовать квад:

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

На этом, половина рендерера написана. Я не шучу 🙂

Теперь генерируем геометрию для кубика. Для наглядности я написал простенький класс для генерации геометрии на лету. Отдельный формат для моделей нам пока не нужен, поэтому я «запеку» геометрию в отдельный массив вершин.

public static Mesh makeCube(int size, int texSize) { Mesh mesh = new Mesh(36); // Front mesh.addVertex(0, 0, 0, 0, texSize); mesh.addVertex(size, 0, 0, texSize, texSize); mesh.addVertex(size, size, 0, texSize, 0); mesh.addVertex(0, 0, 0, 0, texSize); mesh.addVertex(0, size, 0, 0, 0); mesh.addVertex(size, size, 0, texSize, 0); // Back mesh.addVertex(0, 0, size, 0, texSize); mesh.addVertex(size, 0, size, texSize, texSize); mesh.addVertex(size, size, size, texSize, 0); mesh.addVertex(0, 0, size, 0, texSize); mesh.addVertex(0, size, size, 0, 0); mesh.addVertex(size, size, size, texSize, 0); // Left mesh.addVertex(0, 0, 0, 0, texSize); mesh.addVertex(0, 0, size, texSize, texSize); mesh.addVertex(0, size, size, texSize, 0); mesh.addVertex(0, 0, 0, 0, texSize); mesh.addVertex(0, size, 0, 0, 0); mesh.addVertex(0, size, size, texSize, 0); // Right mesh.addVertex(size, 0, 0, 0, texSize); mesh.addVertex(size, 0, size, texSize, texSize); mesh.addVertex(size, size, size, texSize, 0); mesh.addVertex(size, 0, 0, 0, texSize); mesh.addVertex(size, size, 0, 0, 0); mesh.addVertex(size, size, size, texSize, 0); return mesh; }
Результат:

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Текстурированные кубики рисовать умеем, камера у нас тоже есть: уже можно реализовать бродилку 🙂

Уровни

Уровни делать мы будем… прямо в IDE. Каким образом? Уровень будет храниться классическим для подобных игр способом: сетка x на x, где каждое число указывает ID-текстуры (и в оставшихся битах можно разместить какие-нибудь атрибуты), где 0 — блок отсутствует. Все блоки предполагаются твердыми.

private int[] tiles = { 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 2, 4, 0, 4, 0, 4, 4, 2, 4, 4, 4, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 2, 2, 2, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, };
Отрисовка подобного уровня в самом простом случае очень простая: мы просто проходимся по всей сетке и рисуем каждый куб, если ему назначена текстура. Однако, это не очень эффективный метод: в случае больших уровней с множеством перекрытых комнат, на GAPI ложится лишняя работа по сортировке геометрии, а также страдает филлрейт. Лучше всего разделить такие уровни на комнаты и рисовать только видимые участки уровня.

public void render(Renderer renderer) { renderer.drawSky(skyTexture); for(int i = 0; i < worldHeight; i++) { for(int j = 0; j < worldWidth; j++) { int tex = tiles[i * worldWidth + j]; if(tex != 0) renderer.drawMesh(cubeMesh, textureBank[tex — 1], j * CUBE_SIZE, CUBE_SIZE / 2, i * CUBE_SIZE, 0); } } }
Результат:

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Теперь нам необходимо реализовать возможность ходьбы по уровню. Для этого мы будем поворачивать камеру по координате Y при нажатии кнопок вправо и влево, а для движения вперед и назад вычислять forward-вектор, указывающий на направление персонажа относительно угла поворота, который представляет из себя синус и косинус угла поворота персонажа в радианах.

Читать на TechLife:  Капитан ФСБ, шпионы и подставной гендиректор: разбираем атаку на нашего топ-менеджера

Напомню, что углы хранятся в виде 4096 = 360гр. = 3.14 * 2.

int vInput = Game.current.renderer.VerticalInput; float rad = ((float)Rotation / 4096) * (3.14f * 2); float prevX = X; float prevZ = Z; boolean isMoved = vInput != 0; X -= -Math.sin(rad) * (vInput * MOVEMENT_SPEED); Z -= Math.cos(rad) * (vInput * MOVEMENT_SPEED);
Теперь наш персонаж умеет ходить!

Однако без пола и потолка игра выглядит не особо привлекательно. Самое время их добавить! Настоящий вертикальный потолок реализовать будет затруднительно из-за отсутствия коррекции текстур — пол будет постоянно «плыть», что не очень красиво. Поэтому мы воспользуется дедовскими методами и закрасим нижнюю часть экрана серым, а потолок сделаем фоновой картинкой! Таким образом, каким бы большим не был уровень, в центре экрана всегда будет какая-то стенка, из-за чего мы не сломаем нашу перспективную проекцию!

Смешивать Graphics и Graphics3D одновременно нельзя — сильно падает производительность. А вот использовать Graphics для отрисовки интерфейса поверх Graphics3D после отрисовки кадра — можно! Суть хака простая: рисуем с ортографической (параллельной) матрицей половинку экрана с текстурой неба, а вторую половинку — просто серый квад. Всё очень легко и просто!

int[] verts = { 0, 0, 0, viewWidth, 0, 0, viewWidth, viewHeight / 2, 0, 0, 0, 0, 0, viewHeight / 2, 0, viewWidth, viewHeight / 2, 0 }; int[] uv = { 0, 0, 255, 0, 255, 255, 0, 0, 0, 255, 255, 255 }; int color = ((128 << 16) | (128 << 8) | 128); int[] colors = { color, color, color }; Effect3D ef = new Effect3D(); affineMatrix.setIdentity(); matrix.setAffineTrans(affineMatrix); matrix.setCenter(0, 0); matrix.setParallelSize(viewWidth, viewHeight); g3d.renderPrimitives(tex.nativeHandle, 0, 0, matrix, ef, Graphics3D.PRIMITVE_TRIANGLES | Graphics3D.PDATA_TEXURE_COORD, 2, verts, verts, uv, verts); g3d.flush(); affineMatrix.setIdentity(); matrix.setAffineTrans(affineMatrix); matrix.setCenter(0, viewHeight / 2); matrix.setParallelSize(viewWidth, viewHeight); g3d.renderPrimitives(tex.nativeHandle, 0, 0, matrix, ef, Graphics3D.PRIMITVE_TRIANGLES | Graphics3D.PDATA_COLOR_PER_FACE, 2, verts, verts, uv, colors); g3d.flush();
Рисуем оружие поверх экрана и получаем некое подобие 3D-шутера.

Game.current.renderer.getGraphics().drawImage(weapon, Game.current.renderer.getWidth() — weapon.getWidth() + (int)viewBobFactor, Game.current.renderer.getHeight() — weapon.getHeight(), Graphics.LEFT | Graphics.TOP);
Результат:

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Обратите внимание на все артефакты, о которых я рассказывал в разделе оптимизации игр. И текстуры плывут, и мир дребезжит — всё это результаты оптимизаций со стороны разработчиков GAPI. Зато работает шустро!

Обработка столкновений

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

Принцип прост: обычная проверка AABB, весь резолвинг — это откидывание игрока обратно, если после последнего движения он «врезался в стену».

float prevX = X; float prevZ = Z; boolean isMoved = vInput != 0; X -= -Math.sin(rad) * (vInput * MOVEMENT_SPEED); Z -= Math.cos(rad) * (vInput * MOVEMENT_SPEED); if(collideTest()) { X = prevX; Z = prevZ; isMoved = false; } public boolean collideTest(int x, int y, int size) { final int collideMargin = 64; for(int i = 0; i < worldHeight; i++) { for(int j = 0; j < worldWidth; j++) { if(tiles[i * worldWidth + j] != 0 && aabbTest(x, y, (j * CUBE_SIZE) — CUBE_SIZE, i * CUBE_SIZE, CUBE_SIZE + collideMargin, CUBE_SIZE + collideMargin)) { return true; } } } return false; }
Можно считать, что основа игры уже готова.

А где звуки!?

J2ME предоставляет довольно расширяемое API для воспроизведение звуков: никаких внешних JSR для этого не нужно. Из коробки обычно поддерживаются midi и wav.

Загружаем звук:

boolean isWave = fileName.indexOf(«.wav») > 0; try { player = javax.microedition.media.Manager.createPlayer(getClass().getResourceAsStream(«/sound/» + fileName), isWave ? «audio/x-wav» : «audio/midi»); player.realize(); } catch (IOException ex) { Renderer.log(«Failed to open audio stream » + fileName); ex.printStackTrace(); } catch (MediaException ex) { Renderer.log(«MediaException occured on audio stream » + fileName); ex.printStackTrace(); }
Проигрывание:

public void play() { try { if(!isPlaying()) player.start(); } catch (MediaException ex) { ex.printStackTrace(); } }
Корректируем код управления персонажем:

float prevX = X; float prevZ = Z; boolean isMoved = vInput != 0; X -= -Math.sin(rad) * (vInput * MOVEMENT_SPEED); Z -= Math.cos(rad) * (vInput * MOVEMENT_SPEED); if(collideTest()) { X = prevX; Z = prevZ; isMoved = false; } if(isMoved) { viewBob++; sndFoot.play(); }

❯ Что у нас получилось?

Давайте посмотрим. Запускаем бродилку в эмуляторе:

Но как насчет реального девайса? Для тестов у меня есть SE W200i, W610i и J10i2!
Все поддерживают Mascot Capsule, так что с тестами проблем не будет. Начнем с самого слабенького — W200i:

Переходим к девайсу на той же платформе, но с чуть более высоким разрешением экрана:

А как себя ведет игра на одном из последних кнопочных девайсов Sony Ericsson, с разрешением 240х320?

❯ Заключение

Вот как-то так в нулевых и писали 3D-игры для кнопочных телефонов. Да, мы практически не затронули тему 3D-игр на Symbian-смартфонах (ранее я писал статью о разработке игры под WinMobile), но обсудили множество тонкостей и написали собственный Proof of Concept бродилки для подобных Java-мобилок. Исходным кодом бродилки я делюсь, кто угодно может использовать его как основу для своей игры или что-то типа такого. По крайней мере, видел пару лет назад несколько Java-игр, которые кто-то всё ещё делает.

Пишите свой опыт с Java-играми в комментариях 🙂

P. S.: Друзья! Время от времени я пишу пост о поиске различных китайских девайсов (подделок, реплик, закосов на айфоны, самсунги, сони, HTC и т. п.) для будущих статей. Однако очень часто читатели пишут «где ж ты был месяц назад, мешок таких выбросил!», поэтому я решил в заключение каждой статьи вставлять объявление о поиске девайсов для контента. Есть желание что-то выкинуть или отправить в чермет? Даже нерабочую «невключайку» или полурабочую? А может, у этих девайсов есть шанс на более интересное существование! Смотрите в соответствующем посте, что я делаю с китайскими подделками на айфоны, самсунги, макбуки и айпады!

Понравился материал? У меня есть канал в Телеге, куда я публикую бэкстейдж со статей, всякие мысли и советы касательно ремонта и программирования под различные девайсы, а также вовремя публикую ссылки на свои новые статьи. 1-2 поста в день, никакого мусора!

Возможно, захочется почитать и это:

  • ➤ Сам написал, сам погонял: как я написал 3D-гонки «на жигулях» за неделю, полностью с нуля?
  • ➤ Как запустить собственную GSM-сеть за пять минут при помощи SDR
  • ➤ Важные аспекты Unicode, о которых должен знать каждый разработчик JavaScript
  • ➤ Движок для игры от первого лица в 265 строках Javascript
  • ➤ Sid Meier’s Civilization III от Firaxis – история создания

Сам написал, сам поиграл: как работали трёхмерные игры на кнопочных телефонах нулевых? Пишем 3D-шутер с нуля

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста. Всё ли вам было понятно? 67.44% Да, вполне. 29 25.58% Не особо, но это не вина автора. 11 4.65% Не особо и это вина автора. 2 2.33% Постарайся писать такие статьи в более научно-популярном стиле. Лучше проще и понятнее, чем сложно и дроп на середине статьи. 1 Проголосовали 43 пользователя. Воздержались 5 пользователей. Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста. Интересно было узнать о том, как работали 3D игры на кнопочных телефонах? 70.59% Да. Пустил скупую слезу. 36 13.73% Нет. Ничего интересного в материале для себя не нашел. 7 5.88% Эх вы, молодёжь, расслабились совсем… Вот мы в ваши годы полигоны вручную сортировали, без всяких Z-буферов! Шейдеры? Какие шейдеры, сделать красивый эффект в несколько пассов — вот это было круто! 3 9.8% Я креведко (Хабр не дал удалить лишний пункт опроса) 5 Проголосовал 51 пользователь. Воздержались 6 пользователей. Теги:

  • timeweb_статьи
  • bodyawm_ништячки
  • 3D-шутер
  • PlayStation 1
  • WAP
  • Sony Ericsson
  • Nokia
  • J2ME
  • игры
  • HEX
  • API
  • Duke Nukem 3D
  • Java

Хабы:

  • Блог компании Timeweb Cloud
  • Ненормальное программирование
  • Работа с 3D-графикой
  • Разработка игр
  • Смартфоны

Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку Задонатить
Источник

Каталог товаров с купонами и промокодами онлайн

Оставьте ответ

Ваш электронный адрес не будет опубликован.

©Купоно-Мания.ру