In deze tutorial geef ik je een uitgebreid overzicht van wat je moet weten om isometrische werelden te maken. U leert wat de isometrische projectie is en hoe u isometrische niveaus als 2D-arrays vertegenwoordigt. We zullen relaties formuleren tussen de weergave en de logica, zodat we objecten gemakkelijk op het scherm kunnen manipuleren en tegeldetectie op tegelbasis kunnen verwerken. We zullen ook kijken naar dieptesortering en karakteranimatie.
Om je spelontwikkeling te versnellen, kun je een reeks isometrische game-items vinden op Envato Market, klaar om te gebruiken in je game.
Isometrische spelactiva op Envato Market gerelateerde berichtenWilt u nog meer tips over het maken van isometrische werelden? Bekijk de follow-up post, Creating Isometric Worlds: A Primer voor Gamedevs, vervolg, en het boek van Juwal, Starling Game Development Essentials.
Isometrische weergave is een weergavemethode die wordt gebruikt om een illusie van 3D te creëren voor een anders 2D-spel - soms ook wel pseudo 3D of 2.5D. Deze afbeeldingen (ontleend aan Diablo 2 en Age of Empires) illustreren wat ik bedoel:
Diablo 2 Age of EmpiresHet implementeren van een isometrische weergave kan op veel manieren worden gedaan, maar omwille van de eenvoud zal ik me concentreren op een tegel-gebaseerde aanpak, de meest efficiënte en meest gebruikte methode. Ik heb bovenstaande schermafbeelding met een ruitpatroon over elkaar heen gelegd om te laten zien hoe het terrein is opgesplitst in tegels.
In de benadering op basis van tegels wordt elk visueel element opgesplitst in kleinere stukken, tegels genaamd, van een standaardformaat. Deze tegels worden zo ingedeeld dat ze de spelwereld vormen volgens vooraf bepaalde niveaudata - meestal een 2D-array.
gerelateerde berichtenLaten we bijvoorbeeld een standaard top-down 2D-weergave overwegen met twee tegels - een grastegel en een muurtegel - zoals hier getoond:
Enkele eenvoudige tegelsDeze tegels hebben elk dezelfde grootte als elkaar en zijn elk vierkant, dus de tegelhoogte en de tegelbreedte zijn hetzelfde.
Voor een niveau met grasland dat aan alle kanten omsloten is door muren, ziet de 2D-array van de gegevens 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 niveaudata levert de afbeelding op het onderstaande niveau op:
We kunnen dit verbeteren door hoektegels en afzonderlijke verticale en horizontale wandtegels toe te voegen, waarvoor vijf extra tegels nodig zijn:
[[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]]Verbeterd niveau met tegelnummers
Ik hoop dat het concept van de tegel-gebaseerde aanpak nu duidelijk is. Dit is een eenvoudige 2D-rasterimplementatie, die we als volgt kunnen coderen:
voor (i, doorlopende rijen) voor (j, doorlopende kolommen) x = j * tegelbreedte y = i * tegelhoogte tileType = levelData [i] [j] placetile (tileType, x, y)
Hier gaan we ervan uit dat de tegelbreedte en tegelhoogte gelijk zijn (en hetzelfde voor alle tegels) en overeenkomen met de afmetingen van de tegelafbeeldingen. De tegelbreedte en tegelhoogte voor dit voorbeeld zijn dus beide 50px, wat de totale niveaugrootte van 300 x 300 pixels weergeeft, dat wil zeggen zes rijen en zes kolommen met tegels van elk 50x50 px.
Bij een normale, op tegels gebaseerde benadering implementeren we ofwel een bovenaanzicht of een zijaanzicht; voor een isometrische weergave moeten we de isometrische projectie.
De beste technische uitleg over wat "isometrische projectie" betekent, voor zover ik weet, is 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 eenvoudig. Wat we moeten begrijpen is de relatie tussen 2D-ruimte en de isometrische ruimte - dat wil zeggen, de relatie tussen de niveau-gegevens en weergave; de transformatie van "cartesiaanse" top-down-coördinaten naar isometrische coördinaten.
Cartesisch raster versus isometrisch raster.(We overwegen geen hexagonale tegeltechniek, wat een andere manier is om isometrische werelden te implementeren.)
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 omzetten in isometrische coördinaten.
We zullen proberen om de isometrische weergave te maken voor onze in de muur ingesloten weideveiligheidsgegevens:
[[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]]
In dit scenario 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. De 2D-weergave-implementatie van het bovenstaande niveau was een eenvoudige iteratie met twee lussen, waarbij vierkante tegels werden geplaatst die met de vaste tegelhoogte en tegelbreedtewaarden werden verrekend.
voor (i, doorlopende rijen) voor (j, doorlopende kolommen) x = j * tegelbreedte y = i * tegelhoogte tileType = levelData [i] [j] placetile (tileType, x, y)
Voor de isometrische weergave blijft de code hetzelfde, maar de placeTile ()
functie veranderingen.
Voor een isometrisch aanzicht moeten we de bijbehorende isometrische coördinaten binnen de lussen 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;
Deze functies laten zien hoe je van het ene systeem naar het andere kunt converteren:
function isoTo2D (pt: Point): Point var tempPt: Point = new Point (0, 0); tempPt.x = (2 * pt.y + pt.x) / 2; tempPt.y = (2 * pt.y - pt.x) / 2; return (tempPt);
function twoDToIso (pt: Point): Point var tempPt: Point = new Point (0,0); tempPt.x = pt.x - pt.y; tempPt.y = (pt.x + pt.y) / 2; return (tempPt);
De pseudocode voor de lus ziet er dan als volgt uit:
voor (i, doorlopende rijen) voor (j, doorlopende kolommen) x = j * tegelbreedte y = i * tegelhoogte tileType = levelData [i] [j] placetile (tileType, twoDToIso (nieuw punt (x, y) ))Ons in de muur afgesloten grasland in een isometrisch aanzicht.
Laten we als voorbeeld eens kijken hoe een typische 2D-positie wordt omgezet in een isometrische positie:
2D-punt = [100, 100]; // twoDToIso (2D-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]
.
De bovenstaande methode stelt ons in staat om een directe correlatie te creëren tussen de gegevens op 2D-niveau en de isometrische coördinaten. We kunnen de coördinaten van de tile vinden in de niveaudata van zijn cartesiaanse coördinaten met behulp van deze functie:
functie getTileCoordinates (pt: Point, tileHeight: Number): Point var tempPt: Point = new Point (0, 0); tempPt.x = Math.floor (pt.x / tileHeight); tempPt.y = Math.floor (pt.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 (isoTo2D (schermpunt), tegelhoogte);
Dit schermpunt zou bijvoorbeeld een muisklikpositie of een oppikpositie kunnen zijn.
Tip: Een andere plaatsingsmethode is het Zigzag-model, dat een totaal andere aanpak heeft.
Beweging is heel eenvoudig: je manipuleert je gamewereldgegevens in Cartesiaanse coördinaten en gebruikt gewoon de bovenstaande functies om het 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 en zet dan zijn positie om in isometrische coördinaten:
y = y + snelheid; placetile (twoDToIso (nieuw punt (x, y)))
Naast de normale plaatsing, moeten we ervoor zorgen dieptesortering voor het tekenen van de isometrische wereld. Dit zorgt ervoor dat items dichter bij de speler worden geplaatst op items die verder weg liggen.
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 werkt goed zolang we geen sprites hebben die meer dan één tegelruimte innemen.
De meest efficiënte manier voor dieetsortering voor isometrische werelden is om alle tegels in standaard single-tile dimensies te breken en geen grotere afbeeldingen toe te staan. Hier is bijvoorbeeld een tegel die niet past in de standaard tegelgrootte - zie hoe we deze kunnen opsplitsen in meerdere tegels die passen bij de afmetingen van de tegel:
Een groot beeld wordt opgesplitst in meerdere tegels van standaard isometrische afmetingenIsometrische 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
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 berichtenHet implementeren van tekens in de isometrische weergave is niet ingewikkeld, omdat het kan klinken. Tekenkunst moet volgens bepaalde standaarden worden gemaakt. Eerst moeten we bepalen hoeveel bewegingsrichtingen in ons spel zijn toegestaan - meestal bieden games vierweg- of acht-wegsbeweging.
Acht richtingen navigatie-instructies in top-down en isometrische weergaven.Voor een weergave van bovenaf kunnen we een reeks personaganimaties in één richting maken en deze eenvoudig roteren voor alle andere. Voor isometrische karaktertekeningen moeten we elke animatie in elke toegestane richting opnieuw renderen - dus voor beweging in acht richtingen moeten we acht animaties maken voor elke actie. Voor het gemak van begrip geven we meestal de richting aan als Noord, Noordwest, West, Zuidwest, Zuid, Zuidoost, Oost en Noordoost, tegen de klok in, in die volgorde.
Een isometrisch karakter dat in verschillende richtingen is gericht.We plaatsen personages op dezelfde manier waarop we tegels plaatsen. 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 U
, D
, R
en L
duiden op de omhoog, naar beneden, Rechts en Links pijltoetsen, respectievelijk. Een waarde van 1
onder een toets staat voor die toets die 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 als volgt bijwerken:
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 = twoDToIso (nieuw punt (newX, nieuwY))
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.
Botsingsdetectie wordt uitgevoerd door te controleren of de tegel op de berekende nieuwe positie een niet-betreedbare 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 (isoTo2D (iso point), 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.
Beschouw een personage en een boomtegel in de isometrische wereld.
Voor een goed begrip van dieptesortering moeten we begrijpen dat wanneer de x- en y-coördinaten van het karakter minder zijn dan die van de boom, de boom het teken 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.
Een vereenvoudigde versie hiervan is om gewoon opeenvolgend de niveaus te tekenen vanaf de verste tegel - dat wil zeggen, tegel [0] [0]
- 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.
Dieptesortering moet worden uitgevoerd elke keer dat een tegel van positie verandert. We moeten het bijvoorbeeld doen wanneer personages bewegen. Vervolgens werken we de weergegeven scène bij, na het uitvoeren van de dieptesortering, om de dieptewijzigingen weer te geven.
Gebruik uw nieuwe kennis nu goed door een werkend prototype te maken, met toetsenbordbedieningen en juiste dieptesortering en botsingsdetectie. Hier is mijn demo:
Klik om de SWF-focus te geven en gebruik vervolgens de pijltoetsen. Klik hier voor de versie op ware grootte.
Wellicht vindt u deze utiliteitsklasse nuttig (ik heb het in AS3 geschreven, maar u zou het in een andere programmeertaal moeten kunnen begrijpen):
pakket com.csharks.juwalbose import flash.display.Sprite; import flash.geom.Point; public class IsoHelper / ** * converteer een isometrisch punt naar 2D * * / public static function isoTo2D (pt: Point): Point // gx = (2 * isoy + isox) / 2; // gy = (2 * isoy-isox) / 2 var tempPt: Point = new Point (0,0); tempPt.x = (2 * + pt.y pt.x) / 2; tempPt.y = (2 * pt.y-pt.x) / 2; return (tempPt); / ** * converteer een 2d punt naar isometrisch * * / public static function twoDToIso (pt: Point): Point // gx = (isox-isoxy; // gy = (isoy + isox) / 2 var tempPt: Point = nieuw punt (0,0); tempPt.x = pt.x-pt.y; tempPt.y = (pt.x + pt.y) / 2; return (tempPt); / ** * converteer een 2d wijs naar specifieke tegelrij / kolom * * / openbare statische functie getTileCoordinates (pt: Point, tileHeight: Number): Point var tempPt: Point = new Point (0,0); tempPt.x = Math.floor (pt.x / tileHeight); tempPt.y = Math.floor (pt.y / tileHeight); return (tempPt); / ** * converteer specifieke tegelrij / kolom naar 2d punt * * / public static function get2dFromTileCoordinates (pt: Point, tileHeight: Number): Point var tempPt: Point = new Point (0,0); tempPt.x = pt.x * tileHeight; tempPt.y = pt.y * tileHeight; return (tempPt);
Als je echt vastloopt, is hier de volledige code van mijn demo (in Flash en AS3 tijdlijncodeformulier):
// Gebruikt senocular's KeyObject-klasse // http://www.senocular.com/flash/actionscript/?file=ActionScript_3.0/com/senocular/utils/KeyObject.as import flash.display.Sprite; import com.csharks.juwalbose.IsoHelper; import flash.display.MovieClip; import flash.geom.Point; import flash.filters.GlowFilter; import flash.events.Event; import com.senocular.utils.KeyObject; import flash.ui.Keyboard; import flash.display.Bitmap; import flash.display.BitmapData; import flash.geom.Matrix; import flash.geom.Rectangle; var levelData = [[1,1,1,1,1,1], [1,0,0,2,0,1], [1,0,1,0,0,1], [1,0 , 0,0,0,1], [1,0,0,0,0,1], [1,1,1,1,1,1]]; var tileWidth: uint = 50; var borderOffsetY: uint = 70; var borderOffsetX: uint = 275; var facing: String = "south"; var currentFacing: String = "south"; var hero: MovieClip = new herotile (); hero.clip.gotoAndStop (gerichte); var heroPointer: Sprite; var-sleutel: KeyObject = nieuw KeyObject (fase); // Senocular KeyObject Class var heroHalfSize: uint = 20; // de tegels var grassTile: MovieClip = new TileMc (); grassTile.gotoAndStop (1); var wallTile: MovieClip = new TileMc (); wallTile.gotoAndStop (2); // de canvas var bg: Bitmap = nieuwe Bitmap (nieuwe BitmapData (650,450)); addChild (bg); var rect: Rectangle = bg.bitmapData.rect; // om de overlay van de diepte var te behandelenContainer: Sprite = nieuwe Sprite (); addChild (overlayContainer); // om richting te manipuleren var dX: Number = 0; var dY: Number = 0; var idle: Boolean = true; var snelheid: uint = 5; var heroCartPos: Point = new Point (); var heroTile: Point = new Point (); // items toevoegen aan startniveau, game-lusfunctie toevoegen createLevel () var tileType: uint; for (var i: uint = 0; iRegistratie punten
Besteed speciale aandacht aan de registratiepunten van de tegels en de held. (Registratiepunten kunnen worden beschouwd als de oorsprongspunten voor elke specifieke sprite.) Deze vallen over het algemeen niet in de afbeelding, maar zijn eerder de linkerbovenhoek van het begrenzingsvak van de sprite..
We zullen onze tekencode moeten wijzigen om de registratiepunten correct te corrigeren, voornamelijk voor de held.
Collision Detection
Een ander interessant punt om op te merken is dat we botsdetectie berekenen op basis van het punt waar de held is.
Maar de held heeft volume en kan niet nauwkeurig worden weergegeven door een enkel punt, dus we moeten de held als een rechthoek voorstellen en controleren op botsingen tegen elke hoek van deze rechthoek, zodat er geen overlappingen zijn met andere tegels en dus geen dieptedefecten.
shortcuts
In de demo herschrijf ik de scène gewoon opnieuw elk frame op basis van de nieuwe positie van de held. We vinden de tegel die de held inneemt en teken de held bovenop de grondtegel wanneer de renderlussen die tegels bereiken.
Maar als we dichterbij komen, zullen we merken dat het niet nodig is om alle tegels in dit geval door te lussen. De grastegels en de bovenste en linker muurtegels worden altijd getekend voordat de held wordt getekend, dus we hoeven deze helemaal niet opnieuw te tekenen. Ook zijn de onderste en rechtse wandtegels altijd voor de held en dus getekend na de held is getekend.
In wezen hoeven we dus alleen dieptesortering uit te voeren tussen de muur binnen het actieve gebied en de held - dat wil zeggen, twee tegels. Door dit soort snelkoppelingen op te merken, bespaart u veel verwerkingstijd, wat van cruciaal belang kan zijn voor de prestaties.
Conclusie
Inmiddels zou je een geweldige basis moeten hebben voor het bouwen van isometrische games van jezelf: je kunt de wereld en de objecten erin weergeven, niveaugegevens weergeven in eenvoudige 2D-arrays, converteren tussen cartesiaanse en isometrische coördinaten en omgaan met concepten zoals dieptesortering en karakteranimatie. Geniet van het maken van isometrische werelden!
gerelateerde berichten
- Isometrische werelden maken: een primer voor gamedevs, vervolg
- Snelle tip: Goedkope 'n' eenvoudige isometrische niveaus
- Isometrische tegels Math
- 6 Ongelofelijk gedetailleerde gidsen voor spelontwikkeling en -ontwerp voor beginners