Откуда берутся трудности

Прокомментировать Просмотры: 7

Похоже, рассуждения о громоздкости и «грязи» в геймдеве глубоко затронули читателей SE7ENа, и @anz (Андрей, привет!) даже выразил готовность осыпать статью тысячей плюсов, если бы позволял функционал. К слову, он уже около десяти лет самостоятельно пилит собственный игровой движок, так что прекрасно знает кухню изнутри. Представляю вашему вниманию вторую часть — материал, который органично смотрелся бы в первой публикации, но по воле случая задержался.

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

Под метриками подразумеваются конкретные числовые ориентиры: скажем, заветные 16 или 33 миллисекунды на кадр, 8 гигабайт памяти при лимите в 5 доступных, либо жесткое условие «выдать первый интерактивный кадр не позже чем через 10 секунд после старта, иначе прощай сертификация». Как только у программного модуля появляется оцифрованная цель, концепция простоты перестает быть безвозмездной — за неё приходится расплачиваться.


За всё нужно вносить плату, и вместе с числовыми показателями приходят суровые системные ограничения. Речь идет не об абстрактном «игра должна летать», а о безальтернативных рамкам: «кадр обязан укладываться в 16 мс на базовой ревизии консоли» или «вес дистрибутива не должен превышать порог, после которого маркетплейс принудительно требует Wi-Fi». Перечень реальных производственных барьеров гораздо шире, чем кажется на первый взгляд, и производительность в нем далеко не на лидирующих позициях. В последние годы пальму первенства уверенно удерживают контентные объемы и трудозатраты в человеко-часах:

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

  • продолжительность билда и скорость итерации для геймдизайнеров (интервал между правкой и её отображением на экране);

  • пиковое потребление ОЗУ (консоли лишены файла подкачки, поэтому перерасход гарантированно ведет к крашу);

  • объем первоначальной установки и размер патчей (последние скачивают все пользователи и регулярно);

  • бюджет кадра в разрезе CPU и GPU, с учетом средних показателей и 99-го перцентиля;

  • скорость холодного запуска и длительность загрузки локаций;

  • тепловыделение и энергоэффективность для мобильного сегмента и портативных консолей;

Как видите, вопросы чистой оптимизации сейчас барахтаются где-то на уровне заботы о размере билда — такую тенденцию я фиксирую с момента триумфа тяжеловесных универсальных движков. Разумеется, у каждого ограничения есть свои градации: производительность — это средний показатель или худший пик? За всю игровую сессию или за короткий десятисекундный отрезок? Важнее пропускная способность стриминга или отзывчивость управления? Что лучше — стабильные 30 FPS или дерганые 60? Ответ неизменно кроется в жанровой специфике и целевой платформе, именно они диктуют уровень предстоящей технической сложности.

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

Сложность, за которую мы платим

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

// Уровень 0: O(n²)
for (int i = 0; i < n; ++i)
    for (int j = i + 1; j < n; ++j)
        if (overlap(aabb[i], aabb[j]))
            emit_pair(i, j);
// 200 объектов сгенерируют 20 тысяч проверок
// 5000 объектов превратят это в 12.5 миллионов операций

// Уровень 1: равномерная сетка (Grid)
//   + молниеносно, наглядно, просто дебажится
//   - пасует перед разномастными объектами: один гигант
//     захватывает сотню ячеек и рушит всю концепцию

// Уровень 2: Инкрементальное дерево BVH
//   + отлично адаптируется к любым габаритам
//   - требует пересстройки, эвристик разбиения,
//     специфичного кода для динамики и логики удаления узлов

// Уровень 3: То же самое, но с SoA-структурой данных,
//   SIMD-векторизацией (по 4 пары за такт)
//   и изолированным пайплайном для статики
//   + максимальная производительность
//   - отладчик превращается в непроходимую кашу из флоатов

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

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

Два ограничения хуже одного, помноженного надвое

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

Ограничение A: скорость стриминга локаций.
  Требуется последовательное чтение без лишних seek-запросов,
  соответственно, все ассеты уровня должны лежать монолитно
  в строгом порядке их задействования.

Ограничение B: вес игры и размер патчей.
  Требуется хранить каждый файл в единственном экземпляре
  с максимальной дедупликацией.

Эти два требования диаметрально противоположны:
  Идеально под A:                 Идеально под B:
  [level_01: A B C D E]          [pool: A B C D E F G]
  [level_02: A B F G]            [level_01 -> ссылки]
  [level_03: A C E G]            [level_02 -> ссылки]
   Ресурс А дублируется трижды,    Ресурс А лежит в одном месте,
   читается единым потоком       но для загрузки нужны три seek'а

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

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

Ограничения убивают модульность

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

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

// Было: образцово-показательно и модульно
for (IComponent* c : components)
    c->Update(dt);              // виртуальный вызов, промах по кэшу процессора,
                                // разрозненные объекты в куче (heap)

// Стало: прямолинейно и бескомпромисно быстро
UpdateTransforms(transform_soa, count, dt);
UpdateAnimators(animator_soa, count, dt);
UpdateColliders(collider_soa, count, dt);
// компоненты больше не полиморфны, порядок обновления
// жестко зашит в вызывающей процедуре, а добавление
// нового типа сущности требует правки в четырех местах

Перед нами классический Data-Oriented Design (DOD), триумфально захвативший геймдев. Но вдумайтесь, что произошло: мы не просто оптимизировали цикл, мы протащили физику процессорного кэша сквозь все архитектурные слои прямо к месту зарождения игровых сущностей. Суровое железо продырявило абстракции, и высокоуровневая программа отныне пляшет под дудку длины кэш-линии.

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

Жесткие и мягкие пределы

С мягкими лимитами жить проще: их допустимо изредка и незначительно превышать. Просадка до 45 кадров во время кат-сцены отправится в трекер задач с приоритетом «поправим после релиза», а затянувшаяся на 12 секунд вместо 10 загрузка уровня сойдет извинением перед сертификационной комиссией.

Жесткие лимиты не прощают компромиссов. Если вам отведено ровно 5.5 гигабайт памяти, и взять ее буквально физически негде, то превышение порога оборачивается не легкими подлагиваниями, а мгновенным падением, провалом сертификации и срывом дедлайнов.

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

Бюджет памяти, базовая консоль, доступно 5120 МБ:

  исполняемый код и статические данные      180
  резидентные текстуры                     1600
  пул потокового стриминга                 1200
  меши и анимационные скелеты               700
  аудиоданные                               420
  физическая подсистема и навигация         260
  скриптовый слой и игровая логика          300
  рендер-таргеты и промежуточные буферы     380
  резерв на фрагментацию и пиковые нагрузки  80
                                          -----
                                           5120

Резерв в 80 МБ — это не просто «подушка безопасности», 
это буфер на пару часов игры, пока фрагментация безжалостно 
пожирает ОЗУ, так что считайте, что этих мегабайт нет.

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

Парадокс Джевонса в разработке игр

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

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

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

Мы сократили время сборки контента с пяти часов до сорока минут — блестящее чисто инженерное достижение. Спустя два месяца выяснилось, что дизайнеры начали пересобирать проект в пять раз чаще, поголовье версий уровней в системе контроля версий возросло на порядок, тестировщики захлебнулись в потоке правок, и потребовалось срочно расширять штат QA. Суммарная сложность проекта подскочила там, где её совсем не ждали.

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

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

Скрытые ограничения, не попавшие в бэклог

Формулировка «игра должна стабильно бегать на минимальной конфигурации железа» — это не ограничение, а благое пожелание. Ее невозможно прогнать через автотесты, прописать в конфиге сборки или предъявить на планерке. Она трансформируется в жесткое ограничение ровно в тот момент, когда первый запуск на кастомном «ведре» выдает слайд-шоу из 8 FPS, вынуждая вас в пожарном порядке вливать в проект столько архитектурного хаоса, сколько в спокойном режиме накапливалось бы целый год.

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

Сложность всегда концентрируется там, где установлены датчики измерения. Если у вас автоматически мониторится исключительно время кадра — вы получите вылизанный до блеска рендер и 30-секундные экраны загрузки. Добавите метрику загрузки — получите обе зеленые шкалы, но в довесок прихватите размер патча в полтора гигабайта. И вовсе не потому, что команда лентяйничает: инженеры рациональны и оптимизируют именно то, за чем ведется строгий надзор. Что проверяем — то и работает отлично, а что остается за кадром... ну, игроки на релизе обязательно проверят.

Цена вопроса

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

Конфигурации оборудования, давно ушедшие в прошлое; костыли для обхода древних багов видеодрайверов (я говорю именно о драйверах); специализированные эмуляции SDK, которыми уже никто не пользуется — всё это продолжает балластом висеть в проекте просто потому, что изначальное ограничение испарилось, а порожденная им сложность осталась. Удалить ее никто не решается: цена подобного риска туманна и непредсказуема. Индустриальная сложность игровых движков (да и всего софта в целом) за прошедшие десятилетия ни разу не сокращалась — об этом полезно помнить тем, кто всерьез рассчитывает создать что-то простое.

 

Источник

Поделиться:

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов