Як покращити продуктивність для дорогих функцій у 2d city builder


9

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

У моїй теперішній грі (міська забудова на основі 2d) користувач може розміщувати будинки, будувати дороги тощо. Усі будівлі потребують підключення до вузла, який повинен розміщувати користувач на кордоні карти. Якщо будівля не підключена до цього перехрестя, над ураженою будівлею з’явиться знак «Не підключено до дороги» (інакше його потрібно буде зняти). Більшість будівель мають радіус і можуть бути пов’язані між собою (наприклад, пожежна частина може допомогти усім будинкам у радіусі 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.
    }
}

Але я не знаю, чи цього достатньо. Такі ігри, як міста Skylines, повинні мати набагато більше будівель, якщо у гравця є велика карта. Як вони поводяться з цими речами ?! Може виникнути оновлення черги, оскільки не всі будівлі оновлюються одночасно.

Я з нетерпінням чекаю ваших ідей та коментарів!

Дуже дякую!


2
Використання профілера повинно допомогти визначити, який біт коду має проблему. Це може бути спосіб, коли ви знайдете постраждалі будівлі, або, можливо, // робити речі. Як бічна примітка, великі ігри люблять City Skylines вирішувати ці проблеми, використовуючи структури просторових даних, таких як квадратичні дерева, тому всі просторові запити набагато швидше, ніж проходження масиву з циклом for. Наприклад, у вашому випадку ви можете мати графік залежності всіх будівель і, слідуючи за цим графіком, ви зможете одразу знати, що впливає на те, що не має ітерацій.
Exaila

Дякуємо за детальну інформацію. Мені подобається ідея залежностей! Я буду дивитись на це!
Yheeky

Ваша порада була чудовою! Я просто використав VS-профілер, який показав мені, що я мав функцію прокладання маршруту для кожної постраждалої будівлі, щоб перевірити, чи з'єднання з'єднання все ще дійсне. Звичайно, це дорого як пекло! Це лише близько 5 FPS, але краще, ніж нічого. Я позбудусь цього і призначу будівлі дорожній плитці, тому мені не потрібно робити цю перевірку проходження маршрутів знову і знову. Дуже дякую! Ні, мені потрібно лише виправляти будівлі в радіусі, який є більшим.
Yheeky

Я радий, що ти
вважаєш

Відповіді:


3

Кешування покриття будівлі

Ідея кешування інформації, які будівлі знаходяться в діапазоні ефекторної будівлі (яку ви можете кешувати або з ефектора, або в постраждалих), безумовно, хороша ідея. Будинки (як правило) не рухаються, тому є мало підстав для того, щоб переробити ці дорогі розрахунки. "Що впливає на цю будівлю" та "що впливає на цю будівлю" - це те, що вам потрібно лише перевірити, коли будівля створена чи вилучена.

Це класичний обмін циклами процесора на пам'ять.

Обробка інформації про покриття по регіонах

Якщо виявиться, що ви використовуєте занадто багато пам’яті для відстеження цієї інформації, подивіться, чи можна обробляти таку інформацію за регіонами карти. Розділіть свою карту на квадратні регіони n*nплитки. Якщо район повністю охоплений пожежною частиною, всі будівлі в цьому регіоні також охоплені. Тому потрібно зберігати інформацію про покриття лише за регіонами, а не за окремими будівлями. Якщо регіон лише частково охоплений, вам потрібно відмовитися від обробки підключень, що будуються в цьому регіоні. Тож функція оновлення для ваших будинків спочатку перевірила б "Чи знаходиться область, в якій знаходиться ця будівля, пожежна частина?" і якщо ні, "Чи охоплює цю будівлю окремо пожежна частина?". Це також прискорює оновлення, оскільки, коли пожежна частина буде знята, вам більше не потрібно оновлювати стану покриття 2000 будівель, вам потрібно буде лише оновити 100 будівель та 25 регіонів.

Затримка оновлення

Ще одна оптимізація, яку ви можете зробити, - це не оновлювати все відразу і не оновлювати все одночасно.

Незалежно від того, яка будівля все ще підключена до дорожньої мережі, це не те, що потрібно перевіряти кожен кадр (До речі, ви також можете знайти деякі способи оптимізації цього конкретно, заглянувши трохи в теорію графіків). Було б цілком достатньо, якби будівлі перевіряли це періодично кожні кілька секунд після того, як будівля була побудована (І якщо відбулася зміна дорожньої мережі). Це ж стосується ефектів будівельної дальності. Це цілком прийнятно, якщо будівля перевіряє кожні кілька сотень кадрів "Чи принаймні одна з пожежних підрозділів, яка впливає на мене, все ще активна?"

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

Щодо багатопотокової роботи

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


Це пояснює, чому SimCity на SNES потребує певного часу, щоб перезапустити / підключити живлення, я думаю, що це відбувається і з іншими його ефектами в масштабах області.
lozzajp

Дякуємо за ваш корисний коментар! Я також думаю, що зберігання більшої кількості інформації в пам'яті може пришвидшити мою гру. Мені також подобається ідея розділити TileMap на регіони, але я не знаю, чи такий підхід достатньо хороший, щоб позбутися моєї початкової проблеми давно. У мене питання щодо затримки оновлення. Припустимо, що у мене є функція, завдяки якій FPS знижується з 60 до 45. Який найкращий підхід для розділення обчислень, щоб обробити ідеальну кількість, з якою може справитись процесор?
Yheeky

@Yheeky Не існує загальноприйнятного рішення для цього, оскільки це дуже залежить від ситуації, які обчислення ви можете затримати, які ви не можете і яка розумна одиниця обчислення.
Філіп

Я намагався затримати ці обчислення, щоб створити чергу з елементами, на яких є прапор "CurrentUpdating". Оброблявся лише цей елемент, у якого цей прапор встановлено як істинний. Після завершення обчислення елемент був видалений зі списку і оброблявся наступний елемент. Це має працювати, правда? Але який метод можна використовувати, якщо ви знаєте, що один розрахунок сам по собі зведе ваш 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;

де будівлі - це IEnumerableпостійний час ітерації (наприклад, a List<Building>)


Гарна думка! Я думаю, я намагався використовувати Distinct () на тому, що використовує MoreLINQ, але я згоден, що це може бути швидше, ніж перевірка дублікатів.
Yheeky
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.