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).

TileMap

Coordinate

Le “mappe” in questa tipologia di giochi, sono costituite da una griglia (o array quadrato) di immagini isometriche (dette anche tiles).

Tile

Tile

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:

Coordinate Tile

Coordinate Tile

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.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: