company_banner

WebRTC: Делаем peer to peer игру на javascript

  • Tutorial
Недавно мне довелось поработать над прототипом видеочата. Это был отличный повод поближе познакомиться с концепциями 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.
Badoo
371,00
Big Dating
Поделиться публикацией

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

    +4
    Странно, что вы не повесили флажок, что это туториал. Он вышел замечательным. Все просто, понятно и без воды.
    Первым, что приходит в голову — добавить еще уровней, редактор уровней и парочку режимов.
      0
      Спасибо, рад что понравилось. Тут действительно много чего можно добавить — двойной прыжок например =).
      Хороший редактор уровней есть в самом impactJs, если что
        0
        Спасибо за Ваш труд!
        Для меня проявились некоторые ответы на мои вопросы.

        И да, я хотел проверить в браузере какие пакеты передаются во вкладке Network, а там почему то не логируется ничего… Это из-за сокетов?
          0
          Спасибо за отзыв. А какие именно вас интересуют пакеты? те которые к signalling серверу? или которые между пирами?
            0
            Между игроками.
            Я просто с коллегой в офисе тестировали и мне интересно стало ))
              0
              Вот тут я пытался проследить за обмениваемыми данными — http://s020.radikal.ru/i714/1611/9c/0931fa2cde30.png

              Я так понимаю, средствами браузера это не возможно?
                0
                Да, через девтулз вы их не увидите. Можете конечно в отладчике их посмотреть, если найдете место где происходит отправка/получение, но это не удобно
      +4
        0
        Хорошая идея ;) захожу) Правда там к сожалению есть ограничение на количество игроков в комнате
          0
          Игра и правда классная. Единственное, чего хочется добавить — UI для постоянного отображения количества здоровья, патронов и фрагов. Возможно, еще показывать таблицу рекордсменов в данной комнате.
            0

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

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

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

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

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

              0
              Эт точно! На верхнем этаже стоит прокачанный читер с усиливающим зеркалом :)
                +1
                Просто не надо в него стрелять, а так он довольно спокойный…
              +1
              неплохо бы добавить разные цвета спрайтов для игроков, иногда сложно разобрать где я, а где враг
                0
                Движение камеры за вашим игроком немного помогает в этом плане, но вы правы, это добавить цвета — было бы получше.
              0
              Круто, красавчек, все ясно и понятно :)
                0
                Просто шикарно, тоже хотел попробовать WebRTC для создания сетевых игр. И кстати вопрос, есть ли готовая реализация WebRTC клиента для node.js?
                  0
                  Я не использовал, поэтому ничего посоветовать не могу. Но думаю должны быть
                    0

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

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

                        Да, сорри

                    0
                    Отличная статья. Вот только не стоит доверять полученным данным, иначе появится терминатор.
                      0
                      Очень верное замечание, действительно нужно продумывать механизмы защиты от таких ситуаций. Но я старался осветить основы, а это уже более продвинутая тема на мой взгляд
                      +1
                      Офигенная игра, всё классно работает, только вот одно замечание — почему прыжок не стрелкой вверх а иксом?) Жутко неудобно как по мне) И ники игроков бы ещё добавить, вообще супер было бы :) Добавил в избранное
                        0
                        Спасибо! Ников, скорборда, редактора карт и двойного прыжка действительно не хватает — каюсь, это потому, что, к сожалению также не хватает и времени все это сделать и пришлось остановиться на том что есть. По поводу управления — разве неудобно? мне кажется просто надо немного привыкнуть
                          0
                          Я вспоминал где у меня эти кнопки. В файтеры и платформеры давно не играл. Самое универсальное — стрелки или WASD. Дальше Alt, Ctrl, Shift.
                            0
                            Дальше Alt, Ctrl, Shift.
                            Подобные клавиши нужно использовать с осторожностью.
                            Винда не любит, когда их часто нажимают (Попробуйте быстро нажать Shift 5 раз)
                              0
                              Знаю это, стараюсь отключать залипание везде, где это возможно.
                            +1
                            Ну стрелка вверх как-то попривычнее.

                            Давно не играл в такой лютый ламповый хардкор. Отличная статья.
                          0
                          Кажется кто-то нашёл способ самостоятельно генерить выстрелы и месторасположение )
                          Картинка того, что происходит в хабракомнате сейчас
                          image
                            0
                            Или вот к чему приводит рассчет изменений координат на клиенте без проверок на сервере )
                              0
                              Как я уже говорил — защита от такого — это уже тема другой статьи. Но круто что кто-то полез ковырять код и разбираться как оно там устроено. На самом деле это совсем не трудно сделать, если умеешь пользоваться отладчиком в браузере и читать код. Можно еще сделать, чтобы не умирать падая в лаву, отражать попавшие в вас пули и т д
                                0
                                Ваша идея защиты от лавы меня заинтересовала, и я полез в консоль. Ух ты! Спасибо вам за понятный код! Играть безсмертным, с бесконечным количеством патроном и прыгающим как конь персонажем — офигенно :D

                                P.S. осталось найти как ускорить скорость стрельбы, и поправить себе кнопку прыжка
                                  +4
                                  Может тогда и двойной прыжок запилите? и скорборды…
                            0

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


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

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

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

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

                                  А вообще, если кому будет интересно, на хабре был перевод отличной статьи на тему мультиплеера в динамичных играх.
                                +1
                                Реально, очень круто. Автору спасибо огромное! :)
                                  0
                                  Вам спасибо!
                                    0

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

                                  0
                                  Допустим, что нет сигнального промежуточного сервера. Я так понимаю, все равно возможно соединиться друг с другом, передав (например по скайпу) данные полученные от stun друг другу? Или достаточно одному выслать параметры для подключения?
                                    0
                                    Нет никаких правил по которым должен быть реализован сигналлинг — в предложенном вами примере: скайп и будет сигналлингом. Одному выслать параметры недостаточно — вам ведь нужно проверить доступность в обе стороны.
                                    0
                                    Прикольно… А почему окошко таким мелким сделали?
                                      0
                                      Ну задумка была такая, что вы не видите сразу всю карту. То есть вы чаще всего не знаете где ваши противники и узнаете об этом когда они уже совсем близко к вам…
                                        0
                                        парень с планшета в мобильной сети, я с корпоративной сети на работе. я создал комнату 3, дал ему ссылку, не видим друг друга. Что я делаю не так?
                                          0
                                          Скорее всего у вас в корпоративной сети есть настройки для udp которые не позволяют связаться. p2p не всегда возможен к сожалению. Для таких случаев нужно поднимать turn сервер, который будет пускать трафик через себя, но у меня его нет

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

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