Spiiin's blog

daScript - перевод интервью с создателем языка

Свободный перевод части статьи A Look At The Current State of Independent Gamedev Software Production, интервью с автором языка daScript Борисом Баткиным. Курсивом — мои вставки. Иногда осознанно немного перефразирую или уточняю ответы.

Пост посвящен храбрым разработчикам и небольшим инди-командам, создающим собственные технологические решения для разработки игр.

Сегодня у нас есть множество мощных движков, доступных бесплатно: Unreal, Unity, Lumberyard, CryEngine и другие решения. Они содержат полный набор инструментов, позволяющий разработчикам экономить время на разработке собственных тулзов.

хм, ну не то, чтобы прямо совсем бесплатно доступны…

Но всё же некоторые разработчики продолжают находить причины, чтобы разрабатывать собственные движки. И мы говорим не только о гигантах вроде Guirilla Games или Rockstar. Одиночки или небольшие инди-команды предлагают свои решения, которые призваны справиться с недостатками существующих решений. Не слишком ли сложно это для маленькой команды? Мы связались с авторами нескольких таких технологий, чтобы узнать у них об их мотивах, проблемах, временных затратах и другом.

если говорить о daScript, то “Гайдзины” — не то, чтобы маленькая команда, и имеет свои наработки в области скриптовых языков, а также использования их в продакшене, где-то лет так 15 как минимум

Разработка daScript

Мы связались с Борисом Баткиным, разработчиком daScript. Это высокоуровневый скриптовый язык с сильной статической типизацией. Цель разработки — создать быстрый язык для встраивания в качестве скриптового языка в C++ для критичных по производительности приложений, таких как игры. Посмотрим внимательнее на daScript и на то, как он может помочь улучшить производительность.

Расскажи пожалуйста о daScript. Почему и когда ты начал над ним работать?

С самого начала моей карьеры как программиста я хотел создать собственный язык. Фактически, первое, чем я занимался, работая в игровой индустрии в 2000х, было написание кросс-компилятора из UnrealScript в C++. С тех пор я думал о создании своего языка.

В 2005 я создал несколько черновиков и прототипов, когда я работал на Naughty Dogs и они экспериментировали со своим лиспо-подобным языком Goal, от которого им пришлось отказаться при переходе от PlayStation2 к PlayStation3. Тогда у нас было много продуктивных дискуссий, который натолкнули меня на некоторые идеи.

позволю себе втыкнуть сюда собственные размышления о языках программирования, применительно к геймдеву и не только
10 лет в геймдеве — раздел языки программирования, про опыт использования разных языков
Nim in imaginary world — мои критерии выбора языка для хобби- и профессиональной разработки
Заметки о языках программирования — сборник заметок про языки разного уровня и целей
С++ в геймдеве — почему C++ и всё ли с этим правильно

Уже тогда было до боли очевидно, что язык должен быть встраиваемым (в С++). Большинство компаний используют для создания игр подмножество C++ (каждая — своё). Какой бы скриптовый язык мы ни придумали, он должен хорошо совмещаться с существующей кодовой базой. Можно думать о C++ как платформе, тогда специализированный язык для игр должен работать на этой платформе. Подобные вещи происходили с другими платформами. Java получила Scala, а позже Kotlin, браузер в определенный момент стал платформой, и получил несколько языков, транслируемых в JavaScript, у .Net сначала был только C#, но позже получил жизнеспособный F#. daScript является чем-то подобным для C++.

Игровая индустрия давно нуждается в более практичном решении. C++ нельзя назвать идеальным языком программирования, если не сказать ещё хуже. Он многословен, он имеет очень крутую кривую обучения. Мультиплатформенное программирование на C++ напоминает обход минного поля. Вcё может внезапно взорваться. Но при этом, он позволяет создать надёжный код, поэтому мы его используем. Но желание иметь простое решение для быстрых итераций разработки — очень привлекательно.

Встроенные языки, которые обычно используются в индустрии игр, динамически типизируемые, с дорогостоящей передачей управления между C++ и скриптовым языком. Lua (с LuaJIT), Squirrel, DukTape/QuickJS - вот только некоторые из примеров. В большинстве из них лучшая производительность достигается за счёт just-in-time (JIT), но такая возможность вообще не доступна на большинстве закрытых платформ, таких как консоли или iOS. Маршаллинг данных между нативным и скриптовым кодом быстро становится серьёзной проблемой, даже при включенном JIT. Как правило, такая производительность приемлема только для event-driven приложений с отправкой сообщений, и на этом точка. Но такой подход не масштабируется для приложений типа игр. В результате большая часть “тяжёлого” кода либо сразу пишется на C++, либо же сначала пишется прототип на скриптовом языке, который затем переписывается на C++.

Антон Юдинцев в своём докладе про daScript ссылается в качестве примера на доклады про оптимизацию Fortnite на мобильные платформы — большая часть оптимизаций заключается в том, чтобы переписать код с блюпринтов на C++. Мой опыт использования Squirrel в продакшене для мобильных телефонов — ~20% работы состоит в переписывании со скриптов обратно на C++.

Есть примеры статически типизированных скриптовых языков в геймдеве, UnrealScript — один из первых заметных успешных. Однако он показал проблемы с масштабируемостью. Рано или поздно наступает момент, когда “переписать медленную часть на C++” становится типичным решением проблем с производительностью в проекте. В конце концов, Unreal перешёл к BluePrint, у которого есть собственные минусы.

ООП в стиле C++ (данные разбросаны на хипе) - серьёзное препятствие для написания быстрого кода. Большинство языков предназначены в основном для поддержки той или иной модели программирования. Однако в разработке игр в наши дни часто необходим более ориентированный на данные подход (data oriented design). Многие переходят на ECS-фреймворки, которые сопровождаются какой-либо дополнительной инфраструктурой. Unity создала компилятор Burst, использующий подмножество C# для решения проблемы связи фреймворка с языком. Джонатан Блоу разрабатывает язык Jai для решения той же проблемы. Языки, которые изначально спроектированы с учётом возможности использования с data-oriented фреймворками, могут сделать взаимодействие с ними проше, и daScript делает шаг в этом направлении.

Когда в 2018 году Антон Юдинцев из Gaijin Entertaiment обратился ко мне с предложением создать собственный язык для их ECS-фреймворка, я “купился”. Запрос был достаточно конкретным, чтобы заинтересовать меня — язык общего назначения с “большой идеей” это не просто академическое упражнение, его очень трудно создать и еще труднее адаптировать к продакшену.

Какие преимущества использования daScript? Как его можно использовать для разработки игр? Как это повлияет на производительность?

daScript работает быстро. Передача управления между C++ и daScript очень дешёвая. У него очень быстрый интерпретатор. Он выполняется с той же скоростью, что и нативный C++, если использует ahead-of-time компиляцию. По сути, вам никогда не придётся ничего переписывать с daScript на C++.

(!) Это серьёзная заявка на киллер-фичу языка. Одну из. Вторая -- при небольшой настройке должно быть можно получить hot code reload, время перекомпиляции+перезапуска игры на мобилках, да и на ПК, критично

daScript — безопасный, статически типизированный язык с сильной типизацией, с параноидальным уровней проверки ошибок. Однако у него очень надёжный механизм вывода типов, поэтому большая часть времени вам не нужно декорировать свой код информацией о типах. Безопасный код на языке обычно и выглядит просто, а небезопасный — должен быть явно помечен как таковой.

daScript — встраиваемый язык. Он хорошо сочетается с вашей кодовой базой на C++. Он сам компилируется тем же компилятором на тех же платформах. Нет проблемы с тем, что платформа не поддерживает JIT, потому что вы можете просто пред-компилировать все скрипты в C++ (aheod-of-time компиляция). При этом, вы можете “патчить” скопмилированные в C++-скрипты с помощью обновлённой интерпретируемой версии в реальном времени.

(Либо обновляя скрипты на девайсе на лету, либо присылая их “динамиками” с сервера)

Язык предоставляет очень глубокие механизмы интеграции, которые выходят далеко за рамки простого связывания методов, чтобы такие вещи, как ECS-фреймворк, могли получить всю необходимую им дополнительную информацию, как используются данные. Мы можем контроллировать, как именно интерпретируется daScript, и какой именно C++ код будет сгенерирован.

(Тут я чё-то не допонимаю, о чём именно говорит автор, не видел в репозитории примеров, возможно речь о синтаксических макросах)

update
Связался с Борисом лично для уточнения. Помимо макросов, для типов, описанных в C++, возможно также задавать кастомные simulate-функции и то, как они будут трансформироваться в C++ код (aot-компиляция). Т.е. фактически переопределить в типе любые синтаксические формы обращения с ним, как он ведёт себя на любой чих — сделать с помощью этого какой угодно DSL. В отличие от макросов, можно указывать, как генерируется AOT, и за счёт этого получать оптимальный код. Например, различные vector swizzle операции трансформировать в SIMD-инструкции.

daScript — язык для повышения продуктивности программистов. У него очень быстрое время компиляции. Он поддерживает горячую перезагрузку. Обычно изменения в коде отображаются в программе, как только они слетели с кончиков ваших пальцев. При правильной настройке вы можете запустить приложение и затем реализовать совершенно новую фичу, даже не перезапуская его.

(!) а это -- заявка на киллер-фичу #2

Разработка собственного решения может показаться слишком сложной для большинства разработчиков, что же заставляет кого-то создавать что-то новое и сложное?

Некоторые говорят, что необходимость — это мать изобретений. В какой-то момент становится совершенно очевидно, что никакие эволюционные улучшения не позволят сделать следующий шаг в качестве разработки. Наличие хорошего инструмента является признаком хорошего инженерного мышления. В случае с daScript было очевидно, что информацию, необходимую для среды ECS, очень сложно извлечь из кода на C++. Что программисту необходимо будет искуственно размечать код определенным образом, и этот процесс будет постоянно подвержен ошибкам в ходе написания и поддержки, и это просто чтобы предоставить информацию для эффективной работы фреймворка. Добавьте к сравнению разницу в скорости — даже прототип daScript был в 10-35 раз быстрее, чем LuaJit, и вдруг создание новый языка уже покажется очень хорошим вложением.

LuaJit бьл хорошим источником вдохновения. Удивительно, как много можно сделать для языка, который не был спроектирован для достижения максимальной производительности. Если же сделать что-то подобное для языка, который изначально затачивался, чтобы быть производительным, должно быть возможно достичь поразительных результатов. При этом превзойти LuaJit — это непростой челлендж, он задаёт высокую планку, ни один интерпретируемый язык не может даже близко приблизиться к нему.

Когда дело доходит до фич самого языка, то идеи брались из самых различных источников: Kotlin, Python, Ruby, Lisp, F#, HLSL (!!!) — всего лишь некоторые из них. Одной из ключевых идея было “сделать что-то похожее, только работающее с сумасшедшей скоростью”.

Другая идея: “писать меньше лишнего”. Наличие в языке простых, но сильных генериков было обязательным с самого начала. Статически типизируемые языки без обобщений и вывода типов, как правило, очень многословны. С другой стороны, декларативное обобщенное программирование очень контр-интуитивно, поэтому мы пошли путём возможности проверки условий во время компиляции и при этом возможностью явно указать…, ну, вообще всё.

(? в оригинале: and explicit… well, explicit everything.)

Сосредоточение внимания на функциональном программировании и data-oriented design подходе, а не на классическом ООП, было еще одним сознательным решением. Язык должен был быть быстрым, а классическое ООП просто не позволило бы сделать его таким.

Расскажи пожалуйста, о затратах времени. Сколько времени ушло на то, чтобы создать ядро языка? Какие дальнейшие планы?

Потребовалось 3 месяца, чтобы создать прототип. С тех пор прошло около года, и язык сильно повзрослел. Теперь он работает у нас на клиенте и на сервере, и объём кода только растёт. Роадмап развития языка:

  • Доработать язык до спецификации. Сейчас в нём не хватает нескольких важных задуманных вещей.
    — Variant types.
    — Мощный pattern-matching.
    — Нативная поддержка генераторов (yield).
    — Нативная поддержка регулярных выражений.
  • Переписать компилятор daScript и всё остальное (кроме рантайма) на нём самом.
    — Это даст возможность представлять AST языка для чтения и записи на нём самом. Сейчас, сложные макросы и аннотации функция необходимо писать на нём самом.
    (доступ к ast из daScript есть на момент перевода статьи, как и часть фич выше)
  • GPU бекэнд. Сейчас aot-компиляция доступна только в C++, но нет никакой фундаментальной причины для этого ограничения.
  • Стандартная библиотека и дополнительные модули.
    — В стандартной библиотеке не хватает многих вещей, особенно для функционального программирования. Как только будут доделаны генераторы, следом нужно будет реализовать стандартные функции высшего порядка.
    (вроде тоже есть уже)
    — Существует несколько де-факто стандартных библиотек, для которых стоит сделать привязку “из коробки”. PugiXml, RapidJson, UriParser и другие.
  • Оптимизации, множество и множество оптимизаций.
    — daScript уже оптимизирующий компилято с большими возможностями, которые будут только расти.
    — daScript уже экстремально быстрый интерпретатор. И это лишь хорошая причина для того, чтобы сделать его еще быстрее.
    — daScript при AoT-компиляции генерирует вполне читабельный C++-код. Однако мы хотели бы сделать, чтобы результирующий код на C++ был более надёжным.

Несмотря на то, что еще слишком рано для standalone компилятора daScript, уже очевидно, что LLVM-бекэнд позволил бы генерировать значительно лучший код, чем AoT-бекэнд. Компилятор daScript знает некоторые факт о том, как используется код, такие как выравнивание, aliasing, зависимости и т.д., которые очень сложно передать компилятору C++. Так что в какой-то момент, вероятно, появится LLVM-бекэнд и возможность использовать daScript для полного цикла разработки, но я не думаю, что это произойдёт скоро.

Борис Баткин, разработчик языка daScript

От себя добавлю немного:

  • Скорость итераций разработки, который можно добиться с помощью скриптов — ключевой путь к качеству и выживанию игры-сервиса (а такие — почти все игры сейчас)
  • Скорость кода — важный параметр, чтобы у вас телефон с 4х-ядерным процессором, и вообщем-то неплохим GPU и кол-вом RAM не считался “тостером” (у конкурентов-то игра на нём отлично работать будет)
  • Отсутствие необходимости переписывать код с одного языка на другой ради ускорения просто экономит время, за которое можно сделать что-нибудь новое
  • До продакшена долетают типичные ошибки C++ (а это у всех бывает, в тексте ссылка на доклад Антона Юдинцева о статистике ошибок в Warthunder) — если станет меньше C++-кода (и меньше доступа к этому коду у неквалифицированных программистов) - станет и меньше таких ошибок
  • Высокая скорость разработки фич (hot-code reload об этом) — один из необходимых моментов для перехода к созданию более масштабных в плане количества кода игр (эх, ещё бы стоимость создания арта для более масштабных проектов как-нибудь сократить)