Come posso rendere A * più veloce quando la destinazione è impraticabile?


31

Sto realizzando un semplice gioco 2D basato su tessere, che utilizza l'algoritmo di ricerca del percorso A * ("A star"). Ho funzionato tutto bene, ma ho un problema di prestazioni con la ricerca. In poche parole, quando faccio clic su una tessera invalicabile, l'algoritmo apparentemente attraversa l'intera mappa per trovare un percorso verso la tessera invalicabile, anche se mi trovo accanto ad essa.

Come posso eludere questo? Immagino di poter ridurre il percorso verso l'area dello schermo, ma forse mi sto perdendo qualcosa di ovvio qui?


2
Sai quali tessere sono impraticabili o lo conosci semplicemente come risultato dell'algoritmo AStar?
user000user

Come stai memorizzando il tuo grafico di navigazione?
Anko,

Se si memorizzano nodi attraversati in elenchi, è possibile utilizzare heap binari per migliorare la velocità.
ChrisC,

Se è semplicemente troppo lento, ho una serie di ottimizzazioni da suggerire o stai cercando di evitare del tutto le ricerche?
Steven

1
Questa domanda sarebbe probabilmente stata più adatta all'informatica .
Raffaello

Risposte:


45

Alcune idee su come evitare le ricerche che si traducono in percorsi completamente falliti:

ID isola

Uno dei modi più economici per completare in modo efficace le ricerche A * più velocemente è quello di non effettuare alcuna ricerca. Se le aree sono veramente impraticabili da parte di tutti gli agenti, riempire ogni area con un ID isola univoco al carico (o nella conduttura). Durante l'individuazione del percorso, verificare se l' ID isola dell'origine del percorso corrisponde all'ID isola della destinazione. Se non corrispondono, non ha senso effettuare la ricerca: i due punti si trovano su isole distinte e non collegate. Questo aiuta solo se ci sono nodi veramente impraticabili per tutti gli agenti.

Limite limite superiore

Limito il limite superiore del numero massimo di nodi che è possibile cercare. Ciò consente di eseguire ricerche impraticabili per sempre, ma significa che è possibile perdere alcune ricerche percorribili molto lunghe. Questo numero deve essere ottimizzato e non risolve davvero il problema, ma mitiga i costi associati alle ricerche lunghe.

Se quello che stai riscontrando è che sta impiegando troppo tempo , sono utili le seguenti tecniche:

Rendilo asincrono e limita le iterazioni

Lascia che la ricerca venga eseguita in un thread separato o un po 'ogni frame in modo che il gioco non si fermi in attesa della ricerca. Mostra l'animazione della testa che graffia il personaggio o calpesta i piedi o qualsiasi cosa sia appropriata mentre aspetti che la ricerca termini. Per fare questo in modo efficace, vorrei mantenere lo stato della ricerca come oggetto separato e consentire l'esistenza di più stati. Quando viene richiesto un percorso, afferrare un oggetto stato libero e aggiungerlo alla coda di oggetti stato attivi. Nell'aggiornamento del percorso, estrarre l'elemento attivo dalla parte anteriore della coda ed eseguire A * fino a quando A. non viene completato o B. viene eseguito un limite di iterazioni. Se completo, reinserire l'oggetto stato nell'elenco di oggetti stato liberi. Se non è stato completato, inserirlo alla fine delle "ricerche attive" e passare a quello successivo.

Scegli le giuste strutture dati

Assicurati di utilizzare le giuste strutture dati. Ecco come funziona il mio StateObject. Tutti i miei nodi sono pre-assegnati a un numero finito - diciamo 1024 o 2048 - per motivi di prestazioni. Uso un pool di nodi che accelera l'allocazione dei nodi e mi consente anche di archiviare gli indici anziché i puntatori nelle strutture dei dati che sono u16s (o u8 se ho 255 nodi max, cosa che faccio su alcuni giochi). Per il mio pathfinding uso una coda di priorità per l'elenco aperto, memorizzando i puntatori agli oggetti Node. È implementato come un heap binario e ordino i valori in virgola mobile come numeri interi poiché sono sempre positivi e la mia piattaforma ha confronti lenti in virgola mobile. Uso una tabella hash per la mia mappa chiusa per tenere traccia dei nodi che ho visitato. Memorizza NodeIDs, non Nodes, per risparmiare sulle dimensioni della cache.

Cache Cosa puoi

Quando si visita per la prima volta un nodo e si calcola la distanza dalla destinazione, memorizzarlo nella cache memorizzata nell'oggetto State. Se si rivisita il nodo, utilizzare il risultato memorizzato nella cache invece di calcolarlo di nuovo. Nel mio caso aiuta a non dover fare una radice quadrata su nodi rivisitati. Potresti scoprire che ci sono altri valori che puoi precalcolare e memorizzare nella cache.

Ulteriori aree su cui è possibile indagare: utilizzare la ricerca di percorsi bidirezionale per eseguire ricerche da entrambe le estremità. Non l'ho fatto ma, come altri hanno notato, questo potrebbe aiutare, ma non è privo di avvertimenti. L'altra cosa sulla mia lista da provare è la ricerca gerarchica di percorsi o la ricerca di percorsi di clustering. C'è una descrizione interessante nella documentazione di HavokAI Qui che descrive il loro concetto di clustering, che è diverso dalle implementazioni HPA * descritte qui .

Buona fortuna e facci sapere cosa trovi.


Se esistono agenti diversi con regole diverse, ma non troppi, questo può ancora essere generalizzato in modo abbastanza efficiente usando un vettore di ID, uno per classe di agente.
MSalter

4
+1 Per riconoscere che il problema è probabilmente aree interdette (non solo tessere impraticabili) e che questo tipo di problema può essere risolto più facilmente con i primi calcoli del tempo di caricamento.
Slipp D. Thompson,

Riempi o riempi BFS ogni area.
Wolfdawn,

Gli ID isola non devono essere statici. Esiste un semplice algoritmo che sarebbe adatto nel caso in cui sia necessario poter unire due isole separate, ma non può dividere successivamente un'isola. Le pagine da 8 a 20 di queste diapositive spiegano quel particolare algoritmo: cs.columbia.edu/~bert/courses/3137/Lecture22.pdf
kasperd

@kasperd ovviamente non c'è nulla che impedisca il ricalcolo degli ID dell'isola, che si fondono in fase di esecuzione. Il punto è che gli ID dell'isola ti consentono di confermare se esiste un percorso tra due nodi rapidamente senza effettuare ricerche intelligenti.
Steven

26

AStar è un algoritmo di pianificazione completo , il che significa che se esiste un percorso per il nodo, AStar è sicuro di trovarlo. Di conseguenza, deve controllare ogni percorso fuori dal nodo iniziale prima di poter decidere che il nodo obiettivo non è raggiungibile. Questo è molto indesiderabile quando si hanno troppi nodi.

Modi per mitigare questo:

  • Se si conosce a priori che un nodo non è raggiungibile (ad es. Non ha vicini o è contrassegnatoUnPassable ), ritorna No Pathsenza mai chiamare AStar.

  • Limitare il numero di nodi che AStar espanderà prima di terminare. Controlla il set aperto. Se mai diventa troppo grande, termina e ritorna No Path. Tuttavia, ciò limiterà la completezza di AStar; quindi può solo pianificare percorsi di lunghezza massima.

  • Limitare il tempo impiegato da AStar per trovare un percorso. Se scade il tempo, esci e ritorna No Path. Ciò limita la completezza come la strategia precedente, ma si adatta alla velocità del computer. Nota che per molti giochi questo è indesiderabile, perché i giocatori con computer più veloci o più lenti sperimenteranno il gioco in modo diverso.


13
Vorrei sottolineare che cambiare la meccanica del tuo gioco in base alla velocità della CPU (sì, la ricerca del percorso è una meccanica di gioco) potrebbe rivelarsi una cattiva idea perché può rendere il gioco abbastanza imprevedibile e in alcuni casi persino ingiocabile sui computer tra 10 anni. Quindi preferirei limitare A * limitando il set aperto piuttosto che dal tempo della CPU.
Philipp

@Philipp. Modificata la risposta per riflettere questo.
mklingen,

1
Si noti che è possibile determinare (ragionevolmente efficiente, O (nodi)) per un dato grafico la distanza massima tra due nodi. Questo è il problema del percorso più lungo e fornisce un limite superiore corretto per il numero di nodi da controllare.
MSalter

2
@MSalters Come si fa in O (n)? E cos'è "ragionevolmente efficiente"? Se questo è solo per coppie di nodi, non stai solo duplicando il lavoro?
Steven

Secondo Wikipedia, il problema del percorso più lungo è NP-difficile, sfortunatamente.
Desty,

21
  1. Esegui una doppia ricerca A * dal nodo di destinazione anche al contrario nello stesso loop e interrompi entrambe le ricerche non appena ne viene trovata una non risolvibile

Se il bersaglio ha solo 6 tessere accessibili attorno e l'origine ha 1002 tessere accessibili, la ricerca si fermerà a 6 (doppie) iterazioni.

Non appena una ricerca trova i nodi visitati dell'altra, puoi anche limitare l'ambito della ricerca ai nodi visitati dell'altra e terminare più velocemente.


2
L'implementazione di una stella a stella bidirezionale è molto più di quanto sia implicito nella sua dichiarazione, inclusa la verifica che l'euristica rimanga ammissibile in questa circostanza. (Link: homepages.dcc.ufmg.br/~chaimo/public/ENIA11.pdf )
Pieter Geerkens,

4
@StephaneHockenhull: dopo aver implementato un A- * bidirezionale su una carta del terreno con costi asimmetrici, ti assicuro che ignorare il bla bla accademico comporterà una selezione errata del percorso e calcoli errati dei costi.
Pieter Geerkens,

1
@MooingDuck: il numero totale di nodi è invariato e ogni nodo verrà comunque visitato solo una volta, quindi il caso peggiore di suddivisione della mappa esattamente a metà è identico a A- * unidirezionale.
Pieter Geerkens,

1
@PieterGeerkens: nel classico A *, sono raggiungibili solo metà dei nodi, e quindi visitati. Se la mappa viene divisa esattamente a metà, quando si cerca bidirezionalmente, si tocca (quasi) ogni nodo. Sicuramente un caso limite
Mooing Duck

1
@MooingDuck: ho parlato male; i casi peggiori sono grafici diversi, ma hanno lo stesso comportamento - il caso peggiore per unidirezionale è un nodo obiettivo completamente isolato, che richiede la visita di tutti i nodi.
Pieter Geerkens,

12

Supponendo che il problema sia che la destinazione non è raggiungibile. E che la mesh di navigazione non è dinamica. Il modo più semplice per farlo è avere un grafico di navigazione molto più scarso (abbastanza scarso che una corsa completa è relativamente veloce) e usare il grafico dettagliato solo se il percorso è possibile.


6
Questo è buono. Raggruppando le tessere in "regioni" e verificando prima se la regione in cui si trova la tessera può essere collegata alla regione in cui si trova l'altra tessera, è possibile eliminare i negativi molto più velocemente.
Konerak,

2
Corretto - generalmente rientra nell'HPA *
Steven il

@Steven Grazie, ero sicuro di non essere stato il primo a pensare a un simile approccio, ma non sapevo come si chiamava. Rende molto più facile sfruttare la ricerca preesistente.
ClassicThunder

3

Utilizzare più algoritmi con caratteristiche diverse

A * ha alcune belle caratteristiche. In particolare, trova sempre il percorso più breve, se esiste. Sfortunatamente, hai trovato anche alcune cattive caratteristiche. In questo caso, deve cercare in modo esauriente tutti i possibili percorsi prima di ammettere che non esiste alcuna soluzione.

Il "difetto" che stai scoprendo in A * è che non è a conoscenza della topologia. Potresti avere un mondo 2-D, ma non lo sa. Per quanto ne sa, nell'angolo più lontano del tuo mondo è una scala che lo porta proprio sotto il mondo a destinazione.

Prendi in considerazione la creazione di un secondo algoritmo che sia a conoscenza della topologia. Come primo passaggio, potresti riempire il mondo di "nodi" ogni 10 o 100 spazi, quindi mantenere un grafico di connettività tra questi nodi. Questo algoritmo trova un percorso trovando nodi accessibili vicino all'inizio e alla fine, quindi cercando di trovare un percorso tra loro sul grafico, se presente.

Un modo semplice per farlo sarebbe quello di assegnare ogni riquadro a un nodo. È banale dimostrare che è necessario assegnare un solo nodo a ciascun riquadro (non è mai possibile accedere a due nodi che non sono collegati nel grafico). Quindi i bordi del grafico sono definiti per essere ovunque due tessere con nodi diversi sono adiacenti.

Questo grafico ha uno svantaggio: non trova il percorso ottimale. Trova semplicemente un percorso. Tuttavia, ora ti ha mostrato che A * può trovare un percorso ottimale.

Fornisce anche un'euristica per migliorare le sottostime necessarie per far funzionare A *, perché ora sai di più sul tuo paesaggio. È meno probabile che tu debba esplorare completamente un vicolo cieco prima di scoprire che è necessario fare un passo indietro per andare avanti.


Ho motivo di credere che algoritmi come quelli di Google Maps funzionino in modo simile (sebbene più avanzato).
Cort Ammon - Ripristina Monica il

Sbagliato. A * è molto consapevole della topologia, attraverso la scelta dell'euristica ammissibile.
Salterio

Per quanto riguarda Google, nel mio precedente lavoro abbiamo analizzato le prestazioni di Google Maps e scoperto che non poteva essere stato A *. Riteniamo che utilizzino ArcFlags o altri algoritmi simili che si basano sulla preelaborazione delle mappe.
Salterio

@MSalters: questa è una linea sottile interessante da disegnare. Sostengo che A * non è a conoscenza della topologia perché riguarda solo i vicini più vicini. Direi che è più giusto dire che l'algoritmo che genera l'euristica ammissibile è consapevole della topologia, piuttosto che A * stesso. Prendi in considerazione un caso in cui è presente un diamante. A * prende un percorso per un po ', prima di eseguire il backup per provare l'altro lato del diamante. Non è possibile notificare ad A * che l'unica "uscita" da quel ramo è attraverso un nodo già visitato (salvataggio del calcolo) con l'euristica.
Cort Ammon - Ripristina Monica

1
Non posso parlare per Google Maps, ma Bing Map utilizza una stella bidirezionale parallela con punti di riferimento e disuguaglianza triangolare (ALT), con distanze pre-calcolate da (e a) un piccolo numero di punti di riferimento e ogni nodo.
Pieter Geerkens il

2

Altre idee in aggiunta alle risposte sopra:

  1. Risultati della cache della ricerca A *. Salvare i dati del percorso dalla cella A alla cella B e riutilizzarli se possibile. Questo è più applicabile nelle mappe statiche e dovrai lavorare di più con le mappe dinamiche.

  2. Cache i vicini di ogni cella. Un'implementazione * deve espandere ciascun nodo e aggiungere i suoi vicini al set aperto per la ricerca. Se questo vicino viene calcolato ogni volta piuttosto che memorizzato nella cache, potrebbe rallentare notevolmente la ricerca. E se non l'hai già fatto, usa una coda prioritaria per A *.


1

Se la tua mappa è statica, puoi semplicemente avere ciascuna sezione separata con un proprio codice e verificarlo prima di eseguire A *. Questo può essere fatto al momento della creazione della mappa o addirittura codificato nella mappa.

Le tessere impraticabili dovrebbero avere una bandiera e quando ci si sposta su una tessera del genere è possibile scegliere di non eseguire A * o scegliere una tessera accanto ad essa raggiungibile.

Se hai mappe dinamiche che cambiano frequentemente sei quasi sfortunato. Devi decidere in che modo interrompere il tuo algoritmo prima del completamento o eseguire controlli sulle sezioni che vengono chiuse frequentemente.


Questo è esattamente ciò che stavo suggerendo con un ID area nella mia risposta.
Steven

Puoi anche ridurre la quantità di CPU / tempo utilizzato se la tua mappa è dinamica, ma non cambia spesso. Vale a dire che è possibile ricalcolare gli ID area ogni volta che una porta bloccata viene sbloccata o bloccata. Dal momento che ciò accade di solito in risposta alle azioni di un giocatore, escluderesti almeno le aree bloccate di un sotterraneo.
uliwitness

1

Come posso fare in modo che A * concluda più rapidamente che un nodo è impraticabile?

Crea il tuo profilo Node.IsPassable() funzione, scopri le parti più lente, velocizzale.

Quando si decide se un nodo è passabile, posizionare le situazioni più probabili in alto, in modo che la maggior parte delle volte la funzione ritorni immediatamente senza preoccuparsi di verificare le possibilità più oscure.

Ma questo serve a rendere più veloce il controllo di un singolo nodo. È possibile creare un profilo per vedere quanto tempo viene impiegato per eseguire query sui nodi, ma sembra che il problema sia che vengono controllati troppi nodi.

quando faccio clic su una tessera invalicabile, l'algoritmo apparentemente attraversa l'intera mappa per trovare un percorso verso la tessera invalicabile

Se il riquadro di destinazione stesso è impraticabile, l'algoritmo non dovrebbe controllare alcun riquadro. Prima ancora di iniziare a tracciare il percorso, dovrebbe interrogare il riquadro di destinazione per verificare se è possibile e, in caso contrario, restituire un risultato senza percorso.

Se intendi che la destinazione stessa è passabile, ma è circondata da tessere impraticabili, in modo tale che non vi sia alcun percorso, è normale che A * controlli l'intera mappa. In quale altro modo potrebbe sapere che non esiste un percorso?

In quest'ultimo caso, puoi accelerarlo eseguendo una ricerca bidirezionale, in questo modo la ricerca che parte dalla destinazione può rapidamente scoprire che non esiste un percorso e interrompere la ricerca. Vedi questo esempio , circonda la destinazione con i muri e confronta la direzione bidirezionale con quella singola.


0

Fai il percorso indietro.

Se solo la tua mappa non ha grandi aree continue di tessere non raggiungibili, allora funzionerà. Invece di cercare l'intera mappa raggiungibile, la ricerca del percorso cercherà solo nell'area non raggiungibile inclusa.


Questo è ancora più lento se le tessere irraggiungibili sono più numerose delle tessere raggiungibili
Mooing Duck

1
@MooingDuck Le tessere irraggiungibili collegate intendi. Questa è una soluzione che funziona praticamente con qualsiasi progetto di mappa sano ed è molto facile da implementare. Non suggerirò nulla di più elaborato senza una migliore conoscenza del problema esatto, come il modo in cui l'implementazione di A * può essere così lenta che visitare tutte le tessere è in realtà un problema.
aaaaaaaaaaaa

0

Se le aree a cui il giocatore è collegato (nessun teletrasporto, ecc.) E le aree non raggiungibili non sono generalmente ben collegate, puoi semplicemente fare A * a partire dal nodo che vuoi raggiungere. In questo modo è ancora possibile trovare qualsiasi possibile percorso verso la destinazione e A * interromperà rapidamente la ricerca di aree non raggiungibili.


Il punto doveva essere più veloce del normale A *.
Heckel,

0

quando faccio clic su una tessera invalicabile , l'algoritmo apparentemente percorre l'intera mappa per trovare un percorso verso la tessera invalicabile, anche se mi trovo accanto ad essa.

Altre risposte sono fantastiche, ma devo sottolineare l'ovvio: non dovresti assolutamente eseguire il pathfinding su una tessera invalicabile.

Questa dovrebbe essere un'uscita anticipata dall'algo:

if not IsPassable(A) or not IsPasable(B) then
    return('NoWayExists');

0

Per verificare la distanza più lunga in un grafico tra due nodi:

(supponendo che tutti i bordi abbiano lo stesso peso)

  1. Esegui BFS da qualsiasi vertice v.
  2. Usa i risultati per selezionare un vertice più lontano v, lo chiameremo d.
  3. Esegui BFS da u.
  4. Trova il vertice più lontano u, lo chiameremo w.
  5. La distanza tra ue wè la distanza più lunga nel grafico.

Prova:

                D1                            D2
(v)---------------------------r_1-----------------------------(u)
                               |
                            R  | (note it might be that r1=r2)
                D3             |              D4
(x)---------------------------r_2-----------------------------(y)
  • Diciamo che la distanza tra yed xè maggiore!
  • Quindi secondo questo D2 + R < D3
  • Poi D2 < R + D3
  • Quindi la distanza tra ve xè maggiore di quella di ve u?
  • Quindi unon sarebbe stato scelto nella prima fase.

Ringraziamo il prof. Shlomi Rubinstein

Se si utilizzano bordi ponderati, è possibile ottenere la stessa operazione in un tempo polinomiale eseguendo Dijkstra anziché BFS per trovare il vertice più lontano.

Nota che suppongo sia un grafico collegato. Suppongo anche che non sia diretto.


Un * non è davvero utile per un semplice gioco basato su tessere 2D perché se capisco correttamente, supponendo che le creature si muovano in 4 direzioni, BFS otterrà gli stessi risultati. Anche se le creature possono muoversi in 8 direzioni, BFS pigro che preferisce i nodi più vicini al bersaglio otterrà comunque gli stessi risultati. A * è una modifica Dijkstra che è molto più computazionalmente costosa rispetto all'utilizzo di BFS.

BFS = O (| V |) presumibilmente O (| V | + | E |) ma non proprio nel caso di una mappa dall'alto verso il basso. A * = O (| V | log | V |)

Se abbiamo una mappa con solo 32 x 32 tessere, BFS avrà un costo massimo di 1024 e un vero A * potrebbe costarti un enorme 10.000. Questa è la differenza tra 0,5 secondi e 5 secondi, probabilmente di più se si tiene conto della cache. Quindi assicurati che il tuo A * si comporti come un pigro BFS che preferisce le tessere che sono più vicine al bersaglio desiderato.

Un * è utile per le mappe di navigazione dove il costo dei bordi è importante nel processo decisionale. In un semplice gioco basato su tessere aeree, il costo dei bordi non è probabilmente una considerazione importante. Se lo è, (diverse tessere hanno un costo diverso), puoi eseguire una versione modificata di BFS che posticipa e penalizza i percorsi che passano attraverso le tessere che rallentano il carattere.

Quindi sì, BFS> A * in molti casi quando si tratta di tessere.


Non sono sicuro di capire questa parte "Se abbiamo una mappa con solo 32 x 32 tessere, BFS costerà al massimo 1024 e un vero A * potrebbe costarti un enorme 10.000" Puoi spiegare come sei arrivato alla 10k numero per favore?
Kromster dice di sostenere Monica il

Cosa intendi esattamente con "pigro BFS che preferisce i nodi più vicini al bersaglio"? Intendi Dijkstra, semplice BFS o uno con un euristico (bene hai ricreato A * qui, o come fai a selezionare il prossimo nodo migliore da un set aperto)? Ciò log|V|nella complessità di A * deriva davvero dal mantenere quell'insieme aperto, o la dimensione della frangia, e per le mappe della griglia è estremamente piccolo - riguardo al log (sqrt (| V |)) usando la tua notazione. Il registro | V | si presenta solo in grafici iper-connessi. Questo è un esempio in cui un'applicazione ingenua della complessità del caso peggiore dà una conclusione errata.
congusbongus,

@congusbongus Questo è esattamente ciò che intendo. Non utilizzare un'implementazione vanilla di A *
wolfdawn

@KromStern Supponendo che tu usi l'implementazione vaniglia di A * per un gioco basato su tessere, ottieni complessità V * logV, V essendo il numero di tessere, per una griglia di 32 per 32 è 1024. logV, essendo ben approssimativamente il numero di bit necessario per rappresentare 1024 che è 10. Quindi finirai per correre più a lungo inutilmente. Naturalmente, se specializzi l'implementazione per sfruttare il fatto che stai correndo su una griglia di tessere, superi questa limitazione che è esattamente ciò a cui mi riferivo
Wolfdawn,
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.