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

Железо, на котором снимались замеры:

  • CPU 2 ядра (2300 Mhz на ядро)

  • 4Gb Ram

  • php 8.1 

  • SSD

Сервер рассчитан на реальное время: тысяча циклов в секунду, и в каждом цикле обязан отработать код механик — суммарно не дольше миллисекунды. Дальше везде RPS: сколько раз в секунду успевает выполниться код.

Lua в песочнице

Lua до сих пор жив в игровых проектах вторым языком: на нём пишут квесты, события, диалоги и баланс в самописных движках. Скриптовый язык, близкий по духу к JavaScript. Скрипты можно компилировать, хотя здесь это не принципиально — сервер один раз поднимается в память и крутится там.

Для PHP есть два расширения: PHP Lua и PHP LuaSandbox. Я остановился на втором: в нём закрыта часть возможностей, которые автор расширения счёл небезопасными. Он же автор движка, на котором работает Википедия.

Замеры LuaSandbox:

  • call (closure, отдает фиксированную строку )  70 000 RPS (8 ядер + физ сервер дает x10 к скорости)

  • call (closure отдает вызов PHP функции сервера с параметрами  созданной с помощью registerLibrary число параметров значимо не влияет на скорость)  60 000 RPS (8 ядер + физ сервер дает x10 к скорости)

  • callFunction (аналог call но взывает не closure, a именованную глобальную функцию lua) - аналогично параметрам выше

Что при этом важно знать:

  • в данной технологии PHP и LUA не имеют общего хранилища и все данные передаются в виде сообщений двусторонних не передавая ссылки (поэтому напрямую объект не передать, свойства не получить). 

  • Передавая объект из PHP в LUA приходится прибегать к registerLibrary (заранее создавать доступные в LUA функции из PHP) и мета таблицам (аналог объектов) при смене или получения значения которых вызвать указанные функции в php отдающие данные объекта (и изменяющие его)

  • В боевых условиях (кеш, синхронизация данных всех песочниц и тп) простое игровое событие (регенерация) занимает 0.17мс (6000 запросов в секунду где идет вызов LUA и 2 раза обращение в PHP)

Так это выглядело в админ-панели сервиса:

JavaScript в песочнице

Второй кандидат на пользовательский код — JavaScript. Node.js принято считать быстрым, и если разбираться почему, набирается конкретика:

  • Для Websocket почти всегда используется библиотеку Socket IO (грубо говоря у вас сервер не ждет отправку 100500 игрокам TCP сообщений, а одно другому WebSocket серверу, который уже рассылает всем остальным, тем самым достигается неблокируемость основного потока), которая реализует идею , которую можно переложить на другой язык (Си, Php и др.) взамен асинхронного программирования с несколькими сопрограммами (thread) и обменом данных (Shared Memory или сообщения)

  • движок интерпретатор JavaScript под названием....

    V8 от Google - движок для Java Script кода используемый в одном из самых быстрых браузерах Google Chrome. Для PHP имеется возможность библиотека по интеграции под названием V8JS

Движок здесь V8 от Google — тот же, что в Chrome. К PHP его подключает расширение V8JS.

Компиляция на лету — так делать нерационально, но для сравнения:

  • executeString (отдает фиксированную строку )  140 000 RPS  (8 ядер + физ сервер дает x4.5 к скорости)

  • executeString (отдает получение свойств объекта переданного из PHP)  80 000 RPS  (8 ядер + физ сервер дает x5 к скорости)

  • executeString (отдает выполнение метода объекта переданного из PHP)  40 000 RPS  (8 ядер + физ сервер дает x5 к скорости)

  • execiteString возвращающий анонимную функцию (closure) и вызываемый как метод PHP полученного объекта на 20% быстрее (за счет того что executeString  вызван единожды)

То же самое, но код скомпилирован заранее и используется повторно:

executeScript, фиксированная строка — 600 000 RPS (восемь ядер дают ×3,5).

executeScript, чтение свойств объекта, переданного из PHP, — 300 000 RPS (восемь ядер дают ×4).

executeScript, вызов метода объекта из PHP — 60 000 RPS (восемь ядер и физический сервер дают ×5).

Анонимная функция, возвращаемая executeScript, медленнее примерно на 30% — судя по всему, особенность компиляции замыканий.

Передача параметров из PHP — 700 000 RPS на свойство, и здесь восемь ядер дают лишь ×2,5.

Что важно знать:

  • Добавлять новые значения из PHP в V8js  - медленно, для экономии времени добавление новых данных можно передать в пространство V8js объект один раз (тк он передастся по ссылке) , а в нем самом уже из PHP менять свойства (это создаст некое хранилище-посредник)

  • В данной технологии PHP и V8js делят некое общее хранилище памяти  однако в отличие от LUA объект передается по ссылке и сразу доступен.

Что из этого выбрать

  1. В настоящее время JavaScript более популярен за счет игрового движка Phaser2D (33.000 лайков), в то время как для разработки игр где игровые механики (есть движки где LUA используется в незначительной степени для описания действий в игре) написаны на LUA используется Love (3.000 лайков). Lua имеет явный недостаток - это то что для получения свойств объекта PHP нужно вызвать некие функции (как если бы мы взвали методы объектов), но это можно решить кешируя в самом LUA значения , при изменении LUA - менять кеш, при изменении в PHP или JS - отправлять команду на изменение кеша (тк по большей части будет чтение это будет выигрышным выходом). На более мощном железе явный рывок в скорости

  2. В JavaScript на рынке много библиотек для работы с физикой, графикой, есть общая память и получение свойств объектов отрабатывает быстро без нужды что либо кешировать (хотя и это тоже можно сделать по примеру LUA). Однако вызов методов объектов PHP явно проигрывает по скорости LUA на хорошем железе, но такие методы вызываются реже (например 1 раз в 200мс) чем читаются (сотни раз в 1мс) и меняются свойства (например раз в кадр 60мс). Пример методов: добавления на карту новых объектов, добавление объекты новых событий, сохранение игрока в базу. 

  3. В LUA можно сделать некий кеш который кеширует все свойство не тратя время на обращение в PHP для их чтения, однако это сопряжено с тем что при смене свойств в JS или PHP мы должны обновлять их в LUA  однако все эти изменения не обязательно обновлять как только они появились , а отправлять пакетом при следующем запуске LUA 

  4. В таких языках как С++ язык LUA встраиваемый (как в нашем сервисе) и старые игры в тч и онлайн до сих пор используют LUA для написания часто меняющейся игровой логики (игровые мероприятия, диалоги, квесты)

Под любую из песочниц архитектуру приходится перестраивать — это не «подключил расширение и поехали».

Что было дальше

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

Как это работает в игре, показываю на канале проекта: youtube.com/@mmogick_ru.

История:

  1. Введение

  2. Масштабируемость и асинхронность

  3. WebSocket

  4. Redis

  5. LUA и JavaScript

  6. Выбор технологий, протокола и архитектурный шаблон Entity Component System

  7. Игровые локации (тайловые карты)

  8. Клиентская часть на Unity

  9. Игровые серверные механики

  10. Открытый бесшовный мир в 2D игре

  11. FPS, Ping, паузы между командами, интерполяция и экстраполяция

  12. Очереди и параллельное программирование на CPU

  13. Event-driven паттерн, JSON-RPC и почему не сервисная (SOA) архитектура

  14. Сетевая карта и задержка кадра (Latency frame) по RFC 2544 (1242)

  15. Создание сервера для онлайн ММО игр на PHP

  16. Готовое MVP сервиса 2D MMO RPG игр (realtime)

  17. Внедряю ИИ: механики из одного описания

  18. Конвейер контента для MMO: импорт анимации, экипировка и обновление игры без патчей

  19. Каталог ассетов и git: как командой вести игру в облаке

  20. Тесты для кода, который пишет ИИ: контракты вместо ассертов

  21. ИИ-картограф: как карты сшиваются в бесшовный мир