Come migliorare le prestazioni per funzioni costose in 2d city builder


9

Ho già cercato risposte ma non sono riuscito a capire l'approccio migliore per gestire costose funzioni / calcoli.

Nel mio gioco attuale (un edificio di città basato su piastrelle 2d) l'utente è in grado di posizionare edifici, costruire strade, ecc. Tutti gli edifici hanno bisogno di una connessione a un incrocio che l'utente deve posizionare al bordo della mappa. Se un edificio non è collegato a questo incrocio, verrà visualizzato un cartello "Non collegato alla strada" sopra l'edificio interessato (altrimenti deve essere rimosso). La maggior parte degli edifici ha un raggio e potrebbe essere correlata anche tra loro (ad esempio un corpo dei vigili del fuoco può aiutare tutte le case entro un raggio di 30 tessere). Questo è ciò di cui ho bisogno anche per aggiornare / verificare quando cambia la connessione stradale.

Ieri ho riscontrato un grosso problema di prestazioni. Diamo un'occhiata al seguente scenario: Un utente può ovviamente cancellare anche edifici e strade. Quindi, se un utente ora interrompe la connessione subito dopo l'incrocio, devo aggiornare molti edifici contemporaneamente . Penso che uno dei primi consigli sarebbe quello di evitare i cicli nidificati (che sicuramente è una grande ragione in questo scenario) ma devo controllare ...

  1. se un edificio è ancora collegato all'incrocio nel caso in cui una piastrella stradale sia stata rimossa (lo faccio solo per gli edifici interessati da quella strada). (Potrebbe essere un problema minore in questo scenario)
  2. l'elenco delle tessere raggio e ottieni gli edifici nel raggio (anelli annidati - grosso problema!) .

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

Tutto ciò scompone il mio FPS da 60 a quasi 10 per un secondo.

Quindi potrei fare. Le mie idee sarebbero:

  • Non utilizzare il thread principale (funzione di aggiornamento) per questo, ma per un altro thread. Potrei incorrere in problemi di blocco quando inizio a utilizzare il multithreading.
  • Usare una coda per gestire molti calcoli (quale sarebbe l'approccio migliore in questo caso?)
  • Conservare ulteriori informazioni nei miei oggetti (edifici) per evitare ulteriori calcoli (ad es. Edifici nel raggio).

Usando l'ultimo approccio ho potuto invece rimuovere un annidamento in questo foreach invece:

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

Ma non so se sia abbastanza. Giochi come Cities Skylines devono gestire molti più edifici se il giocatore ha una grande mappa. Come gestiscono queste cose ?! Potrebbe esserci una coda di aggiornamento poiché non tutti gli edifici si aggiornano contemporaneamente.

Non vedo l'ora di ricevere idee e commenti!

Molte grazie!


2
L'uso di un profiler dovrebbe aiutare a identificare quale bit del codice presenta il problema. Potrebbe essere il modo in cui trovi gli edifici colpiti, o forse // fare cose. Come nota a margine, i grandi giochi come City Skylines affrontano questi problemi utilizzando strutture di dati spaziali come i quad-tree, quindi tutte le query spaziali sono molto più veloci rispetto a un array con un ciclo for. Nel tuo caso, ad esempio, potresti avere un grafico delle dipendenze di tutti gli edifici e seguendo quel grafico puoi immediatamente sapere cosa influenza ciò che senza iterazioni.
Exaila,

Grazie per le informazioni dettagliate. Mi piace l'idea delle dipendenze! Lo darò un'occhiata!
Yheeky,

Il tuo consiglio è stato fantastico! Ho appena usato il profiler VS che mi ha mostrato che avevo una funzione di pathfinding per ogni edificio interessato per verificare se la connessione di giunzione fosse ancora valida. Certo che è costoso da morire! Sono solo circa 5 FPS ma meglio di niente. Mi libererò di ciò e assegnerò gli edifici alle tessere stradali, quindi non ho bisogno di fare questo controllo di tracciamento più e più volte. Molte grazie! No, ho solo bisogno di riparare gli edifici nel problema del raggio, che è il più grande.
Yheeky,

Sono contento che l'abbia trovato utile: D
Exaila,

Risposte:


3

Copertura dell'edificio nella cache

L'idea di memorizzare nella cache le informazioni su quali edifici si trovano nel raggio di un edificio effettore (che è possibile memorizzare nella cache dall'effettore o nell'effetto interessato) è sicuramente una buona idea. Gli edifici (di solito) non si muovono, quindi ci sono poche ragioni per ripetere questi costosi calcoli. "Cosa influenza questo edificio" e "Cosa influenza questo edificio" è qualcosa che devi controllare solo quando un edificio viene creato o rimosso.

Questo è un classico scambio di cicli della CPU per la memoria.

Gestione delle informazioni sulla copertura per regione

Se si scopre che si sta utilizzando troppa memoria per tenere traccia di queste informazioni, vedere se è possibile gestire tali informazioni dalle aree della mappa. Dividi la tua mappa in regioni quadrate di n*npiastrelle. Se una regione è interamente coperta dai vigili del fuoco, anche tutti gli edifici di quella regione sono coperti. Quindi devi solo memorizzare le informazioni sulla copertura per regione, non per singolo edificio. Se una regione è coperta solo parzialmente, è necessario ricorrere alla gestione delle connessioni di costruzione in quella regione. Quindi la funzione di aggiornamento per i tuoi edifici dovrebbe prima controllare "La regione in cui questo edificio è coperto dai vigili del fuoco?" e se no "Questo edificio è coperto individualmente dai vigili del fuoco?". Questo accelera anche gli aggiornamenti, perché quando un vigili del fuoco viene rimosso, non è più necessario aggiornare gli stati di copertura di 2000 edifici, è necessario aggiornare solo 100 edifici e 25 regioni.

Aggiornamento ritardato

Un'altra ottimizzazione che puoi fare è non aggiornare tutto immediatamente e non aggiornare tutto allo stesso tempo.

Se un edificio è ancora collegato alla rete stradale non è qualcosa che devi controllare ogni singolo frame (A proposito, potresti anche trovare alcuni modi per ottimizzarlo in modo specifico esaminando un po 'la teoria dei grafi). Sarebbe del tutto sufficiente se gli edifici lo controllano periodicamente ogni pochi secondi dopo la costruzione dell'edificio (E se ci fosse una modifica alla rete stradale). Lo stesso vale per la costruzione di effetti di portata. È perfettamente accettabile se un edificio controlla solo ogni poche centinaia di fotogrammi "È ancora attivo almeno uno dei vigili del fuoco che mi riguardano?"

Quindi puoi fare in modo che il tuo ciclo di aggiornamento esegua questi costosi calcoli per poche centinaia di edifici alla volta per ogni aggiornamento. Potresti voler dare le preferenze agli edifici che sono attualmente sullo schermo, in modo che i giocatori ricevano un feedback immediato per le loro azioni.

Per quanto riguarda il multithreading

I costruttori di città tendono ad essere sul lato più costoso dal punto di vista computazionale, soprattutto se si desidera consentire ai giocatori di costruire molto grandi e se si desidera avere un'elevata complessità di simulazione. Quindi a lungo termine potrebbe non essere sbagliato pensare a quali calcoli nel tuo gioco possono essere gestiti in modo asincrono.


Questo spiega perché SimCity su SNES impiega un po 'di tempo prima che l'alimentazione si ricolleghi / si connette, immagino che accada anche con gli altri suoi effetti su tutta l'area.
lozzajp,

Grazie per il tuo utile commento! Penso anche che tenere più informazioni in memoria potrebbe accelerare il mio gioco. Mi piace anche l'idea di suddividere la TileMap in regioni, ma non so se questo approccio sia abbastanza buono da eliminare il mio problema iniziale da lungo tempo. Ho una domanda relativa all'aggiornamento ritardato. Supponiamo che io abbia una funzione che fa scendere il mio FPS da 60 a 45. Qual è l'approccio migliore per dividere i calcoli per gestire la quantità perfetta che la CPU è in grado di gestire?
Yheeky,

@Yheeky Non esiste una soluzione universalmente applicabile per questo, perché è altamente dipendente dalla situazione quali calcoli è possibile ritardare, quali no e quale sia un'unità di calcolo ragionevole.
Philipp,

Il modo in cui ho cercato di ritardare questi calcoli è stato quello di creare una coda con elementi con un flag "Attualmente aggiornato". È stato gestito solo questo elemento con questo flag impostato su true. Quando il calcolo è stato completato, l'elemento è stato rimosso dall'elenco e l'elemento successivo è stato gestito. Dovrebbe funzionare, vero? Ma che tipo di metodo potrebbe essere utilizzato se sapessi che un calcolo stesso ridurrebbe il tuo FPS?
Yheeky,

1
@Yheeky Come ho detto, non esiste una soluzione universalmente applicabile. Cosa proverei di solito (in quell'ordine): 1. Vedi se riesci a ottimizzare quel calcolo usando algoritmi e / o strutture di dati più appropriati. 2. Vedi se riesci a dividerlo in attività secondarie che puoi ritardare individualmente. 3. Vedi se riesci a farlo in una minaccia separata. 4. Sbarazzati della meccanica di gioco che ha bisogno di quel calcolo e vedi se riesci a sostituirlo con qualcosa di meno computazionalmente costoso.
Philipp,

3

1. Lavoro duplicato .

Il tuo affectedBuildingssono presumibilmente vicini l'uno all'altro, in modo che il raggio diverso si sovrappongono. Contrassegna gli edifici che devono essere aggiornati, quindi aggiornali.

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. Datastructures non idonee.

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

dovrebbe essere chiaramente

var buildingsInRadius = tile.Buildings;

dove Edifici è un IEnumerabletempo di iterazione costante (ad es. a List<Building>)


Buon punto! Immagino di aver provato a usare un Distinct () su quello usando MoreLINQ ma sono d'accordo che questo potrebbe essere più veloce del controllo dei duplicati.
Yheeky,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.