Как улучшить производительность для дорогих функций в 2d city builder

9

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

В моей текущей игре (двумерное городское здание на основе плиток) пользователь может размещать здания, строить дороги и т. Д. Все здания нуждаются в соединении с перекрестком, который пользователь должен разместить на границе карты. Если здание не подключено к этому перекрестку, над поврежденным зданием появится всплывающее сообщение «Не связано с дорогой» (в противном случае его необходимо удалить). Большинство зданий имеют радиус и могут быть связаны друг с другом (например, пожарная служба может помочь всем домам в радиусе 30 плиток). Это то, что мне также нужно обновить / проверить, когда меняется дорожное сообщение.

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

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

    // Go through all buildings affected by erasing this road tile.
    foreach(var affectedBuilding in affectedBuildings) {
        // Get buildings within radius.
        foreach(var radiusTile in affectedBuilding.RadiusTiles) {
            // Get all buildings on Map within this radius (which is technially another foreach).
            var buildingsInRadius = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);  
    
            // Do stuff.
        }
    }

Все это снижает мой FPS с 60 до почти 10 за одну секунду.

Так бы я мог сделать. Мои идеи будут:

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

Используя последний подход, я мог бы вместо этого удалить одно вложение в форме:

// Go through all buildings affected by erasing this road tile.
foreach(var affectedBuilding in affectedBuildings) {
    // Go through buildings within radius.
    foreach(var buildingInRadius in affectedBuilding.BuildingsInRadius) {
        // Do stuff.
    }
}

Но я не знаю, достаточно ли этого. Такие игры, как Cities Skylines должны обрабатывать намного больше зданий, если у игрока есть большая карта. Как они справляются с этими вещами ?! Может быть очередь обновления, так как не все здания обновляются одновременно.

Я с нетерпением жду ваших идей и комментариев!

Большое спасибо!

Yheeky
источник
2
Использование профилировщика должно помочь определить, какой бит кода имеет проблему. Это может быть способ найти затронутые здания или, может быть, сделать что-то. Как примечание, такие большие игры, как City Skylines, решают эти проблемы, используя пространственные структуры данных, такие как четырехъядерные, поэтому все пространственные запросы выполняются намного быстрее, чем выполнение массива с циклом for. Например, в вашем случае у вас может быть график зависимости всех зданий, и, следуя этому графику, вы сразу узнаете, что влияет на то, что без итераций.
Exaila
Спасибо за подробную информацию. Мне нравится идея зависимостей! Я посмотрю на это!
Yheeky
Ваш совет был отличным! Я только использовал профилировщик VS, который показал мне, что у меня была функция поиска пути для каждого затронутого здания, чтобы проверить, все ли соединение соединения все еще допустимо. Конечно, это чертовски дорого! Это всего около 5 FPS, но лучше, чем ничего. Я избавлюсь от этого и назначу здания дорожным плиткам, чтобы мне не нужно было выполнять эту проверку снова и снова. Большое спасибо! Нет, мне нужно только исправить здания с радиусом, который больше.
Yheeky
Я рад, что вы нашли это полезным: D
Exaila

Ответы:

3

Покрытие кэширования

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

Это классический обмен процессорных циклов на память.

Обработка информации о покрытии по регионам

Если окажется, что вы используете слишком много памяти для отслеживания этой информации, посмотрите, можете ли вы обрабатывать такую ​​информацию по регионам карты. Разделите вашу карту на квадратные области n*nплитка. Если район полностью охвачен пожарной службой, все здания в этом регионе также покрыты. Таким образом, вам нужно хранить информацию о покрытии только по регионам, а не по отдельным зданиям. Если регион покрыт только частично, вам нужно вернуться к обработке внутренних связей в этом регионе. Таким образом, функция обновления для ваших зданий будет сначала проверять: «Район, в котором находится это здание, покрыт пожарной службой?» и если нет, "Это здание индивидуально закрыто пожарной службой?" Это также ускоряет обновления, поскольку после удаления пожарной части вам больше не нужно обновлять состояния покрытия 2000 зданий, вам нужно обновить только 100 зданий и 25 регионов.

Задержка обновления

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

Независимо от того, подключено ли здание к дорожной сети или нет, вам не нужно проверять каждый кадр (кстати, вы также можете найти некоторые способы оптимизировать это, в частности, изучив теорию графов). Было бы вполне достаточно, если бы здания проверяли это периодически каждые несколько секунд после того, как здание было построено (И если произошло изменение в дорожной сети). То же самое относится к эффектам построения диапазона. Вполне приемлемо, если здание проверяет только каждые несколько сотен кадров. «По крайней мере, одно из пожарных подразделений, которые влияют на меня, все еще активно?»

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

Относительно многопоточности

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

Philipp
источник
Это объясняет, почему SimCity на SNES требует времени для повторного подключения / подключения, я полагаю, что это происходит и с другими его эффектами по всей области.
lozzajp
Спасибо за ваш полезный комментарий! Я также думаю, что хранение большего количества информации в памяти может ускорить мою игру. Мне также нравится идея разбить TileMap на регионы, но я не знаю, достаточно ли этот подход, чтобы избавиться от моей первоначальной проблемы с давними проблемами. У меня вопрос по поводу отложенного обновления. Предположим, у меня есть функция, которая заставляет мой FPS снижаться с 60 до 45. Каков наилучший подход к разделению вычислений для обработки идеального количества, которое может обработать процессор?
Yheeky
@Yheeky Не существует универсально применимого решения для этого, потому что оно сильно зависит от ситуации, какие вычисления вы можете отложить, а какие нет и какая разумная единица вычисления.
Филипп
Я попытался отложить эти вычисления, чтобы создать очередь с элементами, имеющими флаг «В настоящее время». Только этот элемент, имеющий этот флаг установлен в true, был обработан. После завершения расчета элемент был удален из списка, и следующий элемент был обработан. Это должно работать, верно? Но какой метод можно использовать, если вы знаете, что один расчет сам по себе может снизить ваш FPS?
Yheeky
1
@Yheeky Как я уже сказал, универсального решения не существует. Что я обычно стараюсь (в таком порядке): 1. Посмотрим, сможете ли вы оптимизировать эти вычисления, используя более подходящие алгоритмы и / или структуры данных. 2. Проверьте, можете ли вы разделить его на подзадачи, которые вы можете отложить отдельно. 3. Проверьте, можете ли вы сделать это в виде отдельной угрозы. 4. Избавьтесь от игровой механики, которая нуждается в этих вычислениях, и посмотрите, сможете ли вы заменить ее чем-то менее вычислительно дорогим.
Филипп
3

1. Дублирующая работа .

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

var toBeUpdated = new HashSet<Tiles>();
foreach(var affectedBuilding in affectedBuildings) {
    foreach(var radiusTile in affectedBuilding.RadiusTiles) {
         toBeUpdated.Add(radiusTile);

}
foreach (var tile in toBeUpdated)
{
    var buildingsInTile = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);
    // Do stuff.
}

2. Неподходящие структуры данных.

var buildingsInTile = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);

должно быть ясно

var buildingsInRadius = tile.Buildings;

где Buildings - IEnumerableс постоянным временем итерации (например, a List<Building>)

Питер
источник
Хорошая точка зрения! Я думаю, что я попытался использовать Distinct () на этом с использованием MoreLINQ, но я согласен, что это может быть быстрее, чем проверка дубликатов.
Yheeky