Все мы любим строить всё больше и больше, поэтому когда сталкиваешься с ограничением UPS, это сильно расстраивает. Именно поэтому мы обязаны продолжать свой бесконечный процесс оптимизации игры.
▍ Оптимизация роботов (автор: Rseding)
За годы работы над Factorio я профилировал множество файлов сохранений и регулярно встречал сохранёнки, где большая часть времени обновления тратится на логистику и/или строительных дронов. В этом нет ничего нового, но наряду с дронами существуют и дронстанции (в больших количествах).
Типичная фабрика с кучей дронстанций (Roboport)
Дронстанции никогда не были «медленными», но они всегда присутствуют на карте, и у игроков есть мотивация строить их в больших количествах; к тому же, их будет ещё больше в грядущем Space Age, где нужно будет многое делать удалённо. Сохранение, полученное после последней сессии плейтестинга, снова показало, что они отнимают небольшое, но ненулевое количество времени, поэтому я снова задумался о них.
Было бы очень здорово, если бы им не нужно было оставаться активными и обновляться постоянно. Большинство дронстанций ничем не занимается, кроме потребления энергии. Достаточно редко (по сравнению с их сроком жизни) к ним подлетают дроны, которым нужно зарядиться или приземлиться. Но большинство из них на самом деле не выполняет ничего, только расширяет сеть логистики. Поэтому я решил поэкспериментировать: что, если просто отключать их, когда им не нужно делать ничего особенного? Если дрон подлетает и нуждается в зарядке или возникает ещё какая-то редкая ситуация, когда им нужно что-то делать, я буду просто включать их до момента завершения процесса.
Разумеется, в этом были и другие трудности, но в конечном итоге всё получилось.
Благодаря этому в файле сохранения последнего плейтеста время, которое тратится на дронстанции, снизилось с 1 мс до 0,025 мс на тик.
▍ Оптимизация логики радаров (автор: Rseding)
Ранее в этом месяце в списке задач появилась карточка с небольшой фичей: добавить дронстанциям «малое радарное покрытие». В обычной ситуации для этого бы потребовалось пять минут работы, но к карточке было приложено крошечное условие: «сделать это так, чтобы пересекающиеся области не увеличивали затраты производительности».
До этого момента радары как сущность и функции радаров, привязанные к другим элементам (например, к игроку), использовали очень простой принцип: «периодически итеративно обходить их и просить систему карты оставлять их открытыми». Обычно это работало нормально, потому что на карте обычно не бывает множества пересекающихся радаров или игроков. Но теперь нам нужно было, чтобы ту же самую логику выполняли дронстанции — сущность, которую выстраивают в очень плотные кластеры с большой степенью пересечения.
Я решил остановиться на системе регистрации — всё, что хочет раскрыть область карты, просто регистрирует блоки как «оставить открытыми», увеличивая счётчик для блока в системе карты. Пока счётчик больше нуля, блок остаётся видимым. Элементы могут пересекаться как угодно, они просто увеличивают значения счётчиков.
Опция отладки, позволяющая отображать счётчик «оставить открытым».
Если значение счётчика блока больше нуля, он помещается в бакет обновления, и на каждом тике мы циклически обходим один бакет.
Если счётчик блока становится равным нулю, то он удаляется из бакета и перестаёт обновляться.
Сканирование бакетов для отображения блоков (скорость 50%)
Кроме того, благодаря этой новой системе я смог заменить логику сущности радара и других сущностей со свойствами радара так, чтобы они использовали одну общую систему регистрации. Эффект был неожиданным — радары как сущности стали занимать меньше времени обновления, а из-за того, что дронстанции теперь можно использовать как радары, самих радаров требуется не так много.
В другом файле сохранения плейтеста мы повысили общую производительность игры на 3,6%. Учитывая то, что это не должно было дать никакого осязаемого эффекта (я вообще ожидал, что добавление функции радара дронстанциям ухудшит ситуацию), новость нас очень порадовала.
Итак, мы добились успеха, и в версии 2.0 дронстанции имеют небольшой «встроенный» радар с радиусом действия в два блока.
▍ Фонари всегда включены (автор: boskid)
Когда мы ели в ресторане во время недавней офисной LAN-пати, kovarex поделился своей идеей: учитывая, что для фонарей можно устанавливать любой RGB-цвет, можно создавать с их помощью изображения. Единственная проблема заключалась в том, что для подачи питания на фонари нужно было бы размещать большое количество подстанций, из-за чего изображение будет не особо красивым. Я сказал, что эта проблема решается постройкой на космических платформах, ведь на платформах не нужны опоры ЛЭП. Похоже, это его обрадовало, но я решил подождать следующего дня.
Когда настал следующий день плейтестинга, он создал чертёж 100 на 150 с фонарями, которой поместил на космическую платформу. Изображение должно было преобразоваться в RGB-цвета этих фонарей. Однако предстояло решить одну важную проблему: на космической платформе всегда день, поэтому фонари не включаются. Чтобы заставить фонари работать днём, их нужно подключить к логической сети. Мы легко это заметили, потому что время обновления в нашем быстро растущем файле сохранения увеличивалось слишком стремительно.
Один из фонарей принудительно постоянно включён при помощи control behavior
15 тысяч фонарей, включённых при помощи control behavior и обновляемых в каждом тике — это неоптимальная система, поэтому я сделал так, чтобы фонари можно было принудительно включать даже днём.
Фонари, использующие Always ON.
Благодаря этому обновление файла сохранения ускорилось на 1,2 мс, ведь теперь не нужно было подключать фонари к логической сети. Однако изучив время обновления control behavior после внесённого мной изменения, я заметил, что обновление control behavior всё равно слишком долгое. Это меня беспокоило, поэтому нужно было выяснить причины.
▍ Считыватель конвейеров и многопоточность control behavior (автор: boskid)
То, что обновление control behavior по-прежнему выполнялось долго, объяснить было легко. Причина заключалась в созданной мной функции: возможности считывать содержимое всех конвейеров в последовательности (см. FFF-405).
Сам считыватель конвейеров был добавлен примерно по тем же причинам, что и всегда включённые фонари — чтобы снизить количество активных control behavior, потому что для считывания содержимого каждый элемент конвейера необходимо подключать по отдельности. Благодаря возможности считывания содержимого всех конвейеров в последовательности достаточно подключить проводом только один из этих конвейеров, для подсчёта предметов нужно только одно control behavior, а сами транспортировочные линии не нужно разделять на элементы длиной 1 тайл. По сути, добавление считывателя конвейеров позволило существенно снизить затраты на обновление control behavior и затраты на обновление транспортировочных линий, при этом обеспечив новые, ранее недоступные возможности (например, подсчёт предметов на подземных конвейерах).
Проблема считывателя конвейеров в простоте его использования, во время плейтестинга мы очень активно его применяли не только на космических платформах, как предполагалось, но и в других местах.
Считыватель конвейеров используется там, где я этого не ожидал
Могу сказать, что основным саботажником нашего тестирования был Hrusa. Он часто использовал считыватели конвейеров так, что потом разрешить проблемы мы могли только с его участием. Кроме того, на Фулгоре размещаются сундуки активного снабжения, превращающие её в ад логистических роботов: в воздухе постоянно находится более десяти тысяч логистических роботов. Я не могу наказывать Hrusa за то, что он играет в игру, поэтому настала пора оптимизаций. Оптимизацией кода логистических роботов занимался другой разработчик, однако считыватель конвейеров и control behavior были моей задачей.
Считыватель конвейеров устроен крайне просто: каждый тик он должен проверять, какие предметы находятся на конвейерах и сколько их в каждом из стеков, чтобы подсчитать общую сумму.
Оптимизация считывателей выполнялась в несколько итераций. Поворотным моментом для меня стало осознание того, что считыватель — это в основном операция чтения: он просто считывает кучу данных из памяти (содержимое стеков конвейеров) для подсчёта предметов, а в конце создаёт один кадр сигналов, который нужно отправить логической сети. Благодаря такой структуре я мог реализовать многопоточную обработку: множество вычисляемых одновременно считывателей конвейеров не влияет друг на друга, потому что они считывают только содержимое конвейеров, а их выходные данные не используются другими считывателями. Подобную структуру можно заметить и в других местах, например, в дронстанциях, считывающих содержимое логистической сети, арифметических комбинаторах, селекторных комбинаторах и сравнивающих комбинаторах. Все эти системы, за исключением селекторного комбинатора (который больше не может использовать глобальный генератор случайных чисел), достаточно легко можно сделать многопоточными.
После внесения всех этих изменений наш файл сохранения для плейтестинга смог работать примерно на 9,5% быстрее.
Синтетический файл сохранения с 77 тысячами комбинаторов, связанных шестью тысячами логических сетей, работал в 14,9 раз быстрее, при этом стабильно используя CPU на 100% (время бенчмарка снизилось с 131 с до всего 8,2 с).
▍ Неудачная попытка: многопоточное обновление электрических сетей (автор: boskid)
Я часто слышал от пользователей подобную просьбу: сделайте обновление электрических сетей многопоточным.
Мы уже улучшали обновление электрических сетей (FFF-209), однако оно всё равно выполнялось в одном потоке, который по наблюдениям часто выполнялся медленнее остальных. В большинстве случаев сохранёнки содержат всего одну крупную сеть, поэтому многопоточность не даст особого выигрыша. Однако в Space Age есть множество крупных сетей (на разных планетах), поэтому потенциал многопоточности выше.
При внедрении многопоточности первым делом всегда нужно изучить взаимодействие частей системы друг с другим. Это необходимо, потому что игра должна оставаться полностью детерминированной, иначе произойдёт рассинхронизация.
Поначалу может показаться, что это довольно просто: каждый поток работает с отдельной электросетью, и на этом всё. Однако это не так, ведь существует игровая механика, которая всё усложняет: возможность получения питания сущностью от нескольких электрических сетей.
Один из случаев, когда электрические сети зависимы друг от друга
В этом примере при работе нефтеперерабатывающего завода его хранилище энергии разряжается, а электросети должны снова его зарядить. Существует два сценария возможных действий:
- Сначала обновляется левая электросеть, заряжая завод от парового двигателя; бойлер сжигает топливо и активируется манипулятор.
- Сначала обновляется правая электросеть, заряжая завод от солнечных панелей (в этом случае бойлер не начинает работу).
Из-за этого сети зависят друг от друга и должны обновляться в одном потоке.
Выявив все подобные случаи (например, выключатель питания, объединяющий несколько сетей), я смог определить, что должно содержаться в группе обновления. Если две электросети имеют хотя бы одну сущность, питаемую от обеих сетей, то эти сети должны находиться в одной группе обновления и обновляться одним потоком, благодаря чему не возникнет рассинхронизаций.
После этого я был готов к тому, чтобы начать измерения…
▍ Результаты
И здесь идея потерпела полный провал. В сохранённом файле плейтестинга было четыре крупные полностью независимые электросети, потому что они находились на разных планетах, однако время обновления сетей осталось таким же, а использование CPU существенно увеличилось.
Как оказалось, обновление электросети выполняет не так много действий: просто считывает две переменные, производит одно-два сложения и переходит к следующей сущности. Оно ограничено пропускной способностью памяти, и при наличии множества потоков, считывающих эти данные из памяти, процессор просто не может считывать данные быстрее.
В данном случае после реализации многопоточности память ждал уже не один поток, а все потоки, выполнявшие обновление электросетей и замедляющие друг друга.
Чтобы полностью забраковать эту идею, мне пришлось воспользоваться дополнительными инструментами профилирования, например, Intel VTune, которые позволили мне получить больше количественных доказательств того, что электрические сети ограничены пропускной способностью памяти. Время обновления сетей в нашем файле сохранения плейтестинга уменьшилось с 0,5 мс до 0,39 мс, однако использование CPU возросло с 0,5% до 15%. Суммарно файл сохранения не стал работать быстрее.
▍ Более хитрое обновление рабочих дронов (автор: kovarex)
В нашем офисном сохранении я заметил проблему с логистическими дронами: кто-то отключил кабель от автоматизированной системы, добавляющей новых дронов в дронстанции на Фулгоре. Спустя несколько часов мы получили перегруженные логистические сети с десятью тысячами дронов, которым некуда податься, потому что все дронстанции заполнены.
Так как никто не замечал эту проблему часами, первым делом я решил добавить алерт о нехватке мест для приземления рабочих дронов, похожий на алерт при отсутствии места для хранения.
Бездомные роботы, сбившиеся в кучи рядом с дронстанциями
Но на самом деле глубинная проблема заключалась в простоте обновления рабочих дронов. Каждый тик выполнялась итерация по всем дронам с исполнением их логики обновления. Однако чаще всего логика обновления очень проста, например, «продолжить двигаться в сторону цели», «ждать в очереди начала зарядки» и так далее.
В идее имитации сглаженного движения при более редком обновлении сущностей нет ничего нового. Мы уже использовали девять лет назад планировщик обновления чанков (FFF-161) и трюк с обновлением дыма (FFF-84), то есть применение этих техник к рабочим дронам было всего лишь делом времени.
Проблема с рабочими дронами, в отличие от дыма, заключается в том, что они выполняют множество разных задач (разные типы работ, приземление, зарядку и так далее), но у меня была мотивация к поиску ещё одного способа оптимизации игры, поэтому я приступил к делу.
Самым важным был процесс перемещения дронов, потому что основная часть логики работала просто:
- Мы уже на месте?
- Нет! Приближаемся
- Мы уже на месте?
- Да, выполняем следующую часть логики.
Но теперь нам нужно сохранять в дроне намерение двигаться и использовать его, когда нам нужно куда-то двигаться. Как только становится известно, что робот намерен двигаться дальше, он может «погрузиться в сон», а код рендеринга может использовать это намерение, чтобы притвориться, что робот постоянно движется плавно. Также это можно использовать для вычисления времени прибытия в точку, чтобы знать, на какой момент запланировать следующее обновление.
Отладочная визуализация: красный дрон (реальный) обновляется только один раз в двадцать тиков. Дрон-призрак — это прогнозируемая позиция, используемая для рендеринга
Нужно наложить некие ограничения, потому что робот способен двигаться долгое время и какие-то далёкие роботы могут двигаться в центр экрана, но мы не узнаем об этом, так как их физическое расположение будет находиться слишком далеко. Мы схожим образом проверяем края экрана (например, в случае фонарей, освещающих экран из-за его края), так что у нас есть определённая свобода манёвра. Поэтому я просто решил задать чёткие магические числа: максимум в 20 тиков для движения без обновлений и 60 тиков для приземлившегося дрона без обновлений. Вероятно, их можно увеличить, но это практически никак не повлияет на производительность.
▍ Результат
В сохранёнке офисной LAN-пати общая производительность увеличилась на 15%, в общем случае это зависит от количества дронов. Но в сохранениях с большим количеством дронов рост производительности обычно составлял 10-25%. Мне кажется, это успех.
Объединив все эти изменения, мы обеспечили более комфортную производительность для сохранений большего размера, характерных для Space Age. Но было бы здорово увеличить её ещё немного, чтобы игроки могли позволить ещё больше свободы. У нас есть другие идеи, которые могли бы сильно помочь, и надеемся, они будут историями успеха, а не постмортемами о провалах.
Telegram-канал со скидками, розыгрышами призов и новостями IT 💻