Море, пираты — 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/
    Ссылки для желающих быть в курсе новостей и обновлений: Паблик ВК, Телеграм канал
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 22

      0
      Круто, ожидайте наплыва хабра-эффекта на сервер :) Не совсем понятно как управлять и стрелять. Думал левой кнопкой стрелять, а правой вертеть головой, но нет. Стрелять пробелом. Возможно еще увеличить инерцию движения. Сейчас много разных многопользовательских *.IO браузерых проектов, можно что-то такое на 10 человек сделать
        0
        в начале боя появляется изображение как управлять но это на доли секунды даже понять не успеваешь…
          +2
          На всяк случай:
          0
          Очень интересная статья! Спасибо! Но, хотелось бы еще узнать сколько времени было потрачено на реализацию вообщем и на отдельные составляющие игры?
            0
            Прошло около трех месяцев с перерывами на отдых и фриланс, точнее сказать не могу
              0
              Хм… Это очень круто для трех месяцев (по моим «нормам» =) )
            0
            Хорошо бы возможность менять управление, выстрел на [пробел] так себе решение, не много клав на которых пробел работает идеально и без шума… самая проблемная кнопка.

            В статье ничего не сказано про артефакты которые пополняют жизнь.
            В управлении не описана возможность манипулировать 3д-видом, отдалять приближать вид + изменять его наклон
            … это то чего было обнаружено методом тыка, а чего ещё есть?
              0
              Про кнопку для выстрела — проще добавить ещё вариант одновременно рабочий вместо настроек, но какой? (Уже есть Q и E для выстрелов с левого и правого борта соответственно как раз к вопросу «а чего ещё есть?», тоже осталось с ранних тестов, жалко было убирать)

              В статье сказано про пасхалку которая восстанавливает корабль. Кого-то из игроков я минут 5 назад «проводил» к ней специально :) Ник не помню, возможно, это были вы? (Если нет, то мне стоит спрятать её или добавить условий для её нахождения)

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

              Манипулирование 3D видом нужно для отладки, оно работает не идеально и я не решил что с ним делать — либо запретить, либо делать удобным и явно доступным всем (на мобилах сейчас недоступно)

              Спасибо за тестирование и фидбэк
              0
              Добавьте однопользовательский вариант, а то сижу жду — а игроков нету ((( Одновременно нажатие w+d стрелка влево- открывает новую кладку в google chrome. На миниатуюрной крате не хватает обозначения ящиков, а то не понятно где бились- потом уплыли и ящики не найти.
                0
                Осталось только скины добавить и игры будет великолепной)) Интересно было бы почитать про создание этих кораблей с нуля.
                  0

                  До sid meier's pirates далеко

                    +1

                    До каких именно — 2004 года или 1987?


                    image
                    0
                    На мой взгляд, добавлять кракена, чтобы он топил чужие корабли — плохая идея. Для защиты своего норм, но полностью топить вражеский это жёстко.
                    А вот добавить режим игры, где игроки кооперативно убивают кракена уже может быть интересно
                      +1
                      Если добавлять топить — это была бы большая редкость + не более 1 раза за бой и возможно не более 3 раз за сутки для игрока который занял 1 место в недельном топе например.

                      Кооперативно убивать кракена довольно логичный вариант и распространённый.

                      Спасибо за отзыв в любом случае.
                        +2
                        Когда вообще без шансов — такое очень бесит. Бустер на скорость стрельбы (или кораблика) ломает баланс меньше.

                        А вообще, в таких простых играх игроков очень легко увлечь возможностью поставить новые пушки/купить новый корабль/купить новые паруса/приобрести новую команду. Не помешают так же какие-нибудь дополнительные игровые переменные (ветер/скалы о которые можно разбиться). Было бы неплохо, если бы были абилки с кулдаунами, чтобы пвп не превращалось исключительно в четыре кнопки управления и пробел.

                        Если планируете развивать игру — посмотрите что реализовано в EvE O/Танках-корабликах. Даже такой простой геймплей может затягивать при добавлении интересных элементов.
                        +1
                        И грабить корованы!
                        0

                        Я уж думал там будет что-то типа sky2fly, а там морские бои. было бы наверное неплохо добавить какие-нибудь маркеры зоны обстрела как в Assassin's Creed III / IV

                          0
                          Игра — прелесть.
                          Однако надо что-то сделать с камерой, так как бой превращается в dogfight и победит тот, кто зайдёт сзади таким образом, что его не будет видно противнику. Возможно стоит добавить на миникарту (ну или её подобие) чуть больше информации о расстоянии до противников.

                          Вообще, игра производит впечатление. Хорошая графика, музон. Была бы еще кооперативная осада форта или что-нибудь такое было бы просто здорово :)

                          Спасибо за статью.
                            0
                            поворот корабля на месте без движения выглядит немного странным… понятно, что без этой функции да при отсутствии реверса один раз уткнувшись в островок ты уже с места не сдвинешься, но имхо немного ломает геймплей в плане необходимости маневрировать. стоишь себе и крутишься вокруг оси, пока противник с той же скоростью крутится вокруг тебя.
                              0
                              В теории — почему бы не сделать реверс, пусть медленный, но можно. Не подумал об этом, хотя медленный реверс тоже мало что изменит. Учту, возможно, добавлю.
                                0
                                Вам бы гейм-дизайнера. При довольно хорошей технической реализации — бои довольно пустые и малотактические. Ваши текущие бонусы очевидно несбалансированы и попадаются с неправильной частотой. Сейчас если у обоих кораблей 50% хп и ты находишь лечилку — ты победитель. Так не должно работать
                              0

                              Похоже на DOKDO

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое