Uppdaterad primer för att skapa isometriska världar, del 2

Vad du ska skapa

I den här sista delen av handledningsserien bygger vi på den första handledningen och lär dig om implementering av pickup, utlösare, nivåbyte, pathfinding, sökväg, nivårullning, isometrisk höjd och isometriska projektiler.

1. pickup

Pickup är saker som kan samlas in i nivån, vanligtvis genom att helt enkelt gå över dem, till exempel mynt, pärlor, pengar, ammunition mm.

Upphämtningsdata kan anpassas direkt till våra nivådata enligt nedan:

[[1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,8,0,0,1], [1,0,0, 8,0,1], [1,0,0,0,1,1], [1,1,1,1,1,1]]

I denna nivådata använder vi 8 att beteckna en pickup på en gräsplatta (1 och 0 representerar väggar och gångbara plattor, som tidigare). Detta kan vara en enda kakel bild med en gräs kakel överlagd med pickup bilden. Genom att följa denna logik behöver vi två olika kakelplattor för varje kakel som har en pickup, det vill säga en med pickup och en utan att visas efter pickupen samlas in.

Typisk isometrisk konst kommer att ha flera gångbara plattor - antar vi har 30. Ovanstående tillvägagångssätt innebär att om vi har N pickupar behöver vi N x 30 kakel utöver de 30 ursprungliga kakelarna, eftersom varje kakel måste ha en version med pickup och en utan. Detta är inte särskilt effektivt. i stället bör vi försöka att dynamiskt skapa dessa kombinationer. 

För att lösa detta kunde vi använda samma metod som vi brukade placera hjälten i första handledningen. När vi kommer över en hämtningsplatta lägger vi först en gräsplatta och lägger sedan hämtningen ovanpå gräsplattan. På så sätt behöver vi bara N pickupplattor förutom 30 gångbara plattor, men vi skulle behöva talvärden för att representera varje kombination i nivådata. För att lösa behovet av N x 30 representationsvärden kan vi hålla en separat pickupArray att uteslutande lagra uppsamlingsdata bortsett från levelData. Den färdiga nivån med hämtningen visas nedan:

För vårt exempel håller jag saker enkelt och använder inte en extra matris för pickup.

Plocka upp pickup

Upptäckning av pickup görs på samma sätt som att upptäcka kollisionsplattor, men efter flytta karaktären.

om (onPickupTile ()) pickupItem ();  funktion påPickupTile () // kolla om det finns en hämtning på hjälteplattan returnerar (levelData [heroMapTile.y] [heroMapTile.x] == 8);  

I funktionen onPickupTile (), vi kontrollerar om levelData array värde vid heroMapTile koordinat är en hämtningsplatta eller inte. Numret i levelData array vid det kakelkoordinatet betecknar typen av hämtning. Vi kontrollerar kollisioner innan vi flyttar karaktären, men måste kontrollera efter pickup efteråt, eftersom vid kollisioner karaktären inte ska uppta platsen om den redan är upptagen av kollisionsplattan, men vid pickup är karaktären fritt att flytta över den.

En annan sak att notera är att kollisionsdata vanligtvis aldrig ändras, men hämtningsdata ändras när vi hämtar ett objekt. (Detta innebär oftast bara att ändra värdet i levelData array från, säg, 8 till 0.)

Detta leder till ett problem: Vad händer när vi behöver starta om nivån, och sålunda återställa alla pickupar till sina ursprungliga positioner? Vi har inte informationen att göra detta, som levelData array har ändrats när spelaren plockade upp objekt. Lösningen är att använda en duplicerad array för nivån medan du spelar och för att behålla originalet levelData array intakt. Till exempel använder vi levelData och levelDataLive [], klona sistnämnden från den förstnämnda vid början av nivån och ändras bara levelDataLive [] under spel.

För exemplet spruter jag en slumpmässig hämtning på en ledig gräsplatta efter varje hämtning och ökning av pickupCount. De pickupItem funktionen ser ut så här.

funktion pickupItem () pickupCount ++; levelData [heroMapTile.y] [heroMapTile.x] = 0; // spawn nästa hämtning spawnNewPickup (); 

Du bör märka att vi söker efter pickup när karaktären är på den kakel. Detta kan hända flera gånger inom en sekund (vi kontrollerar endast när användaren rör sig, men vi kan gå runt och runda inom en kakel), men ovanstående logik kommer inte att misslyckas. sedan vi satte levelData array data till 0 Första gången vi upptäcker en pickup, alla efterföljande onPickupTile () kontrollerna kommer tillbaka falsk för den kakel. Kolla in det interaktiva exemplet nedan:

2. Triggerplattor

Som namnet antyder orsakar utlösningsplattor att något händer när spelaren stiger på dem eller trycker på en nyckel när de är på dem. De kan teleportera spelaren till en annan plats, öppna en grind eller kasta en fiende, för att ge några exempel. På en viss väg är pickups bara en speciell form av utlösningsplattor: när spelaren går på en kakel som innehåller ett mynt, försvinner myntet och deras mynträknare ökar.

Låt oss titta på hur vi kan implementera en dörr som tar spelaren till en annan nivå. Kakel bredvid dörren kommer att vara en utlösningsplatta; när spelaren trycker på x nyckeln, de går vidare till nästa nivå.

För att ändra nivåer är allt vi behöver göra att byta ström levelData array med den på den nya nivån och ställa in den nya heroMapTile position och riktning för hjältekaraktären. Antag att det finns två nivåer med dörrar för att tillåta passering mellan dem. Eftersom markplattan bredvid dörren kommer att vara utlösningsplattan på båda nivåerna, kan vi använda detta som den nya positionen för karaktären när de dyker upp i nivån.

Implementeringslogiken här är densamma som för pickup, och igen använder vi levelData array för att lagra triggervärden. För vårt exempel, 2 betecknar en dörrplatta, och värdet bredvid det är avtryckaren. jag har använt 101 och 102 med den grundläggande konventionen att alla plattor med ett värde större än 100 är en utlösningsplatta och värdet minus 100 kan vara den nivå som det leder till:

var level1Data = [[1,1,1,1,1,1], [1,1,0,0,0,1], [1,0,0,0,0,1], [2,102,0 , 0,0,1], [1,0,0,1,1,1], [1,1,1,1,1,1]]; var level2Data = [[1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,8,0,0,1], [1,0 0,0,0101,2], [1,0,1,0,0,1], [1,1,1,1,1,1]];

Koden för att söka efter en utlösningshändelse visas nedan:

var xKey = game.input.keyboard.addKey (Phaser.Keyboard.X); xKey.onUp.add (triggerListener); // lägg till en Signal lyssnare för upp händelse funktion triggerListener () var trigger = levelData [heroMapTile.y] [heroMapTile.x]; om (utlösare> 100) // giltig utlösningsplatta trigger- = 100; om (trigger == 1) // växla till nivå 1 levelData = level1Data;  annars // växla till nivå 2 levelData = level2Data;  för (var i = 0; i < levelData.length; i++)  for (var j = 0; j < levelData[0].length; j++)  trigger=levelData[i][j]; if(trigger>100) // hitta den nya utlösningsplattan och placera hjälten där heroMapTile.y = j; heroMapTile.x = i; heroMapPos = ny Phaser.Point (heroMapTile.y * tileWidth, heroMapTile.x * tileWidth); heroMapPos.x + = (tileWidth / 2); heroMapPos.y + = (tileWidth / 2); 

Funktionen triggerListener () kontrollerar om värdet för utlösningsdata array vid den givna koordinaten är större än 100. Om så är fallet, finner vi vilken nivå vi behöver byta till genom att subtrahera 100 från kakelvärdet. Funktionen finner utlösningsplattan i den nya levelData, vilket kommer att vara spötspositionen för vår hjälte. Jag har gjort avtryckaren aktiverad när x är släppte; om vi bara lyssnar på nyckeln trycks då hamnar vi i en slinga där vi byter mellan nivåer så länge som nyckeln hålls nere, eftersom karaktären alltid sprider sig i den nya nivån ovanpå en utlösningsplatta.

Här är en fungerande demo. Försök plocka upp saker genom att gå över dem och byta nivåer genom att stå bredvid dörrarna och slå x.

3. Projektiler

en projektil är något som rör sig i en viss riktning med en viss hastighet, som en kula, en magisk stavning, en boll osv. Allt om projektilen är detsamma som hjältekaraktären, förutom höjden: i stället för att rulla längs marken, projektiler flyter ofta över det vid en viss höjd. En kula kommer att röra sig över midjemåttet på karaktären, och även en boll kan behöva studsa runt.

En intressant sak att notera är att isometrisk höjd är densamma som höjd i en 2D sidovy, men mindre i värde. Det finns inga komplicerade omvandlingar inblandade. Om en boll är 10 pixlar ovanför marken i kartesiska koordinater kan den vara 10 eller 6 pixlar ovanför marken i isometriska koordinater. (I vårt fall är den aktuella axeln y-axeln.)

Låt oss försöka implementera en boll studsar i vårt murade gräsmark. Som en touch av realism lägger vi till en skugga för bollen. Allt vi behöver göra är att lägga till hopphöjdsvärdet till det isometriska Y-värdet av vår boll. Höjdhöjdsvärdet ändras från ram till ram beroende på gravitationen, och när bollen träffar marken vippar vi nuvarande hastighet längs y-axeln.

Innan vi klarar av att studsa i ett isometriskt system ser vi hur vi kan implementera det i ett 2D-kartesiskt system. Låt oss representera bollens hoppkraft med en variabel zValue. Föreställ dig att till att börja med har bollen en hoppkraft på 100, så zValue = 100

Vi använder två ytterligare variabler: incrementValue, som börjar vid 0, och allvar, som har ett värde av -1. Varje ram vi subtraherar incrementValue från zValue, och subtrahera allvar från incrementValue för att skapa en dämpningseffekt. När zValue når 0, det betyder att bollen har nått marken; vid denna tidpunkt vänder vi tecknet på incrementValue genom att multiplicera den med -1, gör det till ett positivt tal. Detta innebär att bollen kommer att röra sig uppåt från nästa ram, så studsande.

Så här ser det ut i kod:

om (game.input.keyboard.isDown (Phaser.Keyboard.X)) zValue = 100;  incrementValue- = gravitation; zValue- = incrementValue; if (zValue<=0) zValue=0; incrementValue*=-1; 

Koden förblir densamma för den isometriska vyn, med den lilla skillnaden som du kan använda ett lägre värde för zValue till att börja med. Se nedan hur zValue läggs till det isometriska y värdet av bollen under återgivning.

funktion drawBallIso () var isoPt = ny Phaser.Point (); // Det är inte tillrådligt att skapa poäng i uppdateringsslinga var ballCornerPt = nya Phaser.Point (ballMapPos.x-ball2DVolume.x / 2, ballMapPos.y-ball2DVolume .y / 2); isoPt = cartesianToIsometric (ballCornerPt); // hitta ny isometrisk position för hjälte från 2D kartposition gameScene.renderXY (ballShadowSprite, isoPt.x + borderOffset.x + shadowOffset.x, isoPt.y + borderOffset.y + shadowOffset.y, false ); // draw skugga för att göra textur gameScene.renderXY (ballSprite, isoPt.x + borderOffset.x + ballOffset.x, isoPt.y + borderOffset.y-ballOffset.y-zValue, false); // teckna hjälte för att göra textur 

Kolla in det interaktiva exemplet nedan:

Förstår att den roll som skuggan spelar är en mycket viktig som lägger till realismen i denna illusion. Observera också att vi nu använder de två skärmkoordinaterna (x och y) för att representera tre dimensioner i isometriska koordinater. Y-axeln i skärmkoordinater är också z-axeln i isometriska koordinater. Detta kan vara förvirrande!

4. Hitta och följa en väg

Pathfinding och sökväg följer ganska komplicerade processer. Det finns olika sätt att använda olika algoritmer för att hitta vägen mellan två punkter, men som vår levelData är en 2D-array, saker är enklare än de annars skulle kunna vara. Vi har väldefinierade och unika noder som spelaren kan uppta, och vi kan enkelt kontrollera om de är walkable.

relaterade inlägg

  • A * Pathfinding för nybörjare
  • Målbaserad Vector Field Pathfinding
  • Snabba upp en * Pathfinding med Jump Point Search Algorithm
  • "Path Following" styrningsbeteende

En detaljerad översikt över sökningsalgoritmer ligger utanför ramen för denna artikel, men jag kommer att försöka förklara det vanligaste sättet det fungerar: den kortaste sökalgoritmen, av vilken A * och Dijkstras algoritmer är kända implementeringar.

Vi strävar efter att hitta noder som ansluter en startnod och en slutkod. Från startnoden besöker vi alla åtta angränsande noder och markerar dem alla som besökte; denna kärnprocess upprepas för varje nybesökt nod, rekursivt. 

Varje tråd spårar de besökta noderna. När du hoppar till angränsande noder, övergår knutpunkter som redan har besökts (rekursionen stannar); Annars fortsätter processen tills vi når slutkoden, där rekursionen slutar och den fulla sökvägen följer tillbaka som en nodmatris. Ibland nås aldrig noden, i vilket fall sökningen misslyckas. Vi brukar sluta hitta flera vägar mellan de två noderna, i vilket fall vi tar den med det minsta antalet noder.

pathfinding

Det är oklokt att återuppfinna hjulet när det gäller väldefinierade algoritmer, så vi skulle använda befintliga lösningar för våra sökresultat. För att kunna använda Phaser behöver vi en JavaScript-lösning, och den jag har valt är EasyStarJS. Vi initierar sökvägsmotorn enligt nedan.

easystar = nya EasyStar.js (); easystar.setGrid (levelData); easystar.setAcceptableTiles ([0]); easystar.enableDiagonals (); // vi vill att sökvägen ska ha diagonaler easystar.disableCornerCutting (); // ingen diagonal väg när man går i vägghängen

Som vår levelData har bara 0 och 1, vi kan direkt överföra det som noddatabasen. Vi bestämmer värdet på 0 som den gångbara noden. Vi aktiverar diagonal gångförmåga men inaktiverar detta när vi går nära hörnen av icke-gångbara plattor. 

Detta beror på att hjälten kan, om den är aktiverad, skära in i den icke-gångbara plattan medan man gör en diagonal promenad. I så fall tillåter vår kollisionsdetektering inte hjälten att passera igenom. Också observera att i exemplet har jag fullständigt avlägsnat kollisionsdetektering eftersom det inte längre är nödvändigt för ett AI-baserat promenadexempel. 

Vi kommer att upptäcka kranen på någon fri kakel inuti nivån och beräkna banan med hjälp av findPath fungera. Återuppringningsmetoden plotAndMove mottar nodraden för den resulterande banan. Vi markerar Mini med den nyfunna sökvägen.

game.input.activePointer.leftButton.onUp.add (findPath) funktion findPath () om (isFindingPath || isWalking) returnera; var pos = game.input.activePointer.position; var isoPt = ny Phaser.Point (pos.x-borderOffset.x, pos.y-borderOffset.y); tapPos = isometricToCartesian (isoPt); tapPos.x- = tileWidth / 2; // justering för att hitta rätt bricka för fel på grund av avrundning tapPos.y + = brickbredd / 2; tapPos = getTileCoordinates (tapPos, tileWidth); if (tapPos.x> -1 && tapPos.y> -1 && tapPos.x<7&&tapPos.y<7)//tapped within grid if(levelData[tapPos.y][tapPos.x]!=1)//not wall tile isFindingPath=true; //let the algorithm do the magic easystar.findPath(heroMapTile.x, heroMapTile.y, tapPos.x, tapPos.y, plotAndMove); easystar.calculate();    function plotAndMove(newPath) destination=heroMapTile; path=newPath; isFindingPath=false; repaintMinimap(); if (path === null)  console.log("No Path was found."); else path.push(tapPos); path.reverse(); path.pop(); for (var i = 0; i < path.length; i++)  var tmpSpr=minimap.getByName("tile"+path[i].y+"_"+path[i].x); tmpSpr.tint=0x0000ff; //console.log("p "+path[i].x+":"+path[i].y);   

Sökväg följande

När vi har vägen som en nodmatris måste vi göra tecknet följa det.

Säg att vi vill göra karaktären till en kakel som vi klickar på. Vi måste först leta efter en väg mellan noden som karaktären för närvarande upptar och noden där vi klickade på. Om en lyckad sökväg hittas måste vi flytta tecknet till den första noden i noddatabasen genom att ange den som destination. När vi når destinationskoden kontrollerar vi om det finns några fler noder i nodmatrisen och, om så, ställer in nästa nod som destination - och så vidare tills vi når den slutliga noden.

Vi kommer också att ändra spelarens riktning baserat på nuvarande nod och den nya destinationsnoden varje gång vi når en nod. Mellan noder går vi bara i önskad riktning tills vi når destinationskoden. Detta är en mycket enkel AI, och i exemplet görs det i metoden aiWalk visas delvis nedan.

funktion aiWalk () om (path.length == 0) // sökväg har slutat om (heroMapTile.x == destination.x && heroMapTile.y == destination.y) dX = 0; dY = 0; isWalking = false; lämna tillbaka;  isWalking = true; om (heroMapTile.x == destination.x && heroMapTile.y == destination.y) // nådde nuvarande destination, ställa in ny, ändra riktning // vänta tills vi är några steg i kakel innan vi vänder stegTaken ++; if (stepsTakendestination.x) dX = -1;  annars dX = 0;  om (heroMapTile.ydestination.y) dY = -1;  annat dY = 0;  om (heroMapTile.x == destination.x) dX = 0;  annars om (heroMapTile.y == destination.y) dY = 0;  // ...

Vi do måste filtrera bort giltiga klickpunkter genom att bestämma om vi har klickat inom det gångbara området, snarare än en väggplatta eller annan icke-gångbar kakel.

En annan intressant punkt för kodning av AI: vi vill inte att karaktären ska vända mot nästa kakel i noddatabasen så fort han har kommit till den nuvarande, eftersom en sådan omedelbar tur leder till att vår karaktär går på gränserna för plattor. I stället borde vi vänta tills karaktären är några steg inuti kakel innan vi letar efter nästa destination. Det är också bättre att manuellt placera hjälten i mitten av den aktuella plattan strax innan vi vänder, för att få det hela att känna sig perfekt.

Kolla in den fungerande demo nedan:

5. Isometrisk rullning

När nivåområdet är mycket större än det tillgängliga skärmområdet måste vi göra det skrolla.

Det synliga skärmområdet kan betraktas som en mindre rektangel inom den större rektangeln av hela nivåområdet. Rullning är i huvudsak bara att flytta den inre rektangeln inuti den större. Vanligtvis, när sådan rullning händer, är hjältepositionen densamma med avseende på skärmrektangeln, vanligtvis vid skärmcentret. Intressant är att allt vi behöver för att genomföra rullning är att spåra hörnpunkten för den inre rektangeln.

Denna hörnpunkt, som vi representerar i kartesiska koordinater, kommer att falla inom en kakel i nivådata. För rullning ökar vi x- och y-positionen för hörnpunkten i kartesiska koordinater. Nu kan vi konvertera denna punkt till isometriska koordinater och använda den för att rita skärmen. 

De nykonverterade värdena, i isometrisk rymd, måste också vara hörnet på vår skärm, vilket innebär att de är nya (0, 0). Så, medan vi analyserar och ritar nivådata, subtraherar vi detta värde från den isometriska positionen för varje kakel och kan bestämma om kakelens nya position faller inom skärmen. 

Alternativt kan vi bestämma att vi kommer att dra enbart en X x Y isometriska kakelnät på skärmen för att göra ritningslingan effektiv för större nivåer. 

Vi kan uttrycka detta i steg som så:

  • Uppdatera kartesiska hörnpunktens x- och y-koordinater.
  • Konvertera detta till isometriskt utrymme.
  • Subtrahera detta värde från den isometriska dragpositionen för varje kakel.
  • Rita bara ett begränsat fördefinierat antal kakel på skärmen från det här nya hörnet.
  • Valfritt: Rita endast plattan om den nya isometriska dragpositionen faller inom skärmen.
var cornerMapPos = ny Phaser.Point (0,0); var cornerMapTile = ny Phaser.Point (0,0); var visibleTiles = ny Phaser.Point (6,6); // ... funktionsuppdatering () // ... om (isWalkable ()) heroMapPos.x + = heroSpeed ​​* dX; heroMapPos.y + = heroSpeed ​​* dY; // flytta hörnet i motsatt riktning cornerMapPos.x - = heroSpeed ​​* dX; cornerMapPos.y - = heroSpeed ​​* dY; cornerMapTile = getTileCoordinates (cornerMapPos, tileWidth); // Hämta den nya hjältekartan heroMapTile = getTileCoordinates (heroMapPos, tileWidth); // deepsort & dra ny scen renderScene ();  funktion renderScene () gameScene.clear (); // rensa tidigare bild och dra sedan igen var tileType = 0; // låt oss begränsa slingorna inom synligt område var startTileX = Math.max (0,0-cornerMapTile.x); var startTileY = Math.max (0,0-cornerMapTile.y); var endTileX = Math.min (levelData [0] .length, startTileX + visibleTiles.x); var endTileY = Math.min (levelData.length, startTileY + visibleTiles.y); startTileX = Math.max (0, endTileX-visibleTiles.x); startTileY = Math.max (0, endTileY-visibleTiles.y); // kolla för gränsvillkor för (var i = startTileY; i < endTileY; i++)  for (var j = startTileX; j < endTileX; j++)  tileType=levelData[i][j]; drawTileIso(tileType,i,j); if(i==heroMapTile.y&&j==heroMapTile.x) drawHeroIso();     function drawHeroIso() var isoPt= new Phaser.Point();//It is not advisable to create points in update loop var heroCornerPt=new Phaser.Point(heroMapPos.x-hero2DVolume.x/2+cornerMapPos.x,heroMapPos.y-hero2DVolume.y/2+cornerMapPos.y); isoPt=cartesianToIsometric(heroCornerPt);//find new isometric position for hero from 2D map position gameScene.renderXY(sorcererShadow,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);//draw shadow to render texture gameScene.renderXY(sorcerer,isoPt.x+borderOffset.x+heroWidth, isoPt.y+borderOffset.y-heroHeight, false);//draw hero to render texture  function drawTileIso(tileType,i,j)//place isometric level tiles var isoPt= new Phaser.Point();//It is not advisable to create point in update loop var cartPt=new Phaser.Point();//This is here for better code readability. cartPt.x=j*tileWidth+cornerMapPos.x; cartPt.y=i*tileWidth+cornerMapPos.y; isoPt=cartesianToIsometric(cartPt); //we could further optimise by not drawing if tile is outside screen. if(tileType==1) gameScene.renderXY(wallSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y-wallHeight, false); else gameScene.renderXY(floorSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y, false);  

Observera att hörnpunkten ökas i motsatt riktning till hjälten positionsuppdatering när han rör sig. Detta säkerställer att hjälten stannar var han är med avseende på skärmen. Kolla in det här exemplet (använd pilar för att bläddra, tryck för att öka synligt rutnät).

Ett par anteckningar:

  • När du rullar kan vi behöva dra ytterligare kakel på skärmens gränser, annars kan vi se att kakel försvinna och uppträder vid skärmens ytterligheter.
  • Om du har kakel som tar upp mer än ett utrymme, måste du rita fler kakel vid gränserna. Om till exempel, om den största plattan i hela uppsättningen mäter X efter Y, måste du rita X fler plattor till vänster och höger och Y flera plattor till toppen och botten. Detta gör att hörnen på den större plattan fortfarande är synliga när du rullar in eller ut ur skärmen.
  • Vi måste fortfarande se till att vi inte har tomma områden på skärmen medan vi drar nära gränsen till nivån.
  • Nivån ska bara bläddra tills den mest extrema kakeln dras på motsvarande skärm extrema. Därefter ska tecknet fortsätta att flytta på skärmutrymmet utan nivån rullar. För detta måste vi spåra alla fyra hörnen på den inre skärmrektangeln och smörja in rullnings- och spelarrörelsens logik i enlighet därmed. Är du ute efter utmaningen att försöka implementera det själv?

Slutsats

Denna serie riktar sig särskilt till nybörjare som försöker utforska isometriska spelvärldar. Många av de förklaringar som förklaras har alternativa tillvägagångssätt som är lite mer komplicerade, och jag har medvetet valt de enklaste. 

De kanske inte uppfyller de flesta scenarier som du kan stöta på, men kunskapen som erhålls kan användas för att bygga på dessa begrepp för att skapa mer komplicerade lösningar. Till exempel kommer den enkla djupsorteringen att genomföras bryta när vi har flera våningar och plattformsplattor som går från en historia till en annan. 

Men det är en handledning för en annan gång.