Недавно мне довелось поработать над прототипом видеочата. Это был отличный повод поближе познакомиться с концепциями WebRTC и опробовать их на практике. Как правило, когда говорят про WebRTC, подразумевают организацию аудио- и видеосвязи, но эта технология может применяться и для других интересных вещей. Я решил попробовать сделать peer-to-peer игру и поделиться опытом ее создания. Видео того что получилось и подробности реализации под катом.




Движок для игры


Как-то давным-давно мне попалась на глаза демка игры с симпатичной пиксельарт графикой. Игра была сделана на JavaScript-движке Impact. Про него даже как-то упоминали на Хабре.



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

Игровые комнаты


Каким образом игрок может попасть в игру и как пригласить в нее своих друзей? Многие онлайн-игры используют так называемые комнаты или каналы, чтобы игроки могли играть друг с другом. Для этого понадобится сервер, который позволит создавать эти самые комнаты и добавлять/удалять пользователей. Схема его работы довольно простая: когда пользователь запускает игру, а в нашем случае — открывает окно браузера с адресом игры, то происходит следующее:

  1. новый игрок сообщает серверу имя комнаты, в которой он хотел бы играть;
  2. сервер в ответ отправляет список игроков этой комнаты;
  3. остальным игрокам приходит уведомление о появлении нового участника.


Все это достаточно просто реализовать, например, на node.js + socket.io. То, что получилось, можно посмотреть тут. После того как игрок попал в игровую комнату, он должен установить peer-to-peer соединение с каждым из присутствующих в этой комнате игроков. Но, до того как перейти к реализации peer-to-peer данных, предлагаю подумать о том, какие это в принципе будут данные.

Протокол взаимодействия


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

message PlayerPosition {
    int16 x;
    int16 y;
}

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

message PlayerPositionAndAnimation {
    int16 x;
    int16 y;
    int8 anim;
    int8 animFrame;
    bool flipped;
}

Отлично! Какие еще сообщения понадобятся? В зависимости от того, что вы планируете делать в игре, у вас получится свой набор, а у меня получилось примерно следующее:

  • игрок умирает ();
  • игрок рождается ( int16 x, int16 y );
  • игрок стреляет ( int16 x, int16 y, boolean flipped );
  • игрок подбирает оружие ( int8 weapon_id).

Типизированные поля в сообщениях


Как вы могли заметить, каждое из полей в сообщениях имеет свой тип данных, например, int16 — для полей, представляющих координаты. Давайте сразу в этом разберемся, заодно я немного расскажу про WebRTC API. Дело в том, что для передачи данных между пирами используется объект типа RTCDataChannel, который, в свою очередь, умеет работать с данными типа USVString, BLOB, ArrayBuffer или ArrayBufferView. Как раз для того, чтобы использовать ArrayBufferView, и нужно четко понимать, какого формата будут данные.

Итак, описав все сообщения, мы готовы продолжить и перейти непосредственно к организации взаимодействия между пирами. Здесь я постараюсь описать матчасть настолько кратко, насколько смогу. Вообще, пытаться рассказать про WebRTC во всех деталях — долгое и сложное занятие, тем более что в открытом доступе есть книга Ильи Григорика, которая является просто кладезем информации на эту и другие темы, касающиеся сетевого взаимодействия. Моя же цель, как я уже сказал, — дать краткое описание основных механизмов WebRTC, с изучения которых придется начать каждому.

Установка соединения


Что нужно для того, чтобы пользователи А и Б смогли установить peer-to-peer соединение между собой? Ну, как минимум каждый из пользователей должен знать адрес и порт, по которому его оппонент слушает и может получить входящие данные. Но как А и Б сообщат друг другу эту информацию, если связь еще не установлена? Для передачи этой информации нужен сервер. В терминологии WebRTC он называется signalling-сервер. И так как уже реализован свой сервер для игровых комнат, его же можно использовать и в качестве signalling-сервера.

Также кроме адресов и портов, А и Б должны договориться о параметрах устанавливаемой сессии.Например об использовании тех или иных кодеков и их параметров в случае аудио- и видео связи. Формат данных, описывающих всевозможные свойства соединения, называется SDP — Session Description Protocol. Более подробно с ним можно познакомиться на webrtchacks.com. Итак, исходя из вышесказанного, порядок обмена данными через signalling следующий:

  1. пользователь А посылает запрос на соединение пользователю Б;
  2. пользователь Б подтверждает запрос от А;
  3. получив подтверждение, пользователь А определяет свой IP, порт, возможные параметры сессии и посылает их пользователю Б;
  4. пользователь Б в ответ посылает свой адрес, порт и параметры сессии пользователю А.

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

Определение адреса и проверка доступности


Когда каждый из пользователей доступен по публичному IP-адресу или оба находятся в рамках одной подсети — все просто. Тогда каждый из них может запросить свой IP у операционной системы и отправить его через signalling своему оппоненту. Но что делать, если пользователь недоступен напрямую, а находится за NAT, и у него два адреса: один локальный, внутри подсети (192.168.1.1), второй — адрес самого NAT (50.76.44.114)? В этом случае ему каким-то образом нужно определить свой публичный адрес и порт.

Идея решения довольно проста: нужен публично доступный сервер, который, получив запрос от нас, отправит в ответ наш публичный адрес и порт.

Такие сервера называются STUN ( Session Traversal Utilities for NAT ). Существуют готовые решения, например, coturn, который можно развернуть в качестве своего STUN-сервера. Но можно поступить еще проще и воспользоваться уже развернутыми и доступными серверами, например от Google.

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

К счастью, задачу взаимодействия со STUN и задачу проверки доступности берет на себя ICE (Interactive Connectivity Establishment) фреймворк, встроенный в браузер. Все, что нам нужно, — обрабатывать события этого фреймворка. Итак, приступим к реализации…

Создание соединения


Поначалу может показаться, что процесс установки соединения достаточно сложный. Но, к счастью, вся сложность скрыта всего лишь за одним интерфейсом RTCPeerConnection, и на практике все проще, чем может показаться на первый взгляд. Полный код для класса, реализующего peer-to-peer соединение, можно посмотреть тут, дальше я поясню его.

Как я уже сказал, установка, мониторинг и закрытие соединения, а также работа с SDP и ICE кандидатами — все это делается через RTCPeerConnection. Более подробную информацию о конфигурации можно посмотреть, например, тут. Нам же в качестве конфигурации понадобится только адрес STUN-сервера от Google, о котором я говорил выше.

iceServers: [{
    url: 'stun:stun.l.google.com:19302'
}],
connect: function() {
    this.peerConnection = new RTCPeerConnection({
      iceServers: this.iceServers
    });
    // ...
}

RTCPeerConnection предоставляет набор колбеков для различных событий жизненного цикла соединения, из которого нам понадобятся:

  1. icecandidate — для обработки найденного кандидата;
  2. iceconnectionstatechange — для отслеживания состояния соединения;
  3. datachannel — для обработки открытого канала данных.

init: function(socket, peerUser, isInitiator) {
    // ...
    this.peerHandlers = {
      'icecandidate': this.onLocalIceCandidate,
      'iceconnectionstatechange': this.onIceConnectionStateChanged,
      'datachannel': this.onDataChannel
    };
    this.connect();
  },
  connect: function() {
    // ...
    Events.listen(this.peerConnection, this.peerHandlers, this);
    // ....
}

Отправка запроса на соединение


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

Определение параметров сессии


Для получения параметров сессии в RTCPeerConnection существуют методы createOffer — для вызова на инициирующей стороне, и createAnswer — на отвечающей. Результатом работы этих методов являются данные в формате SDP, которые необходимо отправить через signalling оппоненту. RTCPeerConnection хранит как локальное описание сессии, так и удаленное, полученное через signalling от оппонента. Для установки этих полей есть методы setLocalDescription и setRemoteDescription. Итак, допустим клиент А инициирует соединение, тогда порядок действий следующий:

1. Клиент А создает SDP-offer, устанавливает локальное описание сессии в своем RTCPeerConnection, после чего отправляет его клиенту Б:

connect: function() {
    // ...
    if (this.isInitiator) {
      this.setLocalDescriptionAndSend();
    }
  },
  
  setLocalDescriptionAndSend: function() {
    var self = this;
    self.getDescription()
      .then(function(localDescription) {
        self.peerConnection.setLocalDescription(localDescription)
          .then(function() {
            self.log('Sending SDP', 'green');
            self.sendSdp(self.peerUser.userId, localDescription);
          });
      })
      .catch(function(error) {
        self.log('onSdpError: ' + error.message, 'red');
      });
  },

  getDescription: function() {
    return this.isInitiator ?
      this.peerConnection.createOffer() :
      this.peerConnection.createAnswer();
  }

2. Клиент Б получает offer от клиента А и устанавливает удаленное описание сессии. После чего создает SDP-answer, устанавливает его в качестве локального описания сессии и отправляет клиенту А:

setSdp: function(sdp) {
    var self = this;
    // Create session description from sdp data
    var rsd = new RTCSessionDescription(sdp);
    // And set it as remote description for peer connection
    self.peerConnection.setRemoteDescription(rsd)
      .then(function() {
        self.remoteDescriptionReady = true;
        self.log('Got SDP from remote peer', 'green');
        // Add all received remote candidates
        while (self.pendingCandidates.length) {
          self.addRemoteCandidate(self.pendingCandidates.pop());
        }
        // Got offer? send answer
        if (!self.isInitiator) {
          self.setLocalDescriptionAndSend();
        }
      });
  }

4. После того как клиент А получает SDP-answer от клиента Б, он также устанавливает его в качестве удаленного описания сессии. В результате каждый из клиентов установил локальное описание сессии и удаленное, полученное от своего оппонента:



Сбор ICE-кандидатов


Каждый раз когда ICE-агент клиента А находит новую пару IP+port, которую можно использовать для связи, у RTCPeerConnection срабатывает событие icecandidate. Данные кандидата выглядят следующим образом:

candidate:842163049 1 <b>udp</b> 1677729535 <b>94.221.38.159 60478 typ srflx raddr 192.168.1.157 rport 60478</b> generation 0 ufrag KadE network-cost 50

Вот что можно понять, глядя на эти данные:

  1. udp: Если ICE-агент решит использовать этот кандидат для связи, то для нее будет использован udp транспорт;
  2. typ srflx — это кандидат, полученный путем обращения к STUN-серверу для определения адреса NAT;
  3. 94.221.38.159 60478 — адрес NAT и порт, который будет использован для связи;
  4. raddr 192.168.1.157 rport 60478 — адрес и порт внутри NAT.

Более подробно о протоколе описания ICE-кандидатов можно почитать тут.

Эти данные нужно передать через signalling клиенту Б, чтобы он добавил их в свой RTCPeerConnection. Точно так же поступает и клиент Б, когда обнаруживает свои пары IP+port:

 // When ice framework discoveres new ice candidate, we should send it
  // to opponent, so he knows how to reach us
  onLocalIceCandidate: function(event) {
    if (event.candidate) {
      this.log('Send my ICE-candidate: ' + event.candidate.candidate, 'gray');
      this.sendIceCandidate(this.peerUser.userId, event.candidate);
    } else {
      this.log('No more candidates', 'gray');
    }
  }

  addRemoteCandidate: function(candidate) {
    try {
      this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
      this.log('Added his ICE-candidate:' + candidate.candidate, 'gray');
    } catch (err) {
      this.log('Error adding remote ice candidate' + err.message, 'red');
    }
  }

Создание канала данных


Ну и, пожалуй, последнее, на чем стоит остановиться, это RTCDataChannel. Этот интерфейс предоставляет нам API, с помощью которого можно передавать произвольные данные, а также настраивать свойства доставки данных:

  • полную или частичную гарантию доставки сообщений;
  • упорядоченную или неупорядоченную доставку сообщений.

Более подробно про конфигурацию RTCDataChannel можно узнать, например, тут. В данный момент будет достаточно свойства ordered = false, чтобы сохранить семантику UDP при передаче наших данных. Как и RTCPeerConnection, RTCDataChannel предоставляет набор событий, описывающих жизненный цикл канала данных. Из него понадобятся open, close и message для открытия, закрытия канала и получения сообщения соответственно:

init: function(socket, peerUser, isInitiator) {
    // ...
    this.dataChannelHandlers = {
      'open': this.onDataChannelOpen,
      'close': this.onDataChannelClose,
      'message': this.onDataChannelMessage
    };
    this.connect();
  },
  connect: function() {
    // ...
    if (this.isInitiator) {
      this.openDataChannel(
          this.peerConnection.createDataChannel(this.CHANNEL_NAME, {
        ordered: false
      }));
    }
  },
  openDataChannel: function(channel) {
    this.dataChannel = channel;
    Events.listen(this.dataChannel, this.dataChannelHandlers, this);
  }

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

Больше игроков


Мы рассмотрели, как установить связь между двумя игроками, и этого, в принципе, достаточно, чтобы играть один на один. А если мы хотим, чтобы в одной комнате могли играть несколько игроков? Что тогда изменится? На самом деле — ничего, просто для каждой пары игроков должно быть свое соединение. Т.е. если вы играете в комнате еще с 3 игроками, у вас должно быть открыто 3 peer-to-peer соединения с каждым из них. Полный код класса, отвечающего за взаимодействие со всеми оппонентами по комнате, можно посмотреть тут.

Итак, signalling-сервер c комнатами готов, формат сообщений и способ их доставки обсудили, как теперь на основе этого сделать так, чтобы игроки видели друг друга?

Синхронизация местоположения


Идея синхронизации довольно простая: нужно один раз в какой-то промежуток времени отправлять оппонентам свои координаты, тогда они на основе этих данных могут достоверно отражать твое местоположение.

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

Отправка синхронизационного сообщения:

update: function() {
    // ...
    // Broadcast state
    this.connection.broadcastMessage(MessageBuilder.createMessage(MESSAGE_STATE)
      .setX(this.player.pos.x * 10)
      .setY(this.player.pos.y * 10)
      .setVelX((this.player.pos.x - this.player.last.x) * 10)
      .setVelY((this.player.pos.y - this.player.last.y) * 10)
      .setFrame(this.player.getAnimFrame())
      .setAnim(this.player.getAnimId())
      .setFlip(this.player.currentAnim.flip.x ? 1 : 0));
    // ...
  }

Получение синхронизационного сообщения:

onPeerMessage: function(message, user, peer) {
  // ...
  switch (message.getType()) {
      case MESSAGE_STATE:
        this.onPlayerState(remotePlayer, message);
        break;

      // ...
    }
  },
  
  onPlayerState: function(remotePlayer, message) {
    remotePlayer.setState(message);
  },
  
  // in RemotePlayer class:
  setState: function(state) {
      var x = state.getX() / 10;
      var y = state.getY() / 10;
      this.dx = state.getVelX() / 10; 
      this.dy = state.getVelY() / 10; 
      this.pos = {
        x: x,
        y: y
      };
      this.currentAnim = this.getAnimById(state.getAnim());
      this.currentAnim.frame = state.getFrame();
      this.currentAnim.flip.x = !!state.getFlip();
      this.stateUpdated = true;
 }

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



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

Экстраполяция координат


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



На практике же получается нечто иное. Интервалы между сообщениями распределены неравномерно, что приводит скачкообразной анимации и изменению координат:



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

Движение было бы гораздо более плавным, если бы в моменты задержек координаты игрока менялись пропорционально, пусть и не всегда достоверно точно:



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

setState: function(state) {
	var x = state.getX() / 10;
	var y = state.getY() / 10;
	this.dx = state.getVelX() / 10;
	this.dy = state.getVelY() / 10;
	this.pos = {
		x: x,
		y: y
	};
	this.currentAnim = this.getAnimById(state.getAnim());
	this.currentAnim.frame = state.getFrame();
	this.currentAnim.flip.x = !!state.getFlip();
	this.stateUpdated = true;
},
update: function() {
	if (this.stateUpdated) {
		this.stateUpdated = false;
	} else {
		this.pos.x += this.dx;
		this.pos.y += this.dy;
	}
	if( this.currentAnim ) {
		this.currentAnim.update();
	}
}

А вот как это выглядит после применения экстраполяции:



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



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

Другие игровые действия


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

Что получилось в итоге


Код (за исключением исходников самого ImpactJS) и инструкции по запуску можно посмотреть на гитхабе.

Рискну оставить тут эту ссылку, где можно попробовать поиграть. Не знаю, что там случится с моим single-core дроплетом, но будь что будет =)

Напоследок


Если вы дочитали до конца — спасибо! Значит, мой труд не пропал даром и вы нашли для себя что-то интересное. Вопросы, замечания и предложения оставляйте, пожалуйста, в комментариях.

Александр Гутников, frontend разработчик, Badoo.
Поделиться с друзьями
-->

Комментарии (50)


  1. Wedmer
    24.11.2016 10:56
    +4

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


    1. alxgutnikov
      24.11.2016 11:24

      Спасибо, рад что понравилось. Тут действительно много чего можно добавить — двойной прыжок например =).
      Хороший редактор уровней есть в самом impactJs, если что


      1. keilman
        25.11.2016 15:54

        Спасибо за Ваш труд!
        Для меня проявились некоторые ответы на мои вопросы.

        И да, я хотел проверить в браузере какие пакеты передаются во вкладке Network, а там почему то не логируется ничего… Это из-за сокетов?


        1. alxgutnikov
          25.11.2016 15:56

          Спасибо за отзыв. А какие именно вас интересуют пакеты? те которые к signalling серверу? или которые между пирами?


          1. keilman
            25.11.2016 15:58

            Между игроками.
            Я просто с коллегой в офисе тестировали и мне интересно стало ))


            1. keilman
              25.11.2016 16:04

              Вот тут я пытался проследить за обмениваемыми данными — http://s020.radikal.ru/i714/1611/9c/0931fa2cde30.png

              Я так понимаю, средствами браузера это не возможно?


              1. alxgutnikov
                25.11.2016 22:23

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


  1. impwx
    24.11.2016 11:23
    +4

    1. alxgutnikov
      24.11.2016 11:30

      Хорошая идея ;) захожу) Правда там к сожалению есть ограничение на количество игроков в комнате


      1. impwx
        24.11.2016 11:52

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


      1. jMas
        24.11.2016 13:49

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


        1. alxgutnikov
          24.11.2016 14:02

          вроде никакой задержки не задумывалось. Прямо ощутимая задержка?


          1. jMas
            24.11.2016 14:42

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


            1. alxgutnikov
              24.11.2016 18:25

              Ну игрок умирает не с первого попадения. А лазеры в хабракомнате — это нормальное явление, со мной такое постоянно происходит, знаете ли


    1. equand
      24.11.2016 13:23
      +1

      Там столько читерства и лагов )


      1. jok40
        25.11.2016 16:53

        Эт точно! На верхнем этаже стоит прокачанный читер с усиливающим зеркалом :)


        1. alxgutnikov
          25.11.2016 22:26
          +1

          Просто не надо в него стрелять, а так он довольно спокойный…


    1. CClarke
      25.11.2016 15:56
      +1

      неплохо бы добавить разные цвета спрайтов для игроков, иногда сложно разобрать где я, а где враг


      1. alxgutnikov
        25.11.2016 15:58

        Движение камеры за вашим игроком немного помогает в этом плане, но вы правы, это добавить цвета — было бы получше.


  1. Lord_Skiminok
    24.11.2016 11:25

    Круто, красавчек, все ясно и понятно :)


  1. CrazyNiger
    24.11.2016 11:34

    Просто шикарно, тоже хотел попробовать WebRTC для создания сетевых игр. И кстати вопрос, есть ли готовая реализация WebRTC клиента для node.js?


    1. alxgutnikov
      24.11.2016 11:48

      Я не использовал, поэтому ничего посоветовать не могу. Но думаю должны быть


    1. equand
      24.11.2016 13:23

      Попадетесь в то, что куча читеров будет


      1. CrazyNiger
        24.11.2016 13:35

        Не совсем понял, может комментарий не туда?


        1. equand
          24.11.2016 14:24

          Да, сорри


  1. Deosis
    24.11.2016 11:34

    Отличная статья. Вот только не стоит доверять полученным данным, иначе появится терминатор.


    1. alxgutnikov
      24.11.2016 11:44

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


  1. vlreshet
    24.11.2016 11:39
    +1

    Офигенная игра, всё классно работает, только вот одно замечание — почему прыжок не стрелкой вверх а иксом?) Жутко неудобно как по мне) И ники игроков бы ещё добавить, вообще супер было бы :) Добавил в избранное


    1. alxgutnikov
      24.11.2016 11:56

      Спасибо! Ников, скорборда, редактора карт и двойного прыжка действительно не хватает — каюсь, это потому, что, к сожалению также не хватает и времени все это сделать и пришлось остановиться на том что есть. По поводу управления — разве неудобно? мне кажется просто надо немного привыкнуть


      1. Nikobraz
        24.11.2016 11:58

        Я вспоминал где у меня эти кнопки. В файтеры и платформеры давно не играл. Самое универсальное — стрелки или WASD. Дальше Alt, Ctrl, Shift.


        1. Deosis
          24.11.2016 12:09

          Дальше Alt, Ctrl, Shift.
          Подобные клавиши нужно использовать с осторожностью.
          Винда не любит, когда их часто нажимают (Попробуйте быстро нажать Shift 5 раз)


          1. Nikobraz
            24.11.2016 13:12

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


      1. KIVagant
        24.11.2016 20:03
        +1

        Ну стрелка вверх как-то попривычнее.

        Давно не играл в такой лютый ламповый хардкор. Отличная статья.


  1. NiPh
    24.11.2016 13:13

    Кажется кто-то нашёл способ самостоятельно генерить выстрелы и месторасположение )

    Картинка того, что происходит в хабракомнате сейчас
    image


    1. NiPh
      24.11.2016 13:19

      Или вот к чему приводит рассчет изменений координат на клиенте без проверок на сервере )


      1. alxgutnikov
        24.11.2016 13:36

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


        1. vlreshet
          24.11.2016 14:21

          Ваша идея защиты от лавы меня заинтересовала, и я полез в консоль. Ух ты! Спасибо вам за понятный код! Играть безсмертным, с бесконечным количеством патроном и прыгающим как конь персонажем — офигенно :D

          P.S. осталось найти как ускорить скорость стрельбы, и поправить себе кнопку прыжка


          1. alxgutnikov
            24.11.2016 14:39
            +4

            Может тогда и двойной прыжок запилите? и скорборды…


  1. TargetSan
    24.11.2016 14:12

    А если передавать не позиции, а действия игрока?
    "Игрок начал двигаться вправо"
    "Игрок начал прыжок"
    "Игрок закончил прыжок"
    с добавкой в виде координат и метадаты для синхронизации
    Ну и может таки отправлять периодически сообщения синхронизации


    Тогда, по идее, и анимацию не надо передавать — она может обсчитываться на клиенте


    1. alxgutnikov
      24.11.2016 14:50

      Плюс передачи координат в том что они достоверно отражают вашу позицию. Действия же — отражают ее косвенно.
      А в чем выигрыш такого подхода?


      1. TargetSan
        24.11.2016 15:28
        +1

        По идее тот, что каждый клиент может просчитывать позицию каждого игрока на поле без непрерывной "подпитки" координатами. Т.е. мы знаем, что игрок движется со скоростью V. Если от оппонента пришло событие "начал двигаться вправо", мы можем полностью просчитать его положение на основании флага движения, известной скорости и текущей координаты. Так как противник предсказуемо переместится из позиции А в позицию А + Dir V Dt. Тогда мы будем менять механику движений только при изменении "флагов движения". А вместе с командами на смену флагов можно присылать координаты точки, в которой это случилось — чтобы подкорректировать ошибки.
        Т.е. мы передаём не состояния игрового мира, а события в игровом мире — что дешевле по объёму, и делать надо реже.
        И интерполяция как органичное следствие, а не костыль.


        1. alxgutnikov
          24.11.2016 18:23

          В этом есть смысл, но мне сложно сказать насколько хорошо это будет на практике — я не проверял такой подход. Но не забывайте про то что мы используем UDP — что-то может не дойти, что-то прийти не в том порядке в котором вы отсылали. Что будет если сообщение в котором вы отправили начал двигаться вправо по каким-то причинам не дойдет? вы на принимающей стороне будете думать что он все еще двигается в предыдущем направлении пока не получите новый сигнал о смене движения? нужно будет что-то придумывать на этот счет


      1. inborn_killer
        24.11.2016 17:51
        +1

        Достоверность — это всё же работа «сервера» или в вашем случае хоста. Пусть он один сможет читерить )

        А вообще, если кому будет интересно, на хабре был перевод отличной статьи на тему мультиплеера в динамичных играх.


  1. viktornaymayer
    24.11.2016 16:18
    +1

    Реально, очень круто. Автору спасибо огромное! :)


    1. alxgutnikov
      24.11.2016 16:18

      Вам спасибо!


      1. foxmuldercp
        26.11.2016 18:41

        Интересно, сильно ли усложнит код подписывание сообщений и проверка их на серверсайде, чтобы читерство исключить..


  1. Dimchansky
    25.11.2016 18:20

    Допустим, что нет сигнального промежуточного сервера. Я так понимаю, все равно возможно соединиться друг с другом, передав (например по скайпу) данные полученные от stun друг другу? Или достаточно одному выслать параметры для подключения?


    1. alxgutnikov
      25.11.2016 20:06

      Нет никаких правил по которым должен быть реализован сигналлинг — в предложенном вами примере: скайп и будет сигналлингом. Одному выслать параметры недостаточно — вам ведь нужно проверить доступность в обе стороны.


  1. Psychosynthesis
    30.11.2016 03:22

    Прикольно… А почему окошко таким мелким сделали?


    1. alxgutnikov
      30.11.2016 10:11

      Ну задумка была такая, что вы не видите сразу всю карту. То есть вы чаще всего не знаете где ваши противники и узнаете об этом когда они уже совсем близко к вам…