Русский
Русский
English
Статистика
Реклама

Море, пираты 3D онлайн игра в браузере

Приветствую пользователей Хабра и случайных читателей. Это история разработки браузерной многопользовательской онлайн игры с low-poly 3D графикой и простейшей 2D физикой.

Позади немало браузерных 2D мини-игр, но подобный проект для меня в новинку. В gamedev решать задачи, с которыми ещё не сталкивался, может быть довольно увлекательно и интересно. Главное не застрять со шлифовкой деталей и запустить рабочую игру пока есть желание и мотивация, поэтому не будем терять время и приступим к разработке!


Об игре в двух словах


Бой на выживание единственный на данный момент режим игры. Сражения от 2 до 6 кораблей без перерождений, где последний выживший игрок считается победителем и получает х3 очков и золота.

Аркадное управление: кнопки W, A, D или стрелки для движения, пробел для залпа по вражеским кораблям. Прицеливаться не нужно, промахнуться нельзя, урон зависит от рандома и угла выстрела. Больший урон сопровождается медалькой точно в цель.

Зарабатываем золото занимая первые места в рейтингах игроков за 24 часа и за 7 дней (сброс в 00:00 по МСК) и выполняя ежедневные задания (на день выдается одно из трех, по очереди). Золото за сражения тоже есть, но меньше.

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

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

Стек технологий


Three.js одна из самых популярных библиотек для работы с 3D в браузере с хорошей документацией и большим количеством различных примеров. Кроме того, я использовал Three.js и раньше выбор очевиден.

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

Node.js потому что просто, быстро и удобно, хотя опыта непосредственно в Node.js не имел. В качестве альтернативы рассматривал Java, проводил пару локальных экспериментов в том числе с веб-сокетами, но сложно ли запустить джаву на VPS выяснять не решился. Ещё один вариант Go, его синтаксис вгоняет меня в уныние не продвинулся в его изучении ни на йоту.

Для веб-сокетов используется модуль ws в Node.js.

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

Получается вот такая схема:



PHP нужен в первую очередь для отдачи клиенту веб-страничек и для редких AJAX запросов, но по большей части клиент общается всё же с игровым сервером на Node.js по веб-сокетам.

Мне совсем не хотелось связывать игровой сервер с БД, поэтому всё идет через PHP. На мой взгляд тут есть свои плюсы, хотя не уверен значимы ли они. Например, так как в Node.js приходят уже готовые данные в нужном виде, Node.js не тратит время на обработку и дополнительные запросы в БД, а занимается более важными делами переваривает действия игроков и изменяет состояние игрового мира в комнатах.

Сначала модель


Разработка началась с простого и самого главного некой модели игрового мира, описывающей морские сражения с серверной точки зрения. Обычный canvas 2D для схематичного отображения модели на экране подходит идеально.



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

Выстрелы и попадания сводятся к простым операциям с векторами направления корабля и направления выстрела. Здесь нет снарядов. Если dot продукт нормализованных векторов вписывается в допустимые значения с учетом расстояния до цели, то быть выстрелу и попаданию, если при этом игрок нажал кнопку.

Клиентский JavaScript для визуализации модели игрового мира, обрабатывающий движение кораблей и выстрелы, я перенес на Node.js сервер почти без изменений.

Игровой сервер


Node.js WebSocket сервер состоит всего из 3 скриптов:

  • main.js основной скрипт, который получает WS сообщения от игроков, создаёт комнаты и заставляет шестеренки этой машины крутиться
  • room.js скрипт, отвечающий за игровой процесс внутри комнаты: обновление игрового мира, рассылка обновлений игрокам комнаты
  • funcs.js включает класс для работы с векторами, пару вспомогательных функций и класс реализующий двусвязный список

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

Актуальный список классов игрового сервера:

  • WaitRoom комната, куда попадают игроки ожидающие начала сражения, здесь есть собственный tick метод, рассылающий свои обновления и запускающий создание игровой комнаты когда больше половины игроков готовы к бою
  • Room игровая комната, где проходят сражения: обновляются состояния игроков/кораблей, затем обрабатываются возможные столкновения, в конце формируется и рассылается всем сообщение с актуальными данными
  • Player по сути обёртка с некоторыми дополнительными свойствами и методами для следующего класса:
  • Ship этот класс заставляет корабли плавать: реализует движение и повороты, здесь также хранятся данные о повреждениях, чтобы в конце игры зачислить очки игрокам, принимавшим участие в уничтожении корабля
  • PhysicsEngine класс, реализующий простейшие столкновения круглых объектов
  • PhysicsBody всё есть круглые объекты со своими координатами на карте и радиусом

Игровой цикл в классе Room выглядит примерно так
let upd = {p: [], t: this.gamet};let t = Date.now();let dt = t - this.lt;let nalive = 0;for (let i in this.players) {this.players[i].tick(t, dt);}this.physics.run(dt);for (let i in this.players) {upd.p.push(this.players[i].getUpd());}this.chronology.addLast(clone(upd));if (this.chronology.n > 30) this.chronology.remFirst();let updjson = JSON.stringify(upd);for (let i in this.players) {let pl = this.players[i];if (pl.ship.health > 0) nalive++;if (pl.deadLeave) continue;pl.cl.ws.send(updjson);}this.lt = t;this.gamet += dt;if (nalive <= 1) return false;return true;


Кроме классов, есть такие функции, как получить данные пользователя, обновить ежедневное задание, получить награду, купить скин. Эти функции в основном отправляют https запросы в PHP, который выполняет один или несколько MySQL запросов и возвращает результат.

Сетевые задержки


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

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

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

Фронт-энд


К сожалению здесь нет четкой структуры или иерархии классов и методов. Весь JS разбит на объекты со своими функциями, которые в каком то смысле являются равноправными. Почти все мои предыдущие проекты были устроены более логично чем этот. Отчасти так вышло потому что сначала целью было отладить модель игрового мира на сервере и сетевое взаимодействие не обращая внимания на интерфейс и визуальную составляющую игры. Когда пришло время добавлять 3D я в буквальном смысле его добавил в существующую тестовую версию, грубо говоря, заменил 2D функцию drawShip на точно такую же, но 3D, хотя по-хорошему стоило пересмотреть всю структуру и подготовить основу для будущих изменений.

3D корабль


Three.js поддерживает использование готовых 3D моделей различных форматов. Я выбрал для себя GLTF / GLB формат, где могут быть вшиты текстуры и анимация, т.е. разработчик не должен задаваться вопросом все ли текстуры загрузились?.

Раньше я ни разу не имел дела с 3D редакторами. Логичным шагом было обратиться к специалисту на фриланс бирже с задачей создания 3D модели парусного корабля с вшитой анимацией залпа из пушек. Но я не удержался от мелких изменений в готовой модели специалиста своими силами, а закончилось это тем, что я создал свою модель с нуля в Blender. Создать low-poly модель почти без текстур просто, сложно без готовой модели от специалиста изучить в 3D редакторе то, что нужно для конкретной задачи (как минимум морально :).



Шейдеры богу шейдеров


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

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

Пустая заготовка, структура кода, которую мне было удобно копировать, дополнять и изменять для создания своего 3D объекта подобным образом:

Показать код
let vs = `attribute vec4 color;varying vec4 vColor;void main(){vColor = color;gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );// gl_PointSize = 5.0; // for particles}`;let fs = `uniform float opacity;varying vec4 vColor;void main() {gl_FragColor = vec4(vColor.xyz, vColor.w * opacity);}`;let material = new THREE.ShaderMaterial( {uniforms: {opacity: {value: 0.5}},vertexShader: vs,fragmentShader: fs,transparent: true});let geometry = new THREE.BufferGeometry();//let indices = [];let vertices = [];let colors = [];/* ... *///geometry.setIndex( indices );geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 4 ) );let mesh = new THREE.Mesh(geometry, material);


Повреждение корабля


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



Море


Море также реализовано с помощью ShaderMaterial. Каждая вершина движется во всех 3-х направлениях по синусоиде образуя рандомные волны. Атрибуты определяют амплитуды для каждого направления движения и фазу синусоиды.

Чтобы разнообразить цвета на воде и сделать игру интереснее и приятнее глазу было решено добавить дно и острова. Цвет дна зависит от высоты/глубины и просвечивает сквозь водную поверхность создавая темные и светлые области.

Морское дно создаётся из карты высот, которая создавалась в 2 этапа: сначала в графическом редакторе было создано дно без островов (в моём случае инструментами были render -> clouds и Gaussian blur), затем средствами Canvas JS онлайн на jsFiddle в случайном порядке были добавлены острова рисованием окружности и размытием. Некоторые острова низкие, через них можно стрелять в противников, другие имеют определённую высоту, через них выстрелы не проходят. Кроме самой карты высот, на выходе я получаю данные в json формате об островах (их положение и размеры) для физики на сервере.



Что дальше?


Есть много планов по развитию игры. Из крупных новые режимы игры. Более мелкие придумать тени/отражение на воде с учетом ограничений производительности WebGL и JS. Про возможность разбудить Кракена я уже упоминал :) Ещё не реализовано объединение игроков в комнаты на основе их накопленного опыта. Очевидное, но не слишком приоритетное усовершенствование создать несколько карт морского дна и островов и выбирать для нового сражения одну из них случайным образом.

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

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

Пасхалка


Кто не любит вспомнить старые компьютерные игры, которые дарили так много эмоций? Мне нравится перепроходить игру Корсары 2 (она же Sea Dogs 2) снова и снова до сих пор. Не мог не добавить в свою игру секрет и явно и косвенно напоминающий о Корсарах 2. Не стану раскрывать все карты, но дам подсказку: моя пасхалка это некий объект, который вы можете найти исследуя морские просторы (далеко плыть по бескрайнему морю не нужно, объект находится в пределах разумного, но всё же вероятность найти его не высока). Пасхальное яйцо полностью восстанавливает поврежденный корабль.

Что получилось


Минутное видео (тест с 2 устройств):


Ссылка на игру: https://sailfire.pw

Там же есть форма для связи, сообщения летят мне в телеграм: https://sailfire.pw/feedback/
Источник: habr.com
К списку статей
Опубликовано: 09.07.2020 16:21:19
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка игр

Three.js

Node.js

Websocket

Игры

3d-графика

Реал-тайм

Категории

Последние комментарии

© 2006-2020, personeltest.ru