Опыт Хоббита 2014

Добавление игрового процесса WebRTC в Hobbit Experience

Дэниел Исакссон
Дэниел Исакссон

Ко времени выхода нового фильма «Хоббит: Битва пяти воинств» мы работали над расширением прошлогоднего эксперимента Chrome « Путешествие по Средиземью» новым контентом. На этот раз основное внимание уделялось расширению использования WebGL, поскольку все больше браузеров и устройств могут просматривать контент и работать с возможностями WebRTC в Chrome и Firefox. В эксперименте этого года у нас было три цели:

  • P2P-геймплей с использованием WebRTC и WebGL в Chrome для Android
  • Создайте многопользовательскую игру, в которую легко играть и которая основана на сенсорном вводе.
  • Хостинг на Google Cloud Platform

Определение игры

Логика игры построена на основе сетки, в которой войска перемещаются по игровому полю. Это облегчило нам возможность опробовать игровой процесс на бумаге, пока мы определяли правила. Использование настройки на основе сетки также помогает при обнаружении столкновений в игре, обеспечивая хорошую производительность, поскольку вам нужно только проверять наличие столкновений с объектами на тех же или соседних плитках. С самого начала мы знали, что хотим сосредоточить новую игру на битве между четырьмя основными силами Средиземья: людьми, гномами, эльфами и орками. Кроме того, игра должна была быть достаточно простой, чтобы в нее можно было играть в рамках эксперимента Chrome, и не требовала слишком большого количества взаимодействий для изучения. Мы начали с определения пяти полей боя на карте Средиземья, которые служат игровыми комнатами, где несколько игроков могут соревноваться в одноранговых битвах. Отображение нескольких игроков в комнате на экране мобильного телефона и предоставление пользователям возможности выбирать, кому бросить вызов, само по себе было непростой задачей. Чтобы упростить взаимодействие и сцену, мы решили оставить только одну кнопку для вызова и принятия, а комнату использовать только для отображения событий и того, кто в настоящее время является царем горы. Это направление также решило некоторые проблемы с подбором игроков и позволило подобрать лучших кандидатов на бой. В нашем предыдущем эксперименте с Chrome Cube Slam мы узнали, что обработка задержки в многопользовательской игре требует больших усилий, если от этого зависит результат игры. Вам постоянно приходится делать предположения о том, где будет находиться противник, где, по мнению оппонента, вы находитесь, и синхронизировать это с анимацией на разных устройствах. В этой статье эти проблемы рассматриваются более подробно. Чтобы упростить задачу, мы сделали эту игру пошаговой.

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

Части игры

Чтобы создать эту многопользовательскую игру, нам нужно было создать несколько ключевых частей:

  • API управления игроками на стороне сервера обрабатывает пользователей, подбор игроков, сеансы и игровую статистику.
  • Серверы, помогающие устанавливать связь между игроками.
  • API для обработки сигналов API каналов AppEngine, используемых для подключения и общения со всеми игроками в игровых комнатах.
  • Игровой движок JavaScript, который обрабатывает синхронизацию состояния и обмен сообщениями RTC между двумя игроками/одноранговыми узлами.
  • Представление игры WebGL.

Управление игроками

Для поддержки большого количества игроков мы используем множество параллельных игровых комнат на каждом поле боя. Основная причина ограничения количества игроков в игровой комнате — дать возможность новым игрокам достичь вершины таблицы лидеров в разумные сроки. Ограничение также связано с размером json-объекта, описывающего игровую комнату, отправленного через Channel API, который имеет ограничение в 32 КБ. Нам нужно хранить игроков, комнаты, результаты, сессии и их взаимоотношения в игре. Для этого мы сначала использовали NDB для сущностей и использовали интерфейс запросов для работы с отношениями. NDB — это интерфейс к хранилищу данных Google Cloud. Поначалу использование NDB работало отлично, но вскоре мы столкнулись с проблемой его использования. Запрос был выполнен к «зафиксированной» версии базы данных (запись NDB подробно объясняется в этой подробной статье ), которая может иметь задержку в несколько секунд. Но у самих сущностей такой задержки не было, поскольку они отвечали непосредственно из кэша. Возможно, будет проще объяснить это на примере кода:

// example code to explain our issue with eventual consistency
def join_room(player_id, room_id):
    room = Room.get_by_id(room_id)
    
    player = Player.get_by_id(player_id)
    player.room = room.key
    player.put()
    
    // the player Entity is updated directly in the cache
    // so calling this will return the room key as expected
    player.room // = Key(Room, room_id)

    // Fetch all the players with room set to 'room.key'
    players_in_room = Player.query(Player.room == room.key).fetch()
    // = [] (an empty list of players)
    // even though the saved player above may be expected to be in the
    // list it may not be there because the query api is being run against the 
    // "committed" version and may still be empty for a few seconds

    return {
        room: room,
        players: players_in_room,
    }

После добавления модульных тестов мы смогли ясно увидеть проблему и отошли от запросов, чтобы вместо этого хранить отношения в списке, разделенном запятыми, в memcache . Это казалось чем-то вроде взлома, но это сработало, и в Memcache AppEngine есть транзакционная система для ключей, использующая отличную функцию «сравнить и установить», так что теперь тесты пройдены снова.

К сожалению, кэш памяти — это не все радуги и единороги, но он имеет несколько ограничений , наиболее заметными из которых являются размер значения 1 МБ (не может быть слишком много комнат, связанных с полем битвы) и срок действия ключа, или, как это объясняется в документации :

Мы рассматривали возможность использования еще одного замечательного хранилища ключей-значений — Redis. Но в то время настройка масштабируемого кластера была немного сложной, и, поскольку мы предпочли сосредоточиться на создании опыта, а не на обслуживании серверов, мы не пошли по этому пути. С другой стороны, Google Cloud Platform недавно выпустила простую функцию развертывания по щелчку , одним из вариантов которой является кластер Redis, так что это было бы очень интересным вариантом.

Наконец мы нашли Google Cloud SQL и перенесли отношения в MySQL. Это была большая работа, но в итоге все сработало отлично: обновления теперь полностью атомарны, а тесты все еще проходят. Это также сделало организацию матчей и ведение счета намного более надежными.

Со временем большая часть данных постепенно перешла из NDB и кэша памяти в SQL, но в целом объекты игрока, поля битвы и комнаты по-прежнему хранятся в NDB, а сеансы и отношения между ними хранятся в SQL.

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

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

Настройка WebRTC

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

Существует несколько сторонних библиотек, которые вы можете использовать для службы сигнализации, что также упрощает настройку WebRTC. Некоторые варианты — PeerJS , SimpleWebRTC и PubNub WebRTC SDK . PubNub использует решение на размещенном сервере, и для этого проекта мы хотели разместить его на облачной платформе Google. Две другие библиотеки используют серверы node.js, которые мы могли бы установить в Google Compute Engine, но нам также нужно было бы убедиться, что они могут обрабатывать тысячи одновременных пользователей, что, как мы уже знали, может сделать Channel API.

Одним из главных преимуществ использования Google Cloud Platform в этом случае является масштабирование. Масштабирование ресурсов, необходимых для проекта AppEngine, легко осуществляется через консоль разработчиков Google, и для масштабирования службы сигнализации при использовании Channels API не требуется никаких дополнительных усилий.

Были некоторые опасения по поводу задержки и надежности API каналов, но ранее мы использовали его для проекта CubeSlam, и он доказал свою эффективность для миллионов пользователей в этом проекте, поэтому мы решили использовать его снова.

Поскольку мы не решили использовать стороннюю библиотеку для работы с WebRTC, нам пришлось создать собственную. К счастью, мы смогли повторно использовать большую часть работы, которую мы проделали для проекта CubeSlam. Когда оба игрока присоединились к сеансу, сеанс становится «активным», и оба игрока затем будут использовать этот идентификатор активного сеанса для инициации однорангового соединения через API канала. После этого вся связь между двумя игроками будет осуществляться через RTCDataChannel .

Нам также нужны серверы STUN и TURN, которые помогут установить соединение и справиться с NAT и брандмауэрами. Подробнее о настройке WebRTC читайте в статье HTML5 Rocks. WebRTC в реальном мире: STUN, TURN и сигнализация .

Количество используемых серверов TURN также должно иметь возможность масштабироваться в зависимости от трафика. Чтобы справиться с этой проблемой, мы протестировали менеджер развертывания Google . Это позволяет нам динамически развертывать ресурсы в Google Compute Engine и устанавливать серверы TURN с помощью шаблона . Он все еще находится в стадии альфа-версии, но для наших целей он работает безупречно. Для сервера TURN мы используем coturn , который является очень быстрой, эффективной и, казалось бы, надежной реализацией STUN/TURN.

API канала

API канала используется для отправки всех сообщений в игровую комнату и из нее на стороне клиента. Наш API управления игроками использует Channel API для уведомлений об игровых событиях.

Работа с Channels API имела несколько затруднений. Одним из примеров является то, что, поскольку сообщения могут быть неупорядоченными, нам пришлось поместить все сообщения в объект и отсортировать их. Вот пример кода того, как это работает:

var que = [];  // [seq, packet...]
var seq = 0;
var rcv = -1;

function send(message) {
  var packet = JSON.stringify({
    seq: seq++,
    msg: message
  });
  channel.send(packet);
}

function recv(packet) {
  var data = JSON.parse(packet);

  if (data.seq <= rcv) {
    // ignoring message, older or already received
  } else if (data.seq > rcv + 1) {
    // message from the future. queue it up.
    que.push(data.seq, packet);
  } else {
    // message in order! update the rcv index and emit the message
    rcv = data.seq;
    emit('message', data.message);

    // and now that we have updated the `rcv` index we 
    // will check the que for any other we can send
    setTimeout(flush, 10);
  }
}

function flush() {
  for (var i=0; i<que.length; i++) {
    var seq = que[i];
    var packet = que[i+1];
    if (data.seq == rcv + 1) {
      recv(packet);
      return; // wait for next flush
    }
  }
}

Мы также хотели сохранить различные API сайта модульными и отделить их от хостинга сайта, и начали с использования модулей , встроенных в GAE. К сожалению, после того, как все это заработало в разработке, мы поняли, что Channel API вообще не работает с модулями в рабочей среде. Вместо этого мы перешли к использованию отдельных экземпляров GAE и столкнулись с проблемами CORS, которые вынудили нас использовать мост iframe postMessage .

Игровой движок

Чтобы сделать игровой движок максимально динамичным, мы создали интерфейсное приложение, используя подход «сущность-компонент-система» (ECS) . Когда мы начали разработку, каркасы и функциональные спецификации не были заданы, поэтому было очень полезно иметь возможность добавлять функции и логику по мере продвижения разработки. Например, первый прототип использовал простую систему рендеринга холста для отображения объектов в сетке. Пару итераций спустя была добавлена ​​система столкновений и система для игроков, управляемых ИИ. В середине проекта мы могли переключиться на систему 3D-рендеринга, не меняя остальной код. Когда сетевые части были запущены, систему искусственного интеллекта можно было модифицировать для использования удаленных команд.

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

Если бы ходы меняли только два пользователя, оба партнера могли бы разделить ответственность за передачу хода противнику, когда они закончили, но в этом участвует третий игрок. Система искусственного интеллекта снова стала удобной (не только для тестирования), когда нам нужно было добавить врагов, таких как пауки и тролли. Чтобы они вписались в пошаговый процесс, их нужно было создавать и выполнять одинаково с обеих сторон. Проблема была решена путем предоставления одному узлу возможности управлять системой поворотов и отправлять текущий статус удаленному узлу. Затем, когда наступает очередь пауков, менеджер хода позволяет системе искусственного интеллекта создать команду, которая отправляется удаленному пользователю. Поскольку игровой движок просто действует на команды и идентификаторы объектов, игра будет моделироваться одинаково с обеих сторон. Все устройства также могут иметь компонент AI, который обеспечивает простоту автоматизированного тестирования.

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

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