Jak poprawić wydajność drogich funkcji w 2d city builder

9

Już szukałem odpowiedzi, ale nie byłem w stanie znaleźć najlepszego podejścia do obsługi drogich funkcji / obliczeń.

W mojej obecnej grze (budynek miasta oparty na kafelkach 2D) użytkownik może stawiać budynki, budować drogi itp. Wszystkie budynki wymagają połączenia ze skrzyżowaniem, które użytkownik musi umieścić na granicy mapy. Jeśli budynek nie jest podłączony do tego skrzyżowania, nad budynkiem dotkniętym katastrofą pojawi się znak „Niepołączony z drogą” (w przeciwnym razie należy go usunąć). Większość budynków ma promień i może być również ze sobą powiązana (np. Straż pożarna może pomóc wszystkim domom w promieniu 30 płytek). Właśnie to muszę zaktualizować / sprawdzić, gdy zmieni się połączenie drogowe.

Wczoraj napotkałem duży problem z wydajnością. Spójrzmy na następujący scenariusz: Użytkownik może oczywiście również usuwać budynki i drogi. Jeśli więc użytkownik przerywa połączenie zaraz po skrzyżowaniu , muszę zaktualizować wiele budynków jednocześnie . Myślę, że jedną z pierwszych rad byłoby unikanie zagnieżdżonych pętli (co jest zdecydowanie dużym powodem w tym scenariuszu), ale muszę sprawdzić ...

  1. jeśli budynek jest nadal podłączony do skrzyżowania w przypadku, gdy płytka drogi została usunięta (robię to tylko dla budynków dotkniętych tą drogą). (W tym scenariuszu może to być mniejszy problem)
  2. lista płytek w promieniu i zdobądź budynki w promieniu (zagnieżdżone pętle - duży problem!) .

    // 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.
        }
    }
    

To wszystko rozkłada mój FPS z 60 do prawie 10 na sekundę.

Mógłbym to zrobić. Moje pomysły to:

  • Nieużywanie głównego wątku (funkcja aktualizacji) dla tego, ale innego wątku. Mogę napotkać problemy z blokowaniem, gdy zacznę korzystać z wielowątkowości.
  • Korzystanie z kolejki do obsługi wielu obliczeń (jakie byłoby najlepsze podejście w tym przypadku?)
  • Zachowaj więcej informacji w moich obiektach (budynkach), aby uniknąć dalszych obliczeń (np. Budynki w promieniu).

Korzystając z ostatniego podejścia, mogę zamiast tego usunąć jedno zagnieżdżenie w tej foreach:

// 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.
    }
}

Ale nie wiem czy to wystarczy. Gry takie jak Cities Skylines muszą obsługiwać znacznie więcej budynków, jeśli gracz ma dużą mapę. Jak sobie z tym radzą ?! Może istnieć kolejka aktualizacji, ponieważ nie wszystkie budynki aktualizują się w tym samym czasie.

Czekam na Wasze pomysły i komentarze!

Wielkie dzięki!

Heه
źródło
2
Użycie profilera powinno pomóc zidentyfikować, który bit kodu ma problem. Może to być sposób znajdowania dotkniętych budynków, a może // robienia rzeczy. Na marginesie, duże gry, takie jak City Skylines, rozwiązują te problemy za pomocą struktur danych przestrzennych, takich jak drzewa quad, więc wszystkie zapytania przestrzenne są znacznie szybsze niż przejście przez tablicę z pętlą for. Na przykład w twoim przypadku możesz mieć wykres zależności wszystkich budynków, a śledząc ten wykres, możesz natychmiast wiedzieć, co wpływa na to, co bez iteracji.
Exaila
Dziękuję za szczegółowe informacje. Podoba mi się pomysł zależności! Spojrzę na to!
Yheeky
Twoja rada była świetna! Właśnie użyłem profilera VS, który pokazał mi, że mam funkcję wyszukiwania ścieżki dla każdego dotkniętego budynku, aby sprawdzić, czy połączenie skrzyżowania jest nadal prawidłowe. Oczywiście to jest drogie jak diabli! To tylko około 5 FPS, ale lepsze niż nic. Pozbędę się tego i przypiszę budynki do kafelków dróg, więc nie muszę powtarzać tego sprawdzania ścieżki. Wielkie dzięki! Nie. Muszę tylko naprawić budynki w promieniu, który jest większy.
Yheeky
Cieszę się, że okaże się przydatny: D
Exaila

Odpowiedzi:

3

Zasięg buforowania budynku

Pomysł buforowania informacji o tym, które budynki znajdują się w zasięgu budynku efektorowego (który można buforować albo z efektora, albo w dotkniętym nim budynku) jest zdecydowanie dobrym pomysłem. Budynki (zwykle) się nie poruszają, więc nie ma powodu, aby przerabiać te kosztowne obliczenia. „Co wpływa na ten budynek” i „Co wpływa na ten budynek” to coś, co trzeba tylko sprawdzić, kiedy budynek zostanie utworzony lub usunięty.

Jest to klasyczna wymiana cykli procesora na pamięć.

Obsługa informacji o zasięgu według regionu

Jeśli okaże się, że używasz zbyt dużej ilości pamięci, aby śledzić te informacje, sprawdź, czy możesz poradzić sobie z takimi informacjami według regionów mapy. Podziel mapę na kwadratowe regiony n*npłytki. Jeśli region jest w całości objęty przez straż pożarną, wszystkie budynki w tym regionie są również objęte. Musisz więc przechowywać informacje o zasięgu tylko według regionu, a nie poszczególnych budynków. Jeśli region jest tylko częściowo objęty, musisz wrócić do obsługi połączeń budowania w tym regionie. Tak więc funkcja aktualizacji dla twoich budynków najpierw sprawdzi „Czy region jest objęty przez straż pożarną?” a jeśli nie „Czy ten budynek jest indywidualnie objęty strażą pożarną?”. Przyspiesza to również aktualizacje, ponieważ po usunięciu straży pożarnej nie trzeba już aktualizować stanów pokrycia 2000 budynków, wystarczy zaktualizować tylko 100 budynków i 25 regionów.

Opóźniona aktualizacja

Kolejną optymalizacją, którą możesz zrobić, jest nie aktualizowanie wszystkiego od razu i nie aktualizowanie wszystkiego w tym samym czasie.

Niezależnie od tego, czy budynek jest nadal podłączony do sieci drogowej, nie trzeba sprawdzać każdej klatki (nawiasem mówiąc, możesz również znaleźć kilka sposobów na optymalizację tego, patrząc trochę na teorię grafów). Byłoby całkowicie wystarczające, gdyby budynki sprawdzały to okresowo co kilka sekund po wybudowaniu budynku (ORAZ gdyby nastąpiła zmiana sieci dróg). To samo dotyczy efektów zasięgu budynku. Jest całkowicie do przyjęcia, jeśli budynek sprawdza tylko co kilkaset ramek „Czy przynajmniej jedna ze straży pożarnych, która mnie dotyczy, jest nadal aktywna?”

Możesz mieć więc pętlę aktualizacji, aby wykonywać te kosztowne obliczenia dla kilkuset budynków jednocześnie dla każdej aktualizacji. Możesz dać preferencje budynkom, które są obecnie na ekranie, aby gracze otrzymywali natychmiastowe informacje zwrotne o swoich działaniach.

Odnośnie wielowątkowości

Budowniczowie miast wydają się być bardziej kosztowni pod względem obliczeniowym, szczególnie jeśli chcesz pozwolić graczom budować naprawdę duże i jeśli chcesz mieć wysoką złożoność symulacji. Dlatego na dłuższą metę może nie być źle myśleć o tym, jakie obliczenia w Twojej grze można traktować asynchronicznie.

Philipp
źródło
To wyjaśnia, dlaczego SimCity na SNES potrzebuje trochę czasu, aby ponownie podłączyć się do zasilania. Myślę, że dzieje się tak również z innymi efektami na całym obszarze.
lozzajp
Dziękuję za użyteczny komentarz! Myślę też, że przechowywanie większej ilości informacji w pamięci może przyspieszyć moją grę. Podoba mi się również pomysł podzielenia TileMap na regiony, ale nie wiem, czy to podejście jest wystarczająco dobre, aby pozbyć się mojego pierwotnego problemu na długo. Mam pytanie dotyczące opóźnionej aktualizacji. Załóżmy, że mam funkcję, która powoduje spadek liczby klatek na sekundę z 60 do 45. Jakie jest najlepsze podejście do podzielenia obliczeń, aby obsłużyć idealną ilość, jaką procesor jest w stanie obsłużyć?
Yheeky
@ Yheeky Nie ma na to uniwersalnego rozwiązania, ponieważ w dużej mierze zależy to od tego, które obliczenia możesz opóźnić, a których nie, i jaka jest rozsądna jednostka obliczeniowa.
Philipp
Próbowałem opóźnić te obliczenia, aby utworzyć kolejkę z elementami, które mają flagę „Obecnie aktualizowana”. Obsługiwany był tylko ten element z ustawioną flagą true. Po zakończeniu obliczeń element został usunięty z listy i obsłużono następny element. To powinno działać, prawda? Ale jaką metodę można zastosować, jeśli wiesz, że jedno obliczenie obniżyłoby twój FPS?
Yheeky
1
@Yheeky Jak powiedziałem, nie ma uniwersalnego rozwiązania. Co zwykle bym próbował (w tej kolejności): 1. Sprawdź, czy możesz zoptymalizować to obliczenie za pomocą bardziej odpowiednich algorytmów i / lub struktur danych. 2. Sprawdź, czy możesz podzielić go na podzadania, które możesz opóźnić indywidualnie. 3. Sprawdź, czy możesz to zrobić w osobnym zagrożeniu. 4. Pozbądź się mechaniki gry, która potrzebuje tego obliczenia i sprawdź, czy możesz go zastąpić czymś mniej kosztownym obliczeniowo.
Philipp
3

1. Zduplikowana praca .

Twoje affectedBuildingssą przypuszczalnie blisko siebie, więc różne promienie będą się pokrywać. Zgłoś budynki, które wymagają aktualizacji, a następnie zaktualizuj je.

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. Nieodpowiednie struktury danych.

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

powinno być wyraźnie

var buildingsInRadius = tile.Buildings;

gdzie budynki mają IEnumerablestały czas iteracji (np. a List<Building>)

Piotr
źródło
Słuszna uwaga! Chyba próbowałem użyć Distinct () na tym, który używa MoreLINQ, ale zgadzam się, że może to być szybsze niż sprawdzanie duplikatów.
Yheeky