Игра на чистом JavaScript за 20 минут
На JS можно создавать сложные и простые игры любых жанров. Мы расскажем как создать 2D игру на JavaScript и HTML5 всего за 20 минут.
На html странице прописывается лишь тег канвас, а также подключение JS файла, в котором будет происходить обработка всей функциональности. К примеру, HTML файл может выглядеть следующим образом:
В JS файле необходимо найти нужный канвас по id и указать способ работы с ним.
Добавление изображений и аудио
Далее необходимо загрузить все изображения, а также аудио файлы, которые будут использоваться в игре. Для этого используйте класс Image и Audio соответсвенно. Ниже вы можете скачать все необходимые картинки, а также аудиофайлы к игре.
Код добавления изображений и аудио в игру:
Рисование объектов
Весь код игры стоит помещать в этот метод, ведь в нем он будет постоянно обрабатываться и игра будет выглядеть живой и анимированной.
Видео урок
Это были лишь небольшие азы перед созданием самой игры. Предлагаем вам ознакомиться с небольшим видео уроком, в ходе которого вы создадите небольшую 2D игру на чистом JavaScript’е.
Весь JS код игры
Ниже вы можете посмотреть на полностью весь код JavaScript файла, который был создан в ходе видео урока выше:
2D игра на чистом JavaScript
К каждому шагу прилагаются редактируемые live-примеры, с которыми можно поиграть, чтобы увидеть, как должна выглядеть игра на промежуточных этапах. Вы изучите основы использования элемента для реализации таких фундаментальных игровых механик, как рендеринг и перемещение изображений, обнаружение столкновений, механизмы управления, а также состояния выигрыша и проигрыша.
Для извлечения максимальной пользы из этой серии статей необходимо иметь средние (или хотя бы базовые) знания языка JavaScript. После прохождения этого урока вы сможете создавать собственные простые браузерные игры.
Детали к урокам
Все уроки и версии игры MDN Breakout доступны в GitHub:
Лучший способ получить надёжные знания в области разработки браузерных игр — это начать с чистого JavaScript. Затем можно выбрать любой фреймворк для использования в своих проектах. Фреймворки — это инструменты, созданные на языке JavaScript; поэтому, даже если вы планируете работать с ними, нелишним будет сначала изучить сам язык, чтобы понимать, что именно происходит внутри. Фреймворки ускоряют разработку и помогают справиться со скучными частями игры, но если что-то работает не так, как ожидалось, всегда можно попытаться отладить код или написать собственное решение на чистом JavaScript.
Примечание. Если вам интересно узнать о разработке двухмерных игр с помощью игровой библиотеки, ознакомьтесь с альтернативной серией статей 2D игра Breakout с использованием Phaser.
Примечание. Эту серию статей можно использовать как материал для практических занятий по разработке игр. Также можно воспользоваться набором инструментов Gamedev Canvas Content Kit, основанном на этом уроке, если нужно сделать доклад о разработке игр в целом.
Следующий шаг
Ладно, давайте начнём! Перейдите к первой главе — Создание Canvas и рисование на нем.
Разработка игр на JavaScript: реально и безболезненно
Почему JavaScript?
Масса людей думает, что все крутые игры (God Of War, Assassin’s Creed, Skyrim, добавь по вкусу) созданы на C++. Это отчасти так. В проекте принимают участие сотни специалистов из разных отраслей, в том числе и разработчики, юзающие другой язык – обычная распространенная практика.
Некоторые классные игры написаны на “непопулярных” языках программирования, и это нормально. Если ты работаешь с JavaScript, то не нужно после этой статьи бросаться изучать “плюсы”, оставайся с JavaScript.
Существуют Unity, Unreal Engine, CryEngine и прочие классные решения для создания игрушек, и если тебе удобно развлекаться с ними – пожалуйста. Поэтому нет никакой разницы, на чем ты будешь кодить, но в нашем случае речь пойдет о JS-фреймворках.
Основы
Не нужно забывать о творении команды Khronos Group. WebGL – это веб-версия спецификации OpenGL ES, позволяющая разработчикам общаться с видеокартой через браузер (поверь, лучше не знать, как это работает).
Таким образом, можно создавать 2D и 3D сцены на GPU (что эффективнее, чем на CPU). Супер! Но если взглянуть на код JavaScript, использующий эти технологии, тебе поплохеет.
Поэтому давай разбираться с фреймворками, оберегающими нас от canvas и абстрагирующими от WebGL.
2D Frameworks
PixiJS
Этот инструмент можно назвать 2D-рендером WebGL. Это означает, что данная библиотека включает в себя множество функций, предназначенных для эффективной отрисовки 2D-сцен и объектов. Так проще сосредоточиться на создании программного кода, а хардкорные “низкоуровневые” вещи оставить разработчикам PixiJS.
Это не полноценный фреймворк, но он делает свою работу настолько здорово, что многие игровые JS-фреймворки юзают его в качестве движка для рендеринга.
Если ты планируешь создать что-то большее, чем анимация, то поищи дополнительные библиотеки для других частей игровой разработки (физика, масштабирование, tilemaps и т. д.).
ExcaliburJS
Здесь у нас полноценный игровой фреймворк, написанный на Typescript. Полная система сцен и камер, спрайты и анимации, звуки, физика и т. д. – все, что пожелаешь. Многим очень нравится API, предоставляемый ExcaliburJS, т. к. с ним уютнее.
Это связано с тем, что создатели продукта из мира веб (некоторые являются веб-разработчиками, другие — DevOps), поэтому большинство шаблонов и подходов – это штуки, которые уже популярны в веб-разработке. Если тебе близка веб-разработка, попробуй этот инструмент.
ImpactJS
ImpactJS начал свой путь со звания “Первый фреймворк для веб-игр”. Большинство фреймворков, рассмотренных ранее, были просто экспериментами, а не коммерческим продуктом. Этот опенсорсный претендент распространяется бесплатно и поставляется с хорошим редактором уровней.
Фреймворк не является самым понятным или документированным, но его надежность уже доказана. Например, разрабы из CrossCode взяли за основу форкнутую версию Impact для своего движка за его производительность и способность масштабироваться под конкретную задачу.
CreateJS
CreateJS – это набор модульных библиотек и HTML5-инструментов, работающих асинхронно или параллельно в зависимости от ситуации.
Инструмент предоставляет все, что нужно для создания игры с нуля, с помощью отдельного модуля языка JavaScript. Например, для рендеринга можно взять PixiJS, а для работы со звуковыми материалами SoundJS и т. д.
PhaserJS
И напоследок самый популярный – PhaserJS. Это мощный набор инструментов для создания веб и мобильных игр. Этот фреймворк имеет огромное и активное сообщество – каждую неделю эти ребята выкладывают много новых статей, демо и туториалов, основанных на PhaserJS. Это обеспечивает отличное подспорье для людей, делающих свои первые шаги в геймдеве и нуждающихся в наставлениях. А еще, начиная с 3-й версии, это один из самых производительных игровых фреймворков.
3D Frameworks
ThreeJS
ThreeJs – самая популярная 3D-библиотека. Она предлагает наборы функций для выполнения общих операций, которые должны происходить в 3D-сцене. Все мероприятия происходят на более высоком уровне, чем raw WebGL, и не надо заморачиваться с горой низкоуровневых действий.
BabylonJS
Этот фреймворк похож на предыдущий, но имеются различия:
Литература
Как в любом уважающем себя мануале, далее идет подборочка книг по теме.
Кстати, у нас есть очень крутая статья по книгам для геймдэва – рекомендуем!
Мы подобрали для тебя литературу по базовым вещам. Когда определишься, какая ветка игровой разработки тебе больше нравится, будет легче подбирать книги JavaScript.
Создание игры на Javascript Canvas
Здравствуйте! Я предлагаю вам со мной создать небольшую казуальную игру на нескольких человек за одним компьютером на Javascript Canvas.
В статье я пошагово разобрала процесс создания такой игры при помощи MooTools и LibCanvas, останавливаясь на каждом мелком действии, объясняя причины и логику добавления нового и рефакторинга существующего кода.
p.s. К сожалению, Хабр обрезает большие раскрашенные статьи где-то на шестидесятитысячном символе, потому я была вынуждена вынести пару участков кода из статьи на pastebin. Если вы хотите прочитать статью, не бегая по ссылках в поисках кода — можно воспользоваться зеркалом.
Правила
Управляем игроком (Player), который должен поймать наживку (Bait) — и при этом увернуться от появляющихся «хищников» (Barrier).
Цель игры — поймать максимальное количество наживок, не соприкоснувшись с хищниками.
При соприкосновении с одним из хищников все они(хищники) пропадают, а очки — обнуляются, что, фактически, равносильно началу игры с нуля.
HTML файл
Создаем начальный файл, на котором будет наше канвас-приложение. Я воспользовалась файлами, размещенными на сайте libcanvas, но это — не обязательно. JavaScript-файлы добавлялись в процессе создания игры, но я не хочу больше возвращаться к этому файлу, потому объявлю их сразу.
[[ Код ./index.html на pastebin ]]
Создаем проект
Для начала создадим сам проект. Нам нужно сделать это только тогда, когда документ будет готов — воспользуемся предоставленным mootools событием » domready »
Также создадим объект LibCanvas.Canvas2D, который поможет нам с анимацией.
Добавляем пользователя
Добавим новый объект — Player, который будет управлятся мышью — его координаты будут равны координатам курсора мыши.
Этот объект будет выглядеть как кружочек определенного цвета и размера (указанные в свойстве)
[[ Код ./js/Player.js на pastebin ]]
Добавим в ./js/start.js перед libcanvas.start(); :
= Шаг 1 =
Можно заметить, что результат не совсем такой, как мы ожидали, потому-что после каждого из кадров холст не очищается автоматически.
Необходимо добавить очищающий и заливающий черным препроцессор в ./js/start.js
= Шаг 2 =
Добавляем наживку
[[ Код ./js/Bait.js на pastebin ]]
Рефакторинг — создаем родительский класс
У нас очень похожие классы Bait и Player. Давайте создадим класс GameObject, от которого у нас они будут наследоваться.
Для начала вынесем createPosition из конструктора класса Player:
./js/Player.js
Теперь создадим класс GameObject
[[ Код ./js/GameObject.js на pastebin ]]
После этого можно облегчить другие классы:
Смотрим, ничего ли не сломалось:
= Шаг 3 =
Ура! Все везде работает, а код стал значительно легче.
Дружим игрока с наживкой
У нас на экране все двигается, но реально никакой реакции на наши действия нету.
Давайте начнем с того, что подружим наживку и игрока — при набегании на неё, наживка должна перемещаться в другое случайное место.
Для этого создадим отдельный от рендеринга таймаут, который будет проверять соприкасание.
Пишем в конец ./js/start.js:
Теперь надо реализовать метод isCatched в ./js/Bait.js:
= Шаг 4 =
Почти отлично, но мы видим, что перемещение грубовато и раздражающе. Лучше было бы, если бы наживка плавно убегала.
Для этого можно воспользоваться одним из поведений ЛибКанвас. Достаточно добавить одну строчку и слегка поменять метод move:
Теперь надо реализовать метод isCatched в ./js/Bait.js:
Очень просто, правда? А результат мне нравится намного больше:
= Шаг 5 =
Добавим хищников
Также чуть изменим ./js/start.js, чтобы при ловле наживки появлялись хищники:
Реализуем добавление барьеров для игрока, ./js/Player.js и двигаем их все каждую проверку:
= Шаг 6 =
Отлично, появилось движение в игре. Но мы видим три проблемы:
1. Хищники улетают за игровое поле — надо сделать «отбивание от стен».
2. Иногда наживка успевает схватиться дважды, пока улетает — надо сделать небольшой таймаут «неуязвимости».
3. Не обработан случай смерти.
Хищники отбиваются от стен, наживка получает небольшое время «неуязвимости»
[[ Код Barrier.move на pastebin ]]
Исправить проблему с наживкой тоже не очень сложно — вносим изменения в класс Bait, в файле ./js/Bait.js
[[ Код Bait.makeInvulnerable на pastebin ]]
= Шаг 7 =
Одиночная игра — закончена!
= Шаг 8 — одиночная игра =
Реализуем многопользовательскую игру за одним компьютером
Движение с клавиатуры
= Шаг 9 =
Неприятный нюанс — игрок заползает за экран и там может заблудиться.
Давайте ограничим его передвижение и немножко отрефакторим код, вынеся получение состояния клавиши в отдельный метод
Также слегка изменим метод isMoveTo — чтобы можно было с легкостью изменять клавиши для управления игроком:
= Шаг 10 =
Вводим второго игрока
Изменяем файл ./js/start.js:
= Шаг 11 =
Осталось сделать небольшие поправки:
1. Сдвинуть счёт второго игрока ниже счёта первого игрока.
2. Раскрасить хищников разных игроков в разные цвета.
3. Ради статистики ввести «Рекорд» — какой максимальный счёт каким игроком был достигнут.
Вносим соответствующие изменения в ./js/Player.js:
Вносим коррективы в ./js/start.js:
players [ 0 ]. color = ‘#09f’ ;
players [ 0 ]. barrierColor = ‘#069’ ;
Поздравляем, игра сделана!
= Шаг 12 — игра на двоих =
Добавляем третьего и четвертого игрока
При желании очень просто добавить третьего и четвертого игрока:
Разработка браузерной онлайн игры без фреймворков и движков
В этом посте будет описан процесс разработки онлайн игры на чистом javascript и WebGL (без фреймворков и движков). Будут рассмотрены некоторые алгоритмы, техники рендеринга, искусственный интеллект ботов и сетевая игра. Проект является полностью опенсорсным, в конце поста будет ссылка на репозиторий.
Сразу же геймплейное видео:
Тут играет бот по имени Лягуха и другие боты
Введение
Клиент написан на javascript с использованием WebGL, а сервер на node.js. В этом проекте используются следующие библиотеки:
Генерация уровня
Для генерации карты используется обычный Шум Перлина плюс некоторые преобразования и фильтрации:
Где perlinNoise() — шум Перлина, в диапазоне от 0 до 1,
abs(x) — модуль числа,
clamp(x) — если x 1, то x = 1,
norm(-0.5, 2) — нормализует шум в данный диапазон.
Именно такая последовательность преобразований на выходе даёт требуемую топологию карты: она состоит из комнат и коридоров, шириной в 2-3 пикселя.
Получившаяся текстура является маской земли (черный цвет — земля, белый — стена).
Далее в отдельном буфере генерируется река лавы. Для этого генерируется ломаная (если нужны притоки, то несколько ломаных) и каждый отрезок ломанной «рисуется» в буфер с помощью алгоритма Брезенхама. Затем этот буфер с отрезками размывается по Гауссу. Радиус размытия выбирается исходя из требуемой ширины реки. Для оптимизации размытия используется двухпроходный алгоритм. Функция размытия нам ещё не раз пригодится. Размытие буфера с лавой нужно для того, чтобы придать реке ширину, а так же в дальнейшем для рендеринга.
В маске земли, где проходит лава и в непосредственной близости от неё, удаляются стены:
Слева направо: маска земли с предыдущего этапа; маска лавы; расчищаем стены от лавы; финальный результат
Перед тем, как получить этот самый финальный результат, над маской земли проводятся несколько преобразований: фильтрация мелких деталей (стены и проходы шириной в один пиксель нежелательны); заливка изолированных областей (в процессе генерации могут образоваться зоны полностью изолированные стенами со всех сторон, их лучше залить белым, чтобы игроки там не спавнились); по краям добавляется слой стен шириной в 5 пикселей, чтобы игроки не сбежали с карты.
Ну и в конце генерируются мосты. Тут все просто: выбираются случайные точки на лаве так, чтобы мосты не были слишком близко друг к другу или слишком далеко, и ориентируются они перпендикулярно потоку лавы в данной точке (естественно мосты не запекаются в левелмапу, они хранятся просто как массив объектов).
Помимо масок земли и лавы генерируются еще две вспомогательные: маска для текстур (т.к. в игре на землю накладываются две разные текстуры, эта маска задает коэффициенты смешивания этих текстур) и маска статичных теней от стен. Маска для текстур — это всё тот же шум Перлина.
Перед тем, как маску земли запечь в левелмапу её следует размыть по Гауссу. Это нужно для создания плавных контуров.
Все вышеперечисленные маски и составляют нашу левелмапу. Вот из чего она состоит: R канал содержит маску лавы; G — маска земли; B — маска для смешивания диффузных текстур; A — тени.
Рендеринг уровня
Если прямо сейчас взять и отрендерить левелмапу, то получится примерно следующее:
Слева прямоугольник на левелмапе показывает охват камеры
Как такое получить? Допустим у нас есть матрица камеры (мы же точно знаем позицию своего игрока и угол его поворота). В вершинном шейдере эта матрица умножается на текстурные координаты полноэкранного квада, а результат передаётся во фрагментный шейдер. Эти текстурные координаты ни что иное, как координаты фрагмента внутри левелмапы. Поэтому просто делаем выборку из неё по этим координатам. На скриншоте выше выводятся только RG каналы левелмапы.
Получилось слишком размыто (фильтр Гаусса даёт о себе знать). Давайте к результату выборки из левелмапы применим клампинг:
Смысл этой формулы: level.rg у нас меняется плавно от 0 до 1 (видно на скриншоте выше). После клампинга все значения выборки меньшие 0.5 обращаются в 0, а большие 0.516 обращаются в 1, а значения на отрезке [0.5, 0.516] «растягиваются» в отрезок [0, 1]. Вот так это выглядит:
Пришло время для текстур: две диффузные текстуры смешиваются по маске из B-канала левелмапы, с ними смешивается текстура стены по G-каналу, а получившийся результат смешивается с текстурой лавы по R-каналу левелмапы (смешивание цветов в шейдерах GLSL производится функцией mix — это обычная линейная интерполяция двух значений по коэффициенту смешивания). Для выборки из текстур используется всё те же текстурные координаты домноженные на некий коэффициент, который влияет на количество повторений этой текстуры по всей карте:
Всё выглядит слишком плоским. Чтобы это исправить в наш шейдер нужно передать ещё одну текстуру с шумом Перлина. Выборку из этой текстуры прибавляем к значению R-канала левелмапы (маска лавы). Получившийся коэффициент используется не только для смешивания лавы и земли, но и для затенения береговой линии. Также добавим статические тени, которые хранятся в A-канале левелмапы:
Выглядит уже лучше, обратите внимание как преобразилась береговая линия, теперь она не такая ровная и плоская.
Осталось решить последнюю проблему: мы можем видеть через стену (то есть видим области, которые отгорожены стеной от нашего персонажа). Для решения этой проблемы используется трассировка фрагмента: из исследуемого фрагмента выпускается луч в сторону персонажа и делаются выборки из левелмапы (нас интересует только G-канал с маской стены), в данном проекте используется 12 выборок. По результатам выборок можно определить, виден ли фрагмент из позиции персонажа или нет. Для полноэкранного прохода такая трассировка слишком дорогая операция, поэтому она выполняется в отдельном фреймбуфере малого размера (64х64). Эта текстура называется картой видимости:
До применения карты видимости и после
Карта видимости используется не только в полноэкранном проходе для рендеринга карты, но и для рендера всех игровых объектов (мосты, пушки, персонажи).
Рендеринг лавы
Теперь давайте анимируем лаву. Для этого нужно создать карту смещений. В этой текстуре в RG-каналах хранится 2D вектор, который складывается с текстурными координатами для выборки из текстуры лавы. Причем нужна не просто карта смещений, а карта смещений меняющаяся со временем. В данной игре для создания карты смещений используется фреймбуфер размером 512х512. В шейдер этого прохода передаётся текстура с шумом Перлина и время, измеряемое от 0 до 1.
Кусок фрагментного шейдера (упрощено):
где noise — шумовая текстура (она статичная).
Смысл этого шейдера в том, что делаются 4 выборки из одной и той же шумовой текстуры, но с разным масштабом текстурных координат. Да ещё и смещаются они со временем в перпендикулярных направлениях (обратите внимание на вторую выборку, там используется смещение time.yx, а в первой — time.xy). Время зашито только в x-компоненту переменной time, а y-компонента содержит 0.
Теперь эту анимированную текстуру можно передать в шейдер лавы. В этом шейдере делаем выборку из карты смещений и прибавляем полученный 2D вектор к текстурным координатам лавы.
Упрощенный кусок шейдера:
где tex_wave — карта смещений (ну или карта волн).
С коэффициентом 0.1 можно поиграться, влияет он на «возмущение» лавы.
В принципе, на этом можно было бы и остановиться, выглядит довольно неплохо. Но можно сделать ещё лучше — заставить лаву течь. Для создания течения нам понадобится сгенерировать ещё одну текстуру: карту скоростей. Когда генерировалась лава, мы создали ломаную и нарисовали отрезки этой ломаной в буфер лавы. Теперь заведем ещё один буфер для карты скоростей. Нарисуем в него отрезки ломаной, но при этом каждый отрезок будет заносить в этот буфер вектор направления отрезка. Затем размоем этот буфер по Гауссу, тем самым мы не только «размешаем» скорости на изломах реки и на пересечениях с притоками, но и получим эффект «вязкой лавы», то есть скорость потока лавы в середине реки выше, чем у берегов.
Карта скоростей используется для смещения текстурных координат лавы. Чтобы появился эффект течения, смещать нужно с учетом времени.
Итак, обновленный шейдер лавы (упрощено):
Тут делается две выборки из текстуры лавы на расстоянии друг от друга в точности равное vel.xy и смешиваются по значению времени.
Всё вместе выглядит так:
Декали в данной игре — это лужи крови, следы от взрывов ракет и других пуль.
Декали, как объекты, сами по себе нигде не хранятся и не процессятся. Каждая декалька, в момент своего появления, рендерится в огромную текстуру, которая покрывает всю карту. Эта текстура потом «натягивается» на левелмапу. То есть, она содержит в себе все когда либо появлявшиеся декали (естественно она не чистится).
Теперь давайте выясним, какое разрешение должно быть у этой текстуры с декалями. Экспериментальным путём было выяснено, что 16х16 текселей текстуры, приходящиеся на один тайл карты выглядит оптимально (если брать 32х32, то это уже больше, чем размер самой декальки, например следа от взрыва, а по памяти 4-кратный проигрыш). Самая большая карта имеет размер 256х256, тогда разрешение текстуры с декалями равно 16 * 256 = 4096х4096.
На самом деле текстура 4К*4К не создаётся, вместо этого всю карту «распиливаем» на зоны 32х32. На каждую такую зону приходится собственная текстура с декалями 512х512. Всего таких текстур для большой карты 64. Чтобы не передавать пачку этих текстур в главный проход рендеринга карты (и не процессить из какой именно делать выборки) был создан ещё один фреймбуфер. Для него настраивается ровно та же матрица камеры, что и для рендеринга карты. И все текстуры с декалями по-очереди рендерятся в неё с учётом своих позиций на карте (естественно большая часть из них отсекается до вызова drawcall). Теперь в главный проход рендеринга карты передаём только этот готовый фреймбуфер.
Физика
Чтобы определить факт столкновения некой точки со стеной или попадание точки в лаву, нужно прочитать значение из массива с размытой левелмапой по целочисленным координатам этой точки, а так же по соседним координатам. Вручную произвести линейную интерполяцию этих значений и сравнить с неким пороговым значением.
Для определения нормали к стене в точке (X, Y) нужно произвести такую операцию трижды: для точек (X, Y), (X + 0.25, Y), (X, Y + 0.25):
Нормали используются не только для вычисления рикошетов, но и для столкновения персонажа со стеной. Когда персонаж пересекает корпусом стену, то в этом же кадре его как бы выпихивает по направлении нормали.
Преимущества и недостатки использования левелмапы по сравнению с тайловым рендером.
Искусственный интеллект ботов
Первая и самая главная задача ИИ ботов — это научить их просто бегать по карте, не обращая внимание на врагов и предметы. То есть адекватное перемещений по коридорам и комнатам, а так же по мостам через лаву. Для этого используется специальный граф, узлы которого назовём вейпоинтами. Такой граф должен покрывать всю карту, проходить через все коридоры и комнаты. Бот просто имеет ссылку на текущий вейпоинт, и, когда достигает его, выбирает случайным образом следующий вейпоинт из тех, на которые ссылается его текущий. Выглядит вроде просто. Но сначала нужно построить этот граф вейпоинтов.
Итак, у нас есть маска земли и маска лавы. Сложим эти две маски, чтобы получить карту проходимости (далее именно с ней и будем иметь дело при построении графа).
Наверняка для того, чтобы построить граф вейпоинтов существует много способов. В данном проекте алгоритм построения графа основан на методе Distance Field (карта расстояний). Вот в этом посте описывается алгоритм построения Distance Field. Смысл карты расстояний — в каждой точке записано расстояние до ближайшей непроходимой точки.
Тут на картинке в цвете закодировано расстояние, чем темнее, тем дальше
В принципе, тут уже угадываются очертания будущего графа. Следующий подготовительный этап — это вычисление градиента карты расстояний. Где градиент близок к нулю, там и будет вейпоинт.
Градиент карты расстояний в точке (i, j) вычисляется примерно так:
Т.о. для всех пикселей карты выполняем этот код (на самом деле только для проходимых пикселей).
Градиент карты расстояний:
Тут, по традиции, RG-каналы кодируют вектор
Зелёные точки — это вейпоинты
Вейпоинты расположились слишком густо. Некоторые из них нужно удалить. В данном проекте минимально допустимое расстояние между вейпоинтами равно 4 пикселя. Поэтому перебираем все пары вейпоинтов и если расстояние меньше допустимого, то удаляем один из них. Естественно пропускаем мостовые вейпоинты, т.к. они никогда не должны быть удалены.
Теперь необходимо соединить эти вейпоинты рёбрами. Перебираем все пары вейпоинтов и соединяем пару ребром тогда, когда оба вейпоинта видимы друг из друга. Для определения видимости используется трассировка: делаем n выборок из карты проходимости вдоль отрезка, соединяющего эти вейпоинты; где n — длина отрезка. Если все выборки дали проходимый пиксель, то соединяем эти два вейпоинта ребром:
Красные линии — это рёбра графа
Получилось слишком много лишних рёбер. Для решения этой проблемы нужно избавиться от треугольников. Как это работает? Возьмём любое ребро с двумя вейпоинтами A и B. Допустим среди соседей A есть вейпоинт C, который в свою очередь является и соседом B. Т.е. образуется треугольник ABC. Самое длинное ребро этого треугольника нужно удалить. То же самое проделываем для всех рёбер:
Некоторые вейпоинты не связаны ребрами с другими — они отфильтровались. Также на картинке видно, что одно ребро прошло по мосту через лаву
Это окончательный граф. Теперь каждый бот может по нему навигироваться.
ИИ бота состоит из стейтмашин, всего их три. Одна стейтмашина задаёт общее состояние бота, а две другие управляют двумя степенями свободы: ноги (перемещение) и корпус. На выходе ИИ бота выдаёт угол поворота и нажатость его клавиш, т.о. единственное отличие бота от реального игрока — персонаж игрока получает ввод от клавиатуры и мыши, а персонаж бота получает ввод от ИИ. Помимо беготни по графу вейпоинтов бот также умеет реагировать на объекты: валяющиеся пушки и пауэрапы, других ботов и некоторые пули. Причем бот реагирует на эти объекты тогда и только тогда, когда они находятся в зоне его видимости: в пределах его «камеры» (как у игрока) и не отгороженные стенами.
В следующем видео демонстрируется ИИ бота по имени Лягуха в поединке с другими ботами на большой карте:
Алгоритм | Время, мс |
---|---|
Генерация текстур частиц | 280 |
Генерация текстур крови | 190 |
Генерация реки лавы | 700 |
Генерация остальной карты | 630 |
Создание графа | 210 (720) |
Всего: | 2010 (2520) |
В скобках указано время без использования хеша для вейпоинтов. Этот хеш служит для быстрого поиска вейпоинтов рядом с данным в некоторых алгоритмах построения графа. Без этого хеша в этих алгоритмах перебираются вейпоинты со сложностью O(n 2 ).
Сетевая часть
Сервер написан на node.js с использованием WebSocket. Весь сервер состоит из трёх частей: master-server — http-сервер, раздаёт статику; game-server — соединяется с мастер-сервером и невидим для пользователя, владеет игровыми комнатами; игровая комната — именно к ней коннектится пользователь по веб-сокету, там бегают другие игроки и боты.
При разработке серверной части был применён подход с использованием фейксокетов. Что это такое? Фейксокеты — это два объекта в клиентской части с интерфесами настоящего клиентского и серверного сокета. Они эмулирует работу реальных сокетов. «Клиентский» и «серверный» фейксокет (в кавычках, потому что они оба клиентские, т.е. браузерные) обмениваются данными друг с другом по средством setTimeout (в целях отладки был выбран интервал он же пинг в 30 миллисекунд). Идея в том, что настоящий серверный код можно отдать браузеру, сказать ему, чтобы он работал в режиме фейксокетов. При этом настоящий клиентский код даже не подозревает, что и сервер работает рядом с ним в браузере, он просто посылает данные в сокет (которым является фейксокет) и получает данные с него. Фейксокеты использовались при разработке и отладки серверного кода. Но настоящий online работает через реальные сокеты.
Даже после окончания разработки серверного кода фейксокеты не пропали зря, они остались в проекте и служат в качестве подстраховки, в случае, если игровая комната по каким-либо причинам недоступна. Как это работает? Когда пользователь заходит в игру, то может случиться два варианта: комната свободна и готова принять нового игрока, тогда ему просто отдаётся ip адрес и порт комнаты и он играет онлайн. Второй вариант: если нет мест в комнатах или все комнаты покрешались, тогда мастер-сервер говорит браузеру переходить в режим фейксокетов, также посылает ему весь серверный код (ну не весь, а только код игровой комнаты). И пользователь играет в игру с ботами исключительно локально (ну не повезло ему), при этом пользователь даже не будет знать, что он играет локально (ну это в идиале).
Фейксокеты сыграли огромную роль при разработке серверной части. Т.к. и клиент и сервер запущены в браузере — их очень легко отлаживать, также можно выводить на экран debug-render, т.е. схемотично рендерить объекты именно в тех позициях, где они реально находятся на сервере в данный момент времени. Можно играться с пингом: менять интервал в функции setTimeout, чтобы реализовать всякие предсказания и посмотреть, как они работают на разных пингах (в данном проекте, правда, не было реализовано никаких предсказаний).
Заключение
Помимо всего вышеперечисленного в игре также есть своя консоль, которая вызывается клавишей тильда (с командами, переменными, автодополнением и историей). Ещё есть довольно неплохой генератор ников и система событий.
Полный исходный код игры выложен на github.
Руководство по запуску (у вас должен быть установлен node.js).
Если у вас уже установлен bower, то этот шаг пропускаем, иначе:
В корне проекта выполняем две команды:
Если 80 порт у вас занят, то можно так:
В принципе, этого уже достаточно, можно зайти на localhost (ну или localhost:8800, если сменили порт) и там уже будет игра, правда локальная через фейксокеты. Если нужен честный онлайн, то в другой консольке выполняем:
Теперь можно зайти в игру не только с этого компьютера, но и с любого другого, просто указав в адресной строке ip мастер-сервера. Чтобы узнать локальная ли игра, нужно вызвать консоль игры и выполнить команду status. Ещё одна интересная команда spectator nick, где nick — имя игрока, тогда камера переместится на этого игрока. По умолчанию будет сгенерирована большая карта на 110 игроков. Чтобы генерировать другие карты, нужно выполнить:
Здесь не опечатка, количество дефисов именно такое.
Эта команда сгенерирует маленькую карту, которая описывалась в этом посте. Поддерживаются следующие size: 0 — маленькая карта, 1 — средняя и 2 — большая карта (по умолчанию).
Запускать лучше всего в браузере chrome, в других браузерах не гарантируется корректная работа.