Практический пример: внутри мирового лабиринта

World Wide Maze — это игра, в которой вы используете свой смартфон, чтобы перемещаться по катящемуся шару через 3D-лабиринты, созданные на веб-сайтах, чтобы попытаться достичь своих целей.

Всемирный лабиринт

В игре широко используются функции HTML5. Например, событие DeviceOrientation получает данные о наклоне со смартфона, которые затем отправляются на ПК через WebSocket, где игроки находят свой путь в 3D-пространствах, созданных WebGL и Web Workers .

В этой статье я подробно объясню, как используются эти функции, общий процесс разработки и ключевые моменты для оптимизации.

Ориентация устройства

Событие DeviceOrientation ( пример ) используется для получения данных о наклоне со смартфона. Когда addEventListener используется с событием DeviceOrientation , обратный вызов с объектом DeviceOrientationEvent вызывается в качестве аргумента через регулярные промежутки времени. Сами интервалы различаются в зависимости от используемого устройства. Например, в iOS + Chrome и iOS + Safari обратный вызов вызывается примерно каждую 1/20 секунды, а в Android 4 + Chrome — примерно каждую 1/10 секунды.

window.addEventListener('deviceorientation', function (e) {
  // do something here..
});

Объект DeviceOrientationEvent содержит данные наклона для каждой из осей X , Y и Z в градусах (не радианах) ( подробнее читайте на HTML5Rocks ). Однако возвращаемые значения также различаются в зависимости от используемой комбинации устройства и браузера. Диапазоны фактических возвращаемых значений представлены в таблице ниже:

Ориентация устройства.

Значения вверху, выделенные синим цветом, определены в спецификациях W3C. Те, что выделены зеленым, соответствуют этим характеристикам, а те, которые выделены красным, отклоняются. Удивительно, но только комбинация Android-Firefox вернула значения, соответствующие спецификациям. Тем не менее, когда дело доходит до реализации, имеет смысл учесть часто встречающиеся значения. Поэтому World Wide Maze использует возвращаемые значения iOS в качестве стандарта и соответствующим образом адаптируется к устройствам Android.

if android and event.gamma > 180 then event.gamma -= 360

Однако он по-прежнему не поддерживает Nexus 10. Хотя Nexus 10 возвращает тот же диапазон значений, что и другие устройства Android, существует ошибка, которая меняет местами значения бета и гаммы. Это рассматривается отдельно. (Возможно, по умолчанию установлена ​​альбомная ориентация?)

Как видно из этого, даже если API, включающие физические устройства, имеют заданные спецификации, нет никакой гарантии, что возвращаемые значения будут соответствовать этим спецификациям. Поэтому их тестирование на всех перспективных устройствах имеет решающее значение. Это также означает, что могут быть введены неожиданные значения, что требует создания обходных путей. World Wide Maze предлагает начинающим игрокам откалибровать свои устройства на первом этапе руководства, но он не сможет правильно откалибровать нулевое положение, если получит неожиданные значения наклона. Поэтому он имеет внутренний лимит времени и предлагает игроку переключиться на управление с клавиатуры, если он не может выполнить калибровку в течение этого срока.

Вебсокет

В World Wide Maze ваш смартфон и компьютер соединены через WebSocket. Точнее, они подключаются через ретрансляционный сервер между собой, т.е. смартфон к серверу и ПК. Это связано с тем, что в WebSocket отсутствует возможность прямого подключения браузеров друг к другу. (Использование каналов передачи данных WebRTC обеспечивает одноранговое соединение и устраняет необходимость в сервере ретрансляции, но на момент реализации этот метод можно было использовать только с Chrome Canary и Firefox Nightly .)

Я решил реализовать это с помощью библиотеки Socket.IO (v0.9.11), которая включает в себя функции повторного подключения в случае истечения времени ожидания или отключения соединения. Я использовал его вместе с NodeJS, поскольку эта комбинация NodeJS + Socket.IO показала лучшую производительность на стороне сервера в нескольких тестах реализации WebSocket.

Сопряжение по номерам

  1. Ваш компьютер подключается к серверу.
  2. Сервер присваивает вашему компьютеру случайно сгенерированный номер и запоминает комбинацию номера и компьютера.
  3. Со своего мобильного устройства укажите номер и подключитесь к серверу.
  4. Если указанный номер такой же, как и на подключенном компьютере, ваше мобильное устройство сопряжено с этим компьютером.
  5. Если назначенного ПК нет, возникает ошибка.
  6. Когда данные поступают с вашего мобильного устройства, они отправляются на компьютер, с которым оно сопряжено, и наоборот.

Вместо этого вы также можете выполнить первоначальное подключение со своего мобильного устройства. В этом случае устройства просто меняются местами.

Синхронизация вкладок

Специальная функция синхронизации вкладок Chrome делает процесс сопряжения еще проще. С его помощью страницы, открытые на ПК, можно легко открыть на мобильном устройстве (и наоборот). ПК принимает номер соединения, выданный сервером, и добавляет его к URL-адресу страницы с помощью history.replaceState .

history.replaceState(null, null, '/maze/' + connectionNumber)

Если синхронизация вкладок включена, URL-адрес синхронизируется через несколько секунд, и ту же страницу можно будет открыть на мобильном устройстве. Мобильное устройство проверяет URL-адрес открытой страницы и, если к нему добавлен номер, немедленно начинает подключение. Это избавляет от необходимости вводить цифры вручную или сканировать QR-коды камерой.

Задержка

Поскольку сервер ретрансляции расположен в США, доступ к нему из Японии приводит к задержке примерно в 200 мс, прежде чем данные о наклоне смартфона достигнут ПК. Время отклика было явно медленным по сравнению с временем отклика в локальной среде, используемой во время разработки, но вставка чего-то вроде фильтра нижних частот (я использовал EMA ) улучшила это качество до ненавязчивого уровня. (На практике фильтр нижних частот был необходим и для целей презентации; значения, возвращаемые датчиком наклона, содержали значительное количество шума, и применение этих значений к экрану приводило к сильной тряске.) Это не помогло. Я не работал с прыжками, которые были явно вялыми, но ничего поделать с этим было нельзя.

Поскольку я с самого начала ожидал проблем с задержкой, я подумал о настройке серверов ретрансляции по всему миру, чтобы клиенты могли подключаться к ближайшему из доступных (таким образом минимизируя задержку). Однако в итоге я использовал Google Compute Engine (GCE) , который в то время существовал только в США, поэтому это было невозможно.

Задача алгоритма Нэгла

Алгоритм Нэгла обычно включается в операционные системы для эффективной связи посредством буферизации на уровне TCP, но я обнаружил, что не могу отправлять данные в реальном времени, пока этот алгоритм включен. (В частности, в сочетании с отложенным подтверждением TCP . Даже при отсутствии отложенного ACK та же проблема возникает, если ACK задерживается в определенной степени из-за таких факторов, как расположение сервера за границей.)

Проблема с задержкой Nagle не возникала с WebSocket в Chrome для Android, который включает параметр TCP_NODELAY для отключения Nagle, но она возникала с WebKit WebSocket, используемым в Chrome для iOS, в котором эта опция не включена. (В Safari, использующем тот же WebKit, также возникла эта проблема. О проблеме было сообщено Apple через Google, и она, по-видимому, была решена в разрабатываемой версии WebKit .

При возникновении этой проблемы данные наклона, отправляемые каждые 100 мс, объединяются в фрагменты, которые достигают ПК только каждые 500 мс. Игра не может функционировать в таких условиях, поэтому она позволяет избежать этой задержки, заставляя серверную часть отправлять данные через короткие промежутки времени (каждые 50 мс или около того). Я считаю, что получение ACK через короткие промежутки времени обманывает алгоритм Нэгла, заставляя его думать, что отправлять данные можно.

Алгоритм Нэгла 1

На приведенном выше графике показаны интервалы получения фактических данных. Он указывает временные интервалы между пакетами; зеленый цвет представляет интервалы вывода, а красный — интервалы ввода. Минимальное — 54 мс, максимальное — 158 мс, а среднее — около 100 мс. Здесь я использовал iPhone с ретрансляционным сервером, расположенным в Японии. Выходное и входное время составляют около 100 мс, работа работает плавно.

Алгоритм Нэгла 2

Напротив, на этом графике показаны результаты использования сервера в США. В то время как зеленые выходные интервалы остаются постоянными на уровне 100 мс, входные интервалы колеблются от минимума 0 мс до максимума 500 мс, что указывает на то, что ПК получает данные порциями.

ALT_TEXT_ЗДЕСЬ

Наконец, на этом графике показаны результаты предотвращения задержки за счет отправки сервером данных-заполнителей. Хотя он работает не так хорошо, как использование японского сервера, очевидно, что интервалы ввода остаются относительно стабильными и составляют около 100 мс.

Жук?

Несмотря на то, что браузер по умолчанию в Android 4 (ICS) имеет API WebSocket, он не может подключиться, что приводит к событию Socket.IO Connect_failed. Внутренне время истекает, и серверная часть также не может проверить соединение. (Я не проверял это только с помощью WebSocket, так что это может быть проблема с Socket.IO.)

Масштабирование серверов ретрансляции

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

Физика

Движение мяча в игре (скатывание вниз по склону, столкновение с землей, столкновение со стенами, сбор предметов и т. д.) осуществляется с помощью 3D-симулятора физики. Я использовал Ammo.js — порт широко используемого физического движка Bullet на JavaScript с использованием Emscripten — вместе с Physijs , чтобы использовать его в качестве «веб-работника».

Веб-работники

Web Workers — это API для запуска JavaScript в отдельных потоках. JavaScript, запущенный как веб-воркер, выполняется как поток, отдельный от того, который его первоначально вызвал, поэтому можно выполнять тяжелые задачи, сохраняя при этом отзывчивость страницы. Physijs эффективно использует Web Workers, чтобы обеспечить бесперебойную работу обычно интенсивного движка 3D-физики. World Wide Maze обрабатывает физический движок и рендеринг изображений WebGL с совершенно разной частотой кадров, поэтому даже если частота кадров падает на машине с низкими характеристиками из-за большой нагрузки рендеринга WebGL, сам физический движок будет более или менее поддерживать 60 кадров в секунду и не будет препятствовать этому. управление игрой.

ФПС

На этом изображении показана результирующая частота кадров на Lenovo G570 . В верхнем поле отображается частота кадров для WebGL (рендеринг изображения), а в нижнем — частота кадров для физического движка. Графический процессор представляет собой интегрированный чип Intel HD Graphics 3000, поэтому частота кадров рендеринга изображения не достигла ожидаемых 60 кадров в секунду. Однако, поскольку физический движок достиг ожидаемой частоты кадров, игровой процесс не сильно отличается от производительности на высокопроизводительной машине.

Поскольку потоки с активными веб-воркерами не имеют консольных объектов, данные необходимо отправлять в основной поток через postMessage для создания журналов отладки. Использование console4Worker создает эквивалент объекта консоли в Worker, что значительно упрощает процесс отладки.

Работники сферы обслуживания

Последние версии Chrome позволяют устанавливать точки останова при запуске Web Workers, что также полезно для отладки. Его можно найти на панели «Рабочие» в инструментах разработчика.

Производительность

Этапы с большим количеством полигонов иногда превышают 100 000 полигонов, но производительность особо не страдает, даже если они были сгенерированы полностью как Physijs.ConcaveMesh ( btBvhTriangleMeshShape в Bullet).

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

Объекты-призраки

Объекты, которые обнаруживают столкновения, но не влияют на столкновение и, следовательно, не влияют на другие объекты, в Bullet называются «объектами-призраками». Хотя Physijs официально не поддерживает объекты-призраки, их можно создавать там, изменяя флаги после создания Physijs.Mesh . World Wide Maze использует объекты-призраки для обнаружения столкновений предметов и точек цели.

hit = new Physijs.SphereMesh(geometry, material, 0)
hit._physijs.collision_flags = 1 | 4
scene.add(hit)

Для collision_flags 1 — это CF_STATIC_OBJECT , а 4 — CF_NO_CONTACT_RESPONSE . Попробуйте выполнить поиск на форуме Bullet, Stack Overflow или документации Bullet для получения дополнительной информации. Поскольку Physijs является оболочкой для Ammo.js, а Ammo.js по сути идентичен Bullet, большинство вещей, которые можно сделать в Bullet, также можно сделать и в Physijs.

Проблема Firefox 18

Обновление Firefox с версии 17 до 18 изменило способ обмена данными Web Workers, в результате чего Physijs перестал работать. О проблеме было сообщено на GitHub, и она была решена через несколько дней. Хотя эта эффективность открытого исходного кода впечатлила меня, этот инцидент также напомнил мне, что World Wide Maze состоит из нескольких различных платформ с открытым исходным кодом. Я пишу эту статью в надежде получить какую-то обратную связь.

asm.js

Хотя это не касается непосредственно World Wide Maze, Ammo.js уже поддерживает недавно анонсированный Mozilla asm.js (неудивительно, поскольку asm.js был в основном создан для ускорения JavaScript, генерируемого Emscripten, а создатель Emscripten также является создателем Ammo.js). Если Chrome также поддерживает asm.js, вычислительная нагрузка физического движка должна значительно снизиться. Скорость была заметно выше при тестировании с Firefox Nightly. Возможно, было бы лучше написать разделы, требующие большей скорости, на C/C++, а затем перенести их на JavaScript с помощью Emscripten?

ВебГЛ

Для реализации WebGL я использовал наиболее активно разрабатываемую библиотеку Three.js (r53). Хотя версия 57 уже была выпущена на последних этапах разработки, в API были внесены серьезные изменения, поэтому для выпуска я придерживался исходной версии.

Эффект свечения

Эффект свечения, добавленный к ядру шара и предметам, реализован с помощью простой версии так называемого « Метода Кавасе MGF ». Однако в то время как метод Кавасе заставляет все яркие области светиться, World Wide Maze создает отдельные цели рендеринга для областей, которые должны светиться. Это связано с тем, что для текстур сцены необходимо использовать скриншот веб-сайта, и простое извлечение всех ярких областей приведет к свечению всего веб-сайта, если, например, он имеет белый фон. Я также рассматривал возможность обработки всего в HDR, но на этот раз отказался от этого, поскольку реализация стала бы довольно сложной.

Светиться

Вверху слева показан первый проход, где области свечения визуализировались отдельно, а затем применялось размытие. Внизу справа показан второй проход, при котором размер изображения был уменьшен на 50 %, а затем применено размытие. Вверху справа показан третий проход, где изображение снова было уменьшено на 50%, а затем размыто. Затем эти три изображения были наложены друг на друга, чтобы создать окончательное составное изображение, показанное в левом нижнем углу. Для размытия я использовал VerticalBlurShader и HorizontalBlurShader , включенные в Three.js, так что еще есть возможности для дальнейшей оптимизации.

Светоотражающий мяч

Отражение на мяче основано на образце из Three.js. Все направления визуализируются с учетом положения мяча и используются в качестве карт окружения. Карты окружения необходимо обновлять каждый раз, когда мяч движется, но поскольку обновление со скоростью 60 кадров в секунду является интенсивным, вместо этого они обновляются каждые три кадра. Результат не такой плавный, как при обновлении каждого кадра, но разница практически незаметна, если не указано иное.

Шейдер, шейдер, шейдер…

WebGL требует шейдеров (вертексных шейдеров, фрагментных шейдеров) для любого рендеринга. Хотя шейдеры, включенные в Three.js, уже позволяют создавать широкий спектр эффектов, написание собственных шейдеров неизбежно для более сложного шейдинга и оптимизации. Поскольку World Wide Maze нагружает процессор физическим движком, я попытался вместо этого использовать графический процессор, написав как можно больше на языке шейдеров (GLSL), даже когда обработка процессором (через JavaScript) была бы проще. Эффекты океанских волн, естественно, зависят от шейдеров, как и фейерверк в точках ворот и эффект сетки, используемый при появлении мяча.

Шейдерные шары

Вышеуказанное взято из тестов эффекта сетки, используемого при появлении мяча. Тот, что слева, используется в игре и состоит из 320 полигонов. Тот, что в центре, использует около 5000 полигонов, а тот, что справа, — около 300 000 полигонов. Даже при таком большом количестве полигонов обработка с помощью шейдеров позволяет поддерживать стабильную частоту кадров 30 кадров в секунду.

Шейдерная сетка

Все мелкие предметы, разбросанные по сцене, объединены в одну сетку, а индивидуальное движение зависит от шейдеров, перемещающих каждый кончик полигона. Это результат теста, целью которого было выяснить, пострадает ли производительность при наличии большого количества объектов. Здесь выложено около 5000 объектов, состоящих примерно из 20 000 полигонов. Производительность ничуть не пострадала.

поли2три

Этапы формируются на основе структурной информации, полученной с сервера и затем полигонизируемой с помощью JavaScript. Триангуляция, ключевая часть этого процесса, плохо реализована в Three.js и обычно терпит неудачу. Поэтому я решил самостоятельно интегрировать другую библиотеку триангуляции под названием Poly2tri . Как оказалось, Three.js, очевидно, пытался сделать то же самое в прошлом, поэтому я добился того, чтобы это заработало, просто закомментировав часть этого. В результате количество ошибок значительно уменьшилось, что позволило играть на гораздо большем количестве игровых этапов. Случайные ошибки сохраняются, и по какой-то причине Poly2tri обрабатывает ошибки, выдавая предупреждения, поэтому я изменил его, чтобы вместо этого генерировать исключения.

поли2три

Выше показано, как синий контур триангулируется и генерируются красные многоугольники.

Анизотропная фильтрация

Поскольку стандартное изотропное MIP-маппинг уменьшает размеры изображений как по горизонтальной, так и по вертикальной осям, просмотр полигонов под косыми углами делает текстуры на дальнем конце этапов World Wide Maze похожими на вытянутые по горизонтали текстуры с низким разрешением. Верхнее правое изображение на этой странице Википедии показывает хороший пример этого. На практике требуется большее горизонтальное разрешение, которое WebGL (OpenGL) решает с помощью метода, называемого анизотропной фильтрацией. В Three.js установка значения больше 1 для THREE.Texture.anisotropy включает анизотропную фильтрацию. Однако эта функция является расширением и может поддерживаться не всеми графическими процессорами.

Оптимизировать

Как также упоминается в этой статье о передовом опыте WebGL , наиболее важным способом повышения производительности WebGL (OpenGL) является минимизация вызовов отрисовки. Во время первоначальной разработки World Wide Maze все внутриигровые острова, мосты и ограждения были отдельными объектами. Иногда это приводило к более чем 2000 вызовам отрисовки, что делало сложные этапы громоздкими. Однако как только я упаковал объекты одного и того же типа в одну сетку, количество вызовов отрисовки сократилось до пятидесяти или около того, что значительно повысило производительность.

Для дальнейшей оптимизации я использовал функцию трассировки Chrome. Профилировщики, включенные в инструменты разработчика Chrome, могут в некоторой степени определять общее время обработки метода, но трассировка может точно сказать вам, сколько времени занимает каждая часть, вплоть до 1/1000 секунды. Подробную информацию о том, как использовать трассировку, можно найти в этой статье .

Оптимизация

Выше приведены результаты трассировки при создании карт окружения для отражения мяча. Вставка console.time и console.timeEnd в соответствующие места в Three.js дает нам такой график. Время течет слева направо, и каждый слой представляет собой что-то вроде стека вызовов. Вложение console.time в console.time позволяет проводить дальнейшие измерения. Верхний график — до оптимизации, нижний — после оптимизации. Как видно на верхнем графике, updateMatrix (хотя это слово усечено) вызывался для каждого рендеринга 0-5 во время предварительной оптимизации. Однако я изменил его так, чтобы он вызывался только один раз, поскольку этот процесс требуется только тогда, когда объекты меняют положение или ориентацию.

Естественно, сам процесс трассировки требует ресурсов, поэтому чрезмерная вставка console.time может привести к значительному отклонению от фактической производительности, что затруднит определение областей для оптимизации.

Регулятор производительности

Из-за особенностей Интернета в игру, скорее всего, будут играть на системах с самыми разными характеристиками. Find Your Way to Oz , выпущенный в начале февраля, использует класс IFLAutomaticPerformanceAdjust для уменьшения эффектов в соответствии с колебаниями частоты кадров, помогая обеспечить плавное воспроизведение. World Wide Maze основан на том же классе IFLAutomaticPerformanceAdjust и уменьшает эффекты в следующем порядке, чтобы сделать игровой процесс максимально плавным:

  1. Если частота кадров падает ниже 45 кадров в секунду, карты окружения перестают обновляться.
  2. Если оно по-прежнему падает ниже 40 кадров в секунду, разрешение рендеринга снижается до 70 % (50 % от площади поверхности).
  3. Если она по-прежнему падает ниже 40 кадров в секунду, FXAA (сглаживание) отключается.
  4. Если она по-прежнему падает ниже 30 кадров в секунду, эффекты свечения исчезают.

Утечка памяти

Аккуратное удаление объектов в Three.js представляет собой своего рода проблему. Но если оставить их в покое, это, очевидно, приведет к утечкам памяти, поэтому я разработал метод, представленный ниже. @renderer относится к THREE.WebGLRenderer . (Последняя версия Three.js использует немного другой метод освобождения, поэтому, вероятно, он не будет работать с ним в исходном виде.)

destructObjects: (object) =>
  switch true
    when object instanceof THREE.Object3D
      @destructObjects(child) for child in object.children
      object.parent?.remove(object)
      object.deallocate()
      object.geometry?.deallocate()
      @renderer.deallocateObject(object)
      object.destruct?(this)

    when object instanceof THREE.Material
      object.deallocate()
      @renderer.deallocateMaterial(object)

    when object instanceof THREE.Texture
      object.deallocate()
      @renderer.deallocateTexture(object)

    when object instanceof THREE.EffectComposer
      @destructObjects(object.copyPass.material)
      object.passes.forEach (pass) =>
        @destructObjects(pass.material) if pass.material
        @renderer.deallocateRenderTarget(pass.renderTarget) if pass.renderTarget
        @renderer.deallocateRenderTarget(pass.renderTarget1) if pass.renderTarget1
        @renderer.deallocateRenderTarget(pass.renderTarget2) if pass.renderTarget2

HTML

Лично я считаю, что самое лучшее в приложении WebGL — это возможность создавать макет страницы в HTML. Создание 2D-интерфейсов, таких как отображение результатов или текста во Flash или openFrameworks (OpenGL), представляет собой довольно трудную задачу. У Flash, по крайней мере, есть IDE, но openFrameworks сложен, если вы к нему не привыкли (использование чего-то вроде Cocos2D может облегчить задачу). HTML, с другой стороны, позволяет точно контролировать все аспекты дизайна интерфейса с помощью CSS, как и при создании веб-сайтов. Хотя сложные эффекты, такие как конденсация частиц в логотип, невозможны, некоторые 3D-эффекты в рамках возможностей CSS Transforms возможны. Текстовые эффекты «ЦЕЛЬ» и «ВРЕМЯ ВЫШЛО» в World Wide Maze анимируются с использованием масштаба в CSS Transition (реализованном с помощью Transit ). (Очевидно, что для градаций фона используется WebGL.)

Каждая страница в игре (заголовок, РЕЗУЛЬТАТ, РЕЙТИНГ и т. д.) имеет свой собственный HTML-файл, и как только они загружаются как шаблоны, в соответствующее время вызывается $(document.body).append() с соответствующими значениями. . Одна из проблем заключалась в том, что события мыши и клавиатуры не могли быть установлены перед добавлением, поэтому попытка el.click (e) -> console.log(e) перед добавлением не работала.

Интернационализация (i18n)

Работа в HTML также оказалась удобной для создания англоязычной версии. Для своих нужд интернационализации я решил использовать i18next , веб-библиотеку i18n, которую я мог использовать как есть, без изменений.

Редактирование и перевод внутриигрового текста выполнялись в электронной таблице Google Docs. Поскольку для i18next требуются файлы JSON , я экспортировал таблицы в TSV, а затем преобразовал их с помощью специального конвертера. Я сделал много обновлений непосредственно перед выпуском, поэтому автоматизация процесса экспорта из таблицы Google Docs значительно облегчила бы задачу.

Функция автоматического перевода Chrome также работает нормально, поскольку страницы созданы с использованием HTML. Однако иногда ему не удается правильно определить язык, принимая его за совершенно другой (например, вьетнамский), поэтому в настоящее время эта функция отключена. ( Ее можно отключить с помощью метатегов .)

ТребоватьJS

В качестве системы модулей JavaScript я выбрал RequireJS . 10 000 строк исходного кода игры разделены примерно на 60 классов (= файлы кофе) и скомпилированы в отдельные js-файлы. RequireJS загружает эти отдельные файлы в соответствующем порядке в зависимости от зависимости.

define ->
  class Hoge
    hogeMethod: ->

Определенный выше класс (hoge.coffee) можно использовать следующим образом:

define ['hoge'], (Hoge) ->
  class Moge
    constructor: ->
      @hoge = new Hoge()
      @hoge.hogeMethod()

Для работы hoge.js должен быть загружен до moge.js, и поскольку «hoge» обозначен как первый аргумент «define», hoge.js всегда загружается первым (вызывается обратно после завершения загрузки hoge.js). Этот механизм называется AMD , и для такого же обратного вызова можно использовать любую стороннюю библиотеку, если она поддерживает AMD. Даже те, которые этого не делают (например, Three.js), будут работать одинаково, если зависимости указаны заранее .

Это похоже на импорт AS3, поэтому это не должно показаться странным. Если у вас окажется больше зависимых файлов, это возможное решение.

r.js

RequireJS включает в себя оптимизатор r.js. Это объединяет основной js со всеми зависимыми файлами js в один, а затем минимизирует его с помощью UglifyJS (или Closure Compiler). Это уменьшает количество файлов и общий объем данных, которые браузеру необходимо загрузить. Общий размер файла JavaScript для World Wide Maze составляет около 2 МБ и может быть уменьшен примерно до 1 МБ с помощью оптимизации r.js. Если бы игру можно было распространять с помощью gzip, ее размер можно было бы еще уменьшить до 250 КБ. (У GAE есть проблема, которая не позволяет передавать файлы gzip размером 1 МБ или больше, поэтому в настоящее время игра распространяется в несжатом виде в виде обычного текста размером 1 МБ.)

Строитель сцены

Данные этапа генерируются следующим образом и полностью выполняются на сервере GCE в США:

  1. URL-адрес веб-сайта, который необходимо преобразовать в сцену, отправляется через WebSocket.
  2. PhantomJS делает снимок экрана, а позиции тегов div и img извлекаются и выводятся в формате JSON.
  3. На основе снимка экрана из шага 2 и данных о положении HTML-элементов специальная программа C++ (OpenCV, Boost) удаляет ненужные области, генерирует острова, соединяет острова мостами, рассчитывает положение ограждений и предметов, устанавливает целевую точку и т. д. Результаты выводятся в формате JSON и возвращаются в браузер.

ФантомJS

PhantomJS — это браузер, не требующий экрана. Он может загружать веб-страницы, не открывая окон, поэтому его можно использовать в автоматических тестах или для создания снимков экрана на стороне сервера. Его браузерный движок — WebKit, тот же, что используется в Chrome и Safari, поэтому его макет и результаты выполнения JavaScript также более или менее такие же, как и в стандартных браузерах.

В PhantomJS для написания процессов, которые вы хотите выполнить, используется JavaScript или CoffeeScript. Делать снимки экрана очень просто, как показано в этом примере . Я работал на сервере Linux (CentOS), поэтому мне нужно было установить шрифты для отображения японского языка ( M+ FONTS ). Даже в этом случае рендеринг шрифтов обрабатывается иначе, чем в Windows или Mac OS, поэтому один и тот же шрифт может выглядеть по-разному на других машинах (хотя разница минимальна).

Получение позиций тегов img и div в основном обрабатывается так же, как и на стандартных страницах. jQuery также можно использовать без проблем.

stage_builder

Сначала я рассматривал возможность использования подхода, в большей степени основанного на DOM, для создания этапов (аналогично 3D-инспектору Firefox ) и попробовал что-то вроде анализа DOM в PhantomJS. В конце концов, однако, я остановился на подходе к обработке изображений. С этой целью я написал программу на C++, использующую OpenCV и Boost, под названием «stage_builder». Он выполняет следующее:

  1. Загружает снимок экрана и файл(ы) JSON.
  2. Преобразует изображения и текст в «острова».
  3. Создает мосты для соединения островов.
  4. Устраняет ненужные мосты для создания лабиринта.
  5. Размещает крупные предметы.
  6. Размещает мелкие предметы.
  7. Устанавливает ограждения.
  8. Выводит данные о местоположении в формате JSON.

Каждый шаг подробно описан ниже.

Загрузка снимка экрана и файлов JSON.

Для загрузки скриншотов используется обычный cv::imread . Я протестировал несколько библиотек для файлов JSON, но с picojson оказалось проще всего работать.

Преобразование изображений и текста в «острова»

Этап сборки

Выше приведен снимок экрана раздела новостей сайта help-dcc.com (нажмите, чтобы просмотреть реальный размер). Изображения и текстовые элементы необходимо преобразовать в острова. Чтобы изолировать эти разделы, нам следует удалить белый цвет фона — другими словами, наиболее распространенный цвет на скриншоте. Вот как это будет выглядеть, когда это будет сделано:

Этап сборки

Белые участки — это потенциальные острова.

Текст слишком мелкий и резкий, поэтому мы утолщаем его с помощью cv::dilate , cv::GaussianBlur и cv::threshold . Содержимое изображения также отсутствует, поэтому мы заполним эти области белым цветом на основе данных тега img, выводимых из PhantomJS. Полученное изображение выглядит следующим образом:

Этап сборки

Текст теперь образует подходящие комки, а каждое изображение представляет собой настоящий остров.

Создание мостов для соединения островов

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

Этап сборки

Устранение ненужных мостов для создания лабиринта

Сохранение всех мостов сделало бы перемещение по сцене слишком простым, поэтому некоторые из них придется убрать, чтобы создать лабиринт. Один остров (например, тот, что вверху слева) выбирается в качестве отправной точки, и все мосты, кроме одного (выбранного случайным образом), соединяющие этот остров, удаляются. Затем то же самое проделывается со следующим островом, соединенным оставшимся мостом. Как только путь заходит в тупик или ведет обратно на ранее посещенный остров, он возвращается к точке, позволяющей получить доступ к новому острову. Лабиринт будет завершен, как только все острова будут обработаны таким образом.

Этап сборки

Размещение крупных предметов

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

Этап сборки

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

Размещение мелких предметов

Этап сборки

Подходящее количество мелких предметов размещается вдоль линий на заданном расстоянии от краев острова. На изображении выше (не с сайта help-dcc.com) проецируемые линии размещения показаны серым цветом, смещены и расположены через равные промежутки от краев острова. Красные точки указывают, где расположены мелкие предметы. Поскольку это изображение взято из версии, находящейся на стадии разработки, элементы расположены прямыми линиями, но в окончательной версии элементы разбросаны немного более неравномерно по обе стороны от серых линий.

Размещение ограждений

Ограждения в основном располагаются вдоль внешних границ островов, но их необходимо отрезать у мостов, чтобы обеспечить доступ. Библиотека Boost Geometry оказалась полезной для этого, упростив геометрические расчеты, такие как определение места пересечения данных о границах острова с линиями по обе стороны моста.

Этап сборки

Зеленые линии, очерчивающие острова, представляют собой ограждения. Возможно, на этом изображении это сложно увидеть, но там, где находятся мосты, нет зеленых линий. Это окончательное изображение, используемое для отладки, в которое включены все объекты, которые необходимо вывести в JSON. Голубые точки — это небольшие предметы, а серые точки — предлагаемые точки перезапуска. Когда мяч падает в океан, игра возобновляется с ближайшей точки возобновления. Точки перезапуска располагаются примерно так же, как и мелкие предметы, через равные промежутки времени на заданном расстоянии от края острова.

Вывод данных о местоположении в формате JSON

Я также использовал picojson для вывода. Он записывает данные в стандартный вывод, который затем получает вызывающая сторона (Node.js).

Создание программы C++ на Mac для запуска в Linux

Игра была разработана для Mac и развернута в Linux, но, поскольку OpenCV и Boost существовали для обеих операционных систем, сама разработка не представляла трудностей после создания среды компиляции. Я использовал инструменты командной строки в Xcode для отладки сборки на Mac, затем создал файл конфигурации с помощью automake/autoconf, чтобы сборку можно было скомпилировать в Linux. Тогда мне просто пришлось использовать «configure && make» в Linux, чтобы создать исполняемый файл. Я столкнулся с некоторыми ошибками, специфичными для Linux, из-за различий в версиях компилятора, но смог относительно легко их устранить с помощью gdb.

Заключение

Подобную игру можно было бы создать с помощью Flash или Unity, что дало бы множество преимуществ. Однако эта версия не требует никаких плагинов, а возможности макетирования HTML5 + CSS3 оказались чрезвычайно мощными. Определенно важно иметь подходящие инструменты для каждой задачи. Я лично был удивлен тем, насколько хорошо получилась игра, полностью созданная на HTML5, и хотя во многих областях ей все еще не хватает, я с нетерпением жду возможности увидеть, как она будет развиваться в будущем.