ARPG на Scheme
Andrew KravchukМежду делом тут думал, как бы половчее вернуться к проекту движка ARPG на лиспе. Вспомнил, как я вот уже почти четыре года назад начинал свои попытки с Racket, внутренне содрогнулся от кринжа 😖 Тот косо слепленный за две недели proof of concept было практически невозможно развивать из-за тормознутости самого Racket, хотя писать на нём, конечно, одно удовольствие. Появление как раз в то время Racket–on–Chez, рантайма Racket с самым быстрым на тот момент компилятором Chez Scheme под капотом, не особо улучшило ситуацию: производительность с этим вариантом и правда подросла на 10–20%, но игра по-прежнему умудрялась выжирать под 60% CPU на моём тогдашнем Core i3, по-видимому, из-за проблем с боксингом и хаотичными паттернами доступа к памяти — впрочем, первое тут всегда порождает второе. Почему я вообще выбрал Racket? На тот момент он был единственной Scheme, в которой можно было вести интерактивную разработку, а-ля Slime/Sly, хоть и не с родным Scheme'овским пакетом Emacs'а Geiser, а со своим racket-mode. Даже вот раскопал видос тех времён, которым я выёживался перед товарищем (Женя привет!):
В общем, долго ли, коротко ли, но со Scheme я после ряда экспериментов съехал на Common Lisp, как на наиболее практичный и производительный из лиспов. На нём-то в течение трёх лет я и пилил проект d2clone–kit, который был прерван экзистенциальным кошмаром 2022 года. В любом случае, пора этот проект перезапускать — есть много планов как по технической части, так и по ребрендингу и прочему маркетингу 😊
Так вот, пришла в голову мысль, а вот бы теперь, спустя года, снова попробовать Scheme в качестве основного языка проекта? CL мы все знаем и любим, но надо признать, что он довольно монструозен и причудлив, недаром у него такой маскот 😅. Теперь у меня есть определённый список требований к языку, выработанный практикой, и по этому списку я и решил пройтись в поисках подходящей реализации Scheme.
1. Внезапно, самая важная языковая фича в лиспах для меня — негигиеничные макросы. Невозможно выразить словами, насколько проще писать макросы вообще и DSL в частности в негигиеничной системе, и вообще я диву даюсь, как вокруг такой простой вещи, как захват идентификаторов, можно навертеть такое адищще, как гигиеничные Scheme'ные define-syntax, syntax-rules или как их там. К счастью, некоторые реализации Scheme поддерживают негигиеничные макросы в стиле Common Lisp, что сразу ощутимо урезает список кандидатов до Bigloo, Chicken, Gambit, Gauche (и, как впоследствии вспомнилось, Guile). Gauche отпадает сразу, т.к. написан с принципиальным пренебрежением к вопросам производительности, просто как внятная замена шелл–скриптам, а производительность в видеоиграх — вопрос чуть ли не первостепенный.
2. Также по причинам производительности отпадают Gambit и Chicken, т.к. хоть оба и являются трансляторами в высокопроизводительный C–шный код, который затем преобразуется компилятором в эффективный нативный, оба, к сожалению, не поддерживают многопоточность уровня ОС из-за используемой в них модели памяти, так что игры, написанные на этих диалектах, будут демонстрировать характерный профиль нагрузки CPU:

3. Далее, важнейшая фича лиспов для разработки игр — интерактивность. Тут же отвалилась и Bigloo, т.к. она не поддерживается в Geiser, а жаль — весьма интересная реализация от французов из INRIA, с высокой производительностью и лёгким FFI. Казалось бы, вариантов не осталось, но я внезапно вспомнил про Guile, которая и имеет негигиеничный defmacro, и в Geiser поддерживается, а также имеет прорву библиотек, свой аналог CLOS, и даже в версии 3.0 обзавелась JIT–компилятором, так что, хоть она по-прежнему выполняется виртуальной машиной, внезапно по результатам бенчмарков обгоняет даже курочку, которая компилируется через C в нативный код.
Раньше в Geiser с интерактивностью были проблемы, так что я начал с некробампа соответствующей issue в багтрекере, потом ещё немного поковырялся в потрохах Geiser сам. Тут есть принципиальная проблема: как хорошо ты ни пиши емаксовый пакет, он не будет способен на интерактивное обновление или вообще какое-либо взаимодействие с запущенным в репле кодом, если этот код, например, занят главным игровым циклом. Очевидное решение — порождать фоновый тред, и в Common Lisp'овских Sly и Slime так и делается, но Geiser поддерживает слишком много разных Scheme, чтобы это везде работало одинаково хорошо — для Gambit, например, соответствующий кусок кода вообще закомментирован и торчит TODO комментарий. Сначала я налабал рабочий proof of concept на SDL2 с REPL–тредом, эксплицитно создаваемым вручную, и это внезапно заработало, несмотря на запинку с geiser-connect. Потом, пока писал этот пост, мне пришло в голову, что можно запустить в фоновом треде код игры, а в основной тред оставить под REPL Geiser'у. Тут, правда, тоже две закавыки — во-первых, под MacOS какие-то жестокие правила насчёт того, в каком треде можно использовать OpenGL'ные функции, а в каком нельзя, из-за чего, например, в liballegro есть такой костыль, как main addon, который подменяет точку входа на функцию, правильно разруливающую под макосью все эти треды. Другая закавыка в том, что придётся городить костыль, чтобы определять, в REPL запущен код или нет, потому что в первом случае тред с игрой не нужно join'ить, а во втором — нужно, ну да это всё решаемо.
4. Ну и немаловажно иметь возможность заглядывать под капот сгенерированному компилятором коду, чтобы понимать, насколько хорошо тебя понял компилятор 😅. С удивлением для себя обнаружил, что в Guile имеет место быть REPL–команда disassemble, так что тут вроде всё схвачено. Конечно, сильно будет не хватать деклараций типов, как инструмента объяснения компилятору недопонятых им фактов о коде, так что для таких низкоуровневых оптимизаций, видимо, придётся спускаться на C–шный уровень.
Итак, демо с интерактивной разработкой на Guile + Geiser прилагаю 😌
Код дёмки, если что, можно посмотреть тут. По итогу получается, что теоретически мой ARPG–движок можно разрабатывать и на Guile — это популярная и матёрая реализация Scheme, с огромным количеством поддерживаемых библиотек, R*RS–стандартов и SRFI–расширений, и даже с автоматическим FFI. Правда, развёртывание этих библиотек и расширений ведётся с использованием autotools, прямо как деды завещали 😂. Этот отвалившийся нос слегка припудривают Guix'ом — в каждой второй инструкции по установке либы–расширения к Guile так и написано, мол, в Guix всё одной командой ставится. Может, это меня сподвигнет на более тесное знакомство с Guix'ом, говорят, его можно даже поверх моей генточки поставить.
Резюмируя, пока от Common Lisp далеко не ухожу, но и на Guile буду внимательно посматривать одним глазом 😜