Как стать автором
Обновить

Почему я отказался от разработки игр на Rust, часть 2

Уровень сложностиСредний
Время на прочтение16 мин
Количество просмотров8.9K
Автор оригинала: LogLog Games

Часть 1

Обобщённые системы не приводят к интересному геймплею

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

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

  • Процедурно генерируемый мир, планеты, космос, подземелья.

  • Всё из вокселей, сильный упор на сами воксели, рендеринг вокселей, размер мира и производительность.

  • Обобщённые взаимодействия вида «всё, что X может сделать с любым другим объектом».

  • Рендеринг максимально оптимальным способом: если ты не используешь draw indirect, то игру ли ты вообще делаешь?

  • Наличие хороших типов и «фреймворка» для создания игр.

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

  • Мультиплеер.

  • Куча GPU-частиц; чем больше частиц, тем лучше VFX.

  • Хорошо структурированная ECS и чистый код.

  • ... и многое другое

Это замечательные цели, если вы экспериментируете с технологией или учите Rust, но я должен повторить то, что сказал в начале первой части статьи: я не оцениваю Rust с точки зрения технической интересности или получения удовольствия от процесса. Я хочу создавать реальные игры, в которые будут играть настоящие люди (не только разработчики), выпускать их за разумное время, чтобы за них платили люди и чтобы у них был шанс попасть на главную страницу Steam. Уточню, что это не расчётливая схема «делай деньги любой ценой», просто я не хочу делать это только «ради развлечения». Вся моя статья написана с точки зрения человека, стремящегося стать серьёзным разработчиком игр, которого интересуют игры, геймплей и игроки, а не только любовь к технологиям.

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

Теперь вернёмся к обобщённым системам. Вот несколько пунктов, напрямую или косвенно противоречащие решениям с обобщённой ECS, но благодаря которым, по моему мнению, игры становятся хорошими:

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

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

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

  • Плейтестинг из множества итераций с многократным совершенствованием фич геймплея, экспериментами и избавлением от того, что не работает.

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

  • Уникальный и запоминающийся игровой процесс.

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

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

Хорошим примером игры здесь может быть The Binding of Isaac — очень простая roguelike с сотнями апгрейдов, изменяющих игру запутанными, интерактивными и сложными способами. Это игра, в которой взаимодействует множество систем, но в то же время они совершенно не обобщены. Это не игра с пятьюстами апгрейдами типа «+15% к урону», а со множеством апгрейдов типа «бомбы прилипают к врагам», или «ты стреляешь лазером, а не пулями», или «первый тип врага, которого ты убиваешь на каждом уровне, больше никогда не появится в игре».

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

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

Более гибкий язык позволяет разработчику игры мгновенно реализовать новую фичу «грязным» способом, а потом запустить игру, протестировать её и проверить, действительно ли фича интересная. Потенциально за короткий промежуток времени можно выполнить множество таких итераций. К тому времени, как разработчик на Rust закончит рефакторинг, разработчик на C++/C#/Java/JavaScript уже реализовал множество различных геймплейных фич, много раз сыграл в игру и протестировал их все, глубже осознал, в каком направлении должен двигаться проект.

Джонас Тайроллер очень хорошо объяснил это в своём видео о геймдизайне как о поиске. Рекомендую его посмотреть всем разработчикам игр, потому что мне оно кажется лучшим объяснением того, почему столь многие игры (в том числе и мои) так ужасны. Хорошая игра делается не в лаборатории, создающей продуманные типы, она разрабатывается автором, который стал гроссмейстером в своём жанре, понимающим каждый аспект дизайна, путём проб и ошибок шагающего к конечной цели. Хорошая игра создаётся при помощи выбрасывания кучи плохих идей, посредством нелинейного процесса.

Создание увлекательных и интересных игр — процесс быстрого прототипирования и итераций, а Rust ценит всё, кроме этого

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

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

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

Но реальный мир геймеров не так благосклонен. Игрокам в Steam не важно, что игра сделана на Rust, что на неё потратили пять лет, им не важно, выложен ли код в open source. Им важен внешний вид игры и возможность за несколько секунд понять, будет ли она пустой тратой времени или чем-то потенциально интересным.

Я видел, как многие люди пренебрегают всем этим, потому что вся эта молодёжь, и её концентрация внимания, и СДВГ, и люди должны отдать должное XYZ. Мне не кажется полезным такое отношение, потому что так игры воспринимают все, а мы как разработчики просто необъективны, когда дело касается наших игр. Когда вы пришли в продуктовый магазин, чтобы купить бананы, то если какие-то из них коричневатые или помятые, то вы выберете другие. При выборе ресторана вы пойдёте в тот, который предлагает хорошую еду по приемлемой цене, или по крайней мере, создаёт важные вам ощущения.

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

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

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

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

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

Процедурные макросы — неудобная версия рефлексии

Разработка игр — это область программирования, часто требующая от автора писать разные типы кода. У нас есть «системный» код, например, для коллизий, физики, частиц. Есть геймплейный код для «скриптинга» поведения сущностей. У нас есть UI, VFX, звук. И ещё есть инструменты. В разных играх размер каждой категории будет разным, но поработав над играми достаточно разных жанров, хочу сказать, что обычно некоторый объём усилий потребуется на реализацию каждого из аспектов.

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

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

  • Декларативные макросы: их относительно просто создавать и они очень полезны, но, к сожалению, довольно ограничены в возможностях. Как и во многих аспектах Rust, здесь превыше всего «безопасность», поэтому то, что было бы вполне нормально в макросе препроцессора C, здесь становится невозможно. Простейший пример этого — конкатенация токенов, для которой теперь есть знаменитый крейт paste, обеспечивающий частичное решение с использованием процедурного макроса. Если не вдаваться в подробности, можно решить, что всё замечательно и проблема решена. Но, к сожалению, это далеко не так; например, встраивание и смешивание процедурных и декларативных макросов срабатывает не всегда, и не сразу понятно, что и почему возможно; приходится тратить много времени на изучение технических тонкостей.

  • Процедурные макросы: по своей сути процедурные макросы позволяют программисту выполнять код во время компиляции, использовать AST Rust и генерировать новый код. К сожалению, с ними возникает множество проблем. Во-первых, процедурные макросы не кэшируются и при повторной компиляции выполняются заново. Из-за этого приходится делить код на множество крейтов, что не всегда возможно, и чем больше вы используете процедурных макросов, тем существеннее увеличивается время компиляции. Существует много полезных процедурных макросов, например макрос function в profiling, которые крайне полезны, но ими в конечном итоге невозможно пользоваться, потому что они повышают время инкрементальных сборок. Во-вторых, процедурные макросы невероятно сложно читать, и большинство людей в итоге использует очень тяжёлые вспомогательные крейты наподобие syn — очень тяжёлого парсера Rust, охотно анализирующего всё, к чему его применяют. Например, если вы хотите аннотировать функцию и просто спарсить её имя в своём макросе, то syn всё равно целиком спарсит тело функции. Кроме того, автор syn является и автором serde — популярного крейта сериализации Rust, который в прошлом году начал поставляться в патч-релизе с двоичным блобом своей установки, несмотря на протесты сообщества. Я не использую эту ситуацию как довод против Rust, но считаю, что её нужно упомянуть, потому что это показывает, что большая часть экосистемы построена на библиотеках, создаваемых одним разработчиком, который может принимать потенциально опасные решения. Разумеется, это может случиться в любом языке, но с точки зрения процедурных макросов это очень важно, потому что почти всё в экосистеме использует крейты, созданные этим конкретным автором (synserdeanyhowthiserrorquote, ...).

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

В отличие от Rust, в C# использование рефлексии реализуется крайне просто, и если производительность не так важна (что часто бывает в случаях, где применяется рефлексия), она может быть очень быстрой и полезной опцией для создания инструментов или для отладки. В Rust нет ничего подобного: последнее придуманное решение рефлексии времени компиляции было фактически отменено после одной из драм прошлого года в сообществе Rust.

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

Горячая перезагрузка важнее для скорости итераций, чем считается

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

Во-первых, если вы не видели Tomorrow Corporation Tech Demo, то рекомендую каждому разработчику игр посмотреть это видео, чтобы понять, что возможно с точки зрения горячей перезагрузки, обратимой отладки и в целом инструментария в разработке игр. Если вы считаете, что знаете о них, то всё равно посмотрите видео. Я давно ощущал, что горячая перезагрузка важна, по крайней мере, в некоторой степени, но увидев то, что эти ребята создали самостоятельно, я теперь испытываю стыд из-за того, что считал определённые рабочие процессы адекватными при разработке интерактивных продуктов.

Вот, что сделали ребята из Tomorrow Corporation:

  • Создали собственный язык программирования, редактор кода, игровой движок, отладчик и игры.

  • Создали поддержку горячей перезагрузки во всём стеке.

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

  • ... просто посмотрите видео! Гарантирую, вы не пожалеете.

Я понимаю, что встраивание чего-то подобного в готовую платформу наподобие .NET или в нативный язык наподобие C++ или Rust практически невозможно, но я всё равно считаю, что мы должны стремиться к реализации этого.

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

С момента выпуска .NET 6 можно реализовать горячую перезагрузку в любом проекте на C#. Я слышал разные отзывы о ней, поэтому попробовал самостоятельно, после чего мне было сложно воспринимать некоторые из аргументов серьёзно, особенно если они были произнесены недавно, а не сразу после появления этой возможности. В контексте Unity теперь существует hotreload.net — особая реализация, созданная специально для Unity, которой я пользуюсь уже четыре месяца; она совершенно потрясающая с точки зрения продуктивности. На самом деле, это основная причина моего возврата к Unity. Это не причина отказа от Rust, но причина того, что я выбрал Unity, а не Godot или UE5. (На момент написания моей статьи Godot не поддерживает горячую перезагрузку .NET, а в UE по-прежнему есть только блюпринты и C++.)

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

Мне очень сложно подчёркивать это в разговорах в контексте разработки игр. Игры совершенно непохожи на обработчики данных без состояний.

Вот некоторые из случаев, в которых горячая перезагрузка оказывается невероятно полезной:

  • Любой Immediate mode, будь то UI или отрисовка. Даже при малом времени компиляции скорость итераций существенно возрастает, потому что разработчику не приходится постоянно заново переходить в одно и то же состояние.

  • Отладка с отрисовкой/геометрией в immediate mode. Наверно, это мой самый любимый пример, особенно отладка контроллеров персонажей и физики, когда может возникнуть неожиданное/забагованное состояние; благодаря горячей перезагрузке я могу просто добавить несколько строк для отрисовки релевантных значений в игре, чтобы посмотреть, что происходит, без необходимости воспроизведения проблемы.

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

Стоит также отметить, что в Rust на самом деле есть решение в виде hot-lib-reloader, но я пробовал его, и он оказался далёким от идеала, даже в самом простом случае с обычной перезагрузкой функций. Это решение случайным образом ломалось у меня много раз, и в конечном итоге я отказался от него, потому что мне требовалось больше усилий для экспериментов с ним, чем я экономил. Но даже если бы этот крейт работал без проблем, он никак не решал бы проблему произвольной настройки значений, потому что он всё равно требует планирования и предварительной подготовки, что снижает его удобство в творческом процессе.

Многие люди приводят такой контраргумент горячей перезагрузке: «Но компилятор делает XYZ», на что я предлагаю нечто, что никогда не смержат, но что было бы удобно. Вот если бы был флаг компилятора... и тут я уже предвижу, как люди начнут кричать «пожалей разработчиков компилятора». Скорее всего, мы этого никогда не получим.

Частично эту проблему можно решить множеством обходных путей, но ни один из них не приближается к удобству истинной горячей перезагрузки, которой я называю то, что сейчас есть в .NET и Unity. Скриптовые языки — это неполное решение, к тому же они по многим причинам проблематичны в Rust; ручная реализация горячих перезагрузок при помощи dylibs ограничена, а любой вид сериализации состояний и перезапуска работает только при больших изменениях в коде, а не при мелких настройках. Я не говорю, что всё это бесполезно, но считаю, что разработчики игр должны стремиться к более высокому уровню инструментария, чем простое «я могу перезагружать некоторые struct в моём коде», особенно учитывая то, что другие состоявшиеся платформы могут поддерживать гораздо более обобщённые рабочие процессы.

Абстракция — это не выбор

На написание этого раздела меня мотивировал очень простой фрагмент кода, который я написал, работая над своей игрой. У меня есть UI со списком персонажей и страница подробностей, появляющаяся при выборе персонажа (утки).

UI структурирован следующим образом: у меня просто есть вспомогательные функции для каждого состояния UI, так как наша команда использует egui, а immediate mode требует, чтобы большинство элементов было доступно в большинстве мест. Это отлично работает, потому что работают вот такие вещи:

egui::SidePanel::left("left_panel").frame(frame).show_inside(
    ui,
    |ui| {
        ui.vertical_centered(|ui| {
            character_select_list_ducks(egui, ui, gs, self);
        });
    },
);

egui::SidePanel::right("right_panel").frame(frame).show_inside(
    ui,
    |ui| {
        character_select_recover_builds(ui, gs, self);
    },
);

egui::TopBottomPanel::bottom("bottom_panel")
    .frame(frame)
    .show_inside(ui, |ui| {
        character_select_missing_achievements(
            egui, ui, gs, self,
        );
    });

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

egui::CentralPanel::default().frame(frame).show_inside(
    ui,
    |ui| {
        character_select_duck_detail(ui, gs, self);
    }
});

fn character_select_duck_detail(..., state: ...) {
	if let Some(character) = state.selected_duck {
	    // some UI
	} else {
		// other UI
	}
}

И он тоже работал нормально; проблема в том, что egui часто требует очень глубокой вложенности просто потому, что почти каждая операция с размещением — это замыкание. Было бы очень здорово, если бы мы могли уменьшить вложенность и переместить if наружу. В результате этого мы также разделим две не связанные друг с другом вещи. Первым делом я подумал о таком:

if let Some(character) = &self.selected_duck {
    character_select_duck_detail(.., character, self);
} else {
   character_select_no_duck(...);
}

Но тут Rust сделал мне выговор: неужели я действительно думал, что смогу передавать self, одновременно также заимствуя поле у self?

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

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

Какое же можно использовать решение? Как и во многих случаях с Rust, мы с коллегой испытали эмоцию 🤡, а затем поменяли код на такой:

if let Some(character) = &self.selected_duck.clone() {
    character_select_duck_detail(.., character, self);
} else {
   character_select_no_duck(...);
}

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

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

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+29
Комментарии31

Публикации

Истории

Работа

Ближайшие события