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

На этом уроке мы будем использовать Phaser 3 и Socket.io для создания простой многопользовательской игры на основе обычной клиент-серверной архитектуры: клиент отвечает за отображение игры для каждого игрока, обработку ввода игрока и связь с сервером, а сервер отвечает за передачу этих данных каждому клиенту.

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

На этом уроке вы узнаете как:

  • Настроить сервер Node.js и Express, который будет отображать нашу игру и взаимодействовать с ней.
  • Установить базовую игру Phaser 3, которая будет действовать как наш клиент.
  • Использовать Socket.io, чтобы позволить серверу и клиенту общаться.

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

Требования к уроку.

Для этого урока мы будем использовать Node.js и Express для создания нашего сервера. Мы также будем использовать менеджер пакетов NPM для установки необходимых для работы сервера пакетов. Чтобы следовать этому руководству, вам необходимо иметь локально установленные Node.js и NPM, или же вам потребуется доступ к среде, в которой они уже установлены. Мы также будем использовать командную строку (Windows) или терминал (Mac, Linux) для установки необходимых пакетов и запуска/остановки нашего Node-сервера.

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

Чтобы установить Node.js, нажмите на ссылку здесь: и выберите версию LTS. Вы можете скачать и использовать текущую версию, однако, версия LTS рекомендуется для большинства пользователей. При установке Node.js, вы одновременно установите и NPM на вашем компьютере. После того, как вы установите эти инструменты, вы можете перейти к следующей части.

Настройка сервера

Первое, что мы собираемся сделать - это создать базовый сервер Node.js, который будет обслуживать нашу игру. Для начала создайте новую папку на своем компьютере, она может называться как угодно. Затем перейдите в эту папку и создайте новый файл с именем server.js. Откройте этот файл и добавьте в него следующий код:

var express = require('express');
var app = express();
var server = require('http').Server(app);

app.use(express.static(__dirname + '/public'));

app.get('/', function (req, res) {
  res.sendFile(__dirname + '/index.html');
});

server.listen(8081, function () {
  console.log(`Прослушиваем ${server.address().port}`);
});

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

  • ссылаемся на модуль express, являющийся веб-фреймворком, и который поможет нам визуализировать наши статичные файлы.
  • создаем новый экземпляр express и называем его app.
  • установили в качестве обработчика модуля http наш экземпляр app, что позволит Express обрабатывать HTTP-запросы.
  • обновили сервер для рендеринга наших статических файлов, используя встроенную в Express функцию express.static.
  • указали серверу использовать файл index.html в качестве главной страницы.
  • запустили прослушивать сервер порт 8081.

Прежде чем мы сможем запустить сервер, нам необходимо установить необходимые модули для сервера. Откройте свой терминал/командную строку и перейдите в папку вашего проекта. Оказавшись там, вам нужно будет выполнить следующую команду: npm init -f. Эта команда создаст файл package.json в папке вашего проекта. Мы будем использовать этот файл для отслеживания всех пакетов, от которых зависит наш проект.

Теперь мы установим Express. В вашем терминале выполните следующую команду: npm install --save express. Эта команда создаст папку с именем node_modules в папке вашего проекта, а, ключ --save укажет npm сохранить этот пакет в нашем файле package.json.

Настройка клиента

Закончив писать базовый код сервера, мы приступим к настройке кода на стороне клиента. В папке вашего проекта создайте новую папку с именем public. Любой файл, который мы помещаем в эту папку, будет отображаться на сервере, который мы настроили. Поэтому мы хотим поместить все наши статические файлы на стороне клиента в эту папку. Теперь внутри папки public создайте новый файл с именем index.html . Откройте этот файл и добавить следующий код к нему:

<!DOCTYPE html>
<html>

    <head>
        <meta charset="utf-8">
    </head>

    <body>
        <script src="https://cdn.jsdelivr.net/npm/phaser@3.20.1/dist/phaser.min.js"></script>
        <script src="js/game.js"></script>
    </body>

</html>

В приведенном выше коде мы создали простую HTML-страницу и сослались на два файла JavaScript: phaser.min.js (игровой фреймворк Phaser) и game.js (наш код игры Phaser). Вернитесь в папку public, создайте в ней подпапку с именем js и в этой папке создайте новый файл с именем game.js . Откройте этот файл и добавьте в него следующий код:

var config = {
  type: Phaser.AUTO,
  parent: 'phaser-example',
  width: 800,
  height: 600,
  physics: {
    default: 'arcade',
    arcade: {
      debug: false,
      gravity: { y: 0 }
    }
  },
  scene: {
    preload: preload,
    create: create,
    update: update
  } 
};

var game = new Phaser.Game(config);

function preload() {}

function create() {}

function update() {}

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

  • Мы создали конфигурацию, которая будет использоваться для нашей игры Phaser.
  • В объекте config в поле type мы устанавливаем тип рендерера для нашей игры. Два основных типа: Canvas и WebGL. WebGL является более быстрым средством рендеринга и имеет лучшую производительность, но не все браузеры поддерживают его. Значение типа AUTO указывает Phaser будет использовать WebGL, если он доступен, в противном случае использовать Canvas.
  • В объекте config поле parent используется, чтобы сообщить Phaser, что нужно рендерить нашу игру в имеющийся элемент <canvas> с этим идентификатором, если он существует. Если он не существует, то Phaser создаст для нас элемент <canvas>.
  • В объекте config мы указываем ширину и высоту видимой области нашей игры.
  • В объекте config мы включили аркадную физику, доступную в Phaser, и установили гравитацию на 0.
  • В объект config мы встроили объект сцены, который будет использовать функции preload, create, update.
  • Наконец, мы передали наш объект конфигурации в Phaser при создании нового экземпляра игры.

Теперь мы протестируем наш сервер и убедимся, что все работает правильно. Вернувшись в терминал/командную строку, выполните следующую команду: node server.js, и вы должны увидеть следующую строку "Прослушиваем 8081". Затем, если вы откроете свой веб-браузер и перейдете по адресу http://localhost:8081/, вы увидите черный прямоугольник на веб-странице , а если вы откроете консоль в инструментах разработчика, вы должны увидеть журнал с версией Phaser, это значит, что ваша игра работает.

Добавление Socket.IO

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

Вернувшись в свой терминал, выполните следующую команду: npm install --save socket.io. Если ваш сервер все еще работает, вы можете либо: открыть новое окно терминала и запустить данную команду в папке вашего проекта, либо остановить сервер (CTRL + C), а затем выполнить ее. Эта команда установит пакет Socket.IO для node и сохранит его в нашем файле package.json.

Теперь в файле server.js добавьте под строчкой var server = require('http').Server(app); следующий код:

var io = require('socket.io').listen(server);

Затем добавьте над строкой server.listen следующий код:

io.on('connection', function (socket) {
  console.log('подключился пользователь');
  socket.on('disconnect', function () {
    console.log('пользователь отключился');
  });
});

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

  • сослались на модуль socket.io и прослушиваем наш объект сервера.
  • добавили логику для прослушивания подключений и отключений. Далее мы обновим код на стороне клиента, чтобы включить библиотеку Socket.IO. Откройте файл index.html и добавьте следующую строку вначале элемента <body> :
    <script src="/socket.io/socket.io.js"></script>

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

    this.socket = io();

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

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

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

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

В файле server.js добавьте следующую строку ниже объявления переменной io:

var players = {};

Мы будем использовать этот объект для отслеживания всех игроков, которые в данный момент находятся в игре. Затем в функцию обратного вызова объекта socket.io для события connection добавьте следующий код после строки console.log('подключился пользователь');:

// создание нового игрока и добавление го в объект players
players[socket.id] = {
  rotation: 0,
  x: Math.floor(Math.random() * 700) + 50,
  y: Math.floor(Math.random() * 500) + 50,
  playerId: socket.id,
  team: (Math.floor(Math.random() * 2) == 0) ? 'red' : 'blue'
};
// отправляем объект players новому игроку
socket.emit('currentPlayers', players);
// обновляем всем другим игрокам информацию о новом игроке
socket.broadcast.emit('newPlayer', players[socket.id]);

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

  • Когда игрок подключается к веб-сокету, мы сохраняем некоторые данные игрока в объекте players, при этом мы используем в качестве ключа значение socket.id.
  • Мы сохраняем значения rotation, x и y игрока, в дальнейшем мы будем использовать эти значения для управления спрайтами на стороне клиента, и используем эти данные для обновления всех игроков. Мы также сохраняем playerId, чтобы мы могли ссылаться на него в игре, и мы добавили атрибут team, который будет использоваться позже.
  • Мы использовали socket.emit и socket.broadcast.emit для отправки события клиентскую часть сокета. socket.emit будет выдавать событие только на этот конкретный сокет (новый подключенный проигрыватель). socket.broadcast.emit (трансляция) отправит событие всем другим сокетам (существующим игрокам).
  • В случае currentPlayers мы передаем объект players новому игроку. Эти данные будут использоваться для заполнения всех спрайтов игроков в игре нового игрока.
  • В случае newPlayer мы передаем данные нового игрока всем остальным игрокам, чтобы можно было добавить в их игру новый спрайт.

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

В функции обратного вызова объекта socket.io для события connection добавьте следующий код после строки console.log('пользователь отключился');:

// удаляем игрока из нашего объекта players 
delete players[socket.id];
// отправляем сообщение всем игрокам, чтобы удалить этого игрока
io.emit('disconnect', socket.id);

Ваш файл server.js должен выглядеть следующим образом:

var express = require('express');
var app = express();
var server = require('http').Server(app);
var io = require('socket.io').listen(server);

var players = {};

app.use(express.static(__dirname + '/public'));

app.get('/', function (req, res) {
  res.sendFile(__dirname + '/index.html');
});

io.on('connection', function (socket) {
  console.log('подключился пользователь');
  // создание нового игрока и добавление го в объект players
  players[socket.id] = {
    rotation: 0,
    x: Math.floor(Math.random() * 700) + 50,
    y: Math.floor(Math.random() * 500) + 50,
    playerId: socket.id,
    team: (Math.floor(Math.random() * 2) == 0) ? 'red' : 'blue'
  };
  // отправляем объект players новому игроку
  socket.emit('currentPlayers', players);
  // обновляем всем другим игрокам информацию о новом игроке
  socket.broadcast.emit('newPlayer', players[socket.id]);

  socket.on('disconnect', function () {
    console.log('пользователь отключился');
    // удаляем игрока из нашего объекта players 
    delete players[socket.id];
    // отправляем сообщение всем игрокам, чтобы удалить этого игрока
    io.emit('disconnect', socket.id);
  });
});

server.listen(8081, function () {
  console.log(`Прослушиваем ${server.address().port}`);
});

Заключение

На этом мы закончим первую часть этого урока. Во второй части мы завершим нашу многопользовательскую игру:

  • Добавим клиентскую логику для добавления игроков в нашу игру.
  • Добавим логику для ввода игрока.
  • Добавим предметы предметы, которые должны собирать игроки.

Оригинал