В Phaser 3.2.0 добавлена поддержка режима безголового рендеринга. Объедините его с jsdom и node-canvas, и вы сможете запускать свою игру на стороне сервера.
Кто-то обезглавил мой игровой движок ...
Игровой движок состоит из множества частей, но больше всего вы видите рендерер, естественно, потому что именно он рендерит вашу игру на экране. Что было бы, если бы вы отрубили эту часть игрового движка? Вы получите автономный игровой движок, игровой движок, который ничего не отображает вообще. Идеально подходит для работы на компьютерах без дисплея, как и на большинстве серверов.
Почему это полезно?
Есть несколько причин, по которым вы можете захотеть запустить игру Phaser на сервере. Я составлю для вас список:
- Чтобы показать своим друзьям
- Написать какие-то автоматические тесты
- Таким образом, вам не нужно доверять клиенту, и вы можете делать все на стороне сервера.
Последний пункт, безусловно, самый жизнеспособный. Хотя можно было бы написать автоматические тесты для вашей игры, я не уверен, сколько вы от этого получите. В этом случае также было бы проще просто запустить весь браузер без головы.
Главный принцип компьютерной безопасности заключается в том, что вы никогда не должны доверять клиенту во взаимоотношениях клиент-сервер. Это также верно и в многопользовательских играх, особенно в тех, где вы ожидали, что игроки могут попытаться обмануть. Запуск полномасштабного экземпляра Phaser на сервере открывает множество возможностей, поскольку одинаковые столкновения и движения могут происходить как на сервере, так и на клиенте. Затем сервер может исправить любые ошибки (или попытки мошенничества), которые делает клиент.
Путешествие в Phaser 3 без головы
В Phaser 2 уже несколько лет работает безголовый режим (хотя я никогда им не пользовался), но Phaser 3 получил его недавно, в обновлении v3.2.0. Я начал с ним экспериментировать, обнаружил ошибку при использовании с некоторыми игровыми объектами (которая была исправлена в v3.2.1), а затем я начал писать текст, который вы сейчас читаете.
О создании многопользовательских игр можно много сказать (например, об эффективном общении по сети, сокращении задержки и т. Д.), Но вам придется найти эти вещи в другом месте. Я собираюсь сосредоточиться на безголовой части этого. Итак, приступим!
Настройка
Я предполагаю, что вы уже знакомы с Node, npm и Phaser перед тем, как начать. Я также предполагаю, что у вас есть какая-то игра, которую вы хотите запустить на сервере. Если нет, просто возьмите один из примеров с сайта Phaser, как это сделал я. Если вы еще не используете файл package.json, создайте его, запустив npm init.
Поскольку Node предназначен для запуска javascript на сервере, а не в браузере, он не поддерживает запуск Phaser из коробки. Нам понадобится пара пакетов: jsdom и node-canvas. jsdom воссоздает большинство API-интерфейсов Javascript DOM из браузера и позволяет загружать файлы HTML и взаимодействовать с ними. При правильной конфигурации он также позволяет запускать сценарии из файла HTML. Будьте осторожны, используйте это только тогда, когда доверяете коду js, поскольку jsdom не предназначен для использования в качестве полноценной песочницы!
Следующий пакет - это узел-холст, который представляет собой реализацию API-интерфейса холста с использованием Cairo. Это может показаться излишним, но Phaser по-прежнему нуждается в доступе к Canvas API при работе в автономном режиме. node-canvas в настоящее время находится на v2.x, но поскольку jsdom имеет хорошую интеграцию с выпусками v1.x, мы просто установим его сейчас. Или, скорее, мы установим canvas-prebuilt, который аналогичен node-canvas v1.x, но с предварительно созданными и включенными собственными зависимостями. Давайте продолжим и установим их (возможно, добавим Phaser в микс, если вы еще этого не сделали):
npm install canvas-prebuilt jsdom
Написание javascript
Пришло время написать javascript. Во-первых, нам нужно включить режим рендеринга без головы. Просто найдите место, где создан экземпляр игры, и установите тип Phaser.HEADLESS. Что-то вроде этого:
var game = new Phaser.Game({ type: Phaser.HEADLESS, parent: 'game', scene: { preload: preload, create: create, update: update }, width: 800, height: 600 })
Теперь мы готовы заняться узловой частью. Давайте создадим файл index.js и потребуем несколько модулей:
const jsdom = require('jsdom') const { JSDOM } = jsdom;
На самом деле нам нужен только модуль jsdom, поскольку он автоматически находит модуль холста. Пришло время загрузить html-файл в только что созданный JSDOM, и, поскольку я хотел бы использовать async / await, нам придется обернуть его все в асинхронный IIFE.
(async function(){ global.dom = await JSDOM.fromFile('index.html', { // To run the scripts in the html file runScripts: "dangerously", // Also load supported external resources resources: "usable", // So requestAnimatinFrame events fire pretendToBeVisual: true }) })()
Мы используем метод JSDOM fromFile и приказываем ему запускать скрипты, загружать внешние ресурсы и притворяться обычным визуальным браузером. Я также назначаю его глобальной переменной dom, чтобы упростить доступ при отладке с помощью Chrome DevTools, но нет причин, по которым вы не можете использовать обычную локальную переменную. Если вы запустите сценарий сейчас, вы должны увидеть в своем терминале что-то похожее на следующий результат:
%c %c %c %c %c Phaser v3.2.1 (Headless | HTML5 Audio) %c https://phaser.io background: #ff0000 background: #ffff00 background: #00ff00 background: #00ffff color: #ffffff; background: #000000 background: #fff
Если вы проверите процесс узла в Chrome DevTools, вы увидите баннер Phaser в надлежащем виде:
Вероятно, вы также увидите что-то не очень хорошее:
У нас ошибка! Похоже, что jsdom не поддерживает метод createObjectURL. Мы должны что-то с этим делать. Поскольку createObjectURL возвращает URL-адрес, представляющий большой двоичный объект, нам нужно будет сделать что-то подобное, и я остановился на использовании пакета datauri для возврата URI данных. В этом случае нам также не нужно ничего делать в revokeObjectURL. Просто установите его с помощью npm, и мы все исправим. Добавьте это в начало файла:
const Datauri = require('datauri'); const datauri = new Datauri();
А затем мы добавим фактическую реализацию createObjectURL и revokeObjectURL в асинхронный IIFE:
dom.window.URL.createObjectURL = function (blob) { if(blob){ return datauri.format(blob.type, blob[Object.getOwnPropertySymbols(blob)[0]]._buffer).content; } };; dom.window.URL.revokeObjectURL = function (objectURL) { // Do nothing at the moment };
Вы увидите, что мы делаем несколько сумасшедших вещей, чтобы вернуть правильное значение, то есть только для того, чтобы получить объект Buffer, который реализация Blob использует внутри, поскольку именно этого и требует datauri. Если вы знаете, как это сделать лучше, дайте мне знать. Перезагрузите сервер, и вы увидите, что все работает нормально.
Использование в многопользовательской игре
При использовании его в многопользовательской игре вы должны делать все это внутри своего серверного скрипта. Вы также можете добавить еще код для управления сетью. Я бы рекомендовал сделать это в серверной версии вашей игры, а не непосредственно в серверном скрипте, так как так будет проще.
Если вы используете socket.io, вы, вероятно, захотите внедрить свой экземпляр socket.io, часто назначаемый переменной io, в jsdom. Просто добавьте одну строку кода, и у вас будет та же переменная, доступная внутри кода игры, запущенного на вашем сервере.
dom.window.io = io
Базовый пример - реакция на движение игрока перемещением эквивалентного спрайта на сервере. Вы можете настроить что-то вроде этого внутри функции create в вашей сцене Phaser:
io.on('connection', function(socket){ socket.on('playermove', function(player){ // Do some logic to move the player }) });
В настоящей многопользовательской игре вам нужно проверить идентификатор клиента, чтобы сервер знал, какой спрайт переместить. Затем вы периодически отправляете сообщение клиенту, чтобы сообщить ему, где игрок (и другие игроки) должны, чтобы клиент мог исправить себя. Вот так внутри функции create в версии вашей игры, которую вы запускаете на клиенте:
socket.on('move', function(move){ player.x = move.x player.y = move.y });
В большой производственной игре в идеале вы должны использовать некоторую логику, чтобы разделить все серверные и клиентские части. Таким образом, вы можете использовать одну и ту же базовую кодовую базу на обоих концах, и вам не нужно поддерживать две отдельные версии. Но для базовых игр это, вероятно, подойдет.
Заключение
Это не предназначалось как полное руководство по реализации многопользовательской игры с использованием Headless Phaser, а скорее как краткое руководство по настройке. Я позволю людям с большим опытом разработки многопользовательских игр показать вам остальное.
Если у вас есть какие-либо вопросы, не стесняйтесь обращаться ко мне. Обычно я зависаю на нашем сервере Phaser Discord, так что присоединяйтесь ко мне (@ 16patsle # 7801) и другим участникам, чтобы задать несколько вопросов, больших или малых, или просто поболтать о Phaser.