Статьи

Создайте многопользовательскую игру в пиратский шутер: в вашем браузере

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

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

Это то, что мы будем строить:

Снимок экрана финальной игры: два корабля атакуют друг друга

Вы можете попробовать живую версию игры здесь ! W или Up, чтобы двигаться в сторону мыши и нажмите, чтобы стрелять. (Если больше никого нет в сети, попробуйте открыть два окна браузера на одном компьютере или одно на вашем телефоне, чтобы посмотреть, как работает мультиплеер). Если вы заинтересованы в локальном запуске, полный исходный код также доступен на GitHub .

Я собрал эту игру, используя художественные ресурсы Kenney’s Pirate Pack и игровую среду Phaser . В этом уроке вы будете играть роль сетевого программиста. Ваша отправная точка будет полностью функционирующая версия этой игры для одного игрока, и ваша задача — написать сервер в Node.js, используя Socket.io для сетевой части. Чтобы этот учебник был управляемым, я сосредоточусь на многопользовательских частях и рассмотрю конкретные концепции Phaser и Node.js.

Там нет необходимости настраивать что-либо локально, потому что мы будем делать эту игру полностью в браузере на Glitch.com ! Glitch — это потрясающий инструмент для создания веб-приложений, в том числе серверной части, базы данных и всего остального. Он отлично подходит для создания прототипов, обучения и совместной работы, и я рад представить вам его в этом уроке.

Давайте погрузимся в.

Я разместил стартовый комплект на Glitch.com .

Несколько быстрых советов по интерфейсу: в любой момент вы можете увидеть предварительный просмотр своего приложения в реальном времени, нажав кнопку « Показать» (вверху слева).

Кнопка показа находится вверху слева на интерфейсе Glitch

Вертикальная боковая панель слева содержит все файлы в вашем приложении. Чтобы отредактировать это приложение, вам нужно «сделать ремикс». Это создаст его копию в вашей учетной записи (или раскошелит ее в git lingo). Нажмите на Remix эту кнопку.

Кнопка ремикса находится вверху редактора кода

На этом этапе вы будете редактировать приложение под анонимным аккаунтом. Вы можете войти (вверху справа), чтобы сохранить свою работу.

Теперь, прежде чем идти дальше, важно ознакомиться с кодом для игры, в которую вы пытаетесь добавить мультиплеер. Взгляните на index.html . В дополнение к объекту игрока (строка 35) необходимо GameLoop три важные функции: preload (строка 99), create (строка 115) и GameLoop (строка 142).

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

  • Сделайте мир больше (строка 29)обратите внимание, что существует отдельный размер мира для игрового мира и размер окна для фактического холста на странице .
  • Сделайте также пробел вперед (строка 53).
  • Измените тип корабля вашего игрока (строка 129).
  • Заставьте пули двигаться медленнее (строка 155).

Socket.io — это библиотека для управления взаимодействием в реальном времени в браузере с помощью WebSockets (в отличие от использования протокола, такого как UDP, если вы создаете многопользовательскую настольную игру). Он также имеет запасные варианты, чтобы убедиться, что он все еще работает, даже если WebSockets не поддерживаются. Таким образом, он заботится о протоколах обмена сообщениями и предоставляет вам удобную систему сообщений на основе событий.

Первое, что нам нужно сделать, это установить модуль Socket.io. На Glitch вы можете сделать это, перейдя в файл package.json и либо введя нужный модуль в зависимости, либо нажав Add package и введя «socket.io» .

Меню добавления пакета можно найти в верхней части редактора кода при выборе файла packagejson.

Это было бы хорошим временем, чтобы указать журналы сервера. Нажмите кнопку « Журналы» слева, чтобы открыть журнал сервера. Вы должны увидеть, что он устанавливает Socket.io вместе со всеми его зависимостями. Это то место, куда вы могли бы перейти, чтобы увидеть любые ошибки или вывод кода сервера.

Кнопка Журналы находится на левой стороне экрана

Теперь перейти на server.js . Здесь живет ваш серверный код. Прямо сейчас, это просто базовый шаблон для обслуживания нашего HTML. Добавьте эту строку вверху, чтобы включить Socket.io:

1
var io = require(‘socket.io’)(http);

Теперь нам также нужно включить Socket.io на клиенте, поэтому вернитесь к index.html и добавьте его вверху внутри <head> :

1
2
<!— Load the Socket.io networking library —>
<script src=»/socket.io/socket.io.js»></script>

Примечание: Socket.io автоматически обрабатывает клиентскую библиотеку по этому пути, поэтому эта строка работает, даже если вы не видите каталог /socket.io/ в ваших папках.

Теперь Socket.io включен и готов к работе!

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

Внизу server.js добавьте этот код:

1
2
3
4
// Tell Socket.io to start accepting connections
io.on(‘connection’, function(socket){
    console.log(«New client has connected with id:»,socket.id);
})

Это говорит Socket.io прослушивать любое событие connection , которое автоматически запускается при подключении клиента. Он создаст новый объект socket для каждого клиента, где socket.id является уникальным идентификатором для этого клиента.

Просто чтобы убедиться, что это работает, вернитесь к вашему клиенту ( index.html ) и добавьте эту строку где-нибудь в функцию create :

1
var socket = io();

Если вы запустите игру, а затем посмотрите журнал вашего сервера (нажмите на кнопку « Журналы» ), вы увидите, что он регистрирует это событие подключения!

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

connection событию было встроенным событием, которое запускает Socket.io для нас. Мы можем слушать любое пользовательское событие, которое мы хотим. Я собираюсь позвонить моему new-player , и я ожидаю, что клиент отправит его, как только он свяжется с информацией об их местонахождении. Это будет выглядеть так:

1
2
3
4
5
6
7
// Tell Socket.io to start accepting connections
io.on(‘connection’, function(socket){
    console.log(«New client has connected with id:»,socket.id);
    socket.on(‘new-player’,function(state_data){ // Listen for new-player event on this client
      console.log(«New player has state:»,state_data);
    })
})

Вы ничего не увидите в журнале сервера, если запустите это. Это потому, что мы еще не сказали клиенту о том, чтобы он выпустил событие для new-player . Но давайте представим, что об этом позаботимся, и продолжим работу на сервере. Что должно произойти после того, как мы получили местоположение нового игрока, который присоединился?

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

1
socket.broadcast.emit(‘create-player’,state_data);

Вызов socket.emit просто отправил бы сообщение тому клиенту. Вызов socket.broadcast.emit отправляет его каждому клиенту, подключенному к серверу, за исключением того, что один сокет был вызван.

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

Код сервера теперь должен выглядеть так:

1
2
3
4
5
6
7
8
// Tell Socket.io to start accepting connections
io.on(‘connection’, function(socket){
    console.log(«New client has connected with id:»,socket.id);
    socket.on(‘new-player’,function(state_data){ // Listen for new-player event on this client
      console.log(«New player has state:»,state_data);
      socket.broadcast.emit(‘create-player’,state_data);
    })
})

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

Теперь, чтобы завершить этот цикл, мы знаем, что нам нужно сделать две вещи на клиенте:

  1. Отправьте сообщение с нашими данными о местоположении, как только мы подключимся.
  2. Слушайте события create-player и порождайте игрока в этом месте.

Для первой задачи, после того как мы создадим игрока в нашей функции create (около строки 135), мы можем отправить сообщение, содержащее данные о местоположении, которое мы хотим отправить, следующим образом:

1
socket.emit(‘new-player’,{x:player.sprite.x,y:player.sprite.y,angle:player.sprite.rotation})

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

Прежде чем двигаться вперед, Проверьте, что это работает . Вы должны увидеть сообщение в журналах сервера, говорящее что-то вроде:

1
New player has state: { x: 728.8180247836519, y: 261.9979387913289, angle: 0 }

Мы знаем, что наш сервер получает наше сообщение о подключении нового игрока, а также правильно получает данные о его местоположении!

Далее мы хотим прослушать запрос на создание нового игрока. Мы можем разместить этот код сразу после нашего emit, и он должен выглядеть примерно так:

1
2
3
4
socket.on(‘create-player’,function(state){
  // CreateShip is a function I’ve already defined to create and return a sprite
  CreateShip(1,state.x,state.y,state.angle)
})

Теперь проверь это . Откройте два окна вашей игры и посмотрите, работает ли она.

Что вы должны увидеть, так это то, что после открытия двух клиентов у первого клиента будет два появившихся корабля, а у второго — только один.

Задача: Можете ли вы выяснить, почему это происходит? Или как вы можете это исправить? Перейдите к логике клиент / сервер, которую мы написали, и попробуйте отладить ее.

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

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

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

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

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

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

Для этого я буду:

  1. Держите словарь игроков, ключом является их идентификатор, а значением — данные о местоположении.
  2. Добавьте проигрыватель в этот словарь при подключении и отправьте событие обновления.
  3. Удалите плеер из этого словаря при отключении и отправьте событие обновления.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
// Tell Socket.io to start accepting connections
// 1 — Keep a dictionary of all the players as key/value
var players = {};
io.on(‘connection’, function(socket){
    console.log(«New client has connected with id:»,socket.id);
    socket.on(‘new-player’,function(state_data){ // Listen for new-player event on this client
      console.log(«New player has state:»,state_data);
      // 2 — Add the new player to the dict
      players[socket.id] = state_data;
      // Send an update event
      io.emit(‘update-players’,players);
    })
    socket.on(‘disconnect’,function(){
      // 3- Delete from dict on disconnect
      delete players[socket.id];
      // Send an update event
    })
})

Клиентская сторона немного сложнее. С одной стороны, нам нужно только беспокоиться о событии update-players Players сейчас, но с другой стороны, мы должны учитывать создание большего количества кораблей, если сервер отправляет нам больше кораблей, чем мы знаем, или уничтожение, если у нас слишком много ,

Вот как я обработал это событие на клиенте:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Listen for other players connecting
// NOTE: You must have other_players = {} defined somewhere
socket.on(‘update-players’,function(players_data){
    var players_found = {};
    // Loop over all the player data received
    for(var id in players_data){
        // If the player hasn’t been created yet
        if(other_players[id] == undefined && id != socket.id){ // Make sure you don’t create yourself
            var data = players_data[id];
            var p = CreateShip(1,data.x,data.y,data.angle);
            other_players[id] = p;
            console.log(«Created new player at (» + data.x + «, » + data.y + «)»);
        }
        players_found[id] = true;
         
        // Update positions of other players
        if(id != socket.id){
          other_players[id].x = players_data[id].x;
          other_players[id].y = players_data[id].y;
          other_players[id].rotation = players_data[id].angle;
        }
         
         
    }
    // Check if a player is missing and delete them
    for(var id in other_players){
        if(!players_found[id]){
            other_players[id].destroy();
            delete other_players[id];
        }
    }
    
})

Я отслеживаю корабли на клиенте в словаре под названием other_players который я просто определил в верхней части моего скрипта (здесь не показан). Поскольку сервер отправляет данные об игроках всем игрокам, я должен добавить проверку, чтобы клиент не создавал дополнительный спрайт для себя. (Если у вас возникли проблемы со структурированием, вот полный код, который должен быть в index.html на данный момент).

Теперь проверьте это . Вы должны иметь возможность создавать и закрывать несколько клиентов и видеть правильное количество судов, появляющихся в правильных позициях!

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

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

И это должно быть! Теперь ваша очередь попробовать и реализовать это самостоятельно.

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

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

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

Еще один способ сделать это — просто отправить серверу обновления с постоянной скоростью, независимо от того, сколько сообщений они получили от игроков. Обновление сервера со скоростью около 30 раз в секунду кажется общим стандартом.

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

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

  • Каждый клиент отправляет позиции всех своих пуль в каждый кадр.
  • Сервер передает это каждому игроку.

Но существует проблема.

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

Чтобы смягчить это, мы попробуем другую схему:

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

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

Теперь, чтобы реализовать это, я добавлю emit при съемке. Я больше не буду создавать настоящий спрайт, поскольку его существование и местоположение теперь полностью определяются сервером. Наш новый код стрельбы пулями в index.html должен выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// Shoot bullet
if(game.input.activePointer.leftButton.isDown && !this.shot){
    var speed_x = Math.cos(this.sprite.rotation + Math.PI/2) * 20;
    var speed_y = Math.sin(this.sprite.rotation + Math.PI/2) * 20;
    /* The server is now simulating the bullets, clients are just rendering bullet locations, so no need to do this anymore
    var bullet = {};
    bullet.speed_x = speed_x;
    bullet.speed_y = speed_y;
    bullet.sprite = game.add.sprite(this.sprite.x + bullet.speed_x,this.sprite.y + bullet.speed_y,’bullet’);
    bullet_array.push(bullet);
    */
    this.shot = true;
    // Tell the server we shot a bullet
    socket.emit(‘shoot-bullet’,{x:this.sprite.x,y:this.sprite.y,angle:this.sprite.rotation,speed_x:speed_x,speed_y:speed_y})
}

Теперь вы также можете закомментировать весь этот раздел, который обновляет маркеры на клиенте:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
/* We’re updating the bullets on the server, so we don’t need to do this on the client anymore
// Update bullets
for(var i=0;i<bullet_array.length;i++){
    var bullet = bullet_array[i];
    bullet.sprite.x += bullet.speed_x;
    bullet.sprite.y += bullet.speed_y;
    // Remove if it goes too far off screen
    if(bullet.sprite.x < -10 || bullet.sprite.x > WORLD_SIZE.w || bullet.sprite.y < -10 || bullet.sprite.y > WORLD_SIZE.h){
        bullet.sprite.destroy();
        bullet_array.splice(i,1);
        i—;
    }
}
*/

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
// Listen for bullet update events
socket.on(‘bullets-update’,function(server_bullet_array){
  // If there’s not enough bullets on the client, create them
 for(var i=0;i<server_bullet_array.length;i++){
      if(bullet_array[i] == undefined){
          bullet_array[i] = game.add.sprite(server_bullet_array[i].x,server_bullet_array[i].y,’bullet’);
      } else {
          //Otherwise, just update it!
          bullet_array[i].x = server_bullet_array[i].x;
          bullet_array[i].y = server_bullet_array[i].y;
      }
  }
  // Otherwise if there’s too many, delete the extra
  for(var i=server_bullet_array.length;i<bullet_array.length;i++){
       bullet_array[i].destroy();
       bullet_array.splice(i,1);
       i—;
   }
                   
                })

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

Теперь на server.js нам нужно отслеживать и моделировать маркеры. Сначала мы создаем массив для отслеживания пуль, так же, как у нас есть один для игроков:

1
var bullet_array = [];

Далее мы слушаем наше событие стрелять пули:

1
2
3
4
5
6
7
// Listen for shoot-bullet events and add it to our bullet array
 socket.on(‘shoot-bullet’,function(data){
   if(players[socket.id] == undefined) return;
   var new_bullet = data;
   data.owner_id = socket.id;
   bullet_array.push(new_bullet);
 });

Теперь мы моделируем пули 60 раз в секунду:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
// Update the bullets 60 times per frame and send updates
function ServerGameLoop(){
  for(var i=0;i<bullet_array.length;i++){
    var bullet = bullet_array[i];
    bullet.x += bullet.speed_x;
    bullet.y += bullet.speed_y;
     
    // Remove if it goes too far off screen
    if(bullet.x < -10 || bullet.x > 1000 || bullet.y < -10 || bullet.y > 1000){
        bullet_array.splice(i,1);
        i—;
    }
         
  }
   
}
 
setInterval(ServerGameLoop, 16);

И последний шаг — отправить событие обновления где-то внутри этой функции (но определенно вне цикла for):

1
2
// Tell everyone where all the bullets are by sending the whole array
 io.emit(«bullets-update»,bullet_array);

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

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

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

  • Проверьте, достаточно ли близка пуля к любому игроку на сервере.
  • Отправлять событие всем клиентам всякий раз, когда определенный игрок получает удар.
  • Пусть клиент прослушает событие удара и заставит корабль мигать при попадании.

Вы можете попробовать сделать это самостоятельно. Чтобы заставить игрока мигать при ударе, просто установите его альфа на 0:

1
player.sprite.alpha = 0;

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

1
2
3
4
5
6
7
for(var id in other_players){
 if(other_players[id].alpha < 1){
        other_players[id].alpha += (1 — other_players[id].alpha) * 0.16;
    } else {
        other_players[id].alpha = 1;
    }
}

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

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

Если вы проделали все шаги до этого момента, я хотел бы поздравить вас. Вы только что создали рабочую многопользовательскую игру! Давай, отправь другу и наблюдай за волшебством многопользовательского онлайн-объединения игроков!

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

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

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

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

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

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

1
2
3
4
5
6
// Update positions of other players
if(id != socket.id){
  other_players[id].target_x = players_data[id].x;
  other_players[id].target_y = players_data[id].y;
  other_players[id].target_rotation = players_data[id].angle;
}

Затем внутри нашей функции обновления (все еще в клиенте) мы зациклимся на всех других игроках и подтолкнем их к этой цели:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
// Interpolate all players to where they should be
for(var id in other_players){
    var p = other_players[id];
    if(p.target_x != undefined){
        px += (p.target_x — px) * 0.16;
        py += (p.target_y — py) * 0.16;
        // Interpolate angle while avoiding the positive/negative issue
        var angle = p.target_rotation;
        var dir = (angle — p.rotation) / (Math.PI * 2);
        dir -= Math.round(dir);
        dir = dir * Math.PI * 2;
        p.rotation += dir * 0.16;
    }
}

Таким образом, ваш сервер будет отправлять вам обновления 30 раз в секунду, но при этом играть в игру со скоростью 60 кадров в секунду, и все будет выглядеть гладко!

Уф! Мы просто много чего рассказали. Напомним, что мы рассмотрели, как отправлять сообщения между клиентом и сервером, и как синхронизировать состояние игры, когда сервер передает ее всем игрокам. Это самый простой способ создать многопользовательскую онлайн-игру.

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

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

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

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

Расширенное меню опций позволяет импортировать экспорт или скачать ваш проект

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