Несколько месяцев назад мы с коллегами решили сделать многопользовательскую 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 и клиент-серверной модели. В этой модели, пользователи посылают только данные ввода, а сама игра моделируется на сервере. Клиенты интерполируют мир между двумя его состояниями, основываясь на текущем времени рендеринга.
Как использовать проект
- Скачать репозиторий
- В терминале запустить сервер «node js/DemoHelloWorld/server.js»
- Открыть страницу "/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)
Автор: simbajoe