[Перевод] Почему я отказался от разработки игр на Rust, часть 3

13b9e5ff5b692edd8319fbf257f0e177.png

Часть 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, а именно Bevy,  Arete и 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, чтобы повысить скорость компиляции, и соглашается с этим, принимая условия лицензии (той самой, которая запрещает нам использовать логотип ©,  официально заверенный 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> = 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)>, но это решает проблему только в тех случаях, когда ничто не пересекается.

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

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

© Habrahabr.ru