В первой части этого руководства мы создали сервер 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( )
. Угловая скорость позволит кораблю вращаться влево и вправо.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.