Часть 1
Часть 2

Ситуация с GUI в Rust просто ужасна

В сообществе Rust ходит шутка, что на 5 игр существует 50 игровых движков; наверно, ещё одна такая шутка нужна про фреймворки GUI. Люди пробуют разные подходы, что, учитывая полную обобщённость Rust как языка, имеет смысл. Но в этой статье мы говорим о разработке игр, и я считаю, что в этой сфере у нас не просто дефицит, а полное отсутствие решений.

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

Я даже не касаюсь тем наподобие частиц в UI и различных эффектов, которые могут понадобиться пользователю. Очевидно, что полностью независимая от всего библиотека GUI не может иметь сложных шейдерных эффектов и частиц, но мне кажется, что это тоже стало частью общей проблемы. Библиотеки GUI перекладывают всю эту ношу на пользователя, поэтому каждому пользователю приходится заново изобретать велосипед в выбранном фреймворке/движке.

Основную часть нашего UI мы создаём в egui; несмотря на неоптимальность и периодически всплывающие затруднения, она, по крайней мере, предоставляет приличный интерфейс Painter для создания полностью настраиваемого UI.

Когда я обсуждаю это с людьми и говорю, что в Unity или Godot ситуация с UI гораздо лучше, мне всегда отвечают что-то типа «о, я пробовал Unity, это было ужасно, мне гораздо больше нравится делать всё в чистом коде». Очень распространённый ответ, и я раньше говорил так же; но здесь полностью упускается тот момент, что создание UI — это навык, а работа с тулкитами UI наподобие предоставляемых Unity или Godot — сложный и раздражающий процесс, потому что этому навыку приходится обучаться.

Реактивный UI — неподходящее решение для создания наглядного, уникального и интерактивного игрового UI

Существует много разных библиотек GUI в экосистеме Rust со множеством разных подходов. Некоторые из них реализуют привязки к уже существующим библиотекам GUI, некоторые реализуют immediate mode, некоторые реактивны, а некоторые даже используют retained mode. Некоторые пытаются использовать flexbox, а другие вообще не работают на фундаментальном уровне со структурой UI.

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

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

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

Я понимаю, что в некоторых играх может потребоваться рендеринг таблицы с миллионом элементов, но не считаю, что это должно быть целью игрового GUI. А ещё я понимаю, что многие, если не все из перечисленных выше библиотек не позиционируют себя как игровые GUI, но в этом разделе я говорю в том числе и об этом.

Насколько я знаю, в экосистеме Rust нет ни единого решения, которое бы поставило своей целью «качественно создавать игровые GUI». Я понимаю, что реализация чего-то наподобие частиц и шейдеров в GUI будет непростой задачей для библиотеки, стремящейся к независимости от движка, но это ещё одна причина, по которой ситуация вряд ли улучшится.

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

Правило сироты должно быть опциональным

Наверно, этот раздел будет довольно коротким, потому что каждый, кто пробовал писать приличные объёмы кода для пользовательского пространства на Rust, ощутил на себе боль правила сироты (orphan rule). Это прекрасный пример того, что я бы назвал стремлением к идеалу и полному избеганию проблем любой ценой, даже если это существенно снизит эргономику для разработчика.

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

Но меня совершенно не волнует соблюдение этого правила в приложениях и библиотеках, разработанных в конечных продуктах. Я специально не говорю «в двоичных крейтах», потому что большинство крупных проектов состоит из нескольких крейтов, и многие будут являться несколькими рабочими пространствами.

Я бы сказал, что на практике это правило следует отключать даже для публикуемых библиотек, потому что некоторые из них не используются другими библиотеками вниз по потоку. Хороший пример этого — игровые движки и фреймворки: людям, пользующимся библиотеками наподобие Macroquad или Comfy, нет необходимости в соблюдении правила сироты в их кодовой базе. «Фреймворкоподобным» библиотекам пошла бы только на пользу возможность расширения существующих функций без форков и повышение унификации для конечных пользователей.

Но, к сожалению, как и со многим другим в Rust, «совершенство только в абсолюте», и только потому, что кто-то потенциально может реализовать конфликтующий трейт, мы должны запретить это для каждого в любых обстоятельствах и без возможности отключить правило.

Время компиляции улучшилось, но не с процедурными макросами

Всего несколько лет назад время компиляции Rust было поистине ужасным, и сейчас ситуация в целом улучшилась, по крайней мере, в Linux. Инкрементальные сборки в Windows по-прежнему существенно медленнее, вплоть до того, что мы изначально мигрировали на Linux (разница в 3-5 раз), но после покупки нового мощного десктопа сборка нашей кодовой базы из десяти тысяч строк занимает всего несколько секунд.

Точнее, после того, как мы потратили огромное количество времени на оптимизацию времени компиляции, устранение процедурных макросов и перемещение частей в отдельные крейты.

Хороший пример: единственная причина существования comfy-ldtk — это обёртывание единственного файла и обеспечение того, чтобы мономорфизация serde происходила в отдельном крейте. Это может показаться мелкой деталью, но на моём десктопе это вылилось в увеличение времени инкрементальных сборок до десяти с лишним секунд вместо всего двух секунд в Linux. Огромная разница для 1600 строк определений struct.

Да, я понимаю, что сериализация — нетривиальная задача, а в serde есть множество фич. Но я не считаю, что разумно расплачиваться восемью секундами на компиляцию 1600 строк кода. Особенно если посмотреть на код и понять, что всё это лишь простые struct. Там нет сложной магии, всё сводится только к тому, что serde медленная.

Многих это не волнует; я лично много раз поднимал вопрос времени инкрементальной компиляции во множестве разных контекстов, и всегда находилось приличная доля людей, убеждавших меня, что это нормально, что их сборки занимают 20-30 секунд или дольше, а они всё равно остаются продуктивными.

Рискую кого-то рассердить, но я связываю это с отсутствием опыта работы с более качественным инструментарием или, возможно, игры этих людей ещё не достигли того этапа, после которого нужны быстрые итерации. По крайней мере, мне кажется, что некоторые люди осознают, насколько более совершенными могли бы быть их игры, если бы время компиляции составляло 0,5 с, а не 30 с. Вещи наподобие GUI нужно постоянно настраивать, чтобы всё выглядело хорошо. Возможно, ваш опыт отличается, тогда я бы хотел посмотреть на пример хорошего, качественного и нетривиального GUI, созданного при времени инкрементальных сборок от 30 секунд и выше.

Экосистема разработки игр на Rust живёт хайпом

Все знают, что экосистема разработки игр на Rust молода. Если спросить об этом в сообществе, большинство людей признает это; по крайней мере, в 2024 году у нас уже нет проблем с признанием наличия трудностей.

Но мне кажется, что снаружи складывается иное впечатление; я связываю это с очень хорошим маркетингом со стороны Bevy и некоторых других. Несколько дней назад Brackeys выпустил видео о возврате в геймдев и разработку на Godot. Когда я смотрел его и услышал о потрясающих опенсорсных игровых движках, у меня уже возникло предчувствие. Примерно на 5:20 в видео показывают карту рынка игровых движков, и для меня стало полной неожиданностью (ага), что я увидел там три игровых движка на Rust, а именно BevyArete и Ambient.

Должен заявить, что этот пост не задумывался как критика какого-то конкретного проекта, и я понимаю, что эти проекты не ответственны за то, что другие люди говорят в своих видео. Но в тоже время, в мире Rust это стало такой темой, а может, даже и мемом, что я чувствую необходимость поговорить об этом.

В целом экосистема Rust работает так: широкое признание получает тот проект, который даёт больше всех обещаний, демонстрирует лучший веб-сайт/readme, хвастается красивыми gif и самое важное — апеллирует к нужным абстрактным ценностям. Это происходит вне зависимости от истинного удобства пользования этим проектом. А есть и другие проекты, которые часто остаются незамеченными, потому что они не ярки и не обещают невозможного, а просто пытаются делать то, что работает. И такие проекты почти никогда не упоминают или упоминают как второстепенные варианты.

Первый пример этого — Macroquad, очень практичная библиотека для 2D-игр, работающая почти на всех платформах и имеющая очень простой API; она невероятно быстро компилируется, почти не имеет зависимостей и создаётся одним человеком. Также есть сопровождающая её библиотека miniquad, предоставляющая графическую абстракцию поверх Windows/Linux/MacOS/Android/iOS и WASM. Однако Macroquad совершила одно из самых тяжких преступлений в экосистеме: она использует глобальное состояние и даже потенциально ненадёжна. Я говорю «потенциально», потому что несмотря на возражения пуристов, при всех способах применения она абсолютно безопасна, если только вы не решите использовать самый нижний уровень API для работы с контекстом OpenGL. Проработав с Macroquad уже почти два года, я никогда не сталкивался с такой проблемой. Однако об этом постоянно напоминают предлагающим эту библиотеку, ведь она не отвечает главной ценности Rust — абсолютной безопасности и корректности.

Второй пример — это Fyrox, движок для 3D-игр с редактором сцен в полном 3D, системой анимаций и всем, что необходимо для создания игры. Этот проект тоже создан одним человеком, разработавшим на этом движке завершённую 3D-игру. Сам я с Fyrox не работал, потому что, как и говорится в этом разделе, лично впал в хайп и выбирал проекты с красивыми веб-сайтами и кучей звёзд на Github, и презентующие себя определённым образом. Недавно Fyrox стал довольно популярен в Reddit, но очень печально, что его почти никогда не упоминают в видео несмотря на то, что там есть полнофункциональный редактор, который разработчики Bevy обещают уже несколько лет.

Третий пример — это godot-rust, привязки Rust к Godot Engine. Самое тяжкое преступление, совершённое этой библиотекой — что это решение не на чистом Rust, а просто привязки к порочному движку на C++. Я немного преувеличиваю, но те, кто наблюдает за Rust снаружи, удивятся, насколько это близко иногда к реальности. Rust чист, Rust корректен, Rust безопасен. C++ старый, плохой, некрасивый, небезопасный и сложный. Именно поэтому при разработке игр на Rust мы не пользуемся SDL, у нас есть winit, мы не пользуемся OpenGL, у нас есть wgpu, мы не пользуемся Box2D или PhysX, у нас есть rapier, у нас есть kira для игрового аудио, мы не пользуемся Dear ImGUI, у нас есть egui. А самое главное — мы не можем пользоваться готовым игровым движком, написанным на C++. Это будет нарушением кодекса крабов, священного для всех, кто использует rustup default nightly, чтобы повысить скорость компиляции, и соглашается с этим, принимая условия лицензии (той самой, которая запрещает нам использовать логотип (tm)(c), официально заверенный Rust foundation).

Если вы действительно настроены на создание реальной игры на Rust, и в особенности в 3D, то я в первую очередь рекомендую использовать Godot и godot-rust, потому что тогда у вас, по крайней мере, есть вероятность обеспечения всех необходимых вам фич, ведь у вас будет возможность участвовать в создании реального движка и помочь в реализации этих фич. Мы потратили год на создание BITGUN на Godot 3 и gdnative с использованием godot-rust, и хотя это был во многом мучительный процесс, в этом виноваты не наши привязки, а попытки смешивать большие объёмы GDScript и Rust всевозможными динамическими способами. Это был наш первый и самый крупный проект на Rust, благодаря которому мы пошли по пути Rust; могу сказать, что все остальные игры, которые мы позже делали на Rust, были играми в меньшей степени просто потому, что мы тратили много времени на разбор несущественных технических проблем с Rust как с языком, с какой-то частью экосистемы или просто с каким-то решением, которое оказывалось сложно реализовать из-за негибкости языка. Не хочу сказать, что взаимодействие GDScript и Rust было легко реализовать, это определённо не так. Но, по крайней мере, есть представляемая движком Godot возможность «сделать только одну вещь и двигаться дальше». Мне кажется, этого не ценит большинство людей, пробующих создавать решения на основе только кода, в особенности на Rust, где язык может встать на пути у творчества множеством неудобных способов.

Не могу сказать ничего конкретного об Ambient, потому что он довольно новый и я пока с ним не работал; но опять же, я не слышал ни о ком, кто бы им пользовался, однако он попал в видео Brackeys.

Arete вышел лишь несколько месяцев назад в версии 0.1 и получил довольно негативный приём со стороны сообщества Rust, потому что очень неясно заявил о своих целях и в то же время имел закрытые исходники. Несмотря на это, не относящиеся к сообществу люди очень часто его упоминают, делая при этом очень смелые заявления.

Что касается Bevy, то я считаю, что его позиционирование как «главного» игрового движка на Rust по большей мере оправданно, хотя бы из-за масштабов проекта и задействованного количества участников. Им удалось создать достаточно большое сообщество, и хотя я могу не соглашаться с их обещаниями и решениями руководства, нельзя отрицать того, что Bevy популярен.

В этом разделе я хотел только привлечь внимание к странному состоянию дел: не входящие в сообщество люди часто просто смотрят на качество маркетинга самого движка и на то, что написано в заявлениях разработчиков. Мне показалось, что всё это нужно упомянуть, ведь я много раз шёл по этому пути и много раз слышал очень убедительные аргументы, но потом осознавал, что эти люди просто хорошо умеют говорить, но хуже справляются с выпуском обещанных фич.

Вне рамок игровых движков стоит упомянуть rapier — физический движок, который рекомендуют очень часто, поскольку он обещает стать решением для физики на чистом Rust, отличной альтернативой уродливому внешнему миру Box2D, PhysX и остальных. Ведь Rapier написан на чистом Rust, а значит, имеет все преимущества поддержки WASM, в то же время ужасно быстрый, параллельный по своей природе и, разумеется, очень безопасный... ведь так?

В основном я работал с ним в 2D; скажу следующее: самые простые вещи в нём работают, некоторые из продвинутых API фундаментально поломаны, например, разбиение на выпуклые многоугольники вылетает при относительно простых данных или  шарнирные соединения нескольких тел вызывают сбой при своём удалении. Последнее особенно смешно, потому что похоже, что я был первым, кто попытался удалить шарнир, хотя это не такой уж сложный пример использования. Возможно, эти примеры походят на пограничные случаи, но в целом и сама симуляция показалась мне довольно нестабильной, вплоть до того, что в конечном итоге я написал собственный физический 2D-движок, и выяснилось (по крайней мере, при моём тестировании), что он вызывает меньше проблем в простых ситуациях типа «предотвращать наложение врагов друг на друга».

Это не реклама моей библиотеки физики, пожалуйста, не пользуйтесь ею, она не очень тщательно протестирована. Смысл моих слов в том, что когда новичок в Rust спрашивает рекомендации по физике, ему порекомендуют rapier, и многие скажут, что это отличная популярная библиотека. А ещё у неё красивый веб-сайт и она хорошо известна в сообществе. Я был таким новичком, мне приходилось месяцами сражаться с этой библиотекой и думать «наверно, проблема во мне, это я что-то делаю не так». Похоже, мне удалось избавиться от этого заблуждения только потому, что я попытался реализовать физику самостоятельно.

Большая часть экосистемы Rust обладает таким свойством: она заставляет пользователя считать, что он делает нечто фундаментально ошибочное, что он не должен хотеть сделать какую-то вещь, что проект, который он хочет собрать, нежелателен или некорректен. Это похоже на то, как при работе с Haskell вам захочется иметь побочные эффекты... а этого «вы хотеть не должны».

Только в случае Rust проблема заключается в том. что очень часто библиотеки, заставляющие пользователя ощущать подобное, получают всеобщие похвалы и популярность, потому что большинство экосистемы живёт хайпом, а не законченными проектами.

Глобальное состояние раздражает/неудобно не по тем причинам, игры однопоточны

Знаю, что даже просто сказав «глобальное состояние», я сразу задену множество людей, глубоко уверенных, что это неправильно. Я думаю, это одно из проявлений созданных сообществом Rust очень вредных и непрактичных правил, которым заставляют подчиняться людей и проекты. У разных проектов бывают совершенно разные требования, и мне кажется, по крайней мере в контексте разработки игр многие люди ошибочно понимают, что же такое реальные проблемы. Общая «ненависть» к глобальному состоянию имеет разную окраску, и многие настроены против него не на 100%, но я всё равно считаю, что во многом сообщество в целом движется не в том направлении. Опять повторюсь, что я говорю не о создании движков, тулкитов, библиотек, симуляций и тому подобного. Мы говорим об играх.

В играх есть только одна система аудио, одна система ввода, один физический мир, одна deltaTime, один рендерер, один загрузчик ассетов. Возможно, в каких-то пограничных случаях было бы удобнее, если бы что-то не было глобальным, а если вы создаёте MMO на основе физики, ваши требования могут быть другими. Но большинство людей создаёт или 2D-платформер, или шутер с видом сверху, или воксельный симулятор ходьбы.

За несколько лет я уже много раз пробовал чистый подход, при котором всё инъецируется как параметры (начиная с Bevy 0.4 и до 0.10), и пробовал создать создать собственный движок , в котором всё глобально, а для воспроизведения звука достаточно сделать play_sound("beep"), поэтому у меня сложилось чёткое понимание, что более полезно.

Я не против конкретно Bevy, а считаю, что в этом виновна большая часть экосистемы, за единственным исключением macroquad, но я использую Bevy как пример, потому что он находится на другом краю спектра, где всё передаётся явным образом.

Вот использующие глобальное состояние элементы, которые оказались очень полезным в Comfy и которые я постоянно использую в своих играх:

  • play_sound("beep") для воспроизведения одного SFX. Если требуется больше контроля, то можно использовать play_sound_ex(id: &str, params: PlaySoundParams).

  • texture_id("player") для создания TextureHandle с целью обращения к ассету. Нет никакого сервера ассетов для передачи данных, потому что в худшем случае я мог бы использовать в качестве идентификаторов пути, а поскольку пути уникальны, то и идентификаторы будут уникальными.

  • draw_sprite(texture, position, ...) или draw_circle(position, radius, color) для отрисовки. Так как во всех реальных движках всё равно используются вызовы пакетной отрисовки, они вряд ли будут делать что-то большее, чем просто отправлять команду отрисовки в какую-нибудь очередь. Мне отлично подходит глобальная очередь, потому что зачем мне передавать что-то, а не просто отправить в очередь команду «нарисуй круг».

Если вы разработчик на Rust, но не создаёте игры, то можете подумать «но как же потоки?», и да, здесь серверы Bevy тоже станут хорошим примером. Потому что Bevy тоже задавался этим вопросом и попытался ответить на него максимально обобщённым образом: а давайте просто заставим все наши системы работать параллельно.

Теоретически это хорошая идея, которая может казаться привлекательной для новичков в геймдеве, потому что как и в мире бэкенда, где всё асинхронно и работает в пулах потоков, это может привести к «бесплатному» повышению производительности.

Но, к сожалению, я считаю, что это одна из самых больших ошибок Bevy; когда я задаю вопросы, многие тоже начинают это осознавать, хоть и немногие признают это. Модель параллельных систем Bevy настолько гибка, что не сохраняет постоянство порядка даже между кадрами (по крайней мере, в последний раз, когда я проверял). Если вы хотите поддерживать порядок, необходимо указать ограничение.

Поначалу это тоже кажется разумным, но я много раз пробовал создавать нетривиальную игру на Bevy (месяцы времени разработки, десятки тысяч строк кода), и оказалось, что в итоге пользователю всё равно приходится определять кучу зависимостей, потому что обычно в играх действия должны происходить в конкретном порядке, чтобы выполнение одного действия не откладывалось на один кадр; хуже того, иногда что-то ведёт себя странно, потому что получается AB вместо BA. Если подвергнуть это сомнению, на тебя обрушатся серьёзные аргументы о том, что Bevy работает технически корректно, но при разработке игр это в результате оборачивается огромным количеством бессмысленных церемоний.

Но, наверно, у такого подхода есть и плюсы? Ведь вся эта бесплатная параллельность полезна и игры благодаря ней работают удивительно быстро?

К сожалению, после выполнения всей работы по упорядочиванию систем для распараллеливания остаётся не так много. И на практике этот небольшой выигрыш, который можно получить, возникает благодаря параллельности чистой data driven system, которую можно было бы тривиально реализовать при помощи параллелизма данных на основе rayon.

Вспоминая годы разработки игр, могу сказать, что я написал больше параллельного кода на Unity при помощи Burst/Jobs, чем в играх на Rust (и в Bevy, и в собственном коде); так получилось просто потому, что основная часть работы над играми в результате оказывается игрой, а на решение интересных задач нужно оставлять достаточное количество мыслительной энергии. А почти во всех проектах на Rust основная часть моей мыслительной энергии уходила на борьбу с языком, или на проектирование чего-то в обход языка, или на то, чтобы не потерять слишком много эргономики разработки, потому что что-то нужно сделать определённым образом, ведь Rust требует делать это таким образом.

Глобальное состояние — прекрасный пример в этой категории. Мне кажется, стоит объяснить это подробнее. Давайте начнём с определения проблемы. В языке Rust в общем случае есть несколько вариантов:

  • static mut — небезопасно; означает, что для каждого использования требуется unsafe, из-за чего код становится очень уродливым, а каждое ошибочное применение приводит к UB.

  • static X: AtomicBool (или AtomicUsize, или любой другой поддерживаемый тип) — достойное решение; несмотря на его неудобство, по крайней мере, его не очень неудобно использовать. Но работает оно только для простых типов

  • static X: Lazy<AtomicRefCell<T>> = Lazy::new(|| AtomicRefCell::new(T::new())) — в конечном итоге оказывается необходимым для большинства типов; его не только неудобно определять и использовать, но оно ещё и приводит к потенциальным вылетам в среде исполнения из-за double borrow.

  • ... и, разумеется, решение «просто всё передавать без использования глобального состояния»

У меня возникало множество случаев, когда я случайно вызывал вылет из-за double borrow, и не потому, что «код изначально проектировался неверно», а потому, что какая-то другая часть кодовой базы требовала рефакторинга, а в результате него мне приходилось реструктурировать использование глобального состояния, что и приводило к неожиданным вылетам.

Пользователи Rust скажут, что это значит, что мой код делал что-то не так и на самом деле язык нашёл баг за меня, и что это хороший пример того, что глобальное состояние — это плохо и его нужно избегать. В этом есть правда, существуют баги, которые можно предотвратить подобной проверкой. Но на практике в языках, где глобальное состояние реализуется легко, например, в C#, в контексте геймдева подобные типы ошибок крайне редко возникают в реальном коде.

С другой стороны, вылеты из-за double borrow, когда ты делаешь что угодно с проверкой dynamic borrow, возникают очень легко, и очень часто это происходит по ошибочным причинам. Примером могут служить запросы к пересекающимся архетипам при ECS. Для новичков что-то подобное станет проблемой в Rust (код немного упрощён для повышения читаемости):

for (entity, mob) in world.query::<&mut Mob>().iter() {
  if let Some(hit) = physics.overlap_query(mob.position, 2.0) {
    println!("hit a mob: {}", world.get::<&mut Mob>(hit.entity));
  }
}

Проблема в том. что мы обращаемся к одному и тому же элементу из двух мест. Ещё более простой пример — итерации по парам при помощи такого кода (тоже упрощённого)

for mob1 in world.query::<&mut Mob>() {
  for mob2 in world.query::<&Mob>() {
    // ...
  }
}

Правила Rust запрещают наличие двух изменяемых ссылок на один объект, и всё, что потенциально может привести к этому, не разрешается. В приведённых выше случаях мы получим вылет в среде исполнения. Некоторые ECS-решения используют обходные пути, например, в Bevy можно, по крайней мере, реализовать частичные пересечения, когда запросы разделены: Query<(Mob, Player)> и Query<(Mob, Not<Player>)>, но это решает проблему только в тех случаях, когда ничто не пересекается.

Я говорю об этом в разделе о глобальном состоянии, потому что существование таких ограничений становится особенно очевидно, когда всё становится глобальным, потому что тогда очень легко случайно затронуть RefCell<T>, которого касается другая часть кодовой базы через какую-то глобальную ссылку. Разработчики на Rust снова скажут, что «это хорошо, мы предотвращаем потенциальный баг», но я снова отвечу, что у меня было не так много случаев, когда это на самом деле спасало меня от того, чтобы сделать что-то не так, или когда реализация этого в языке без таких ограничений вызвала бы проблемы.

Есть ещё и вопрос потоков, но я думаю, что здесь у разработчиков игр на Rust возникает заблуждение: они думают, что игры аналогичны бэкенд-сервисам, где для хорошей производительности всё должно работать асинхронно. В коде игры разработчик вынужден выполнять обёртывание в Mutex<T> или в AtomicRefCell<T> не «для того, чтобы избежать проблем, которые бы возникли, если бы они писали на C++ и забыли синхронизировать доступ», а просто для того, чтобы удовлетворить всеобъемлющее стремление компилятора сделать всё потокобезопасным, даже во всей кодовой базе нет ни одного thread::spawn.

Комментарии (36)


  1. alexs963
    18.05.2024 09:34
    +2

    Veloren такой: ну да, ну да, пошел я нахер.


    1. SadOcean
      18.05.2024 09:34
      +6

      Ну человек же не говорит, что на расте ничего не возможно сделать.
      Скорее то, что у ниструмента есть проблемы, а сообщество не хочет их признавать на концептуальном уровне.


      1. 1755
        18.05.2024 09:34
        +1

        Я как понимаю, основная проблема в том что инструменты все еще новые, по сравнению с Unity, Godot, UE. Соответственно не хватает зрелости и много чего еще нужно руками делать. Но прогресс постепенно идёт


        1. SadOcean
          18.05.2024 09:34
          +1

          Это тоже важно, но может так же оказаться, что раст плохой инструмент для этого концептуально.
          Ну то есть хорошо собирать фигурки из лего, но не соберешь небоскреб.
          А материалы небоскребов плохо подходят для фигурок - получаются прочные и выдерживают 3 тонны, но очень дорогие, тяжелые и неудобные в покраске.


          1. 1755
            18.05.2024 09:34

            Да, но мне кажется если сравнивать языки именно для игрового движка, а не игровой логики, то сравнивтаь нужно с C++. Rust в этом апмлуа не хуже. А для того чтобы быстро собрать прототип, да, игры оба языка не очень.


            1. Falstaff
              18.05.2024 09:34

              Тут в игру (pun intended!) вступает такой момент что движков вокруг уже полно и движок - это обычно штука которая не страдает от чрезмерного обилия багов после начального периода развития, так что сложно придумать вразумительную причину по которой нам очень-очень нужен движок именно на Rust. И наоборот, если какая-то компания берёт движок и допиливает его для нужд крупного AAA-проекта (как делали с Lumberyard например), им уже начнёт вставлять палки в колёса тот факт что быстро допиливать будет не так-то просто потому что Rust их бьёт по рукам а сроки горят и надо выкатывать игру уже вот-вот, а бизнес модель "пипл схавает, а краши поправим первым хотфиксом после релиза" реально работает и приносит деньги.


              1. 1755
                18.05.2024 09:34

                Мне кажется, допиливать сторонний проект на Rust проще, потому что с одной стороны строгий компилятор бьет по рукам везде где можно, где на С++ нужно иметь и большую экспертизу и внимательность, а с другой стороны единая экосистема по сборке и управлению зависимостями.


                1. Falstaff
                  18.05.2024 09:34

                  Ну-у-у... "проще" - это слово открытое к интерпретации и зависит от того что считать конечной целью. Если допиливать до вылизанного и свободного от багов состояния - да. Если допиливать до состояния где оно не падает уж очень часто и не лишает игру играбельности - то это уже совсем другой коленкор, там надо чтобы компилятор с пониманием относился к срокам релиза и тому что капельницы с кофе уже на исходе. :) Игровая индустрия обычно предпочитает второе.


                  1. 1755
                    18.05.2024 09:34

                    Мне кажется, на то чтобы потом все отлавливать в рантайме: утечки памяти, краши, проблемы с нулевыми указателями, рейс кондишены и прочие прелести можно потратить кратно больше времени, чем удовлетворить компилятор rust.

                    Но соглашусь, без конкретных примеоов это абстрактный спор)


      1. Dgolubetd
        18.05.2024 09:34

        Так ведь никто не создавал Rust с претензией, что он прямо на разработку игр заточен. С какой целью просить разработчиков\фанатов отверток признавать, что у отверток есть проблемы (ими трудно забивать гвозди)?


        1. SadOcean
          18.05.2024 09:34
          +2

          Никто не говорил, что он про разработку игр заточен.
          Но просто кажется, что ЯП с безопасностью и скоростью по умолчанию неплохо под это подходит.
          Конечно же стоит просить фанатов отверток признать, что у отверток есть проблемы и начать монтировать на этой ручке молоток. Иначе они так и будут продолжать улыбаясь забивать ими гвозди.
          В конечном итоге это ведь ЯП, а не отвертка. Языки вполне могут быть дописаны (даже не в смысле компилятор или синтаксис - ЯП можно дописать на ЯП), возможно если подумать, можно придумать хорошие методы, как решать эти проблемы на раст, или придумать другие паттерны, или, в конечном счете, признать, что нужно регулярно встраивать LUA к примеру.
          Ну то есть никто не заставляет забивать гвозди отверткой и никто не мешает решить эту проблему по другому.


          1. Dgolubetd
            18.05.2024 09:34

            Но тут человек именно что гвозди отверткой забивать хочет всю статью. У него везде прослеживается мысль, что ему не нужна ни безопасность, ни надежность, а нужны быстрое прототипирование, хот релоад и "just get things done".

            ЯП невозможно сделать идеальным во всём. У Раст приоритеты обозначены. В рамках этих приоритетов язык улучшается, насколько это возможно. Но никто не будет перестраивать язык, отказываться от них, чтобы угодить какой-то одной сфере.

            Всегда можно что-то придумать. Но кто этим должен заниматься за автора? У него 10 лет опыта иргостроения и несколько лет Раста. Сделал бы библиотеку упрощающую какие-то элементы разработки, пул реквесты в Bevy, придумал бы свои паттерны в конце концов.


            1. Falstaff
              18.05.2024 09:34
              +4

              А зачем ему этим заниматься, по здравому размышлению? Ему за паттерны и пул реквесты денег не платят, он выбирает инструменты для достижения практических целей. Вот он и пишет статью про то, что вот мол он попробовал инструмент, как молоток не работает. А переделывать его в молоток вместо того чтобы взять молоток и гвозди начать забивать - это не его дело.

              Просто разработчики/фанаты отвёрток любят всем говорить что их отвёртки созданы как убийцы молотков, вот люди и проверяют их на гвоздях. :)


            1. SadOcean
              18.05.2024 09:34

              Такие большие проекты не делаются в одиночку, поэтому идеологическая работа не менее важна.

              Зачем писать поперек ветра и ловить хейт сообщества если можно убедить сообщество что с этими проблемами нужно что-то делать?

              Это тоже важно.

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


              1. Dgolubetd
                18.05.2024 09:34

                Здоровая критика всегда полезна. И её в посте автора действительно много (например время компиляции, orphan-rule в конечном приложении и много другого). Такая критика должна, по идее,привести к улучшениям в языке.

                Но в тоже время есть критика неверно выбранного инструмента. Автор целый раздел назвал: "Rust being great at big refactorings solves a largely self-inflicted issues with the borrow checker", в то время как этот "Self-infliced issue" получился из-за несовпадения приоритетов о которых я уже писал.

                Т.е. можно сколько угодно критиковать borrow-checker, но если он является краеугольным камнем языка - это не конструктивно, т.к. он никуда не денется. Можно лишь частично его улучшить, но до определенного предела. Прийдется с ним жить.


            1. KawaiiSelbst
              18.05.2024 09:34

              Автор не хочет забивать гвозди отвёрткой, а пробует это сделать из любопытства, и того, что многие советуют попробовать это сделать =)
              Я рад что вышла такая статья, и моё мнение по поводу разработки игр на расте подтвердилось. Расту не хватает динамичности, для таких задач, с большим количеством связных и не связных систем как игры. Немало примеров, когда один элемент игры, должен иметь связь с другим настолько редко, что если возникнет проблема в реализации этого, то проще выкинуть фичу.

              Кстати если говорить о языках, которые не предназначены для игростроя, то C++ тоже не предназначен, и в UE например, ты практически полностью пишешь на фреймворке, даже дефолтные фичи языка многие заменены (и вызовут ошибки компиляции или рантайма), для совместимости с горячей перезагрузкой и фичами движка. Да даже так, хоть скриптинг на плюсах, но прототипирование даётся легко и непринуждённо на блюпринтах. Причём ещё и с возможностью транслировать блюпринты в плюсы.

              Моё имхо, пытаться писать на расте (да даже на чистых плюсах) скрипты для игры, мазохизм, на расте можно и я думаю это будет вполне даже удобно, писать внутренности движка, а скриптовать на quirrel, lua, haxe, да хоть на том же nim, если уж очень хочется что то скомпилировать.


              1. KawaiiSelbst
                18.05.2024 09:34

                А. И самое главное то забыл. Про быстрому проектированию. На расте можно быстро проектировать, только код молниеносно превращается в хрючево. Согласен с автором. Временно (или нет) добавить десяток выделений памяти, за одну итерацию, чтобы не рефакторить систему, обычное дело.


        1. Ken-Sei
          18.05.2024 09:34

          Так ведь никто не создавал Rust с претензией, что он прямо на разработку игр заточен

          Rust создан для полной замены C++


          1. SadOcean
            18.05.2024 09:34
            +1

            Справедливости ради, многие языки были созданы для замены С++, в том числе и в играх, так что заменить си ещё не означает стать идеальным во всех отраслях, где и си не идеален.


  1. mdfitumi
    18.05.2024 09:34
    +5

    Несколько лет пишу на расте, подтверждаю все сказаное. Делать R&D задачи на расте, где конечная цель не определена - больно и долго. В одном проекте несколько тысяч RPC и добавить 2 новые структуры занимает 10-20 минут, с учетом времени работы rust-analyzer, cargo check и пересборки проекта.


    1. Filipp42
      18.05.2024 09:34

      Скажите, а есть ли задачи, в которых писать на Rust приятно?


      1. Dgolubetd
        18.05.2024 09:34
        +9

        Тут даже не в задаче дело.

        Писать на Rust приятно если вам нравится разрабатывать софт и делать это максимально качественно: если вы кайфуете от того, что ваша программа потребляет мало памяти, работает быстро, а все её компоненты надёжно стыкуются и не ломаются при рефакторинге.

        Если же вам важен именно продукт - быстро выкатывать новые фичи, экспериментировать и т.п. - вряд ли вы оцените Rust.

        Тип задачи может лишь сгладить ситуацию. Чем более задача типовая - тем протореннее будет дорожка. Например, вряд ли у вас возникнут проблемы с каким-нибудь REST API.


    1. Dgolubetd
      18.05.2024 09:34
      +5

      А на питоне можно добавить за 10 секунд, зато потом спустя час обработки данных обнаружить, что где-то типы не сошлись..


      1. Onito
        18.05.2024 09:34
        +1

        Через час этот прототип уже на свалке окажется и то что где-то там в будущем можно было прийти к конфликту с типами никто не узнает.


  1. qiper
    18.05.2024 09:34
    +4

    Ситуация с GUI в Rust просто ужасна

    Как и везде, окромя C# и Delphi


  1. Panzerschrek
    18.05.2024 09:34

    Не понятна жалоба автора на конфликты доступа к одним и тем же объектам во время выполнения. Он бы предпочёл вместо этого вместо panic! тихое похеривание данных?


    1. mdfitumi
      18.05.2024 09:34

      double borrow не гарантирует, что будет "похеривание" данных


      1. Panzerschrek
        18.05.2024 09:34
        +1

        Не гарантирует, но может к нему привести, при чём очень вероятно. Именно поэтому вся суть языка Rust сводится к тому, чтобы такого не допускать.


        1. Onito
          18.05.2024 09:34

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


  1. kolya7k
    18.05.2024 09:34

    Rust неудобный и некачественный язык. Разработка на нём не уменьшает количество ошибок при увеличении времени разработки раз в 5 и ухудшения читаемости кода.

    Тот же современный C++ намного более безопасный, удобный и быстрый во всех смыслах язык. Одно только правило "не больше 1 изменяемой ссылки на объект" - страшная тупость. Для этого правила нет никаких логических и разумных причин. Это как правило "не больше 1 грязной кружки в квартире" И не важно сколько людей хотят пить, 1 или 10 и как удобнее их мыть - по 1 или по 10 за раз.


    1. Chiffario
      18.05.2024 09:34
      +4

      Одно только правило "не больше 1 изменяемой ссылки на объект" - страшная тупость. Для этого правила нет никаких логических и разумных причин

      Запрет на наличие нескольких мутабельных ссылок на объект как минимум предотвращает инвалидацию всех форм итераторов (например, попытки добавить значения в вектор во время итерирования через этот самый вектор), инвалидацию энумов (при создании референса на значение, хранящееся в элементе энума), и некоторые другие довольно лёгкие способы "сломать" ссылку на объект даже в однопоточном коде, не говоря уж о многопоточном


      1. kolya7k
        18.05.2024 09:34

        Ну да, это как выкинуть из дома все ножи и вилки чтобы нечайно не воткнуть их себе в глаз :) Для тупых это сделано, нормальные умные программисты и так никогда такого не сделают, плюс sanitizer-ы сразу покажут проблему, или тесты на худой конец...
        За 29 лет опыта программирования я такой глупости не совершал ни разу, поэтому для меня Rust - язык для идиотов :)


    1. KawaiiSelbst
      18.05.2024 09:34

      А чего в этом страшного то по сути? В однопоточном коде, в любом случае не получится изменить одну переменную сразу в из двух мест, а в многопоточном коде у вас есть возможность пользоваться мьютексами и рефкаунтерами, язык просто чётко обозначает вам, что вы должны осознавать, и планировать, что доступ к объекту у вас в программе осуществляется по очереди. А не одновременно из двух мест. Практика благая.

      Но могу согласиться, что иногда компилятор слишком сильно опекает.


      1. KawaiiSelbst
        18.05.2024 09:34

        Что меня больше бесит, что многие зависимости в расте (ну соответственно претензия скорее к разработчикам наверно, хотя я им денег и не платил, чтобы делали так как мне надо, но всё же), разработаны в формате фреймворков, и не редко бывает, что когда пытаешься использовать какой то крейт в коде, то выходит так, что крейты крутятся независимыми изолированными системами в коде, из которых ты костылями пытаешься доставать данные и тасовать между ними нужным тебе образом, чтобы получить нужный результат


        1. kolya7k
          18.05.2024 09:34

          Я вам больше скажу, сейчас нет ни одной задачи, которую можно быстрее, красивее и эффективнее решить с помощью Rust нежели с помощью C++26.

          Единственный плюс Rust в унификации и каталогизации библиотек.


      1. kolya7k
        18.05.2024 09:34

        Страшно то, что это ограничение для тупых... И Rust развивается как раз в сторону отупления программистов. Из-за этого сильно страдают алгоритмы.
        В этой же статье сказано про двойную вложенную итерацию по одному контейнеру для поиска пересечений, которая уже просто так работать не будет и придётся извращаться.

        И дело даже не в изменении, а в хранении двух ссылок на объект в разных местах.