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

Пост не будет ни научной оценкой, ни A/B-исследованием. Это моё личное мнение после разработки игр на Rust маленькой инди-командой (два человека) в попытках заработать достаточно денег, чтобы финансировать процесс. Мы не одни из тех разработчиков с бесконечными финансами от инвестора и многолетним запасом времени. Если вы находитесь в этой категории и получаете удовольствие от многолетней разработки систем, то всё написанное ниже к вам не относится. Я рассматриваю всё с такой точки зрения: «Мне хочется создать игру максимум за 3-12 месяцев, чтобы люди могли сыграть в неё, а я — немного заработать». Статья не написана с точки зрения «Я хочу изучить Rust, а разработка игр — это весело», хотя это и вполне нормальная цель; просто она никак не согласуется с тем, чего хотим мы — заниматься разработкой игр коммерчески успешным и самодостаточным образом.

Мы выпустили несколько игр на Rust, Godot, Unity и Unreal Engine, и многие люди сыграли в них в Steam. Мы создали с нуля собственный игровой 2D-движок с простым рендерером, а также в течение нескольких лет использовали Bevy и Macroquad во многих проектах, некоторые из которых были очень нетривиальными. Кроме того, я бэкенд-разработчик на полную ставку и пишу код на Rust. Этот пост — не какое-то поверхностное мнение после изучения нескольких туториалов или разработки небольшой игры для геймджема. За три с лишним года мы написали сильно больше ста тысяч строк кода на Rust.

Задача этого поста — развеять популярные и часто повторяемые аргументы. Но это всё-таки субъективное мнение; по большей части я написал пост, чтобы не объяснять снова и снова одно и то же. Пусть это будет справочный материал о том, почему мы, скорее всего, откажемся от Rust как от инструмента для разработки игр. Мы ни в коем случае не планируем прекращать создавать игры, просто не будем делать это на Rust.

Если ваша цель — изучить Rust, потому что он кажется интересным и вам нравятся технические трудности, то это совершенно нормально. Однако в этом посте я в том числе хотел обсудить то, в каком свете часто представляют разработку игр на Rust и какие советы люди часто дают другим, даже не зная, создают ли те техническое демо или пытаются выпустить готовую игру. В массе своей сообщество слишком сосредоточено на технологиях, вплоть до того, что слово «игры» в словосочетании «разработка игр» оказывается вторичным. Например, я вспоминаю одну беседу на Rust Gamedev Meetup: вероятно, это было сказано в шутку, но, как мне кажется, демонстрирует проблему. Кто-то сказал: «можно ли показать на встрече игру, допускается ли это вообще?» Не хочу сказать, что у всех должны быть те же цели, что и у нас, но считаю, что многие аспекты необходимо проговаривать чётче, а люди должны честнее говорить о том, что они делают.

Как только ты достаточно освоишь Rust, все эти проблемы исчезнут

Изучение Rust — интересный опыт, потому что хотя многие вещи кажутся какой-то особенной проблемой, «которая возникла только у меня», позже приходит осознание, что есть несколько фундаментальных паттернов, и что каждый обучающийся должен открыть их для себя, чтобы быть продуктивным. Это могут быть простые вещи, например, различия между &str и String или между .iter() и .into_iter() , а также необходимость постоянного их использования, или просто осознание того, что partial borrow часто противоречит некоторым абстракциям.

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

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

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

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

Самая фундаментальная проблема заключается в том, что borrow checker вынуждает выполнять рефакторинг в самые неудобные моменты. Разработчики на Rust считают это положительным моментом, потому что это заставляет их «писать хороший код», но чем больше я работаю с языком, тем сильнее сомневаюсь, что это так. Хороший код пишется благодаря итеративной работе с идеями и экспериментам, и хотя borrow checker может заставить увеличить количество итераций, это не означает, что таков желательный способ написания кода. Часто я обнаруживал, что именно невозможность просто пойти дальше, решить свою задачу, а проблему устранить позже, мешает писать хороший код.

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

То, что Rust прекрасен в рефакторинге, в основном решает его собственные проблемы с borrow checker

Очень часто говорят, что одна из самых сильных сторон Rust — простота рефакторинга. Совершенно верно, и я много раз бесстрашно рефакторил существенные части своей кодовой базы, после чего всё работало без проблем. Значит, реклама права?

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

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

Многие люди возразят, что если в конечном итоге вам приходится сражаться с borrow checker или рефакторить свой код, это на самом деле хорошо, ведь код становится лучше. Мне кажется, это здравый аргумент в случае, когда вы знаете, что создаёте. Но в большинстве случаев мне нужен не «качественный код», а «быстрее сыграть», чтобы я мог быстрее протестировать игру и убедиться, что идея хороша. Часто приходится делать выбор между «нарушить ли своё состояние потока и потратить два часа на рефакторинг, чтобы проверить идею, или сделать кодовую базу объективно хуже?».

Я утверждаю, что удобство поддержки — это ошибочная ценность для инди-игр, потому что нам в первую очередь нужно стремиться к скорости итераций. Другие языки позволяют гораздо проще обходить непосредственно возникающие проблемы без того, чтобы жертвовать качеством кода. В Rust выбор всегда выглядит так: добавить ли одиннадцатый параметр к этой функции или добавить ещё один Lazy<AtomicRefCell<T>>, или поместить ли это в ещё один божественный объект, или добавить косвенность и снизить удобство итераций, или потратить время на очередное перепроектирование этой части кода.

Косвенность решает только некоторые проблемы, и всегда ценой эргономики разработки

У Rust есть одно фундаментальное решение, которое он очень любит и которое часто срабатывает: добавление слоя косвенности. Канонический пример этого — события Bevy, которые стали рекомендованным решением для всего, что связано с проблемой «для выполнения своей задачи моей системе требуется 17 параметров». Я пробовал быть и на той, и на другой стороне: и активно использовал события, особенно в контексте Bevy, и пытался просто поместить всё в единую систему. Однако это лишь один пример.

Многие проблемы с borrow checker можно обойти, выполняя действия косвенно. Или если скопировать/переместить что-то, выполнить задачу, а затем вернуть всё обратно. Или если сохранить что-то в буфер команд и выполнить это позже. Часто это может привести к открытию чего-то нового в шаблонах проектирования, например, довольно удобным мне показалось то, что большую часть проблем можно решить, заранее зарезервировав id сущностей (например, World::reserve в hecs, обратите внимание, что там &world, а не &mut world) в сочетании с буфером команд. Эти шаблоны потрясающи, когда работают и они решают очень сложные проблемы. Ещё один пример — это кажущийся очень специализированный get2_mut в thunderdome , который поначалу кажется странной идеей, пока не понимаешь, что это возникает постоянно и решает множество неожиданных проблем.

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

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

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

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

То, что могло быть тремя строками на C#, внезапно превращается в тридцать строк Rust, разбитые на две части. Самый каноничный пример: «пока я итеративно обрабатываю этот запрос, мне нужно проверить компонент другой вещи и затронуть несколько связанных систем» (создавать частицы, воспроизводить звук и так далее). Я представляю, что мне скажут: ну, так ведь это, очевидно, Event, не нужно писать этот код линейно.

Представьте, какой ужас начнётся, если нам нужно будет сделать нечто подобное (крепитесь, ниже код для Unity; или просто сделайте вид, что это Godot):

if (Physics.Raycast(..., out RayHit hit, ...)) {
  if (hit.TryGetComponent(out Mob mob)) {
    Instantiate(HitPrefab, (mob.transform.position + hit.point) / 2).GetComponent<AudioSource>().clip = mob.HitSounds.Choose();
  }
}

Это относительно простой пример, но он вполне может потребоваться в игре. И особенно при реализации новой механики или тестировании такой код можно просто написать. Тут не нужно думать о каком-то удобстве поддержки, мне просто нужно сделать очень простые вещи, и я хочу, чтобы они происходили там, где должны происходить. Мне не нужно MobHitEvent, потому что, вероятно, для этого raycast мне придётся проверить ещё пять разных вещей.

Ещё я не хочу проверять, «есть ли у Mob компонент Transform» Разумеется, он есть, я ведь делаю игру, у всех моих сущностей есть transform. Но Rust не позволит мне иметь .transform, не говоря уже о такой его реализации, которая никогда не поломается с double borrow error, если я случайно окажусь внутри запросов с пересекающимися архетипами.

Возможно, мне также не нужно проверять, существует ли источник звука. Разумеется, я могу сделать .unwrap().unwrap(), но внимательный любитель Rust заметит отсутствие передачи world  — мы что, предполагаем глобальный мир? Разве мы не используем инъекцию зависимости для записи нашего запроса как ещё одного параметра в системе, где всё подготовлено заранее? Неужели .Choose предполагает наличие глобального генератора случайных чисел? А как же потоки??? А где конкретно мир физики, неужели мы серьёзно допускаем, что он тоже будет глобальным?

Если вы думаете «но такой код не будет масштабироваться», или «позже он может начать вылетать», или «нельзя допускать наличие глобального мира по причине XYZ», или «а что, если появится мультиплеер», или «это просто плохой код», то я с вами соглашусь. Но к тому времени, когда вы закончите мне объяснять, почему я неправ, я уже закончу реализовывать фичу и пойду дальше. Я написал свой код за один проход, не задумываясь о коде, и в процессе его написания я думал о реализуемой геймплейной фиче и о том, как она повлияет на игрока. Я не думаю «как будет правильно реализовать здесь генератор случайных чисел», или «могу ли я допустить, что этот код будет однопоточным», или «нахожусь ли я во вложенном запросе и не пересекутся ли мои архетипы». А ещё после этого я не получил ошибку компилятора и вылет borrow checker в среде исполнения. Я работал с тупым языком в тупом движке, и в процессе написания кода думал только об игре.

ECS решает проблему не того типа

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

Для начала упомянем некоторые вещи, которые мы не можем делать по различным причинам (существуют определённые нюансы, но я упрощу, потому что статья и так вышла слишком длинной):

  • Указателеподобные данные и настоящие указатели. Проблема здесь проста: если персонаж A следует за B, а B удаляется (и освобождается), то указатель будет неверным.

  • Rc<RefCell<T>> в сочетании со слабыми указателями. Хотя это и может работать, в играх важна производительность, и излишняя трата ресурсов при этом велика из-за локальности памяти.

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

Теперь я расскажу о волшебном решении, позволяющем избавиться от всех этих проблем — generational arena; наилучшим образом его действие показывает thunderdome. (Кстати, я крайне рекомендую эту библиотеку, потому что она маленькая и лёгкая, к тому же выполняет то, что должна, при этом сохраняя читаемость своей кодовой базы; последний пункт довольно редко встречается в экосистеме Rust.)

По сути, generational arena — это просто массив, только id не служит индексом, это кортеж из (index, generation). Сам массив при этом хранит кортежи (generation, value); чтобы не усложнять, можно просто представить, что каждый раз когда что-то удаляется по индексу, мы просто увеличиваем счётчик generation по этому индексу. Далее нам достаточно сделать так, чтобы индексация в arena всегда проверяла, совпадает ли generation указанного индекса с generation в массиве. Если элемент был удалён, то слот будет иметь более высокую generation, а индекс тогда будет «неверным» и действовать так, как будто элемент не существует. Существуют и некоторые другие проблемы, достаточно простые в решении, например, хранение свободного списка слотом, куда можно выполнять вставку, чтобы она была быстрой, но ничего из этого совершенно нерелевантно для пользователя.

Самое главное в том, что это позволяет языку наподобие Rust полностью обойти использование borrow checker и обеспечить «ручное управление памятью при помощи arenas», не притрагиваясь к опасным указателям, и гарантировать полную безопасность. Если бы меня попросили сказать, что мне нравится в Rust, то я бы упомянул именно это. Особенно вместе с библиотекой наподобие thunderdome; это очень хорошее сочетание, а эта структура данных хорошо подходит для языка.

А теперь мы переходим к интересному. То, что многие люди считают преимуществами ECS, на самом деле, по большей мере, оказывается преимуществами generational arena. Когда люди говорят: «ECS обеспечивает отличную локальность памяти», но их единственный запрос, связанный с mob, выглядит как Query<Mob, Transform, Health, Weapon>, то создаваемое ими, по сути, оказывается эквивалентом Arena<Mob> , где struct определена следующим образом:

struct Mob {
  typ: MobType,
  transform: Transform,
  health: Health,
  weapon: Weapon
}

Разумеется, подобные определения не имеют всех преимуществ ECS, но мне кажется, нужно чётко сказать: то, что мы работаем с Rust, и то, что мы не хотим, чтобы всё было Rc<RefCell<T>>, не означает, что нам нужна ECS, это просто может значить, что на самом деле нам нужна generational arena.

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

ECS как динамическая композиция, обеспечивающая возможность совместного хранения, запрашивания и изменения комбинаций компонентов без необходимости привязки к одному типу. Очевидный пример: работая с Rust, многие люди помечают сущности с компонентами «state» (потому что никаких других правильных способов сделать это нет). Например, нам нужно выполнить запрос ко всем Mob, но, возможно, некоторые из них были преобразованы в другой тип. Мы можем просто выполнить world.insert(entity, MorphedMob), и тогда в нашем запросе мы можем выполнить или запрос (Mob, MorphedMob), или что-то типа (Mob, Not<MorphedMob>), или (Mob, Option<MorphedMob>), или проверить присутствие нужного компонента в коде. В разных реализациях ECS может оказаться, что эти операции выполняют разные действия, но на практике мы используем их, чтобы «пометить» или «разделить» сущности.

Композиция может быть и гораздо разнообразнее. Представленный выше пример тоже сюда подходит: вместо создания одной большой struct Mob можно создать отдельные TransformHealthWeapon, а может, и другие элементы. Mob без оружия может не иметь компонент Weapon, а после того, как он подберёт оружие, мы вставим его в сущность. Это позволит нам итеративно работать со всеми mob с оружием в отдельной системе.

К динамической композиции я также отнесу методику EC из Unity; хотя она может и не быть традиционной чистой «ECS с системами», она активно использует компоненты для композиции, и, если не учитывать вопросы производительности, обеспечивает практически те же самые возможности, что и «чистая ECS». Также я бы хотел упомянуть систему нодов Godot, в которой дочерние ноды часто используются как «компоненты», и хотя это никак не связано с ECS, это целиком относится к «динамической композиции», так как позволяет вставлять/удалять ноды в среде исполнения и менять в зависимости от этого поведение сущностей.

Стоит также отметить, что подход «разбиение компонентов на как можно более мелкие части для максимально многократного использования» многие считают достоинством. Меня много раз пытались убедить отделить Position и Health от моих объектов, потому что иначе код превратится в спагетти.

Я много раз пробовал эти подходы и теперь настроен абсолютно против них (если, конечно, отсутствует необходимость в максимальной производительности); в этих спорах я уступаю только в случаях, когда для сущностей важна производительность. Попробовав другие подходы с «толстыми компонентами» (до и после попыток разделения компонентов), я считаю, что «толстые компоненты» гораздо лучше подходят для игр, ведь они имеют кучу логики, уникальной для происходящего. Например, моделирование Health как механизма общего назначения может быть полезным в простой симуляции, но во всех моих играх мне нужна была разная логика для здоровья игрока и здоровья врага. Ещё мне часто оказывалась нужна разная логика для разных типов сущностей, например, здоровья стены и здоровья монстра. Если бы я попытался обобщить это до «одного здоровья», то пришлось бы писать в системе здоровья непонятный код с кучей if player { ... } else if wall { ... } вместо того, чтобы сделать их частью большой толстой системы игрока или стены.

ECS как динамическая структура массивов, в которой благодаря способу хранения компонентов ECS мы можем получить преимущество, выполняя итерации, например, только для компонентов Health и храня их в памяти рядом друг с другом. Это значит, что вместо Arena<Mob> у нас было бы следующее:

struct Mobs {
  typs: Arena<MobType>,
  transforms: Arena<Transform>,
  healths: Arena<Health>,
  weapons: Arena<Weapon>,
}

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

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

ECS как решение проблемы borrow checker Rust: на мой взгляд, именно это делает большинство людей, использующих ECS, а точнее, именно по этой причине они используют ECS. ECS — очень популярное решение, рекомендованное для Rust, потому что обычно она позволяет обойти многие проблемы. Нам не нужно заботиться о сроках жизни, достаточно передавать struct Entity(u32, u32), ведь всё это удобно, и выполнять Copy, ведь именно это любит Rust.

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

ECS как динамически создаваемые generational arena: мне бы хотелось, чтобы такой подход существовал, и я попытался создать его, но осознал, что для того, чтобы получить нужное мне, придётся заново изобретать множество вещей, связанных с внутренней изменяемостью, чего я и стремился избежать; и всё это для того, чтобы обеспечить возможность одновременного выполнения вещей наподобие storage.get_mut::<Player>() и storage.get_mut::<Mob> . Rust обладает милым свойством: пока ты делаешь вещи нужным ему образом, всё интересно и красиво, но как только ты хочешь сделать то, что ему не нравится, всё быстро превращается в «нужно самостоятельно реализовать собственный RefCell , выполняющий нужное мне действие», а то и хуже.

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

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

Стоит также отметить, что несмотря на моё несогласие со многим, сложно отрицать, сколько полезного Bevy привнёс в ECS API и в эргономику самой ECS. Все, кто когда-либо пользовался чем-то наподобие specs, понимает, насколько лучше и удобнее Bevy реализует ECS и насколько он улучшился за годы.

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

Отойду на минуту от нашей темы, чтобы поговорить о Unity. Что бы ни происходило с лицензиями, руководством или бизнес-моделью движка, было бы глупо воспринимать Unity не как одной из основных причин успеха инди-игроделов. Судя по статистике SteamDB, в Steam сейчас есть почти 44 тысячи игр на Unity; на втором месте Unreal c 12 тысячами игр, а остальные движки сильно от них отстают.

Все, кто следил за Unity в последние годы, знает о Unity DOTS, который, по сути, стал «ECS» движка (и другими вещами, ориентированными на данные). Как прошлый, настоящий и будущий пользователь Unity, я в восторге от этой системы, и одна из основных причин восторга заключается в том, что она существует параллельно со старой системой Game Object. Существует множество тонкостей, но по сути своей это именно то, чего можно было ожидать. В одной игре может для чего-то применяться DOTS, а для остального использоваться стандартное дерево сцен с Game Object, и они без проблем работают совместно.

Не думаю, что в мире Unity можно найти разбирающегося в DOTS человека, который бы считал, что это нечто плохое и его не должно существовать. Но я и не думаю, что можно найти человека, считающего, что DOTS — это всё, что понадобится нам в будущем, Game Object необходимо уничтожить, а Unity целиком нужно перенести на DOTS. Даже если забыть о поддерживаемости и обратной совместимости, это было бы очень глупым решением, поэтому существует множество процессов разработки, в которые естественным образом вписывается система с Game Object.

Те, кто работает с Godot, вероятно, имеют ту же точку зрения, особенно если они работали с gdnative (например, при помощи godot-rust): хотя деревья нодов могут и не быть идеальной структурой для всего, они крайне удобны для множества ситуаций.

Вернёмся к обсуждению Bevy: мне кажется, не многие люди осознают, насколько всеохватывающим оказывается подход «превратить всё в ECS». На мой взгляд, здесь есть очевидный пример: точка отказа в виде системы UI Bevy, которая долгое время была болевой точкой, особенно учитывая постоянные обещания вида «мы точно начнём работать над редактором в этом году!». Если посмотреть на примеры UI Bevy, сразу становится понятно, что возможностей в нём не очень много, а взглянув на исходный код чего-то столь простого, как кнопка, меняющая свой цвет при наведении курсора и нажатии, становится понятно, почему. Я пробовал использовать UI Bevy для чего-то нетривиального, и могу подтвердить, что мучения ещё сильнее, чем кажется, потому что количество церемоний, необходимых для того, чтобы ECS сделал нечто, связанное с UI, абсолютно безумно. Поэтому ближайшее, что походит в Bevy на редактор — это сторонний крейт, использующий egui. Я сильно упрощаю ситуацию, и, разумеется, для создания редактора нужно гораздо больше, чем UI, но мне кажется, что упорные попытки запихнуть всё, включая UI, в ECS, определённо не помогают развитию.

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

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

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

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


  1. MountainGoat
    30.04.2024 09:55
    +21

    Итого: Rust мешает делать тяп-ляп и в прод, а надо.


    1. passiboom1991
      30.04.2024 09:55
      +3

      естественно, зачем им конкурент


    1. softaria
      30.04.2024 09:55
      +38

      Раст мешает быстрому прототипированию. В прод это и не должно пойти.

      Я так понял мысль автора


      1. SadOcean
        30.04.2024 09:55
        +4

        Не в оправдание проблем, но кого мы обманываем.


        1. softaria
          30.04.2024 09:55
          +23

          Для каждой задачи нужен свой инструмент. Если писать ОС, системные утилиты или скажем браузер - раст отличный выбор.

          Но есть задачи, где много исследовательской деятельности. К таким относятся, например, ML или создание игр. Здесь нет четкого понимания, что надо сделать. Есть десятки или сотни экпериментов. Из них будет один удачный, который уже можно аккуратно запрограммировать. Для таких задач раст, как будто, и вправду не лучший выбор. Он не позволяет писать некачественно даже то, что заведомо будет выброшено.


          1. SadOcean
            30.04.2024 09:55
            +14

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


            1. softaria
              30.04.2024 09:55

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


              1. SadOcean
                30.04.2024 09:55
                +2

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


            1. snuk182
              30.04.2024 09:55

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


              1. SadOcean
                30.04.2024 09:55

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


                1. GospodinKolhoznik
                  30.04.2024 09:55
                  +1

                  Вот не понимаю этого. Что интересного в желании игр? Нудная работа. Движки делать интересно, а вот сами игры уже нет. Какое ни будь финансовое ПО интереснее делать. Ну а самое интересное это делать что то где в основе лежит сложная мат. модель.


                1. Ken-Sei
                  30.04.2024 09:55

                  В геймдеве - все мечтают стать Кармаками и купить себе Феррари после успеха игры, и потому согласны "потерпеть" ради такой цели.


          1. JordanCpp
            30.04.2024 09:55

            По мне так, чем меньше языков тем лучше. Веб видели, тащить всё это в десктоп?


      1. domix32
        30.04.2024 09:55
        +1

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


      1. vkni
        30.04.2024 09:55
        +1

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

        • не пишем сигнатуры функций/пишем излишне общие - functionF :: a -> b -> c

        • экспортируем всё из модуля

        • включаем модули целиком import Data.List

        • вставляем undefine где ни попадя (и, соответственно, прописываем частичные сигнатуры где нам нужно прямо в середине текста, что ухудшает читаемость)

        Разумеется, в PROD коде это всё недопустимо/нежелательно, но в черновиках вполне себе. Вполне возможно, что и в Rust, когда делают наброски, надо вести себя примерно также. К примеру, работать сразу в режиме unsafe.

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


        1. GospodinKolhoznik
          30.04.2024 09:55
          +2

          И всё таки прототипирование в Хаскеле делается очень и очень медленно.

          Даже в питоне, или в каком ни будь js прототипирование медленное. А знаете где прототипирование быстрое? А в экселе! Особенно если в excel сделать линковку на некоторые таблицы mdb access (а в mdb линковаться на некоторые таблицы из excel) то вообще что угодно можно запротипировать за пол дня. Экселевские таблички с их формулами, плюс возможность писать на лету запросы к этим экселевский табличкам в аксессе, а результаты этих запросов снова обработать в экселе, позволяют накидать бизнес логику любой сложности в кратчайшее время.


          1. vkni
            30.04.2024 09:55
            +1

            А знаете где прототипирование быстрое? А в экселе!

            Так это функциональщина с динамической типизацией. Причём на грани визуального программирования.

            Вы Пролог пробовали? Там, к сожалению, стандартная библиотека относительно бедна, но для ряда задач алгоритмы получаются в разы короче чем на Хаскеле.

            Я тут нашёл чудесный сайт-книжку по современному использованию Пролога — https://www.metalevel.at/prolog (The Power of Prolog). Очень полезно то, что автор является современным практиком-энтузиастом, и поэтому позволяет не оставаться в 80х в обнимку с Бортко.


            1. GospodinKolhoznik
              30.04.2024 09:55
              +1

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

              За ссылку на книжку спасибо. Как ни будь полистаю. Может даже пойму что такое может пролог, чего не может обход графа вручную ))


    1. SadOcean
      30.04.2024 09:55
      +5

      Поэтому мы нагородим целую систему с параллельным индексированием и ЖЦ поверх (ECS) и будем игнорировать безопасность раст на высоком, архитектурном уровне.


      1. MountainGoat
        30.04.2024 09:55
        +2

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


        1. SadOcean
          30.04.2024 09:55
          +1

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


    1. KanuTaH
      30.04.2024 09:55
      +9

      Итого: Rust мешает делать тяп-ляп и в прод, а надо.

      Мда. Неудивительно, что автор пишет такое:

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

      Кстати говоря, мои личные впечатления от общения с сообществом раста где-то совпадают с этим описанием.


      1. MountainGoat
        30.04.2024 09:55
        +2

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


    1. ElleSolomina
      30.04.2024 09:55
      +2

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

      При этом я совершенно за то, чтобы уже готовые и развившиеся проекты или компоненты больших систем переписали на Rust. Примеров таких масса, например ruffie, части Firefox, части Windows и Linux.


    1. spiritedflow
      30.04.2024 09:55
      +2

      Скорее:

      • что в коммерческой разработке игр продаются хорошие игры а не идеальный код

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

      • что грустно рефакторить и вылизывать код, а потом приходит гейм-дизайнер и говорит: а давайте по другому все сделаем.


      1. MountainGoat
        30.04.2024 09:55
        +1

        И это отлично заметно: уже можно взять за правило, что ни одна ААА игра в первую неделю после релиза неиграбельна.


  1. alysnix
    30.04.2024 09:55
    +27

    и наконец нашлись мыши, которым надоело колоться.


  1. SadOcean
    30.04.2024 09:55
    +7

    Я в целом согласен с посылом статьи, хотя тут и намешано много разных идей.

    Я бы сформулировал это как несколько разных тезисов:

    • Rust отличный язык, но у него есть ограничения и недостатки. Ставя во главу угла надежность и Zero Cost Abstractions, он увеличивает как порог входа так и сложность разработки

    • Требования к инструментам могут быть противоречивы. Разработка игр требует высокой скорости итераций, крайне позднего связывания и data-driven дизайна. Некоторые особенность rust противоречат этим требованиям, поэтому движки и пытаются их исправить. В частности те же ECS отказывается от нативного обращения к данным по ссылкам и контроля ЖЦ, создавая свою систему поверх (индексы через сущности и свой контроль времени жизни сущностей)

    • ECS сама по себе является очень специализированной архитектурой для разработки высокопроизводительных игр. Она хоть и позиционирует себя как хорошую замену классической организации объектов, на самом деле накладывает крайне много сложных ограничений на организацию кода, данных и логики и не совсем помогает в создании простого кода. То есть мы платим за эту производительность по умолчания более сложной организацией кода по умолчанию. В случае же нарушения этих ограничений мы вероятно ухудшаем производительность


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


  1. event1
    30.04.2024 09:55
    +3

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


    1. NikitaKinyaev
      30.04.2024 09:55
      +8

      Вот это зачастую с кодом вообще не связано. Это 3Д модели и коллайдера на них.


  1. domix32
    30.04.2024 09:55
    +2

    Напомнило старый добрый доклад про от одного из первопроходцев ржавого геймдева Кэтрин Уэст. Проблемы известны с ещё с момента появления геймдева на ржавчине, видимо поэтому в какой-то момент и появились всякие Any и глобальный объект мира.


  1. san-smith
    30.04.2024 09:55
    +5

    Знаю, что это перевод, но всё равно напишу: на rust есть довольно любопытный движок Fyrox, он выглядит более зрелым инструментом, по сравнению с Bevy (в частности, из коробки есть весьма функциональный редактор).


    1. kipar
      30.04.2024 09:55
      +1

      о нем, кстати, в оригинальной статье тоже упоминается (в позитивном ключе).


  1. Gorthauer87
    30.04.2024 09:55
    +4

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

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

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


    1. domix32
      30.04.2024 09:55
      +4

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


      1. Gorthauer87
        30.04.2024 09:55
        +1

        Ну так Раст для этого слишком низкоуровневый и так сказать душный. Но блин, для самой логики игры больше какой нибудь dsl подойдёт, а Раст для условно говоря, движка для этого dsl.


        1. domix32
          30.04.2024 09:55

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


  1. voldemar_d
    30.04.2024 09:55
    +3

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

    Почему-то мне это напоминает некоторые наезды на C++.


    1. Kahelman
      30.04.2024 09:55
      +4

      В C++ при желании можно сделать все что хочется и как хочется. Среда не указывает вам как писать- каждый себе сам злобный Буратино. Что в принципе правильно -если программист хочет себе проблем - не надо его удерживать.


      1. voldemar_d
        30.04.2024 09:55
        +2

        При этом что, не надо "вот прямо разобраться"?


      1. Za4emsu
        30.04.2024 09:55
        +10

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

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

        Haskell - это опытный, но старый родитель - дедушка (отец Rust), бывший профессор, который всегда поможет и расскажет что-нибудь интересное из своего прошлого (начало ML и функционального программирования), поможет советами (классы типов), но не будет мешать. С ним всегда интересно, но никто не воспринимает его всерьез - для всех он просто дед, пережиток прошлого и история. Потомки обращаются к нему за помощью в теоретической части (удобство языка).


        1. sdramare
          30.04.2024 09:55
          +27

          А javascript тогда кто? Дочь-проститутка?


          1. indestructable
            30.04.2024 09:55
            +12

            Младший брат-даун. С таким же близнецом-Go


        1. ElleSolomina
          30.04.2024 09:55
          +2

          C++ это свобода ^^


  1. humbug
    30.04.2024 09:55

    Ололо, у растишек бомбит от справедливой критики.


    1. Ak-47
      30.04.2024 09:55

      "ололо"? в 2024??


  1. dgkdfjvnjndsigs89
    30.04.2024 09:55
    +1

    Браво автор.
    Наконец кто-то не поленился расписать всё изнутри, без упоротости и фанатизма. Раст весьма нишевый продукт совершенно непригодный для генерального использования. Именно поэтому на нём за столько лет ничего серьезного так и не было написано.


    1. sdramare
      30.04.2024 09:55
      +3

      А "серьезное" это что?


    1. ElleSolomina
      30.04.2024 09:55

      Боньк, это не так. Воть https://habr.com/en/articles/811163/#comment_26781483


  1. snuk182
    30.04.2024 09:55
    +3

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


    1. sdramare
      30.04.2024 09:55
      +1

      А можете привести примеры таких шаблонов проектирования?


      1. snuk182
        30.04.2024 09:55

        https://github.com/fadeevab/design-patterns-rust?tab=readme-ov-file вот примеры для гоф, разной степени устрашения.


        1. sdramare
          30.04.2024 09:55

          Я сходу ничего страшного не увидел, максимум известный подход Rc<RefCell<T>>. Можете указать куда конкретно смотреть?


          1. snuk182
            30.04.2024 09:55

            То есть вас сам факт безальтернативного требования синхронизационных сущностей не смущает? При том, что он непортабельный, и для многопотока уже может понадобиться Arc<RwLock<>T>, да и в целом концепция владения тут явно мешает не берется во внимание..


            1. sdramare
              30.04.2024 09:55
              +1

              Ну в джаве или плюсах для мнопоточности вам тоже понадобиться писать критическую секцию так или иначе, если вы планируете изменяемый доступ к объекту в рамках гонки. Раст просто не дает изначально допустить ошибку, требуя реализации send/sync при многопоточности. А концепция владения не исчезает, а переносится в рантайм, при RefCell у вас всеравно только одна мутабельная ссылка может быть.


        1. Gorthauer87
          30.04.2024 09:55
          +1

          Вот это вот выглядит прямо дико, и так точно не стоит делать.

          https://github.com/fadeevab/design-patterns-rust/blob/main/creational/factory-method/render-dialog/gui.rs#L14

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

          Плюс в языке есть и свои хитрые паттерны, которых нет в оригинальной GoF.


          1. snuk182
            30.04.2024 09:55
            +1

            иначе называется и немного иначе применяется

            Вот к этому в общем у меня и претензия. Оно существует в более приличном виде, но систематизированной инфы я по Rust-специфичеким реализациям не находил.

            Если что, языком я неспешно и некоммерчески с удовольствием пользуюсь уже лет десять.


            1. Gorthauer87
              30.04.2024 09:55

              Тут есть довольно много хорошего, но немного специфичного.

              https://rust-unofficial.github.io/patterns/


  1. Lekret
    30.04.2024 09:55
    +1

    Спасибо за очень интересную и познавательную статью.
    В чём-то укрепил своё мнение касательно Rust.
    Ещё Джонатан Блоу на своих стримах говорил, что Rust будет сильно мешать на этапе прототипирования, и что если бы он писал Braid и Witness на Rust, то из-за потери в скорости итерации на 10-15% его игры просто не дожили бы до релиза.
    Данная статья подобное рассуждение по сути подтверждает.


  1. selcon
    30.04.2024 09:55

    Спасибо за статью. Да, Rust заставляет заморочиться. Но хорошо там, где нас нет. У других ЯП и их экосистем есть свои плюсы и минусы, везде есть свои заморочки. И бывает, что если смотреть по верхушкам, то всё красиво, а если копнуть, то выяснится, что не всё так радостно. Автор не сказал, на что он хочет пнресесть с Rust'а?


  1. AskePit
    30.04.2024 09:55
    +2

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

    Еще в оригинальной статье есть интересный поинт про hot-reload (эта часть перевода еще не дошла до той части), о котором я не задумывался - что, даже если тебе кажется, что тебе не так уж и нужен hot-reload для разработки игры, то это значит, что ты просто не имел опыта быть в цикле разработки, где есть hot-reload, и что это в какой-то мере game-changer, потому что ты на лету можешь творить такую дичь о которой и не мечтал при привычном пайплайне "внес изменения -> пересобрался -> запустился -> проверил -> повторяем по-кругу до потери пульса"