Jump'n'bump

Мультиплеерная игрушка на php и javascript: SDL, devtools-protocol

Репозиторий

Игра

Давно, когда веб развивался, JavaScript обрастал возможностями, PHP завоевывал мир, появлялись первые
игры в браузере, а каждый юный разработчик, мечтал написать свою игрушку. С тех пор утекло много воды, php
потерял часть популярности, а JavaScript стал зрелым и развитым, но я все равно решил реализовать давнюю
мечту, и написать на этих языках.

Для разнообразия я напишу клиента на PHP, а сервер на JavaScript

С нуля придумывать сбалансированный сюжет и геймплей тяжело, поэтому я буду портировать одну старую игрушку
Jump ‘n Bump

Есть прекрасный порт на javascript с которым я буду сверяться, но он
без мультиплеера. Хорошо реализована клиентская часть, но именно её я собираюсь писать на PHP. Так что придется
построчно копировать и преобразовывать.

Клиент

Начнем с клиентской части. Это графика, анимация, управление и звук (правда, звуки сейчас не
реализованы).

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

Установка

Модуль sdl-php на OSX с M1 через pecl не устанавливается, но легко собирается:

1
2
3
phpize
./configure
make

Получившийся файл можно добавить в php.ini, либо указать при запуске

1
php -d "extension=./bin/sdl.so" ./jnb.php

Также потребуется парочка библиотек - react/promise для промисов и ratchet/pawl для работы с веб-сокетами. Их можно
установить через

1
composer install

Сервер

Сервер я буду писать на JavaScript. Проще всего было бы сделать это на Node.js, но, поскольку, JavaScript это
браузерный язык, я предпочту писать серверную часть в браузере.

Браузер можно запускать в headless режиме на сервере, поэтому браузерное окошко нам не будет мешать. Но для разработки
можно запускать браузер со средствами разработчика.

Взаимодействие с сервером

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

Решение есть - можно запускать браузер с поддержкой удаленной отладки, а PHP-клиенты будут подключаться как отладочные
инструменты. Я запускаю отдельный процесс хромиума с параметром --remote-debugging-port

1
/Applications/Chromium-102.app/Contents/MacOS/Chromium --remote-debugging-port=9222 jnb.html

Таким образом, клиентская часть может делать запросы на сервер, на котором запущен браузер, на порт 9222, подключаться
по веб-сокетам, вызывать функции через console API, читать вывод в консоль.

Подключение к инстансу

Первым запросом получим список открытых вкладок (GET /json/list)

1
2
3
4
5
6
$service_url = 'http://127.0.0.1:9222/json/list';
$curl = curl_init($service_url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$curl_response = curl_exec($curl);
curl_close($curl);
$decoded = json_decode($curl_response);

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

У найденной вкладке будет значение webSocketDebuggerUrl - адрес, по которому следует соединяться вебсокетом.

1
2
3
4
5
6
7
8
9
10
11
foreach ($tabs as $tab) {
if (str_ends_with($tab->url, 'jnb.html')) {
$webSocketDebuggerUrl = $tab->webSocketDebuggerUrl;
}
}

\Ratchet\Client\connect($webSocketDebuggerUrl)
->then(function($connection) {
// Здесь мы подключились к нужной вкладке браузера
// сохраняем $connection
})

По веб-сокета будут приходить ответы за запросы клиента, мы будем их получать в одном обработчике, который будет
распределять ответы по id, и вызывать callback-функцию. Список обработчиков будет храниться в переменной $callbacks

1
2
3
4
5
6
7
$connection->on('message', function($msg) {
global $callbacks;
$decoded = json_decode("$msg");
if ($decoded->id && $decoded->result && ($callbacks[$decoded->id] ?? null)) {
call_user_func($callbacks[$decoded->id], $decoded->result);
}
});

В объект $callbacks складываются колбэки при вызове функций. Вызов функций осуществляется отправкой в веб-сокет
сообщения

1
2
3
4
5
{
"id": <генерируется для ответа>,
"method": "Runtime.evaluate",
"params": ["Код на JavaScript"]
}

А так выглядит вызов функции на PHP. Возвращается Promise, генерируется id, а в объекте $callbacks запоминается
функция, которую надо вызвать (потом, когда из вебсокета придет ответ по этой функции, колбэк будет вызван и Promise станет
заершенным)

1
2
3
4
5
6
7
8
9
10
11
12
13
function runtime_evaluate($code) {
return new Promise(function(callable $callback) use ($code) {
global $request_id, $conn, $callbacks;
$request_id++; // генерируем id
$callbacks[$request_id] = $callback; // сохраняем callback
$msg = json_encode(array(
'id' => $request_id,
'method' => 'Runtime.evaluate', // RPC функция
'params' => array('expression' => $code), // и ее аргументы (JS-код)
));
$conn->send($msg);
});
}