Een bijgewerkte primer voor het creëren van isometrische werelden, deel 1

Wat je gaat creëren

We hebben allemaal ons deel van geweldig gespeeld isometrische spellen, of het nu de originele Diablo of Age of Empires of Commandos is. De eerste keer dat je een isometrisch spel tegenkwam, had je je misschien afgevraagd of het een was 2D-spel of a 3D-spel of iets heel anders. De wereld van isometrische games heeft ook een mystieke aantrekkingskracht op game-ontwikkelaars. Laten we proberen het mysterie van de isometrische projectie te ontrafelen en in deze tutorial een eenvoudige isometrische wereld proberen te creëren.

Deze zelfstudie is een bijgewerkte versie van mijn bestaande zelfstudie over het maken van isometrische werelden. De originele tutorial gebruikte Flash met ActionScript en is nog steeds relevant voor Flash- of OpenFL-ontwikkelaars. In deze nieuwe tutorial heb ik besloten om Phaser met JS-code te gebruiken, waardoor interactieve HTML5-uitvoer wordt gecreëerd in plaats van SWF-uitvoer. 

Houd er rekening mee dat dit geen Phaser-ontwikkelingstool is, maar we gebruiken Phaser eenvoudig om de kernconcepten van het maken van een isometrische scène te communiceren. Bovendien zijn er veel betere en eenvoudigere manieren om isometrische inhoud in Phaser te maken, zoals de Phaser Isometric Plugin. 

Voor de eenvoud zullen we de op tegels gebaseerde benadering gebruiken om onze isometrische scène te creëren.

1. Tegelgebaseerde spellen

In 2D-games waarbij de tegelmethode wordt gebruikt, wordt elk visueel element opgesplitst in kleinere stukjes, tegels genaamd, van een standaardformaat. Deze tegels worden zodanig ingedeeld dat ze de spelwereld vormen volgens vooraf vastgestelde gegevens op niveaus, meestal een tweedimensionale matrix.

gerelateerde berichten

  • Tony's tutorials op basis van tegels

Meestal tegel-gebaseerde spellen gebruiken een ondersteboven weergave of een zijaanzicht voor de gamescène. Laten we een standaard top-down 2D-weergave overwegen met twee tegels-a gras tegel en een muurtegel-zoals hier getoond:

Beide tegels zijn vierkante afbeeldingen van dezelfde grootte, vandaar de tegel hoogte en tegel breedte zijn hetzelfde. Laten we eens kijken naar een spelniveau dat een grasland is dat aan alle kanten omsloten wordt door muren. In een dergelijk geval zien de niveaugegevens die worden weergegeven met een tweedimensionale array er als volgt uit:

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

Hier, 0 geeft een grastegel aan en 1 geeft een muurtegel aan. Het rangschikken van de tegels op basis van de niveaugegevens zal ons ommuurd grasland produceren zoals in de afbeelding hieronder wordt getoond:

We kunnen iets verder gaan door hoektegels en afzonderlijke verticale en horizontale wandtegels toe te voegen, waarvoor vijf extra tegels nodig zijn, wat ons naar onze bijgewerkte niveau-gegevens leidt:

[[3,1,1,1,1,4], [2,0,0,0,0,2], [2,0,0,0,0,2], [2,0,0, 0,0,2], [2,0,0,0,0,2], [6,1,1,1,1,5]]

Bekijk de afbeelding hieronder, waar ik de tegels heb gemarkeerd met hun overeenkomstige tegelnummers in de levelgegevens:

Nu we het concept van de op tegels gebaseerde benadering hebben begrepen, wil ik u laten zien hoe we een eenvoudige 2D-pseudo-code kunnen gebruiken om ons niveau weer te geven:

voor (i, doorlopende rijen) voor (j, doorlopende kolommen) x = j * tegelbreedte y = i * tegelhoogte tileType = levelData [i] [j] placetile (tileType, x, y)

Als we de bovenstaande tegelafbeeldingen gebruiken, zijn de tegelbreedte en tegelhoogte gelijk (en hetzelfde voor alle tegels) en komen overeen met de afmetingen van de tegelafbeeldingen. Dus de tegelbreedte en tegelhoogte voor dit voorbeeld zijn beide 50 px, wat de totale niveaugrootte van 300 x 300 px is, dat wil zeggen zes rijen en zes kolommen met tegels van elk 50 x 50 px.

Zoals eerder besproken, implementeren we bij een normale op tegels gebaseerde benadering een bovenaanzicht of een zijaanzicht; voor een isometrische weergave moeten we de isometrische projectie.

2. Isometrische projectie

De beste technische verklaring van wat isometrische projectie betekent, voor zover ik weet, uit dit artikel van Clint Bellanger:

We richten onze camera langs twee assen (draai de camera 45 graden naar één kant en dan 30 graden naar beneden). Dit creëert een ruitvormig ruitvormig raster waarin de rasteroppervlakken tweemaal zo breed zijn als ze groot zijn. Deze stijl werd gepopulariseerd door strategiespellen en actie-RPG's. Als we in deze weergave naar een kubus kijken, zijn drie zijden zichtbaar (bovenste en twee tegenoverliggende zijden).

Hoewel het een beetje ingewikkeld klinkt, is het implementeren van deze weergave heel eenvoudig. Wat we moeten begrijpen is de relatie tussen de 2D-ruimte en de isometrische ruimte, dat wil zeggen, de relatie tussen de niveau-gegevens en de weergave; de transformatie van boven naar beneden cartesiaanse coördinaten van isometrische coördinaten. De onderstaande afbeelding toont de visuele transformatie:

Isometrische tegels plaatsen

Laat me proberen de relatie te vereenvoudigen tussen niveau-gegevens die zijn opgeslagen als een 2D-array en de isometrische weergave - dat wil zeggen, hoe we cartesiaanse coördinaten in isometrische coördinaten transformeren. We zullen proberen om de isometrische weergave te maken van ons nu beroemde ommuurde grasland. De 2D-weergave-implementatie van het niveau was een eenvoudige iteratie met twee lussen, waarbij vierkante tegels werden geplaatst die met de vaste tegelhoogte en tegelbreedtewaarden waren verschoven. Voor de isometrische weergave blijft de pseudo-code hetzelfde, maar de placeTile () functie veranderingen.

De originele functie tekent alleen de tegelafbeeldingen op de voorziene coördinaten X en Y, maar voor een isometrisch aanzicht moeten we de bijbehorende isometrische coördinaten berekenen. De vergelijkingen om dit te doen zijn als volgt, waar isoX en isoY representeren isometrische x- en y-coördinaten, en cartX en Carty representeren cartesiaanse x- en y-coördinaten:

// Cartesisch naar isometrisch: isoX = cartX - cartY; isoY = (cartX + cartY) / 2;
// Isometrisch naar Cartesisch: cartX = (2 * isoY + isoX) / 2; cartY = (2 * isoY - isoX) / 2; 

Ja dat is het. Deze eenvoudige vergelijkingen zijn de magie achter de isometrische projectie. Hier zijn Phaser-helperfuncties die kunnen worden gebruikt om van het ene systeem naar het andere te converteren met behulp van het zeer handige Punt klasse:

function cartesianToIsometric (cartPt) var tempPt = nieuwe Phaser.Point (); tempPt.x = cartPt.x-cartPt.y; tempPt.y = (cartPt.x cartPt.y +) / 2; terugkeer (tempPt); 
function isometricToCartesian (isoPt) var tempPt = nieuwe Phaser.Point (); tempPt.x = (2 * + isoPt.y isoPt.x) / 2; tempPt.y = (2 * isoPt.y-isoPt.x) / 2; terugkeer (tempPt);  

Dus we kunnen de gebruiken cartesianToIsometric hulpmethode om de binnenkomende 2D-coördinaten om te zetten in isometrische coördinaten in de placeTile methode. Afgezien hiervan blijft de weergavecode hetzelfde, maar we moeten nieuwe afbeeldingen voor de tegels hebben. We kunnen de oude vierkante tegels die worden gebruikt voor onze top-down weergave niet gebruiken. De onderstaande afbeelding toont de nieuwe isometrische gras- en wandtegels samen met het gerenderde isometrische niveau:

Ongelooflijk, nietwaar? Laten we eens kijken hoe een typische 2D-positie wordt omgezet in een isometrische positie:

2D-punt = [100, 100]; // isometrisch punt wordt berekend zoals hieronder isoX = 100 - 100; // = 0 isoY = (100 + 100) / 2; // = 100 Iso punt == [0, 100];

Evenzo een invoer van [0, 0] zal resulteren in [0, 0], en [10, 5] zal geven [5, 7.5].

Voor ons ommuurde grasland kunnen we een beloopbaar gebied bepalen door te controleren of het array-element dat is 0 op die coördinaat en geeft daarmee het gras aan. Hiervoor moeten we de array-coördinaten bepalen. We kunnen de coördinaten van de tile vinden in de niveaudata van zijn cartesiaanse coördinaten met behulp van deze functie:

functie getTileCoordinates (cartPt, tileHeight) var tempPt = new Phaser.Point (); tempPt.x = Math.floor (cartPt.x / tileHeight); tempPt.y = Math.floor (cartPt.y / tileHeight); return (tempPt); 

(Hier veronderstellen we in wezen dat de tegelhoogte en de tegelbreedte gelijk zijn, zoals in de meeste gevallen.) 

Vandaar dat we vanaf een paar scherm (isometrische) coördinaten tegelcoördinaten kunnen vinden door te bellen naar:

getTileCoordinates (isometricToCartesian (schermpunt), tegelhoogte);

Dit schermpunt zou bijvoorbeeld een muisklikpositie of een oppikpositie kunnen zijn.

Registratie punten

In Flash kunnen we willekeurige punten instellen voor een afbeelding als middelpunt of [0,0]. Het Phaser-equivalent is dat spil. Wanneer u de afbeelding op zeg plaatst [10,20], dan dit spil punt zal worden uitgelijnd met [10,20]. Standaard wordt de linkerbovenhoek van een afbeelding beschouwd als de afbeelding [0,0] of spil. Als u het bovenstaande niveau probeert te maken met behulp van de opgegeven code, krijgt u het weergegeven resultaat niet. In plaats daarvan krijg je een vlak land zonder de muren, zoals hieronder:

Dit komt omdat de tegelafbeeldingen verschillende formaten hebben en we niet het hoogte-kenmerk van de wandtegel aanspreken. De onderstaande afbeelding toont de verschillende tegelafbeeldingen die we gebruiken met hun selectiekaders en een witte cirkel waar hun standaardwaarde [0,0] is:

Zie hoe de held uitgelijnd raakt bij tekenen met de standaard draaipunten. Let ook op hoe we de hoogte van de wandtegel verliezen als deze wordt getekend met standaard draaipunten. De afbeelding rechts laat zien hoe ze op de juiste manier moeten worden uitgelijnd, zodat de wandtegel de hoogte krijgt en de held in het midden van de grastegel wordt geplaatst. Dit probleem kan op verschillende manieren worden opgelost.

  1. Maak alle tegels op dezelfde afbeeldingsgrootte met de afbeelding op de juiste manier in de afbeelding uitgelijnd. Dit creëert veel lege gebieden binnen elke tegelafbeelding.
  2. Stel draaipunten handmatig in voor elke tegel, zodat ze op de juiste manier worden uitgelijnd.
  3. Teken tegels met specifieke verschuivingen zodat ze correct uitlijnen.

Voor deze zelfstudie heb ik ervoor gekozen om de derde methode te gebruiken, zodat dit ook werkt met een kader zonder de mogelijkheid om draaipunten in te stellen.

3. Verplaatsen in isometrische coördinaten

We zullen nooit proberen om ons karakter of projectiel direct in isometrische coördinaten te verplaatsen. In plaats daarvan zullen we onze gegevens over de gamewereld in cartesiaanse coördinaten manipuleren en alleen de bovenstaande functies gebruiken om die op het scherm bij te werken. Als u bijvoorbeeld een teken vooruit wilt verplaatsen in de positieve y-richting, kunt u het eenvoudig verhogen Y eigenschap in 2D-coördinaten en converteer de resulterende positie vervolgens naar isometrische coördinaten:

y = y + snelheid; placetile (cartesianToIsometric (nieuwe Phaser.Point (x, y)))

Dit is een goed moment om alle nieuwe concepten te bekijken die we tot nu toe hebben geleerd en om een ​​werkend voorbeeld te geven van iets dat beweegt in een isometrische wereld. U kunt de benodigde afbeeldingsitems vinden in de middelen map van de bron git repository.

Dieptesortering

Als je de balafbeelding in onze ommuurde tuin probeerde te verplaatsen, dan zou je de problemen tegenkomen dieptesortering. Naast de normale plaatsing, moeten we ervoor zorgen dieptesortering voor het tekenen van de isometrische wereld, als er bewegende elementen zijn. Een juiste dieptesortering zorgt ervoor dat items dichter bij het scherm bovenop items verder weg worden getekend.

De eenvoudigste dieptesorteermethode is eenvoudigweg om de carthesische y-coördinaatwaarde te gebruiken, zoals vermeld in deze snelle tip: hoe verder op het scherm het object staat, hoe eerder het getekend zou moeten worden. Dit kan goed werken voor heel eenvoudige isometrische scènes, maar een betere manier is om de isometrische scène opnieuw te tekenen zodra een beweging plaatsvindt, volgens de matrixcoördinaten van de tegel. Laat me dit concept in detail uitleggen met onze pseudo-code voor niveautekenen:

voor (i, doorlopende rijen) voor (j, doorlopende kolommen) x = j * tegelbreedte y = i * tegelhoogte tileType = levelData [i] [j] placetile (tileType, x, y)

Stel je voor dat ons item of personage op de tegel staat [1,1]-dat is de bovenste groene tegel in de isometrische weergave. Om het niveau goed te kunnen tekenen, moet het personage worden getekend na het tekenen van de hoekmuurtegel, zowel de linker- als de rechtermuurtegel, en de grondtegel, zoals hieronder:

Als we onze tekenlus volgen volgens de bovenstaande pseudo-code, tekenen we de middelste hoekmuur eerst en blijven we alle muren in het gedeelte rechtsboven tekenen totdat deze de juiste hoek bereikt. 

Vervolgens tekent het in de volgende lus de muur links van het personage en vervolgens de grastegel waarop het personage staat. Zodra we vaststellen dat dit de tegel is die ons karakter bezet, zullen we het personage tekenen na tekening van de grastegel. Op deze manier, als er muren op de drie vrije grastegels zijn verbonden met de tegel waarop het personage staat, overlappen die wanden het personage, wat resulteert in een juiste diepte gesorteerd renderen.

4. Creëren van de Art

Isometrische kunst kan pixelkunst zijn, maar dat hoeft het niet te zijn. Wanneer het gaat om isometrische pixelart, vertelt de gids van RhysD u bijna alles wat u moet weten. Sommige theorieën zijn ook te vinden op Wikipedia.

Bij het maken van isometrische kunst zijn de algemene regels:

  • Begin met een leeg isometrisch raster en volg de pixel-perfecte precisie.
  • Probeer kunst te breken in isometrische tegelbeelden.
  • Probeer ervoor te zorgen dat elke tegel een van beide is beloopbaar of non-beloopbaar. Het zal gecompliceerd zijn als we een enkele tegel nodig hebben die zowel beloopbare als niet-beloopbare gebieden bevat.
  • De meeste tegels moeten naadloos in één of meerdere richtingen worden geplaatst.
  • Schaduwen kunnen lastig zijn om te implementeren, tenzij we een gelaagde benadering gebruiken waarbij we schaduwen op de grondlaag tekenen en vervolgens de held (of bomen of andere objecten) op de bovenste laag tekenen. Als de benadering die je gebruikt niet meerlagig is, zorg dan dat de schaduwen naar voren vallen zodat ze niet op de held vallen als hij achter een boom staat.
  • In het geval dat u een tegelafbeelding groter dan de standaard isometrische tegelgrootte wilt gebruiken, probeert u een dimensie te gebruiken die een veelvoud is van de iso-tegelgrootte. Het is beter om in dergelijke gevallen een gelaagde aanpak te hebben, waarbij we de kunst in verschillende stukken kunnen splitsen op basis van de hoogte. Een boom kan bijvoorbeeld in drie stukken worden verdeeld: de wortel, de stam en het gebladerte. Dit maakt het gemakkelijker om dieptes te sorteren, omdat we stukken in overeenkomstige lagen kunnen tekenen die overeenkomen met hun hoogte.

Isometrische tegels die groter zijn dan de afmetingen van de enkele tegel, veroorzaken problemen bij het sorteren van de diepte. Sommige van de problemen worden besproken in deze links:

gerelateerde berichten

  • Grotere tegels
  • Splitsen en Painter's algoritme
  • OpenSpace's bericht over effectieve manieren om grotere tegels op te splitsen

5. Isometrische karakters

Eerst moeten we bepalen hoeveel bewegingsrichtingen in ons spel zijn toegestaan. Meestal bieden games vierwegen of acht bewegingen. Bekijk de afbeelding hieronder om de correlatie tussen de 2D-ruimte en de isometrische ruimte te begrijpen:

Houd er rekening mee dat een personage verticaal omhoog beweegt wanneer we op de pijltje omhoog toets een topdown-spel in, maar voor een isometrisch spel beweegt het personage in een hoek van 45 graden naar de rechterbovenhoek. 

Voor een weergave van bovenaf kunnen we één set personaganimaties in één richting maken en deze eenvoudig voor alle andere roteren. Voor isometrische tekens moeten we elke animatie opnieuw weergeven in elk van de toegestane richtingen, dus voor een beweging in acht richtingen moeten we acht animaties maken voor elke actie. 

Voor een beter begrip geven we meestal de richtingen Noord, Noordwest, West, Zuidwest, Zuid, Zuidoost, Oost en Noordoost aan. De personagekaders hieronder tonen inactieve beelden vanuit Zuidoost en met de klok mee:

We zullen tekens op dezelfde manier plaatsen als waarop we tegels hebben geplaatst. De beweging van een personage wordt bereikt door de beweging in cartesiaanse coördinaten te berekenen en vervolgens om te zetten in isometrische coördinaten. Laten we aannemen dat we het toetsenbord gebruiken om het personage te besturen.

We zullen twee variabelen instellen, dX en dY, gebaseerd op de richtingstoetsen ingedrukt. Standaard zijn deze variabelen 0 en wordt bijgewerkt volgens de onderstaande grafiek, waar UDR, en L duiden op de omhoognaar benedenRechts, en Links pijltoetsen, respectievelijk. Een waarde van 1 onder een toets geeft aan dat de toets wordt ingedrukt; 0 impliceert dat de toets niet wordt ingedrukt.

 Sleutel Pos UDRL dX dY ================ 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 0 -1 0 0 1 0 1 0 0 0 0 1 -1 0 1 0 1 0 1 1 1 0 0 1 -1 1 0 1 1 0 1 -1 0 1 0 1 -1 -1

Nu, met behulp van de waarden van dX en dY, we kunnen de Cartesiaanse coördinaten bijwerken zoals:

newX = currentX + (snelheid dX *); newY = currentY + (dY * speed);

Zo dX en dY staan ​​voor de verandering in de x- en y-posities van het karakter, gebaseerd op de ingedrukte toetsen. We kunnen eenvoudig de nieuwe isometrische coördinaten berekenen, zoals we al hebben besproken:

Iso = cartesianToIsometric (nieuwe Phaser.Point (newX, newY))

Zodra we de nieuwe isometrische positie hebben, moeten we dat doen verhuizing het personage naar deze positie. Gebaseerd op de waarden waar we voor hebben dX en dY, we kunnen beslissen in welke richting het personage kijkt en de corresponderende personagekunst gebruiken. Nadat het teken is verplaatst, vergeet dan niet om het niveau opnieuw te schilderen met de juiste dieptesortering omdat de tegelcoördinaten van het teken mogelijk zijn gewijzigd.

Collision Detection

Botsingsdetectie wordt uitgevoerd door te controleren of de tegel op de nieuw berekende positie een niet-beloopbare tegel is. Dus zodra we de nieuwe positie hebben gevonden, verplaatsen we het personage daar niet meteen, maar kijk eerst welke tegel die ruimte inneemt.

tile coordinate = getTileCoordinates (isometricToCartesian (current position), tile height); if (isWalkable (tile coordinate)) moveCharacter ();  else // niets doen; 

In de functie isWalkable (), we controleren of de niveau gegevensarraywaarde op de gegeven coördinaat een beloopbare tegel is of niet. We moeten ervoor zorgen dat de richting waarin het personage staat, wordt bijgewerkt-zelfs als hij niet beweegt, zoals in het geval dat hij een niet-betreedbare tegel raakt.

Dit klinkt misschien als een goede oplossing, maar het werkt alleen voor items zonder volume. Dit komt omdat we alleen een enkel punt beschouwen, wat het middelpunt is van het karakter, om de botsing te berekenen. Wat we echt moeten doen, is alle vier de hoeken van het personage vinden op basis van de beschikbare 2D-middelpuntcoördinaten en botsingen berekenen voor al deze punten. Als een hoek in een niet-betreedbare tegel valt, moeten we het personage niet verplaatsen.

Dieptesortering met karakters

Beschouw een personage en een boomtegel in de isometrische wereld, en zij beide hebben dezelfde beeldformaten, hoe onrealistisch dat ook klinkt.

Om dieptesortering goed te begrijpen, moeten we begrijpen dat telkens wanneer de x- en y-coördinaten van het karakter minder zijn dan die van de boom, de boom het karakter overlapt. Wanneer de x- en y-coördinaten van het karakter groter zijn dan die van de boom, overlapt het teken de boom. Als ze dezelfde x-coördinaat hebben, dan bepalen we alleen op basis van de y-coördinaat: de waarde van de y-coördinaat die het hoogste is, overlapt de andere. Wanneer ze dezelfde y-coördinaat hebben, dan bepalen we alleen op basis van de x-coördinaat: welke de hogere x-coördinaat heeft overlapt de andere.

Zoals eerder uitgelegd, is een vereenvoudigde versie hiervan om gewoon opeenvolgend de niveaus te tekenen vanaf de verste tegel - dat wil zeggen, tegel [0] [0]-en teken vervolgens alle tegels in elke rij één voor één. Als een personage een tegel inneemt, tekenen we eerst de grondtegel en maken vervolgens de tekentegel. Dit werkt prima, omdat het personage geen wandtegel kan bezetten.

6. Demotijd!

Dit is een demo in Phaser. Klik om je te concentreren op het interactieve gebied en gebruik je pijltjestoetsen om het personage te verplaatsen. U kunt twee pijltoetsen gebruiken om in diagonale richtingen te bewegen.

Je vindt de volledige bron voor de demo in de bronrepository voor deze tutorial.