Создание многопользовательской игры в Phaser 3 с Socket.IO - Часть 2

В первой части этого руководства мы создали сервер Node.js, настроили базовую игру Phaser и добавили Socket.io, чтобы обеспечить взаимодействие между ними.В этом уроке мы собираемся сосредоточиться на добавлении клиентского кода, который будет: добавлять и удалять игроков из игры, обрабатывать ввод игроков и позволять игрокам собирать предметы.

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

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

Добавление игроков на клиенте

Теперь, когда у нас есть серверный код для добавления игроков, мы будем работать над кодом на стороне клиента. Первое, что нам нужно сделать, это загрузить ресурсы, которые будут использоваться для игрока. На этом уроке мы будем использовать некоторые изображения из пакета ресурсов Kenny’s Space Shooter Redux. Изображение для игрока можно скачать здесь.

В папке public создайте новую папку assets и поместите данное изображение в ней. Чтобы загрузить изображение в нашу игру, вам нужно добавить следующую строку внутри функции preload в файле game.js:

this.load.image('ship', 'assets/spaceShips_001.png');

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

Обновите функцию create в game.js в соответствии с нижеследующим кодом:

function create() {
  var self = this;
  this.socket = io();
  this.socket.on('currentPlayers', function (players) {
    Object.keys(players).forEach(function (id) {
      if (players[id].playerId === self.socket.id) {
        addPlayer(self, players[id]);
      }
    });
  });
}

Давайте рассмотрим код, который мы только что добавили:

  • Мы использовали socket.on для прослушивания события currentPlayers, и когда это событие срабатывает, то будет вызываться анонимная функция, аргументом которой выступает объект players, который мы получили от нашего сервера.
  • Когда эта функция вызывается, мы перебираем каждого игрока и проверяем, совпадает ли идентификатор этого игрока с идентификатором сокета текущего игрока.
  • Чтобы перебрать игроков, мы используем Object.keys() для создания массива всех ключей в передаваемом объекте. Полученный массив мы перебираем с помощью метода forEach( ) для получения каждого элемента в массиве.
  • Наконец, мы вызываем функцию addPlayer( ) и передаем ей информацию о текущем игроке и ссылку на текущую сцену.

Теперь давайте добавим функцию addPlayer в game.js. Добавьте следующий код в конец файла:

function addPlayer(self, playerInfo) {
  self.ship = self.physics.add.image(playerInfo.x, playerInfo.y, 'ship').setOrigin(0.5, 0.5).setDisplaySize(53, 40);
  if (playerInfo.team === 'blue') {
    self.ship.setTint(0x0000ff);
  } else {
    self.ship.setTint(0xff0000);
  }
  self.ship.setDrag(100);
  self.ship.setAngularDrag(100);
  self.ship.setMaxVelocity(200);
}

В приведенном выше коде мы:

  • Создаем корабль нашего игрока, используя координаты x и y, которые мы сгенерировали в серверном коде.
  • Вместо того, чтобы просто использовать self.add.image для создания корабля нашего игрока, мы использовали self.physics.add.image, чтобы этот объект мог использовать аркадную физику.
  • Мы использовали setOrigin(), чтобы установить начало игрового объекта в центре объекта, а не в верхнем левом углу. Мы это сделали, т.к. объект у нас будет вращаться, а вращается он вокруг начальной точки.
  • Мы использовали setDisplaySize( ), чтобы изменить размер и масштаб игрового объекта. Первоначально изображение нашего корабля было размером 106 × 80 пикселей, что было немного большим для нашей игры. После вызова setDisplaySize( ) изображение в игре теперь 53 × 40 px.
  • Мы использовали setTint( ), чтобы изменить цвет игрового объекта корабля, и мы выбираем цвет в зависимости от команды, которая была сгенерирована, когда мы создавали информацию об игроке на сервере.
  • Наконец, мы использовали методы setDrag(), setAngularDrag() и setMaxVelocity(), чтобы изменить реакцию игрового объекта на аркадную физику. Методы setDrag и setAngularDrag используются для управления уровнем сопротивления (вертикальный, горизонтальный и угловой), с которым объект столкнется при движении. setMaxVelocity используется для управления максимальной скоростью, которую может достичь игровой объект.

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

Добавление других игроков

Теперь, когда в игре появился наш игрок, мы добавим логику для отображения других игроков в нашей игре. В первой части руководства мы уже настроили Socket.io для генерации событий newPlayer и disconnect. Мы будем использовать эти два события и немного изменим код для события currentPlayers, чтобы добавлять и удалять других игроков из нашей игры. Для этого обновите функцию create, чтобы она соответствовала следующему коду:

function create() {
  var self = this;
  this.socket = io();
  this.otherPlayers = this.physics.add.group();
  this.socket.on('currentPlayers', function (players) {
    Object.keys(players).forEach(function (id) {
      if (players[id].playerId === self.socket.id) {
        addPlayer(self, players[id]);
      } else {
        addOtherPlayers(self, players[id]);
      }
    });
  });
  this.socket.on('newPlayer', function (playerInfo) {
    addOtherPlayers(self, playerInfo);
  });
  this.socket.on('disconnect', function (playerId) {
    self.otherPlayers.getChildren().forEach(function (otherPlayer) {
      if (playerId === otherPlayer.playerId) {
        otherPlayer.destroy();
      }
    });
  });
}

Давайте рассмотрим код, который мы только что добавили:

  • Мы создали новую группу под названием otherPlayers, которая будет использоваться для управления всеми другими игроками в нашей игре. Группы в Phaser позволяют нам управлять похожими игровыми объектами как одним целым. Одним из примеров является то, что вместо того, чтобы проверять наличие столкновений на каждом из этих игровых объектов в отдельности, мы можем проверять наличие столкновений между группой и другими игровыми объектами.
  • Мы обновили функцию, которая вызывается при возникновении события currentPlayers. Теперь в ней вызывается функция addOtherPlayers для всех игроков, кроме текущего игрока.
  • Мы использовали метод socket.on() для прослушивания событий newPlayer и disconnect.
  • Когда происходит событие newPlayer, мы вызываем функцию addOtherPlayers, чтобы добавить этого нового игрока в нашу игру.
  • Когда происходит событие disconnect, мы берем идентификатор этого игрока и удаляем этого игрока из игры. Мы делаем это, вызывая метод getChildren( ) в нашей группе otherPlayers. Метод getChildren( ) возвращает массив всех игровых объектов, находящихся в этой группе, а затем с помощью метода forEach( ) мы перебираем элементы полученного массива.
  • Наконец, мы используем метод destroy( ) для удаления этого игрового объекта из игры.

Теперь давайте добавим функцию addOtherPlayers в нашу игру. Добавьте следующий код в конец файла game.js:

function addOtherPlayers(self, playerInfo) {
  const otherPlayer = self.add.sprite(playerInfo.x, playerInfo.y, 'otherPlayer').setOrigin(0.5, 0.5).setDisplaySize(53, 40);
  if (playerInfo.team === 'blue') {
    otherPlayer.setTint(0x0000ff);
  } else {
    otherPlayer.setTint(0xff0000);
  }
  otherPlayer.playerId = playerInfo.playerId;
  self.otherPlayers.add(otherPlayer);
}

Этот код очень похож на код, который мы добавили в функцию addPlayer(). Основное отличие состоит в том, что мы добавляем игровой объект другого игрока в группу otherPlayers.

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

Это изображение нужно будет поместить в папку assets. Как только изображение появится, мы можем загрузить его в нашу игру. В функцию preload добавьте следующую строку:

this.load.image('otherPlayer', 'assets/enemyBlack5.png');

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

Обработка ввода игрока

Научившись отображать всех игроков, мы можем приступить к обработке ввода игрока, чтобы позволить нашему космическому кораблю двигаться. Мы будем обрабатывать ввод с помощью встроенного в Phaser диспетчера клавиатуры. Для этого добавьте следующую строку внизу функции create в game.js:

this.cursors = this.input.keyboard.createCursorKeys();

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

Добавьте следующий код в функцию update в game.js:

if (this.ship) {
    if (this.cursors.left.isDown) {
      this.ship.setAngularVelocity(-150);
    } else if (this.cursors.right.isDown) {
      this.ship.setAngularVelocity(150);
    } else {
      this.ship.setAngularVelocity(0);
    }

    if (this.cursors.up.isDown) {
      this.physics.velocityFromRotation(this.ship.rotation + 1.5, 100, this.ship.body.acceleration);
    } else {
      this.ship.setAcceleration(0);
    }

    this.physics.world.wrap(this.ship, 5);
  }

В приведенном выше коде мы сделали следующее:

  • Мы проверяем, нажаты ли клавиши "влево", "вправо" или "вверх".
  • Если нажать "левую" или "правую" клавишу, то мы изменяем угловую скорость игрока, вызвав метод setAngularVelocity( ). Угловая скорость позволит кораблю вращаться влево и вправо.
  • Если ни "левая", ни "правая" клавиши не нажаты, мы сбрасываем угловую скорость обратно в 0.
  • Если нажата клавиша "вверх", мы обновляем скорость корабля, в противном случае мы устанавливаем ее на 0.
  • Наконец, если корабль уходит с экрана, мы хотим, чтобы он появился с другой стороны экрана. Мы делаем это, вызывая метод physics.world.wrap(), которому мы передаем ограничиваемый игровой объект и смещение. Если вы обновите свою игру, вы сможете перемещать свой корабль по экрану.

Обработка движений других игроков

Теперь, когда наш игрок может двигаться, мы переходим к управлению движением других игроков в нашей игре. Чтобы отслеживать движения других игроков и перемещать их спрайты в нашей игре, нам нужно будет генерировать новое событие каждый раз, когда игрок движется. В функции update файла game.js добавьте следующий код в конец блока с условием if за строчкой this.physics.world.wrap(this.ship, 5);:

// генерация события движения
var x = this.ship.x;
var y = this.ship.y;
var r = this.ship.rotation;
if (this.ship.oldPosition && (x !== this.ship.oldPosition.x || y !== this.ship.oldPosition.y || r !== this.ship.oldPosition.rotation)) {
  this.socket.emit('playerMovement', { x: this.ship.x, y: this.ship.y, rotation: this.ship.rotation });
}

// сохраняем данные о старой позиции
this.ship.oldPosition = {
  x: this.ship.x,
  y: this.ship.y,
  rotation: this.ship.rotation
};

Давайте рассмотрим код, который мы только что добавили:

  • Мы создали три новые переменные и используем их для хранения информации об игроке.
  • Затем мы проверяем, изменились ли вращение или положение игрока, сравнивая эти переменные с предыдущим вращением и положением игрока. Если позиция или ротация игрока изменились, то мы генерируем новое событие playerMovement и передаем ему информацию об игроке.
  • Наконец, мы сохраняем текущее вращение и положение игрока.

Далее нам нужно обновить код работы с Socket.io в файле server.js, чтобы прослушивать новое событие playerMovement. В файле server.js добавьте следующий код под блоком socket.io('disconnect'):

// когда игроки движутся, то обновляем данные по ним
socket.on('playerMovement', function (movementData) {
  players[socket.id].x = movementData.x;
  players[socket.id].y = movementData.y;
  players[socket.id].rotation = movementData.rotation;
  // отправляем общее сообщение всем игрокам о перемещении игрока
  socket.broadcast.emit('playerMoved', players[socket.id]);
});

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

Наконец, нам нужно обновить код на стороне клиента, чтобы прослушать это новое событие, и когда это событие будет сгенерировано, нам нужно будет обновить спрайт этого игрока в игре. В функции create в файле game.js добавьте следующий код:

this.socket.on('playerMoved', function (playerInfo) {
  self.otherPlayers.getChildren().forEach(function (otherPlayer) {
    if (playerInfo.playerId === otherPlayer.playerId) {
      otherPlayer.setRotation(playerInfo.rotation);
      otherPlayer.setPosition(playerInfo.x, playerInfo.y);
    }
  });
});

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

Собираем звезды

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

Мы добавим две новые переменные на наш сервер: star и scores. Переменная star будет использоваться для хранения позиции нашего собираемого предмета, а переменная scores будет использоваться для отслеживания результатов обеих команд. В файле server.js добавьте следующий код ниже строки var players = {};:

var star = {
  x: Math.floor(Math.random() * 700) + 50,
  y: Math.floor(Math.random() * 500) + 50
};
var scores = {
  blue: 0,
  red: 0
};

Далее мы отправим два новых события при подключении нового игрока к игре: starLocation и scoreUpdate. Мы будем использовать эти два события, чтобы отправить новому игроку местоположение звезды и текущие результаты обеих команд. В файле server.js добавьте следующий код ниже строки socket.emit('currentPlayers', players);:

// отправляем объект star новому игроку
socket.emit('starLocation', star);
// отправляем текущий счет
socket.emit('scoreUpdate', scores);

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

НВ файле server.js добавьте следующий код ниже блока socket.on('playerMovement'):

socket.on('starCollected', function () {
  if (players[socket.id].team === 'red') {
    scores.red += 10;
  } else {
    scores.blue += 10;
  }
  star.x = Math.floor(Math.random() * 700) + 50;
  star.y = Math.floor(Math.random() * 500) + 50;
  io.emit('starLocation', star);
  io.emit('scoreUpdate', scores);
});

Теперь, когда логика нашего сервера была обновлена, нам нужно будет обновить код на стороне клиента в файле game.js. Для отображения результатов двух команд мы будем использовать текстовый объект Phaser. В конец функции create добавьте следующий код:

this.blueScoreText = this.add.text(16, 16, '', { fontSize: '32px', fill: '#0000FF' });
this.redScoreText = this.add.text(584, 16, '', { fontSize: '32px', fill: '#FF0000' });

this.socket.on('scoreUpdate', function (scores) {
  self.blueScoreText.setText('Синие: ' + scores.blue);
  self.redScoreText.setText('Красные: ' + scores.red);
});

Давайте рассмотрим код, который мы только что добавили:

  • Мы создали два новых текстовых объекта с помощью метода this.add.text(). В этом методе мы указали местоположение текста, текст умолчанию (пустая строка), а также размер шрифта и цвет заливки.
  • При возникновении события ScoreUpdate мы обновляем текст игровых объектов, вызывая метод setText( ) и передаем очки команд соответствующему текстовому объекту. Если вы обновите свою игру, вы должны увидеть счет каждой команды: Наконец, последнее, что нам осталось добавить - это логика для сбора звезд. Во-первых, нам нужно загрузить изображение звезды в функции preload. Картинку для звезды можно скачать здесь.

В функцию preload добавьте следующий код:

this.load.image('star', 'assets/star_gold.png');

Затем в функцию create добавьте следующий код под только что добавленным кодом обновления очков:

this.socket.on('starLocation', function (starLocation) {
  if (self.star) self.star.destroy();
  self.star = self.physics.add.image(starLocation.x, starLocation.y, 'star');
  self.physics.add.overlap(self.ship, self.star, function () {
    this.socket.emit('starCollected');
  }, null, self);
});

В приведенном выше коде мы прослушиваем событие starLocation, и когда оно получено, мы делаем следующее:

  • Мы проверяем, существует ли объект star, и если он существует, мы уничтожаем его.
  • Мы добавляем новый объект star в игру и используем информацию, передаваемую событию, для заполнения его местоположения.
  • Наконец, мы добавили проверку, чтобы увидеть, пересекается ли корабль игрока и звезда, и если это так, то мы генерируем событие starCollected. Вызывая physics.add.overlap, Phaser автоматически проверит наличие пересечения и запустит предоставленную функцию при наличии столкновения. Если вы обновите свою игру, вы должны увидеть на экране звезду:

    Заключение

    На этом данный урок завершен. Таким образом, в этом руководстве показано, как создать базовую многопользовательскую игру в Phaser с использованием Socket.io и Node.js.

Оригинал