Создание многопользовательской realtime игры на node.js

http://onedayitwillmake.com/blog/2011/08/creating-realtime-multiplayer-games-using-node-js/
  • Перевод


Несколько месяцев назад мы с коллегами решили сделать многопользовательскую realtime игру, которая могла бы работать в вебе. Мы решили использовать node.js для нашего сервера. Это решение привело к очень убедительному успеху — наш сервер работал несколько месяцев без единого падения или перезагрузки процесса.

Мы решили написать нашу игру на node.js, потому что мы слышали много хорошего об этой платформе и очень хотели немного с ней поиграть. И это было потрясающе — мы очень быстро вошли в тему. Для node.js существует множество любопытных библиотек, способных решать абсолютно разные задачи. Побочным преимуществом использования node для серверной части является, собственно, javascript — очень простой в обращении язык. Это позволило нам сфокусироваться на проблемах, которые встречаются во всех realtime играх, без лишней суеты, ограничений и необходимости компилировать код, как это случается при использовании менее динамических языков.

Также node.js проявил себя как очень легковесный язык, даже в моменты пиковой нагрузки. Для нашей игры, процесс node.js использовал только один поток и потреблял всего около 3-4% CPU при одновременной работе 8-10 копий игры, каждая со своим собственным движком обнаружения столкновений.


Изначальный подход


Изначальный (наивный) подход при написании многопользовательской realtime игры выглядит так:

var info = {};
info.position = {x:10, y:15};
info.velocity = {x:0.5,y: 0.2};
info.currentWeapon = 1;
info.radius = 10;
this.netChannel.send( info );

Сервер пересылает все полученные пакеты данных каждому подключенному клиенту:

function broadcastInfo( sendingClient, messageInfo )
{
     for(var aClient in allClients) {
          if(aClient != sendingClient) {
               aClient.connection.send( messageInfo );
          }
     }
}

Основная проблема

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

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

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



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

Другой подход — клиент-серверная модель


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



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

Рендеринг мира

Наша нервная система может подстраиваться под задержку. Если мы используем одно и то же небольшое время задержки для отрисовки объекта, человек с легкостью подстраивается под это и перестает замечать задержку вообще. Ключевым моментом, разрешающим нашу проблему, является то, что мы рендерим мир для всех клиентов в прошедшем времени, таким, каким он был N миллисекунд назад. Число N выбирается произвольно. Например, мы использовали 75 миллисекунд.

Основная процедура



  • Изначально, мы настраиваем систему так, чтобы клиент получал от сервера высокоточные изменения мира в дискретные промежутки времени.
  • Мы храним изменения мира в массиве.
  • Когда приходит время отрисовать игроков на сцене, мы делаем это в момент времени, равный текущему времени минус время интерполяции.
  • Назовем этот момент времени — время рендеринга, которое в действительности равно текущему времени минус 75 миллисекунд.
  • При каждом рендеринге, мы находим два изменения, между которыми находится наше время рендеринга.
  • Как только изменения найдены, мы используем функцию линейной интерполяции, чтобы расположить объекты точно на их траекториях.
  • Если пакет был потерян, например, если мы не получили пакет 343 (см. рисунок), мы все еще можем использовать интерполяцию между двумя полученными пакетами 342 и 344.

RealtimeMultiplayerNodeJS


Мы преданно верим в open-source, поэтому решили под конец разработки немного вычистить код и выложить как open-source проект. Так как мы не особо преуспели в выборе хороших имен, мы назвали его RealtimeMultiplayerNodeJS.

RealtimeMultiplayerNodeJS — это фреймворк, предназначенный специально для создания многопользовательских realtime игр с использованием HTML5 и клиент-серверной модели. В этой модели, пользователи посылают только данные ввода, а сама игра моделируется на сервере. Клиенты интерполируют мир между двумя его состояниями, основываясь на текущем времени рендеринга.

Как использовать проект

  1. Скачать репозиторий
  2. В терминале запустить сервер «node js/DemoHelloWorld/server.js»
  3. Открыть страницу "/DemoHelloWorld.html" (Учтите, что файл должен быть виден с сервера, чтобы можно было подключиться, используя websockets)

Демонстрация физики

Это демо сделано с целью показать идею синхронизирующихся физических процессов, когда все моделирование происходит на сервере. Для создания мира используется Box2D.js. Также показана синхронизация взаимодействия нескольких пользователей, а также пример отправки сообщения на сервер с его последующей интерпретацией снова на стороне клиента.



DemoHelloWorld

Наиболее простое, но интересное демо, которое мы смогли придумать. Объекты движутся слева направо.



DemoCircle

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



Напоследок, просто чтобы подчеркнуть основную идею — диаграмма интерполяции сущностей, выполненная в технике ASCII:

                           (actual time)
 (snapshot interval)                |
         |                          |
         _    (rendered time)       |
       /   \       |                /
      v     v     /                |
------|-----|----v|---|----|----|--v--------->
0.0sec  0.1sec   0.2sec   0.3sec      time
                  |                |
                   \              /
                    \------\/----/
                           |
                           |
                 (interpolation time)
Поделиться публикацией

Похожие публикации

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

    +4
    А экстраполяция присутствует? А откатывание времени на сервере назад присутствует? Ведь в клиент-серверной реалтайм игре основная фишка в том что игровое время на сервере опережает время клиентов на время пинга, зачастую это 50-100 мс. И если клиент что-то делает то нужно откатывать время на сервере назад и смотреть мог ли он это сделать действительно. Еще у клиентов есть данные для интерполяции наперёд только на 100-200 мс. В реальности случаются пинги и в 1000+ мс, игра запросто получит ситуацию когда просто не будет информации для рендера.
      +6
      Вся статья о том, что экстраполяция — это зло. Дело в том, что это не сервер опережает, а клиент запаздывает. Собственно, об этом и статья. Достаточно тонкий момент, который я почему-то долго не понимал. Возможно, вы имели ввиду «предсказание на клиенте». Но это работает только для локально изменяемых объектов (которые контролирует конкретный клиент) и, в случае промаха, создает баг. Это как раз случай про длинные пинги в 1000мс. Предсказание, на сколько я понял, в общем виде вообще не имеет решения.

      Откатывать можно, но не всегда нужно (от игры зависит). У всего есть свой предел. Если пинг — полчаса, и мы откатываем, то мы смоделируем эффект бабочки. И все будут страдать от самого медленного.

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

      В целом, сейчас экспериментирую с друзьями на эту тему, так что не судите строго — рассказывайте как надо, если я не прав.
        +6
        Для шахмат можно считать что событие произошло когда пришло на сервер, для реалтайм (об этом статья же) например стрелялки (fps и не только) — это не так, игрок нажал на кнопку и догадайтесь когда игра должна среагировать? Верно — сразу же, точнее — через 1-2 кадра, это 16-32 мс. Ответ от сервера прийдет как минимум через 100 мс, если рендер будет рисовать ответы от сервера то будет жесткое ощущение лагов и в такую реалтайм игру никто играть не будет. Потому время на клиенте как раз «сейчас», а серверу приходится существовать в «будущем», да еще таком что время любого клиента было меньше времени сервера, но время сервера не было сильно больше времени любого клиента, иначе слишком много нужно данных хранить о действитях клиентов. Поскольку все клиенты имеют время «сейчас», а канал связи между ними — 2 пинга, то экстраполяция наше всё, без неё вообще никак. Так устроены все сетевые стрелялки, а это самые требовательные к сетевой составляющей игры.
          0
          Абсолютно верно, но это называется prediction. И на клиенте у нас получается два времени — одно в будущем, а одно в прошлом относительно сервера. Об этом можно отдельно статью написать или перевести.

          Здесь речь идет о том, как сделать плавным движение противника.

          Если же события от клиента на сервере запаздывают — в этом нет никакой проблемы, потому что действия этого клиента происходят на сервере в правильном порядке и через те же интервалы, что и на клиенте. А как они накладываются на действия противников — это уже второй вопрос и, в целом, не такой важный, так как правду все равно никто не узнает =)
            +1
            В реалтайм сетевой игре у каждого клиента «свой мир» и серверу нужно чётко проверять не только по дельтам времени, а то получится что клиент у себя убил противника, а на сервере противник уже чуть сдвинулся и клиент не попал.
              +1
              Конечно, так тоже можно попробовать сделать, но в данной статье описывается модель, в которой сервер всегда прав. То есть это не сервер подстраивается под каждого клиента (что невозможно), а клиент подстраивается под сервер. По этому вопросу надо подробнее читать «weapon prediction». Там для меня пока темный лес.
    +2
    Какое-то время назад мне казалось что у многопользовательских игр на Node.JS + HTML5 большое будущее. Мне все еще не очень понятно почему нету чего-то такого сильно популярного.
      –2
      Есть, вы просто не знаете что они на node.js + HTML5 :). Например — «Полный Пи».
        –1
        раньше он был на ajax а не вебсокетах. давно его переписали?
          –1
          Он до сих пор на ajax. Но, насколько я знаю, там вполне себе node и вполне себе HTML5, местами. Вебсокеты != HTML5.
        0
        Ну вот к примеру bombermine.com/
          +1
          «Powered by Jetty» — вроде там Ява использовалась в качестве сервера? Если я правильно понял это та игра прототип которой тестировался на хабре, но я не знаю что довели проект до такого законченного состояния.
          0
          Вопрос отличный. Я ни одной популярной игры не знаю.

          Но для прототипирования, связка очень удобная — один код на клиенте и на сервере (как раз к вопросу предсказания на клиенте). При этом, разработка быстрее, чем на C++. Для экспериментов — самое то.
          +4
          На главной картинке супер-герой Flash. Это намек на чем нужно было делать клиент?
            0
            И правда смешно, но нет. Но Flash на клиенте у меня работал на порядок хуже (просто моделирование игры, без сервера).

            А супер-герой — это то ощущение, которое возникает, когда ты находишь баг и prediction начинает работать.
              0
              Важное замечание.

              А можете рассказать подробнее, что именно имеете в виду под «на порядок хуже»? Очень заинтересовало, т.к. собираю информацию об этом сейчас
                0
                Делал достаточно сложное моделирование (много расчетов на каждом кадре). Там где флеш ложился и не собирался вставать, js+canvas продолжал работать отлично еще очень долго. Возможно это мой частный случай, но он выглядел для меня очень показательно.

                При предсказании положения клиента, на нем приходится делать моделирование мира, как на сервере. Если мир будет достаточно сложным, на этом этапе могут включиться тормоза.
                  0
                  Очень спорное замечание, видимо что-то на флеше рисовалось в лоб, без использования Bitmapdata.draw и прочих техник, я уж и не говорю про включение аппаратного ускорения рисования. Никогда не будет интерпретируемый в браузере javascript работать быстрее чем заранее откомпилированный actionscript во флеше.
                    0
                    Будет, еще как будет. Вы немного отстали от технологий: ни один современный браузер не интерпретирует javascript, его сейчас только компилируют с помощью JIT. А скорость работы откомпилированного кода на том же V8 уступает разве что плюсам и другим «низкоуровневым» языкам.
                    0
                    О как, интересно… А что тормозило во флеше, отрисовка или логика в коде, и в каких пропорциях вносило вклад в «торможение»?
                      0
                      Не измерял, но логика в коде была чуть более чем насыщенная. Алгоритма сложности n^2. Флеш зависал где-то на n=30, а js нормально работал при n=100. Это навскидку.
              0
              А нет демок где-то размещенных с публичным доступом? Хотелось бы посмотреть глазами без клонирования репозитория.
              +3
              В этой модели имеется единый авторитетный сервер, который и моделирует игру. А также клиенты, посылающие лишь свои данные ввода. Например, я посылаю только информацию о том, что моя клавиша «пробел» была нажата, а сервер определяет, что это обозначает в терминах игры. Благодаря данной модели, мы освободились от множества проблем предыдущей реализации.


              1) 75 миллисекунд от цели до сервера, +75 мс от сервера до стрелка = 150 мс. Стрелок стреляет по своему разумению (в то, что видит) и отсылает пакет с заявкой на попадание с вектором снаряда в момент попадания, что даст теперь 225мс.

              При скорости движения 16 км/ч (=4.444м/с) (это PzKpfw III на пересечённой местности) за это время цель успеет переместиться вплоть до 1 метра. Строго говоря, в неопределённом направлении, то есть ±1 метр. Такая неопределённость составляет 2 метра в диаметре.

              а) Стрельба по слабым местам бронирования теряет смысл.

              На практике 75 мс могут получить только игроки Дефолт-сити, и, вероятно, окрестностей. У меня до дефолт-сити вплоть до 300 мс, а это ±4 метра с противником, который тоже имеет 300 мс. Длина корпуса PzKpfw III составляет 6 метров, что делает стрельбу почти бессмысленной.

              И это БЕЗ учёта подлётного времени снаряда, которое на, например, танковой дистанции в 500 метров составит, в лучшем случае, 615 мс (это при 650 м/с начальной скорости снаряда Ф-34 и БЕЗ учёта торможения в воздухе — лень искать баллистический калькулятор, да и с ним будет только хуже).

              В случае Дефолт-сити: 75 мс + 75 мс + 615 мс + 75 мс = 840 мс => ±3.73 метра
              В случае 300 мс: 300 мс + 300 мс + 615 мс + 300 мс = 1515 мс => ±6.73 метра

              б) Стрельба вообще теряет смысл

              Даже если исключить время возврата попадания (цель экстраполируется), результаты всё равно весьма печальны.

              2) Для авиасимов с их, ну скажем, хотя бы 400 км/ч = 111.11 м/с архитектура заведомо и гарантированно обеспечивает промах. (Длина B-17 Flying Fortress 22.6 м)

              3) Всё это сказано без учёта пинга «терминального доступа к моему юниту», то есть лага по моему же джойстику, к которому автор призывает привыкнуть.

              Вероятно я ошибаюсь, но у меня получается так.
                +2
                P.S. Я не упомянул рассеивание снарядов из ствола стрелка?

                P.P.S. В статье не разглядел тему визуализации попадания. В какой момент рисовать вспышку на мишени: когда я шлю факт попадания, когда сервер подтверждает попадание, когда мишень получает повреждение?

                В первом случае игроки должны «привыкать», что мало какая вспышка является попаданием и ждать текста от сервера (не очень атмосферно),
                во втором случае игрокам придётся ждать 75-300 мс (в кучу к 615 мс подлёта), но попадание, вроде, гарантировано,
                в третьем случае игрокам придётся ждать 300-1200 мс (в кучу к 615 мс подлёта), мишень согласилась оказаться подбитой и начинает разваливаться на глазах.

                Пока что получается, что защита от единичных читеров заставляет поголовно всех игроков «привыкать» и к лагу джойстика и к лагу попадания.

                P.P.P.S. Что же касается нулевой скорости подпрыгнувшего мяча, то грамотные игроделы имеют в загашнике вплоть до второй производной скорости по словам игрока. И новая скорость (в дальнейшем и новые координаты) всегда вычисляется с учётом ускорения игрока, или даже с учётом изменения ускорения.
                  0
                  Все это можно попробовать сделать за счет магии на клиенте (см. prediction и т.д.). Я пробовал — работает очень круто при «реальном» пинге. Но, конечно, если у вас пинг — две секунды, то играть вы сможете только в шашки.
                    +1
                    Тогда вам придётся отказаться от вашего утверждения «Пользователь всегда будет сообщать, что он находится там, где ему надо, попадает во всех противников со стопроцентной точностью и имеет полный запас здоровья.»

                    К тому же я не утверждал, что «у меня пинг 2000 мс».
                    «У меня лично пинг 100-300 мс».
                      0
                      Если пользователь отсылает только сэмплы клавиатуры, а не местоположение с координатами, то он будет двигаться инкрементально (см статью).

                      Я никоим образом не хотел обидеть ваш пинг =) 100-300 — очень хороший пинг.
                    0
                    пинг гугла в спб 8мс, яндекс 15мс, до сервера уфы к примеру 40мс
                    если пинг будет скажем в 30мс то это 15мс на отправку пакета, не так уж и много
                    если делать игру на webrtc можно подключатся к игрокам напрямую, а на сервере будет просто проверка всех действий игроков
                    если игроки будут с одного города то можно играть абсолютно без никаких лагов
                      +1
                      Да, так тоже можно, но это кажется гораздо сложнее и при этом не факт, что быстрее, так как надо некоторое время на синхронизацию событий. Не совсем понятно как добиться того, чтобы все клиенты моделировали один и тот же мир (состоящий их одних и тех же событий в нужной последовательности и через одинаковые интервалы).
                        0
                        если игроков не много я думаю можно делать подключение многое ко многим, т.е. каждый игрок посылает свои действия для всех + для сервера, а те уже в свою очередь формируют мир в зависимости от поступаемых данных
                        сделал выстрел, если реально убил то значит посылаешь команду о попадании для всех игроков, а сервер уже проверит так оно было или нет
                    +2
                    похоже, что вся инфа отсюда:
                    Source Multiplayer Networking

                      +1
                      Именно. Но, почему-то после прочтения именно статьи из перевода, у меня все встало на свои места. Она написана более просто. Поэтому решил поделиться.
                      +2
                      Тем кто понимает что описано в статье, советую продолжить читать: Trailing State Synchronization

                      «Наша нервная система может подстраиваться под задержку.» — Ха Ха Ха! Если это какая-нибудь кнопка, триггер — то всё ок. А если надо отображать перемещение персонажа, или то как он блок из стены вынул, или там бомбу поставил?

                      WebSockets => если при использовании TCP пакет потеряется то пинг будет побольше 75мс.

                      P.S. у меня простой Time Warp с кучей хаков.

                      Ваш bombermine.com
                        +2
                        Данная статья, действительно, для начинающих. Мне лично она помогла побороть порог на вход, во многом из-за простоты изложения.

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

                        Спасибо за ссылку. Буду изучать.

                        На ваш проект посматривал. Если опишите, как оно работает с точки зрения отзывчивости, наложения эффектов и звуков, с какими проблемами боролись и как, какие были ошибки при реализации, буду весьма благодарен.
                        0
                        Такое ощущение, что на ноде написали уже все html5 игры.
                        Было бы интересно увидеть что-то более экзотическое. Erlang + lua или go например.
                          +1
                          Правду сказать, тут больше не про node.js, а про клиент-серверную модель в многопользовательских realtime приложениях. Нод хорошо подходит для экспериментов в этой области и для создания маркетингового эффекта (шутка с долей правды). Как он себя поведет при нагрузке, все-таки хотелось бы сначала посмотреть вживую. А для этого надо привлечь несколько тысяч пользователей (достаточно большие риски).
                          0
                          на локалхосте хорошо выглядит, без лагов. интересно было бы взглянуть если бы пинги были по 100 мс
                            0
                            Может как-нибудь добавить sleep() на сервере, чтобы сэмулировать пинг?
                              0
                              setInterval побольше.
                              0
                              для этого есть специальные прокси, эмулирующие любые линии и скорости клиентов
                              0
                              Блин, я ничего не понял примерно после середины статьи))
                              Так какая методика более-менее нормальная для реализации «плавного» и хорошего реалтайма?
                                0
                                Берешь горстку грабель, раскидываешь их по сторонам, завязываешь глаза и начинаешь бегать. В общем, экспериментальная. Часто достаточно не самого «хорошего» реалтайма :)
                                  0
                                  Зависит от механики игры. В eve online, например, они замедляют время, когда много пользователей накапливается в одной локации. Во многих action играх (например counter strike), упрощают механику стрельбы так, что пуля долетает за бесконечно малое время.

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

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

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