Lo scrolling nei nostri giochi

Una caratteristica fondamentale di un buon gioco, e' quella di presentare livelli esplorabili in lungo e in largo, senza costringere il giocatore ad una schermata fissa.

Questa caratteristica e' detta Scroller, per l'appunto scorrimento, si tratta della possibilita' di avere livelli che scorrano orizzontalmente e verticalmente a seconda del movimento del personaggio, permettendo cosi' di avere livelli di gioco non limitati alla dimensione della finestra di gioco.

Questa tecnica e' abbastanza semplice da realizzarsi, in rete vi si possono trovare svariate implementazioni che differiscono di poco l'una dall'altra, in questo articolo cercheremo di implementarla sulla base del nostro esempio di tile map, espandendo il nostro labirinto a schermata fissa in una versione piu' grande e scorrevole.

Prima di iniziare a mettere mano sul codice, forse e' bene avere di base qualche nozione teorica riguardo lo scrolling e il suo utilizzo, partiamo prendendo in esame un possibile livello di gioco che si espande orizzontalmente.

La nostra finestra di gioco, potra' non essere grande abbastanza per visualizzare interamente il livello, ma solo una parte di esso, questa parte visibile la chiameremo Camera, intesa come la parte di mappa interamente visibile sullo schermo e rappresentata dal rettangolo rosso nell'immagine sottostante.
La grande griglia nera che si puo' vedere nella parte alta dell'immagine, e' il livello sotto forma di matrice, la parte inferiore circondata dal rettangolo rosso e' la parte di matrice che attualmente il programma sta disegnando sullo schermo.

In questo caso siamo a circa meta' del livello, la distanza percorsa dall'inizio del livello fino alla posizione di Camera, e' detta offset, ci servira' piu' avanti per disegnare correttamente e gradualmente il livello durante il gioco.

Fino ad ora abbiamo visto come un personaggio si muova in una superficie ed abbiamo tenuto conto della sua posizione tramite le sue coordinate, qui la storia cambia.
Non sara' il nostro personaggio a muoversi, o almeno non del tutto, sara' ben si il livello a muoversi dando l'illusione al giocatore di proseguire.

Si tenga conto che non e' il rettangolo rosso a muoversi lungo la matrice, ma la matrice di per se a indietreggiare o avanzare colonna dopo colonna, come un nastro trasportatore, tutto cio' che passera' nell'area delimitata di rosso sara' visibile al giocatore su schermo.

Il nostro giocatore, muovendosi verso e oltre la meta' dell'area di Camera, o per meglio dire, la meta' dello schermo, esprimera' la sua volonta' a visualizzare la colonna successiva della matrice, fino a che questa non avra' portato la sua ultima colonna all'interno dell'area apposita.

Possiamo vedere nell'immagine qui accanto i tre casi principali a cui potremmo trovarci di fronte.

Il primo caso e' il Center Case, le coordinate del giocatore sono maggiori della dimensione massima della finestra di gioco.
In questo caso si avra' il giocatore costantemente disegnato a meta' dello schermo, sia che esso si stia muovendo avanti o indietro.

Il giocatore e' il centro del nostro universo e sara' la mappa a muoversi al posto suo graficamente parlando, anche se matematicamente le sue coordinate non corrispondono a quelle visualizzate sullo schermo.
Facendo cio', si evita di avere un livello che scorra troppo velocemente rispetto al giocatore, o viceversa.

Il secondo caso e' il Right Case, in questo caso le coordinate del giocatore, corrispondo a quasi la fine della nostra mappa, quest'ultima non avanzera' piu' in quanto non vi saranno altre colonne da mostrare, il nostro offset sara' arrivato al suo punto massimo (MaxOffset).
Il giocatore in questo caso, potra' muoversi liberamente a livello grafico, nella meta' destra dello schermo.

Discorso analogo per l'ultimo caso, Left Case, qui il giocatore potra' muoversi liberamente nella meta' sinistra dello schermo, non appena superera' la meta' verso destra si cominciera' a muovere la mappa colonna dopo colonna verso sinistra, e si terra' il giocatore disegnato a meta' schermo.

Molti giochi, tendono ad avere uno scrolling progressivo, nel momento in cui il giocatore raggiunge la meta' del livello, giusto per non mostrare troppo di cio' che il livello offrira' e togliere quindi l'effetto sorpresa.

Oltre che ad essere una buona regola di game design, su hardware poco potenti, aiuta a risparmiare le risorse nel caso in cui vi siano entita' come nemici o quant'altro da attivare, nel caso questi siano numerosi e ne si voglia limitare la presenza su schermo.

Non vi preoccupate se a livello teorico la spiegazione vi ha lasciato un po' di confusione, implementando il tutto con il codice, si dovrebbero appianare gli ultimi dubbi.

Partendo dal nostro vecchio esempio sul tile mapping, possiamo espandere la vecchia mappa e aggiungere un effetto scrolling con poche modifiche al codice, partiamo dichiarando le variabili necessarie al nostro lavoro.
#define ROWS 15
#define COLS 20

#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 320

#define ROWS 15
#define COLS 20

#define TILE_SIZE 32

#define PLAYER_W 10
#define PLAYER_H 10

int maxOffsetX = -(TILE_SIZE*COLS - SCREEN_WIDTH);
int maxOffsetY = -(TILE_SIZE*ROWS - SCREEN_HEIGHT);

struct _Camera {
int offsetX, offsetY;
} Camera;

int matrice[ROWS][COLS] =
{ 0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,1,0,1,1,1,1,0,1,0,1,1,1,1,0,1,1,1,1,
1,0,1,0,0,0,0,1,0,1,0,1,1,0,1,0,1,1,1,1,
1,0,1,1,1,1,1,1,0,1,0,0,0,0,1,0,1,0,0,1,
1,0,1,0,0,0,1,0,0,1,0,1,1,1,1,0,1,1,0,1,
1,0,1,0,1,0,1,0,1,1,0,1,1,1,1,0,1,1,0,1,
1,0,1,0,1,0,1,0,1,1,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,1,0,0,0,0,0,0,0,1,1,0,1,0,1,1,1,
1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,
1,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,1,
1,0,1,0,1,0,1,0,1,0,1,0,0,0,0,1,0,0,0,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
};
La nostra mappa ora e' quasi 2 volte piu' grande della finestra di gioco, ed i suoi tiles sono grandi 32x32 pixel.

Come gia' detto in precedenza, la nostra area visibile della matrice, sara' chiamata Camera, una semplice struttura con due variabili per tenere conto delle sue coordinate.

Imposteremo anche due variabili globali, che terranno conto del limite massimo in larghezza e altezza della nostra mappa.

Vediamo ora come disegnare la nostra mappa, in base al punto in cui si trova il giocatore.
void drawTileMap()
{
int firstTileX = -(Camera.offsetX / TILE_SIZE);
int lastTileX = firstTileX + (SCREEN_WIDTH / 2);

int firstTileY = -(Camera.offsetY / TILE_SIZE);
int lastTileY = firstTileY + (SCREEN_HEIGHT / 2);

int x, y;
// Limitiamo il disegno solo ai tiles visibili
for (y = firstTileY; y < lastTileY; y++)
{
for (x = firstTileX; x <= lastTileX; x++)
{
if(matrice[y][x]==0)
drawRect((x * TILE_SIZE) + Camera.offsetX,
(y * TILE_SIZE) + Camera.offsetY, TILE_SIZE, TILE_SIZE, 0xffffff);
}
}
}
A questo giro, ho optato per una funzione apposita per il disegno della mappa, il meccanismo di disegno si avvale sempre di due cicli annidati come nell'esempio precedente, l'unica aggiunta e' il valore offset di Camera.

Come gia' detto in precedenza, offset e' la distanza dai bordi della mappa attualmente percorsa, ci aiutera' a trasportare gradualmente il disegno della mappa su schermo.
La matrice di per se e' un'immagine, come le immagini stesse una volta caricate sulle proprie superfici, diventano matrici.
Qui l'immagine viene trasportata avanti e indietro sullo schermo, per fare cio', agiamo sulle sue coordinate con l'ausilio degli offset calcolati.

Nel caso presentato dall'immagine qui accanto, la mappa verra' disegnata a -320 pixel, che saranno pari a offsetX, nello schermo vedremo l'area di immagine che parte da 320 pixel fino a 640 pixel.

Volendo muovere il personaggio, gradualmente muoveremo l'immagine avanti e indietro.

Nel codice ho optato per un'ottimizzazione del disegno, leggendo solo quelle righe e colonne attualmente visibili e tenute in conto da Camera, dato che non ha molto senso disegnare cio' che non si vede, specie su mappe esageratamente grandi.
int firstTileX = -(Camera.offsetX / TILE_SIZE);
int lastTileX = firstTileX + (SCREEN_WIDTH / 2);

int firstTileY = -(Camera.offsetY / TILE_SIZE);
int lastTileY = firstTileY + (SCREEN_HEIGHT / 2);
Per fare cio' ho convertito le posizioni X e Y di Camera in un equivalente in tile, avendo cosi' le posizioni iniziali e finali di lettura della matrice, da applicare ai due cicli for annidati che provvederanno a leggerla e disegnarla sullo schermo.

Passiamo ora a vedere il disegno del giocatore, che fara' anch'esso riferimento ai valori di offset delle immagini.
void drawPlayer()
{
int screenX =
Player.x + Camera.offsetX;
int screenY = Player.y + Camera.offsetY;

drawRect(screenX, screenY, PLAYER_W, PLAYER_H, 0xff0000);
}
Il giocatore a seconda dei casi, verra' disegnato al centro dello schermo o liberamente nella meta' di esso disponibile, a gestire i casi, ci penseranno i valori di Offset X e Y, gestiti in un'apposita funzione per la Camera che trovate qui sotto.
void updateCamera()
{
// Calcoliamo gli offset X e Y della mappa
int centerX = (SCREEN_WIDTH - TILE_SIZE) / 2;

Camera.offsetX = -(Player.x - centerX);

if (maxOffsetX > 0)
maxOffsetX = 0; // Schermo maggiore dell'immagine

//Left Case
if (Camera.offsetX > 0 || Player.x < centerX)
{
Camera.offsetX = 0;
} //Right Case
else if (Camera.offsetX < maxOffsetX || Player.x > centerX - maxOffsetX)
{
Camera.offsetX = maxOffsetX;
}

//Posizionamento della visuale in verticale
int centerY = (SCREEN_HEIGHT - TILE_SIZE) / 2;

Camera.offsetY = -(Player.y - centerY);

if (maxOffsetY > 0)
maxOffsetY = 0; // screen > image

if (Camera.offsetY > 0 || Player.y < centerY)
{
Camera.offsetY = 0;
}
else if (Camera.offsetY < maxOffsetY || Player.y > centerY - maxOffsetY)
{
Camera.offsetY = maxOffsetY;
}
}
Qui nella gestione di Camera, di partenza aggiorniamo il valore di offset per entrambe le direzioni, partendo dal presupposto che il giocatore sia lontano dai bordi della mappa, successivamente controlliamo in quale dei 3 casi il giocatore sia effettivamente e correggiamo i valori di offset.
Questi valori poi verranno usati per disegnare correttamente il giocatore, come si puo' vedere nella funzione precedente.

Questo e' quanto vi serve per implementare un effetto scrolling nei vostri giochi, ovviamente questo esempio puo' essere perfezionato e non e' l'unico modo per realizzarlo, ma forse e' il piu' semplice e chiaro che si possa trovare in rete.
Questo esempio che vi ho proposto in questo articolo, e' liberamente ispirato ad un tutorial Java che trovate qui, tratto a sua volta dall'ottimo libro di David Brackeen, Developing Games in Java.
Se invece cercate una alternativa di scrolling in C, per una comparazione o quant'altro, potete dare uno sguardo all'ottimo esempio di Losersjuegos, purtroppo non munito di documentazione.

Potete espandere questo tutorial, magari aggiungendo tiles colorati o quant'altro, o magari unirlo al tutorial precedente sui platform game e realizzare un semplice platform game a scorrimento.

Trovate il sorgente completo dell'esempio nel repository ufficiale del blog, Buon divertimento!

Nessun commento :

Posta un commento

Related Posts Plugin for WordPress, Blogger...