Isometria 2D e rendering in MonoGame / Xna
Oggi vi andrò ad illustrare come strutturare un possibile sistema di coordinate in un videogame con visualizzazione isometrica 2d (per intenderci, qualcosa di simile all’immagine successiva). Ha senso parlare ancora di 2D e di rendering isometrico? Il 2D esiste e resiste? Non ne ho idea, ma resta il fatto che la roba vecchia è sempre meglio, siamo tutti un pò nostalgici. Ed inoltre uno dei miei giochi preferiti utilizza questo tipo di visualizzazione (RollerCoasterTycoon).
Coordinate
Le “mappe” in questa tipologia di giochi, sono costituite da una griglia (o array quadrato) di immagini isometriche (dette anche tiles).
La cosa più complessa, è capire in che modo vengono gestite le coordinate di rendering, rispetto a quelle dell’array. Per capirlo velocemente basta guardare la seguente immagine:
Intuitivamente notate che le coordinate (x,y) nell’array (riportate nelle celle), vengono riscritte durante il rendering in questo modo (tileSize = larghezza di un singolo tile, (x,y) = cella nell’array):
(x,y) -> ((x + y) * tileSize / 2, (x - y) * tileSize / 4)
Rendering dei Tile
Un altro fatto tedioso, è l’ordine in cui devono essere disegnati i Tiles sullo schermo per evitare sovrapposizioni; bisogna disegnare per primo il tile all’estremo superiore (nel disegno precedente quindi la cella (0,7), poi si passa alla fila successiva, e così via. In alcuni articoli che ho trovato in rete vengono proposti complessi algoritmi iterativi, o riordinamenti dell’array dei Tile; la mia soluzione utilizza una semplice funzione ricorsiva:
member this.Draw (indexX, indexY, startX, startY, tileSize) = // dir = 0 -> sin, 1 -> des, 2 -> sindes let rec drawLoop x y dir = if x < 0 || y < 0 || x >= this.Width || y >= this.Height then () else let nx = startX + (x + y) * tileSize / 2 let ny = startY + (x - y) * tileSize / 4 this.Cells.[y * this.Width + x].Draw (nx, ny, tileSize) if dir > 0 then drawLoop (x+1) (y) 1 if dir <> 1 then drawLoop (x) (y-1) 2 () drawLoop 0 (this.Height - 1) 2
L’idea di base è, partendo dal tile più in alto, renderizzare il tile alla posizione corrente (x,y), dopodichè richiamare la funzione drawLoop sulla cella in basso a destra ed in basso a sinistra. La cella in basso a sinistra a sua volta disegna se stessa e di nuovo richiama drawLoop per entrambe le direzioni; la cella in basso a destra invece, disegna se stessa e richiama drawLoop solo per la cella in basso a destra: ovviamente potete fare anche il contrario.
Classi
Per quanto riguarda l’implementazione. la soluzione più ovvia è realizzare una classe TileMap contenente l’array delle celle (MapCell) che costituiscono la mappa. Entrambe le classi presentano un metodo Draw(): la vostra implementazione di Game::Draw() richia TileMap::Draw() che a sua volta chiamerà MapCell.Draw() per ogni cella della mappa.
Vi allego il sorgente completo in un unico file: https://gist.github.com/dakk/6432134
Ringrazio Paul Firth per le immagini, che ho felicemente preso dal suo articolo.