KPHP теперь поддерживает механизм Foreign Function Interface (FFI). Мы с Владом решили продемонстрировать его возможности и за сутки написали первую в мире графическую игру на KPHP. Игру делали с использованием библиотеки SDL: API и семантика нашего FFI идентичны PHP. Поэтому созданная игра запустится и на KPHP, и на PHP. Если вам интересны детали реализации, заглядывайте под кат!  Немного контекста о том, как KPHP поддерживает Foreign Function Interface и чем это полезно. KPHP теперь разрабатывается как проект с открытым исходным кодом. Любой может клонировать репозиторий и собрать новую версию компилятора и рантайма. Но в ядро KPHP входят не все PHP-расширения, а добавить недостающие было сложно: для этого нужно разобраться во внутренностях самого KPHP, а также пройти процесс внедрения своего патча в чужой репозиторий. Конечно, можно держать свой форк с дополнительными фичами, но это тоже задача не из лёгких — особенно если планируется время от времени обновлять версию KPHP. Исправить эту ситуацию призван Foreign Function Interface (FFI). В PHP уже с версии 7.4 можно создавать обёртки для C-библиотек без необходимости написания C-кода и последующей компиляции PHP-расширений. Теперь в KPHP есть аналогичный функционал, созданный по тому же RFC. Так что к KPHP можно подключать расширения, не включая их в основной репозиторий. А само расширение можно распространять как composer-пакет. Мне захотелось продемонстрировать возможности этого нового механизма коллегам на грядущем внутреннем хакатоне. После мозгового штурма мы с Владом решили взять SDL и написать какую-нибудь игрушку. Исходные коды самой игры размещены в репозитории github.com/quasilyte/kphp-game, а обёртки к SDL в composer-пакете kphp-sdlite. Это разделение не было обязательным, но оно помогло проверить, что писать переиспользуемые биндинги возможно. Итак, пишем рогалик на KPHP за сутки, без игрового движка. Основные игровые идеи выглядят следующим образом: Я успел придумать и реализовать три заклинания. Паттерны поражения заклинаний:  Может показаться, что Ice shards во всём превосходит Fireball, однако поскольку мана не восстанавливается, грамотное её распределение — одна из основных механик. Если вы можете победить одним лишь огненным шаром, именно это и будет выгодной стратегией. Более того, ледяные осколки пролетают по одному тайлу за ход, а огненный шар атакует мгновенно, не давая противнику шанса увернуться. Thunder интересен тем, что позволяет атаковать противника, не поворачиваясь к нему, а атака по диагонали безопасна (финальный босс — единственное исключение). Каждый уровень протагониста увеличивает базовые характеристики: магический урон, количество здоровья (HP) и маны (MP). Есть и другие механики, без умелого использования которых пройти игру практически невозможно. Например, через тайлы с обломками нельзя пройти, но через них можно «стрелять» заклинаниями. Через обычные стены нельзя ни колдовать, ни перемещаться. Здесь нет ничего KPHP-специфичного. Создаём директорию, инициализируем composer, ставим зависимости. Вот так выглядит наш autoload (копия из composer.json): В корне проекта создадим main.php, который загрузит все composer-зависимости и запустит приложение. Чтобы протестировать сборку, создадим пустую реализацию класса KPHPGame\Game (src/Game.php): Я рекомендую добавлять такие вещи в Makefile, чтобы упростить выполнение этих операций. Сборка и запуск игры становятся проще: Начнём с создания графического окна, в котором и будет отображаться вся графика. Нам нужна функция SDL_CreateWindow. Чтобы воспользоваться ей из KPHP, нам нужно описать сигнатуру в формате, понятном FFI, — то есть с обычными C-декларациями. Есть два основных способа размещения этих деклараций: Я буду использовать вариант с FFI::load. Создадим файл sdl.h: В FFI по умолчанию определены многие популярные типы вроде uint32_t. Но Uint32 — это алиас типа, специфичный для SDL. Поэтому нам нужно ввести этот псевдоним самостоятельно. SDL_Window — непрозрачный для нас тип, поэтому достаточно сделать forward declaration. FFI_SCOPE и FFI_LIB — это особые для PHP FFI определения. Чтобы вывести окно по центру, нам потребуется константа WINDOWPOS_CENTERED. FFI поддерживает простые константы через enum. Но зачастую константы, которые нам хочется использовать в публичном коде, стоит выносить в класс-обёртку. Простейший скрипт для использования из KPHP: Теперь мы умеем создавать графические окна! Дело за малым.  Использовать FFI напрямую не так приятно, поэтому лучше прятать его внутри PHP-класса: На что обратить внимание: Использовать SDL через KPHP становится приятнее: Отдельное преимущество — теперь у нас есть хороший autocomplete для методов SDL. Теперь закроем это окно нажатием на красный крестик… Подождите, ничего не закрывается! Это мы поправим позже, когда научимся обрабатывать события. А пока закрываем через менеджер задач (или любым другим способом, который вы любите). Что планируем использовать для отрисовки изображения: Функция IMG_Load находится в отдельной библиотеке, libSDL2_image. Поэтому создаём второй заголовочный файл, sdl_image.h: Добавим новые определения в уже существующий sdl.h: Это почти рабочий вариант. Проблема в том, что sdl и sdl_image декларируют SDL_Surface. Эти типы будут несовместимыми. Вот простой пример, который демонстрирует это поведение: Получаем ошибку: Хе-хе, struct Foo* несовместим с struct Foo*. Приятно. Есть несколько путей обхода. Можно, например, заменить везде struct Foo* на void*, всё равно для нас это opaque-указатель на ресурс. Альтернативным решением может быть FFI::cast. Всё, теперь мы готовы вывести картинку на экран! Обычно вы хотите переиспользовать текстурку, возвращаемую createTextureFromSurface(), и загружать её в самом начале. Аналогично с рендерером: вы создаёте его где-то в начале жизненного цикла игры, а потом передаёте объект рендерера во все места, где он может потребоваться. Один из методов немного отличается от того, что мы делали ранее. Вот его внутренности: Так как SDL_Rect определён в sdl.h полностью, мы можем создавать эти объекты через метод new и обращаться с полями этой структуры так, будто бы это обычный PHP-класс. Было бы не очень эффективно каждое изображение загружать как отдельный png и ассоциировать с ним текстуру. В своей игре мы использовали текстурные атласы для тайлов.  В одном изображении размера 128 × 128 пикселей мы можем разместить 16 тайлов размером 32 × 32 пикселя. Всё это будет одной текстурой. Чтобы рендерить такой фрагмент внутри атласа, нам нужно будет передать дополнительный аргумент в renderCopy, который ранее был null. Анимации можно делать по тому же принципу. Одна текстурка с несколькими кадрами. В зависимости от текущего кадра выбираем правильное смещение внутри текстуры.  Кадр анимации переключается игровыми фреймами, а её скорость регулируется их количеством. В нашей игре мы стараемся выдавать 60 фреймов в секунду.  Мы упомянули игровые фреймы. Самое время писать основной цикл обработки. Сердцем игры будет вот такой цикл: Этот цикл отрабатывает примерно 60 раз в секунду. Помните, что у нас не закрывается игровое окно? Самое время это исправить. Вот теперь приложение корректно закрывается через кнопку . Расширим цикл обработки событий так, чтобы мы понимали, какие действия хочет совершить игрок. Исходя из наброска выше, нам потребуются функции: Добавляем всё необходимое в sdl.h: В union SDL_Event входит больше вариантов, чем мы описали выше. Настоятельно рекомендуется заполнять union-типы идентично тому, как они определены в библиотеке, потому что это может влиять на их размер. В статье мы сократили определение, выбрав только нужные для нас события, однако в реальном приложении следовало бы перечислить более десяти структур, которые могут быть членами SDL_Event. Для работы со шрифтами подключим SDL_ttf. Нас в первую очередь интересуют UTF-8 варианты API, поэтому вместо TTF_RenderText_Blended мы выбираем TTF_RenderUTF8_Blended и дальше по аналогии. Создаём sdl_ttf.h: Используется это так: сначала делается TTF_Init(), чтобы инициализировать библиотеку; затем через TTF_OpenFont() открываем шрифт и держим его при себе, как и с другими ресурсами. После этого используем TTF_RenderUTF8_Blended() для рендеринга текста выбранным шрифтом. Функция TTF_SizeUTF8() нужна для того, чтобы вычислить ширину текста, если его отрисовать указанным шрифтом. При этом она возвращает код ошибки. Настоящий результат функции записывается в $w и $h. Чтобы отрисовать простейшие элементы графического интерфейса, достаточно уметь отображать прямоугольники разных цветов и размеров. Добавим следующие функции в sdl.h: Передавать цвета через несколько отдельных значений не всегда удобно. Особенно расстраивает увеличивающееся количество аргументов. Мы можем определить простой класс Color, который позволит более лаконично описывать связанные с цветом аргументы. Удобную обёртку для SDL_SetRenderDrawColor можно написать, например, так: Код ниже рисует красный квадрат 32 × 32: В нашей игре мы реализовали диалоговое окно с «рамкой» — для этого отрисовали два прямоугольника разного цвета, немного отличающихся по размеру.  Для работы со звуком мы взяли библиотеку SDL_Mixer. Хочется уметь проигрывать два вида звуков: одноразовые (для спецэффектов) и длинные аудио с повтором (для музыки). Примерно такой API у нас получился: Для отдельных звуковых эффектов, которые нужно повторять много раз во время игры, нужно сохранять загруженные ресурсы и переиспользовать их — подобно тому, как мы сохраняем текстуры. Поскольку бездумно закидывать канал запросами на воспроизведение не очень умно, нам нужно либо ставить их в очередь, либо уметь как-то сказать «Горшочек, не вари». Поскольку код писался под хакатон, я решил просто игнорировать эту ошибку и считать, что если игрок зажимает все клавиши одновременно, то мы имеем право не воспроизвести какой-то из звуков. Обращаем внимание на то, что в SDL_Mixer имеется символ Mix_LoadWAV. Вот только определён этот символ не как функция, а как макрос. Поэтому мы не можем загрузить Mix_LoadWAV из динамической библиотеки. Макрос определён следующим образом: Мы можем определить в классе SDL метод с таким же API: Все функции для работы с SDL_Mixer должны быть определены в отдельном файле, назовём его sdl_mixer.h: Здесь всё зависит от конкретной функции, которую мы используем. Есть два основных способа, которыми функции из SDL сигнализируют об ошибках: Если ошибка произошла, через SDL_GetError можно получить текст этой ошибки. Для второго способа часто можно упростить тип возврата с int до bool: Функции, которые возвращают null, проверять стоит обычным сравнением с null, а не через \FFI::isNull. В коде нашей игры мы использовали некоторые KPHP-специфичные возможности. Например, tuple-типы. Чтобы этот код заработал на PHP, достаточно подключить к проекту KPHP-полифиллы: PHP умеет регистрировать FFI scope только на этапе preload, поэтому создадим скрипт php_preload.php: Запускать игру нужно примерно так: Поддержка MacOS в KPHP частичная. Запустить полноценный сервер получится только на Linux.  Однако в случае нашей игры веб-сервер не нужен, поэтому запустить её можно на обеих системах. Здесь отдельное внимание стоит уделить тому, как игра ищет свои ресурсы при работе. Все доступы к ресурсам (изображения, звуковые файлы, шрифты и прочее) стоит делать через некоторый AssetsManager, который знает, как найти нужный asset в зависимости от окружения. Простейший AssetsManager может выглядеть так: Переменную окружения KPHP_GAME_TARGET мы задаём при запуске игры. Какое значение туда выставить, должен знать сам launcher для игры. Этот launcher можно генерировать при подготовке релизной сборки. Доступ к изображениям в коде меняется следующим образом:  Мы создали игру, которая запускается как на PHP, так и на KPHP. Однако это не всё, чего можно добиться благодаря FFI. С его помощью можно реализовать некоторые отсутствующие в KPHP расширения PHP. Например, через FFI можно добавить в KPHP поддержку: Это только то, на чём я успел протестировать текущую реализацию. Уверен, что список этим не ограничивается. И напоследок — видео с игровым процессом: В ролике вы видите именно ту версию, что была сделана к дедлайну хакатона. На следующий день я добавил в игру пару дополнительных механик для улучшения геймплея. Например, алтари на уровне, которые дают случайный бонус. Making developer tools & video games