Bookmark and Share

Svårighetsgrad
Svårighetsgrad
Betyg (1 röster)
BetygBetygBetygBetygBetyg
 

A* - En algoritm för bästa vägen

Inledning

A*, eller A-stjärna, är en algoritm för att hitta bästa vägen. Som många vet så är ju bästa vägen på en karta den så kallade fågelvägen från punkt A till punkt B. Problemet är att det kan finnas hinder mellan punkt A och B. Det kan också vara som så att det finns terräng som tar längre tid att korsa än annan terräng, t.ex. borde det vara skillnad på asfalterad väg och träskmarker.

För att hitta en optimal väg, oavsett om det är för fiender eller spelare, så måste vi komma fram till något bättre än endast fågelvägen. Vi kommer i denna artikel att gå igenom A* och förklara hur algoritmen fungerar

bild

I bilden ovan så har vi en karta med hinder. Vi ska gå från punkt A (grön) till punkt B (röd). Den snabbaste och enkla vägen (röd linje) går ej att gå. Vi måste hitta en framkomlig väg till målet (grön linje).

Karta och data

För att beräkna en bästa väg så måste det finnas en karta. I ett enkelt scenario så har vi en 2D-karta uppbyggd av så kallade "tiles" (plattor). Plattorna kan antingen vara väg eller vägg, dvs. antingen kan vi gå på dem eller inte.

För att beräkna bästa vägen behöver vi en datarepresentation av något som vi kallar "nod" så vi skapar en klass Node för detta. En nod är en koordinat tagen från vår karta fast med lite mer information. Vi kikar på klassen.

Det är främst datan vi ska förstå i denna klass. X, Y är koordinaten på kartan för noden. G, H, F är beräknad data som beräknas i A* algoritmen. G är kostnaden att komma till denna nod, dvs. hur lång tid har det tagit att komma hit.

Vad kostar det då att gå från ett enda steg?

Det beror på vilken modell vi använder. Vi tänker oss att en förflyttning i vertikal eller horisontell led kostar 10 poäng. Diagonal förflyttning kostar 14p. Anledningen till detta är att du helt enkelt rör dig längre med ett steg diagonalt då du både rör dig 1 steg i sidled och 1 steg vertikalt. Talet 14 kommer från beräkning av diagonalen med Pythagoras sats, 10*10 + 10*10 = 200. Roten ur 200 = roten ur 2 * 10. Alltså en faktor roten ur 2 längre vilket vi avrundar till 1.4.

bild

Här kan vi då göra modellen av kartan lite mer avancerad och låta kostnaden för förflyttning bero på underlaget. I vårt exempel gör vi inte detta utan låter all förflyttning i grunden kosta lika mycket men ändå kompensera för diagonal förflyttning.

H är ett så kallat heuristiskt värde. Ett finare ord för ett uppskattat värde som kan se som en tumregel men är likväl ett ofullständigt värde. Den uppskattning vi beräknar H till är avståndet vi har kvar till målet. Här använder vi inte fågelvägen som beräkning utan använder något som kallas för Manhattangeometri. Med "Manhattanavståndet" så beräknar vi avståndet utan att tillåta diagonal förflyttning. Enkelt sett kan vi se det som antal förflyttningar i sidled + antalet vertikala förflyttningar summerat. Därefter multiplicerat med 10.

Vilket heuristik vi använder kommer att påverka hur A* presterar. Vi har valt att använda Manhattangeometri, dels för att det är en vanlig implementation men också för att man slipper en massa kostsamma beräkningar.

F är helt enkelt G + H och används för att avgöra vilken nod som hittills verkar bäst. Valet av variabelnamn kan tyckas dåliga. Faktum är att algoritmen i sitt ursprung har betecknat dessa data som G, H, F och det har blivit kotym.

Sx, Sy talar om från vilken annan position på kartan man kom ifrån för att hamna här. Kostnaden, G, beräknas alltså som G från "föregående" nod plus förflyttningen hit. Som vi tidigare diskuterade så beror kostnaden på om det var en diagonal förflyttning eller ej. Koden nedan reder ut problemet.

Resterande variabler, MapType, IsVisited, Time, används inte av A* egentligen. Vi använder dessa till som hjälp för att visualisera beräkningarna och den väg som ska visas.

Hjärtat i A*

Vi vill hitta en väg från punkt A (x1, y1) till punkt B (x2, y2).

I förväg har vi översatt kart-data till en struktur med noder som vi kallar MapData.

Algoritmen jobbar med två listor: En för "öppna" noder och en för "stängda" noder.

Den öppna listan är noder som måste undersökas innan vi med säkerhet kan säga att vi hittat den bästa vägen. Den stängda listan är noder som vi beräkningsmässigt är klara med och som vi inte vill återvända till igen.

Vi börjar med att lägga till en enda nod i listan för de öppna noderna. Denna nod är startpunkten (x1, y1). Kostnaden att komma hit är 0 och avståndet till målet är Manhattanavståndet. Vi håller också reda på om vi har nått målet eller ej med variabeln finished.

Algoritmen börjar med en loop som formuleras; "så länge vi inte är klara och vi har noder kvar att undersöka".

Vi tar sedan den bästa noden i listan över öppna noder och undersöker alla nodens nod-grannar om:

Nod-grannarna undersöks sedan:

1) Om de redan finns i listan över stängda noder. Om så är fallet så uppdaterar vi den stängda noden om det visar sig att vi kunde nå den stängda noden snabbare via den "bästa" noden vi just nu undersöker.

2) Om de inte finns i den öppna listan så beräknar vi G, H och stoppar in dem i den öppna listan.

När alla nod-grannar har undersökt så läggs den hittills bästa noden i listan över stängda noder. Om den bästa noden var målet så är vi nu klara. Därefter tar vi bort den hittills bästa noden från den öppna listan.

Vi hoppas att detta låter begripligt och presenterar nu koden för algoritmen.

På rad 10 använder vi Aggregate som en tjusig LINQ-metod för att hitta den bästa noden utan att sortera om listan. Ordningen för noderna är mycket viktig för att algoritmen ska fungera. Förklaringen till detta är tyvärr mycket avancerad, läs mer i avslutningen.

Ok men var är bästa vägen?

När algoritmen är klar så finns det ett spår utlagt i listan över stängda noder. Den sista noden i listan är där vi hamnade. Förhoppningsvis hittade vi till målet men det är inte alls säkert. Vi kan ha varit instängda på kartan! Vägen som vi tagit till målet måste vi nu spåra tillbaka via noderna i den stängda listan. Vi plockar egentligen inte ut bästa vägen i vårt exempel men metoden PlotRoute markerar den i datan.

Implementationen

Vi har valt att göra implementationen i 3 olika projekt. Ett klassbibliotek kallat AStarLogic som vi sedan använder oss av både i ett Console Application och ett Windows Game (XNA) projekt.

Alltså koden som du kan ladda hem går att köra på två olika sätt.

I XNA kan du bygga om kartan med vänster och höger musknapp genom att sätta eller ta bort väggar. Start sätts ut med knappen Q och målet med knappen E. Tryck sedan ENTER för att illustrera beräkningarna. Se videon nedan.

Kör du Console-varianten så blir det lite mer sparsamt men ändå en presentation.

bild

Avslutning

Vill man fördjupa sig i olika bästa-vägen-algoritmer eller underöka hur A* prestera i olika varianter så rekommenderar vi fortsatt läsning från Stanford University om A*.

Komplexiteten för att beräkna bästa vägen är rätt hög. Du vill alltså inte göra detta för ofta med allt för stora kartor. Det finns en uppsjö av varianter på A* som förbättrar prestandan under olika speciella förutsättningar. Du kanske också måste beräkna om bästa vägen ibland då saker och ting rör sig på kartan. Det finns också en uppsjö av olika strategier för att beräkna om bästa vägen då vi har rörelse på kartan.

Tänk på att denna implementation som vi erbjuder är gjord delvis för att kunna presentera och illustrera beräkningarna grafiskt. En del saker i implementationen har alltså inte så mycket med just algoritmen att göra. Vi har ändå försökt hålla koden avskalad och begriplig.

Viktiga begrepp

  • heuristiskt
  • Manhattangeometri

Kommentarer

1 inlägg