Un primer aggiornato per la creazione di mondi isometrici, parte 1

Cosa starai creando

Tutti abbiamo giocato la nostra giusta parte di sorprendenti giochi isometrici, si tratti del Diablo originale, o Age of Empires o Commandos. La prima volta che ti sei imbattuto in un gioco isometrico, potresti esserti chiesto se fosse un Gioco 2D o a Gioco 3D o qualcosa di completamente diverso. Il mondo dei giochi isometrici ha la sua mistica attrazione anche per gli sviluppatori di giochi. Cerchiamo di svelare il mistero della proiezione isometrica e provare a creare un semplice mondo isometrico in questo tutorial.

Questo tutorial è una versione aggiornata del mio tutorial esistente sulla creazione di mondi isometrici. L'esercitazione originale utilizzava Flash con ActionScript ed è ancora pertinente per gli sviluppatori Flash o OpenFL. In questo nuovo tutorial ho deciso di utilizzare Phaser con il codice JS, creando in tal modo un output HTML5 interattivo anziché l'output SWF. 

Si prega di notare che questo non è un tutorial di sviluppo di Phaser, ma stiamo semplicemente usando Phaser per comunicare facilmente i concetti chiave della creazione di una scena isometrica. Inoltre, esistono modi molto migliori e più semplici per creare contenuti isometrici in Phaser, come il Phaser Isometric Plugin. 

Per semplicità, utilizzeremo l'approccio basato su tessere per creare la nostra scena isometrica.

1. Giochi basati su tessere

Nei giochi 2D che utilizzano l'approccio basato su tessere, ogni elemento visivo è suddiviso in pezzi più piccoli, chiamati tessere, di dimensioni standard. Queste tessere saranno disposte in modo da formare il mondo del gioco in base a dati di livello predeterminati, di solito una matrice bidimensionale.

Post correlati

  • Tutorial su piastrelle di Tony Pa

Solitamente i giochi basati su tessere usano a dall'alto al basso vista o a vista laterale per la scena del gioco. Consideriamo una vista 2D top-down standard con due tessere-a tessera erba e a piastrella da muro-come mostrato qui:

Entrambe queste tessere sono immagini quadrate della stessa dimensione, da cui il altezza delle piastrelle e larghezza delle piastrelle sono uguali Prendiamo in considerazione un livello di gioco che è un prato racchiuso su tutti i lati da mura. In tal caso, i dati di livello rappresentati con una matrice bidimensionale avranno il seguente aspetto:

[[1,1,1,1,1,1], [1,0,0,0,0,1,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]]

Qui, 0 denota una tessera erba e 1 denota un muro di piastrelle. Organizzare le tessere in base ai dati di livello produrrà la nostra prateria murata come mostrato nell'immagine qui sotto:

Possiamo andare un po 'oltre aggiungendo riquadri angolari e separando i pannelli verticali e orizzontali, richiedendo cinque tessere aggiuntive, che ci portano ai nostri dati di livello aggiornati:

[[3,1,1,1,1,4], [2,0,0,0,0,2,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]]

Guarda l'immagine qui sotto, dove ho segnato le tessere con i loro numeri di tessera corrispondenti nei dati di livello:

Ora che abbiamo compreso il concetto dell'approccio basato su tile, lascia che ti mostri come possiamo usare uno semplice pseudo-codice di griglia 2D per rendere il nostro livello:

for (i, loop through rows) per (j, loop through columns) x = j * tile width y = i * tile tile tileType = levelData [i] [j] placetile (tileType, x, y)

Se utilizziamo le immagini delle tessere precedenti, la larghezza e l'altezza delle tessere sono uguali (e uguali per tutte le tessere) e corrisponderanno alle dimensioni delle immagini delle tessere. Quindi la larghezza della piastrella e l'altezza della piastrella per questo esempio sono entrambi di 50 px, che costituisce la dimensione del livello totale di 300 x 300 px, ovvero sei righe e sei colonne di tessere di 50 x 50 px ciascuna.

Come discusso in precedenza, in un normale approccio basato su tile, implementiamo una vista dall'alto o una vista laterale; per una vista isometrica, dobbiamo implementare il proiezione isometrica.

2. Proiezione isometrica

La migliore spiegazione tecnica di cosa proiezione isometrica significa, per quanto ne so, è tratto da questo articolo di Clint Bellanger:

Angoliamo la videocamera su due assi (spostiamo la fotocamera di 45 gradi su un lato, quindi di 30 gradi verso il basso). Ciò crea una griglia a forma di rombo (rombo) in cui gli spazi della griglia sono due volte più larghi dell'altezza. Questo stile è stato reso popolare dai giochi di strategia e dai giochi di ruolo d'azione. Se guardiamo un cubo in questa vista, sono visibili tre lati (lato superiore e due lati opposti).

Anche se sembra un po 'complicato, l'implementazione di questa vista è molto semplice. Quello che dobbiamo capire è la relazione tra lo spazio 2D e lo spazio isometrico, cioè la relazione tra i dati di livello e la vista; la trasformazione dall'alto verso il basso cartesiano coordinate alle coordinate isometriche. L'immagine sotto mostra la trasformazione visiva:

Posizionamento di piastrelle isometriche

Lasciatemi provare a semplificare la relazione tra i dati di livello memorizzati come array 2D e la vista isometrica, ovvero come trasformiamo le coordinate cartesiane in coordinate isometriche. Cercheremo di creare la vista isometrica per la nostra famosa prateria murata. L'implementazione della vista 2D del livello è stata una semplice iterazione con due loop, posizionando le tessere quadrate sfalsando ciascuna con i valori di altezza della piastrella fissa e larghezza della piastrella. Per la vista isometrica, lo pseudo codice rimane lo stesso, ma il placeTile () la funzione cambia.

La funzione originale disegna le immagini delle tessere alle coordinate fornite X e y, ma per una vista isometrica dobbiamo calcolare le coordinate isometriche corrispondenti. Le equazioni per fare questo sono le seguenti, dove isoX e isoY rappresentare le coordinate x e isometriche e cartX e Carty rappresentare le coordinate X e Y cartesiane:

// Cartesiano a isometrico: isoX = cartX - cartY; isoY = (cartX + cartY) / 2;
// Isometric to Cartesian: cartX = (2 * isoY + isoX) / 2; cartY = (2 * isoY - isoX) / 2; 

Sì, è così. Queste semplici equazioni sono la magia dietro la proiezione isometrica. Qui ci sono le funzioni di aiuto di Phaser che possono essere utilizzate per convertire da un sistema all'altro usando il molto conveniente Punto classe:

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

Quindi possiamo usare il cartesianToIsometric metodo di supporto per convertire le coordinate 2D in arrivo in coordinate isometriche all'interno del placeTile metodo. A parte questo, il codice di rendering rimane lo stesso, ma abbiamo bisogno di avere nuove immagini per le tessere. Non possiamo usare le vecchie tessere quadrate usate per il nostro rendering top-down. L'immagine sotto mostra la nuova erba isometrica e le piastrelle muro insieme al livello isometrico reso:

Incredibile, vero? Vediamo come una tipica posizione 2D viene convertita in una posizione isometrica:

Punto 2D = [100, 100]; // il punto isometrico verrà calcolato come sotto isoX = 100 - 100; // = 0 isoY = (100 + 100) / 2; // = 100 Iso point == [0, 100];

Allo stesso modo, un input di [0, 0] risulterà in [0, 0], e [10, 5] darà [5, 7.5].

Per i nostri prati murati, possiamo determinare un'area percorribile controllando se l'elemento dell'array è 0 a quella coordinata, indicando in tal modo l'erba. Per questo abbiamo bisogno di determinare le coordinate dell'array. Possiamo trovare le coordinate della piastrella nei dati di livello dalle sue coordinate cartesiane usando questa funzione:

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

(Qui, assumiamo essenzialmente che l'altezza della piastrella e la larghezza delle piastrelle siano uguali, come nella maggior parte dei casi). 

Quindi, da una coppia di coordinate (isometriche) dello schermo, possiamo trovare le coordinate della piastrella chiamando:

getTileCoordinates (isometricToCartesian (screen point), height tile);

Questo punto dello schermo potrebbe essere, ad esempio, una posizione di clic del mouse o una posizione di ripresa.

Punti di registrazione

In Flash, potremmo impostare punti arbitrari per un'immagine come il suo punto centrale o [0,0]. L'equivalente di Phaser è Perno. Quando posizioni l'immagine, diciamo [10,20], Poi questo Perno il punto sarà allineato con [10,20]. Per impostazione predefinita, l'angolo in alto a sinistra di un grafico è considerato suo [0,0] o Perno. Se si tenta di creare il livello precedente utilizzando il codice fornito, non si otterrà il risultato visualizzato. Invece, otterrai una terra piatta senza le pareti, come di seguito:

Questo perché le immagini delle tessere sono di dimensioni diverse e non stiamo affrontando l'attributo altezza della tessera muro. L'immagine qui sotto mostra le diverse immagini delle tessere che usiamo con i loro rettangoli di selezione e un cerchio bianco in cui il loro valore predefinito [0,0] è:

Guarda come l'eroe diventa disallineato quando si disegna usando i pivot predefiniti. Notate anche come perdiamo l'altezza del pannello del muro se disegnato usando i pivot di default. L'immagine a destra mostra come devono essere allineati correttamente in modo che la tessera del muro raggiunga la sua altezza e l'eroe venga posizionato al centro della tessera dell'erba. Questo problema può essere risolto in diversi modi.

  1. Rendi tutte le tessere della stessa dimensione dell'immagine con il grafico allineato correttamente all'interno dell'immagine. Questo crea molte aree vuote all'interno di ogni grafico di riquadri.
  2. Imposta manualmente i punti di articolazione per ciascun riquadro in modo che si allineano correttamente.
  3. Disegna le tessere con offset specifici in modo che si allineano correttamente.

Per questo tutorial, ho scelto di utilizzare il terzo metodo in modo che funzioni anche con un framework senza la possibilità di impostare i punti di pivot.

3. Spostamento in coordinate isometriche

Non proveremo mai a spostare direttamente il nostro personaggio o proiettile in coordinate isometriche. Invece, manipoleremo i dati del nostro mondo di gioco in coordinate cartesiane e useremo semplicemente le funzioni di cui sopra per aggiornare quelli sullo schermo. Ad esempio, se vuoi spostare un carattere in avanti nella direzione positiva y, puoi semplicemente incrementarlo y proprietà in coordinate 2D e quindi convertire la posizione risultante in coordinate isometriche:

y = y + velocità; placetile (cartesianToIsometric (new Phaser.Point (x, y)))

Questo sarà un buon momento per rivedere tutti i nuovi concetti che abbiamo imparato finora e per cercare di creare un esempio funzionante di qualcosa che si muove in un mondo isometrico. È possibile trovare le risorse immagine necessarie nel risorse cartella del repository git source.

Ordinamento della profondità

Se hai provato a spostare l'immagine della palla nel nostro giardino recintato, ti verrebbe in mente il problema ordinamento in profondità. Oltre al normale posizionamento, dovremo occuparcene ordinamento in profondità per disegnare il mondo isometrico, se ci sono elementi in movimento. Un corretto ordinamento delle profondità fa in modo che gli oggetti più vicini allo schermo siano disegnati sopra gli oggetti più lontani.

Il metodo di ordinamento della profondità più semplice consiste semplicemente nell'utilizzare il valore di coordinata y cartesiana, come menzionato in questo suggerimento rapido: più lo schermo è sopra l'oggetto, prima dovrebbe essere disegnato. Questo potrebbe funzionare bene per scene isometriche molto semplici, ma un modo migliore sarà ridisegnare la scena isometrica una volta che si verifica un movimento, in base alle coordinate dell'array della tessera. Lasciatemi spiegare questo concetto in dettaglio con il nostro pseudo codice per il disegno a livello:

for (i, loop through rows) per (j, loop through columns) x = j * tile width y = i * tile tile tileType = levelData [i] [j] placetile (tileType, x, y)

Immagina che il nostro oggetto o personaggio sia sulla piastrella [1,1]-cioè, il riquadro verde più in alto nella vista isometrica. Per disegnare correttamente il livello, il personaggio deve essere disegnato dopo aver disegnato la tessera della parete d'angolo, sia la tessera sinistra e destra, sia la tessera terreno, come di seguito:

Se seguiamo il nostro ciclo di sorteggio seguendo lo pseudo codice qui sopra, disegneremo prima il muro dell'angolo centrale e poi continueremo a disegnare tutti i muri nella sezione in alto a destra finché non raggiungerà l'angolo destro. 

Quindi, nel ciclo successivo, disegnerà il muro a sinistra del personaggio, e quindi il riquadro di erba su cui si trova il personaggio. Una volta stabilito che questa è la tessera che occupa il nostro personaggio, disegneremo il personaggio dopo disegnando la tessera dell'erba. In questo modo, se ci fossero delle pareti sulle tre tessere di erba libere collegate a quella su cui si trova il personaggio, quelle mura si sovrapporranno al personaggio, risultando in una corretta resa di ordinamento in profondità.

4. Creare l'arte

L'arte isometrica può essere pixel art, ma non deve essere. Quando si tratta di pixel art isometrici, la guida di RhysD ti dice quasi tutto ciò che devi sapere. Alcune teorie possono essere trovate anche su Wikipedia.

Quando si crea l'arte isometrica, le regole generali sono:

  • Inizia con una griglia isometrica vuota e aderisci alla precisione perfetta dei pixel.
  • Prova a rompere l'arte in singole immagini di piastrelle isometriche.
  • Cerca di assicurarti che ogni tessera sia calpestabile o non percorribile. Sarà complicato se dovessimo ospitare una singola tessera che contenga aree percorribili e non percorribili.
  • La maggior parte delle tessere dovrà essere tessuta senza problemi in una o più direzioni.
  • Le ombre possono essere difficili da implementare, a meno che non usiamo un approccio a strati in cui disegniamo ombre sul livello di base e quindi disegniamo l'eroe (o alberi o altri oggetti) sul livello superiore. Se l'approccio che usi non è multistrato, assicurati che le ombre cadano in avanti in modo che non cadano, per esempio, sull'eroe quando si trova dietro un albero.
  • Nel caso in cui sia necessario utilizzare un'immagine della tessera più grande della dimensione standard della piastrella isometrica, provare a utilizzare una dimensione che è un multiplo della dimensione della piastrella iso. È meglio avere un approccio a strati in questi casi, in cui possiamo dividere l'arte in diversi pezzi in base alla sua altezza. Ad esempio, un albero può essere diviso in tre pezzi: la radice, il tronco e il fogliame. In questo modo è più facile ordinare le profondità in quanto possiamo disegnare pezzi in strati corrispondenti che corrispondono alle loro altezze.

Le tessere isometriche che sono più grandi delle dimensioni delle singole tessere creeranno problemi con l'ordinamento in profondità. Alcuni dei problemi sono discussi in questi collegamenti:

Post correlati

  • Piastrelle più grandi
  • Algoritmo di Splitting e Painter
  • Il post di OpenSpace sui modi efficaci per dividere le tessere più grandi

5. Caratteri isometrici

Per prima cosa dovremo correggere quante direzioni di movimento sono consentite nel nostro gioco, in genere i giochi forniranno il movimento a quattro o il movimento a otto. Guarda l'immagine qui sotto per capire la correlazione tra lo spazio 2D e lo spazio isometrico:

Si noti che un personaggio si muoverà verticalmente verso l'alto quando premiamo il tasto freccia in su digitare un gioco top-down, ma per un gioco isometrico il personaggio si sposterà con un angolo di 45 gradi verso l'angolo in alto a destra. 

Per una vista dall'alto, potremmo creare una serie di animazioni di personaggi rivolte in una direzione e ruotarle semplicemente per tutte le altre. Per l'arte dei caratteri isometrici, dobbiamo ricreare ogni animazione in ciascuna delle direzioni consentite, quindi per il movimento a otto direzioni, dobbiamo creare otto animazioni per ogni azione. 

Per facilità di comprensione, di solito denotiamo le direzioni come Nord, Nord-Ovest, Ovest, Sud-Ovest, Sud, Sud-Est, Est e Nord-Est. I riquadri dei personaggi in basso mostrano fotogrammi inattivi partendo da Sud-Est e procedendo in senso orario:

Posizioneremo i personaggi nello stesso modo in cui abbiamo posizionato le tessere. Il movimento di un personaggio si ottiene calcolando il movimento in coordinate cartesiane e quindi convertendo in coordinate isometriche. Supponiamo di usare la tastiera per controllare il personaggio.

Imposteremo due variabili, dX e dY, in base ai tasti direzionali premuti. Di default, queste variabili saranno 0 e sarà aggiornato come da tabella qui sotto, dove UDR, e L denota il SuGiùDestra, e Sinistra tasti freccia, rispettivamente. Un valore di 1 sotto una chiave rappresenta che il tasto è stato premuto; 0 implica che il tasto non venga premuto.

 Chiave 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

Ora, usando i valori di dX e dY, possiamo aggiornare le coordinate cartesiane in questo modo:

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

Così dX e dY indica il cambiamento delle posizioni x e y del carattere, in base ai tasti premuti. Possiamo facilmente calcolare le nuove coordinate isometriche, come abbiamo già discusso:

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

Una volta ottenuta la nuova posizione isometrica, è necessario mossa il personaggio in questa posizione. Basato sui valori che abbiamo per dX e dY, possiamo decidere in quale direzione si trova il personaggio e usare l'arte del personaggio corrispondente. Una volta che il personaggio è stato spostato, per favore non dimenticare di ridipingere il livello con l'ordinamento di profondità appropriato in quanto le coordinate del carattere del personaggio potrebbero essere cambiate.

Rilevamento collisione

Il rilevamento delle collisioni viene eseguito controllando se il riquadro nella posizione appena calcolata è una tessera non percorribile. Quindi, una volta trovata la nuova posizione, non spostiamo immediatamente il personaggio lì, ma prima controlliamo per vedere quale tessera occupa quello spazio.

tile coordinata = getTileCoordinates (isometricToCartesian (posizione corrente), altezza tile); if (isWalkable (tile tile)) moveCharacter ();  else // non fare nulla; 

Nella funzione isWalkable (), controlliamo se il valore dell'array di dati di livello alla coordinata data è una tessera calpestabile o meno. Dobbiamo fare attenzione ad aggiornare la direzione in cui il personaggio si trova ad affrontare-anche se non si muove, come nel caso di lui che colpisce una tessera non percorribile.

Ora questo può sembrare una soluzione adeguata, ma funzionerà solo per gli elementi senza volume. Questo perché stiamo solo considerando un singolo punto, che è il punto medio del personaggio, per calcolare la collisione. Quello che dobbiamo veramente fare è trovare tutti i quattro angoli del personaggio dalla sua coordinata del punto medio 2D disponibile e calcolare le collisioni per tutti questi. Se un angolo cade all'interno di una tessera non percorribile, non dovremmo spostare il personaggio.

Ordinamento della profondità con caratteri

Considera un personaggio e una tessera albero nel mondo isometrico, e loro entrambi hanno le stesse dimensioni di immagine, per quanto irrealistico suoni.

Per comprendere correttamente l'ordinamento in profondità, dobbiamo capire che ogni volta che le coordinate xey del personaggio sono inferiori a quelle dell'albero, l'albero si sovrappone al personaggio. Ogni volta che le coordinate xey del personaggio sono maggiori di quelle dell'albero, il personaggio si sovrappone all'albero. Quando hanno la stessa coordinata x, decidiamo in base solo alla coordinata y: quale che abbia la coordinata y più alta si sovrappone all'altra. Quando hanno la stessa coordinata y, decidiamo in base alla sola coordinata x: a seconda di quale sia la coordinata x più alta si sovrappone all'altra.

Come spiegato in precedenza, una versione semplificata di questo è solo disegnare sequenzialmente i livelli a partire dalla tessera più lontana, cioè, tile [0] [0]-e quindi disegna tutte le tessere di ciascuna fila una alla volta. Se un personaggio occupa una tessera, prima disegniamo la tessera terreno e poi la tessera personaggio. Questo funzionerà bene, perché il personaggio non può occupare una tessera muro.

6. Demo Time!

Questa è una demo in Phaser. Fai clic per concentrarti sull'area interattiva e usa i tasti freccia per spostare il personaggio. È possibile utilizzare due tasti freccia per spostarsi nelle direzioni diagonali.

Puoi trovare la fonte completa per la demo nel repository sorgente per questo tutorial.