[5] Free Pascal per NDS: background a tiles e mappe

Procediamo ora con un argomento che risulta utilissimo nello sviluppo di giochi: la gestione di tiles e mappe. Nella tabella relativa alle modalità grafiche, abbiamo visto che ogni modalità video permette di accedere a un numero di background differenti. In passato, nelle vecchie versioni di libnds, l'unico modo che si aveva per impostare ed utilizzare i background era quello di utilizzare direttamente i registri, eseguendo operazioni sui bit.

Oggi libnds offre un layer di astrazione di livello più alto, che rende molto più semplice l'utilizzo dei background. Vediamo due esempi: nel primo imposteremo un background utilizzando il vecchio sistema, attivando bit nei registri; nel secondo eseguiremo lo stesso compito, ma utilizzando il nuovo set di istruzioni.

In questi esempi utilizzeremo il Mode 0, che ci permetterà di avere 4 background di tipo text.

program Mode0_a;
{$mode objfpc}

uses
  ctypes, nds9;

begin
  // Attivazione del Mode 0 e del Background 0
  REG_DISPCNT^ := MODE_0_2D or DISPLAY_BG0_ACTIVE;  
  VRAM_A_CR^ := VRAM_ENABLE or VRAM_A_MAIN_BG; 

  // Impostare il Background 0 attraverso il suo registro BGO 
  REG_BG0CNT^ := BG_32x32 or BG_COLOR_256 or
                 BG_MAP_BASE(0) or BG_TILE_BASE(1);

  while true do;  // loop infinito! 

end.

Vediamo il codice in dettaglio. Il primo registro utilizzato è REG_DISPCNT e serve per impostare la modalità grafica Mode0 (con MODE_0_2D) e per attivare il background 0 (tramite DISPLAY_BG0_ACTIVE).

Il secondo registro è VRAM_A_CR e si riferisce al banco di memoria A. Tramite gli switch VRAM_ENABLE e VRAM_A_MAIN_BG lo attiviamo e lo impostiamo per l'utilizzo come background sul main engine.

Utilizziamo poi il registro REG_BG0CNT del BG0 per attivare un background di tipo testo di 32x32 tiles (BG_32x32), in modo che supporti 256 colori (BG_COLOR_256), dove la mappa sia memorizzata in Map Base 0 (BG_MAP_BASE(0)) e il tileset sia memorizzato a partire da Tile Base 1 (BG_TILE_BASE(1)).

Da ricordare che REG_BG0CNT (più in generale REG_BGnCNT, dove n indica il numero del background, da 0 a 3), così come VRAM_A_CR e REG_DISPCNT, sono puntatori a indirizzi di memoria; per questo motivo c'è bisogno di dereferenziarli per avere accesso al contenuto di quelle zone di memoria.

Vediamo ora lo stesso esempio, ma utilizzando il nuovo set di funzioni.

program Mode0_b;
{$mode objfpc}

uses
  ctypes, nds9;

var
  bg0: integer;

begin
  // Attivazione del Mode 0 e del Background 0
  videoSetMode(MODE_0_2D or DISPLAY_BG0_ACTIVE);  
  vramSetBankA(VRAM_A_MAIN_BG); 

  // Impostare il Background 0 attraverso la funzione BgInit 
  bg0 := BgInit(0, BgType_Text8bpp, BgSize_R_256x256, 0, 1);

  while true do;  // loop infinito! 

end.

La funzione videoSetMode() si occupa di impostare la modalità video Mode 0 (tramite MODE_0_2D) e di attivare il background 0 (con DISPLAY_BG0_ACTIVE). Dovreste già conoscere il significato di or; In caso contrario, consideratelo come una semplice addizione, del tipo "Attivare il Mode 0 2D e il background 0".

La funzione vramSetBankA() serve ad impostare il banco di memoria A in modo tale che la sua memoria possa essere utilizzata come background principale (VRAM_A_MAIN_BG).

Come è facile notare, la funzione BgInit richiede cinque parametri:

  1. il numero del background (0, 1, 2 o 3),
  2. il tipo di background,
  3. la sua dimensione (vedere le tabelle negli articoli precedenti),
  4. la map base,
  5. la tile base

Da tenere presente che ogni map base può immagazzinare una mappa di 32x32 tile, (256x256 pixel) e un singolo background. Per questo motivo, se si ha bisogno di una mappa di 64x64 tile, bisognerà ricordare che la mappa richiederà 4 blocchi nella map base. Allo stesso modo, una mappa di 32x32 tiles su 3 background richiederà 3 blocchi. Il vantaggio più grande nell'usare la funzione BgInit è il controllo sui parametri implementato in essa. Grazie a questo meccanismo, se ad esempio si prova ad usare un background di tipo extended rotation su un background di tipo text, verrà mostrato a schermo un messaggio che indicherà il tipo di errore riscontrato, nonché il file e la riga dove l'errore si è verificato.

Lanciando la ROM generata dal codice qui sopra, noterete che sullo schermo non viene mostrato niente. In effetti fino ad ora abbiamo soltanto impostato la console, senza disegnare niente. Come detto in precedenza, avremo bisogno di un tool per convertire i files contenenti la grafica in un formato utilizzabile con il DS. La scelta migliore è GRIT, una utility a linea di comando (disponibile anche con una comoda interfaccia grafica, WIN GRIT) presente sia nella distribuzione del devkitARM che in fpc4nds.

Nel nostro caso, supponiamo di avere un'immagine bitmap di 256x256 pixel, con una profondità di colore di 8bpp, contenente la nostra immagine/tileset; vogliamo convertirla in tiles per utilizzarla in una delle modalità grafiche a tiles e, per risparmiare memoria, vogliamo rimuovere le tiles ripetute. La linea di comando da usare con GRIT sarà:

grit MyImage.bmp -fts -gt -gB8 -mRtpf

Una piccola descrizione:

GRIT è davvero un programma molto potente: il mio consiglio è quello di prendervi il tempo necessario per leggerne il manuale, che contiene anche molti esempi, in modo tale da padroneggiarlo.

La linea di comando mostrata in precedenza restituisce due files, MyImage.s e MyImage.h. Il primo è il file che contiene i dati relativi all'immagine, alla mappa e alla palette suddivisi 3 array; il secondo file è un header in c, che ovviamente non possiamo usare direttamente con Free Pascal, ma che ci permette di conoscere alcune informazioni che ci saranno utili.

Prima di poter essere utilizzato nel nostro programma in Pascal, il file asm generato da GRIT deve essere assemblato:

arm-none-eabi-as -o MyImage.o MyImage.s

Convertiamo ora l'header. Aprendo il file MyImage.h con un editor di testo ci apparirà qualcosa di simile:

#ifndef GRIT_MYIMAGE_H
#define GRIT_MYIMAGE_H

#define MyImageTilesLen 16448 extern const unsigned int MyImageTiles[4112];

#define MyImageMapLen 2048 extern const unsigned short MyImageMap[1024];

#define MyImagePalLen 512 extern const unsigned short MyImagePal[256];

#endif // GRIT_MYIMAGE_H

La conversione in Pascal di queste linee ci permetterà di accedere ai dati generati da GRIT. Il procedimento è davvero molto semplice:

const
  MyImageTilesLen = 16448;
var
  MyImageTiles: array [0..0] of cuint; cvar; external;

const
  MyImageMapLen = 2048;
var
  MyImageMap: array [0..0] of cushort; cvar; external;

const
  MyImagePalLen = 512;
var
  MyImagePal: array [0..0] of cushort; cvar; external;

Come potete vedere, le chiamate contenenti #define sono state tradotte con delle semplici costanti; le altre variabili sono degli array, che è sufficiente dichiarare di un solo elemento. Il compilatore è abbastanza intelligente da stabilirne da solo la dimensione.

Ora abbiamo davvero tutto quello che ci serve per creare la nostra demo in Mode 0:

program Mode0_c;
{$L build/wood.o}

{$mode objfpc}

uses
  ctypes, nds9;

const
  woodTilesLen = 16448;
var
  woodTiles: array [0..0] of cuint; cvar; external;

const
  woodMapLen = 2048;
var
  woodMap: array [0..0] of cushort; cvar; external;

const
  woodPalLen = 512;
var
  woodPal: array [0..0] of cushort; cvar; external;


var
  bg0: cint;
begin     
  videoSetMode(MODE_0_2D);
  vramSetBankA(VRAM_A_MAIN_BG);
  bg0 := bgInit(0, BgType_Text8bpp, BgSize_T_256x256, 0,1); 

  dmaCopy(@woodTiles, bgGetGfxPtr(bg0), woodTilesLen);
  dmaCopy(@woodMap, bgGetMapPtr(bg0),  woodMapLen);
  dmaCopy(@woodPal, BG_PALETTE, woodPalLen);

  while true do;  // loop infinito

end.

Analizziamo il codice. Tramite la direttiva $L del compilatore linkiamo il file oggetto contenente la grafica (ricordate? Quello generato con arm-none-eabi-as), quindi inseriamo le variabili e le costanti ricavate dal file header .h generato da GRIT.

Inizializziamo quindi la console come visto in precedenza negli altri esempi: il background è di tipo text (anche perché in Mode0 non è possibile utilizzare altri tipi di background!); la mappa risiede nel blocco 0, mentre le tiles sono memorizzate nel blocco 1.

L'ultimo passo è quello di copiare la grafica, la mappa e la palette nelle giuste locazioni di memoria. A tale scopo utilizzeremo la funzione dmaCopy(), che nel 99% dei casi è il metodo più veloce per copiare dati da una regione di memoria all'altra:

  1. l'indirizzo della locazione di memoria dell'array dove sono memorizzate le tiles (@woodTiles);
  2. l'indirizzo del background (la funzione bgGetGfxPtr() restituisce proprio l'indirizzo del background passato come parametro);
  3. la quantità dei dati da copiare, data dalla costante woodTilesLen
  1. l'indirizzo della locazione di memoria dell'array dove è memorizzata la mappa (@woodMap);
  2. la locazione di memoria dove copiare la mappa (la funzione bgGetMapPtr() restituisce l'indirizzo della mappa associata al background passato come parametro);
  3. la quantità dei dati da copiare, data dalla costante woodMapLen
  1. l'indirizzo della locazione di memoria dell'array dove è memorizzata la palette (@woodPal);

  2. a locazione di memoria dove copiare la palette (BG_PALETTE è un puntatore all'area dove risiede la palette per il background);
  3. la quantità dei dati da copiare, data dalla costante woodPalLen

Con opportuni accorgimenti si potrebbe utilizzare anche la funzione pascal move():

Move(MyImageTiles, bgGetGfxPtr(bg0)^, MyImageTilesLen);
Move(MyImageMap, bgGetMapPtr(bg0)^, MyImageMapLen);
Move(MyImagePal, BG_PALETTE^, MyImagePalLen);

oppure la funzione c memcpy():

memcpy(bgGetGfxPtr(bg0), @MyImageTiles, MyImageTilesLen);
memcpy(bgGetMapPtr(bg0), @MyImageMap, MyImageMapLen);
memcpy(BG_PALETTE, @MyImagePal, MyImagePalLen);

Solitamente move e memcpy sono da 2 a 5 volte più lente di dmaCopy nella velocità di scrittura. Bisogna però segnalare che, in alcuni casi specifici, dmaCopy potrebbe non essere il modo più veloce per copiare i dati. Non c'è comunque bisogno di preoccuparsi, perché per i nostri scopi è più che sufficiente.


Muovere e ruotare i background

Una delle caratteristiche più interessanti del Nintendo DS è la possibilità di muovere, scalare e ruotare i background direttamente dall'hardware della console. Effettuare lo scrolling di un background richiede una semplice, singola chiamata ad una procedura:

bgScroll(id, dx, dy: integer);

oppure l'equivalente:

bgSetScroll(id, dx, dy: integer);

Queste procedure richiedono come parametri l'ID del background restituito da bgInit (o da bgInitSub) e il valore in pixel dello spostamento desiderato del background lungo gli assi x e y. Potreste nel caso utilizzare anche:

bgScrollf(id, dx, dy: integer);

oppure l'equivalente:

bgSetScrollf(id, dx, dy: integer);

In questo secondo caso dx e dy devono essere dei valori fixed point, in virgola fissa. Modifichiamo l'ultimo esempio visto per provare a muovere il background:

while true do // loop infinito
begin
  bgScroll(bg0, 1, 1); // scrolling del background
  bgUpdate();          // aggiorna il background
  swiWaitForVBlank();  // attende il VBlank per evitare
                       // il flickering
end;

La procedura bgScroll() da sola non è sufficiente per muovere il background. Occorre chiamare anche la procedura bgUpdate(), che aggiorna lo stato dei registri impiegati nello scrolling. Nel codice precedente viene introdotta una nuova, utile procedura: swiWaitForVBlank(). Lo schermo del DS funziona come un qualsiasi schermo: una sorta di "pennello" passa su ogni pixel, colonna per colonna, riga per riga, e copia il framebuffer dalla memoria video allo schermo. Questo compito richiede circa 1/60 di secondo, quindi è buona norma attendere che il framebuffer sia completamente copiato sullo schermo prima di cambiarlo. Il DS ci viene in aiuto per mezzo di un interrupt, che viene lanciato ogni volta che il redraw dello schermo viene completato. La procedura swiWaitForVBlank() si occupa di attendere questo evento, fermando il flusso dell'esecuzione fino a quando l'interrupt non si attiva. Come si può vedere dalla esecuzione dell'esempio, il background viene ripetuto infinitamente. Quando viene raggiunto il limite della mappa, lo scroll ricomincia dal lato opposto.

Negli esempi precedenti abbiamo usato un singolo layer, ma ne abbiamo altri tre da utilizzare. Nel prossimo (e ultimo, per il Mode 0) esempio posizioneremo sullo schermo tre layer, utilizzando trasparenza e scrolling. Da ricordare che i layer utilizzano un sistema di priorità che va da 0 (il livello - per così dire - più vicino al vetro dello schermo, ed è il layer più in alto) a 3 (il layer più in profondità).
Nella nostra "scena" avremo un cielo stellato fisso, delle montagne e alcune nuvole, che scorreranno a velocità differenti, dando l'illusione di un movimento laterale. Il background impiegato per il cielo avrà priorità 3, le montagne priorità 1 e le nuvole priorità 0. Il DS considera come trasparente il primo colore della palette, cioè quello con indice 0, quindi prestate attenzione nell'impostare il colore per la trasparenza del tileset.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
program Mode0_e;
{$L build/mounts.o} 
{$L build/sky.o}
{$L build/clouds.o} 

{$mode objfpc}

uses
  ctypes, nds9;

const
  mountsTilesLen = 5120; 
  mountsMapLen = 2048;
  mountsPalLen = 512;
  
  skyTilesLen = 2112;
  skyMapLen = 2048;
  skyPalLen = 512;

  cloudsTilesLen = 3328;
  cloudsMapLen = 2048;
  cloudsPalLen = 512;

var
  mountsTiles: array [0..0] of cuint; cvar; external;
  mountsMap: array [0..0] of cushort; cvar; external;
  mountsPal: array [0..0] of cushort; cvar; external;

  skyTiles: array [0..0] of cuint; cvar; external;
  skyMap: array [0..0] of cushort; cvar; external;
  skyPal: array [0..0] of cushort; cvar; external;

  cloudsTiles: array [0..0] of cuint; cvar; external;
  cloudsMap: array [0..0] of cushort; cvar; external;
  cloudsPal: array [0..0] of cushort; cvar; external; 

var
  mounts, clouds, sky: integer;
begin
  videoSetMode(MODE_0_2D);
  vramSetBankA(VRAM_A_MAIN_BG);
  
  clouds := bgInit(0, BgType_Text8bpp, BgSize_T_256x256, 0, 1); 
  mounts := bgInit(1, BgType_Text8bpp, BgSize_T_256x256, 1, 2);
  sky    := bgInit(2, BgType_Text8bpp, BgSize_T_256x256, 2, 3); 

  dmaCopy(@mountsTiles, bgGetGfxPtr(mounts), mountsTilesLen);
  dmaCopy(@mountsMap, bgGetMapPtr(mounts),  mountsMapLen);
  dmaCopy(@mountsPal, BG_PALETTE, mountsPalLen);
  
  dmaCopy(@skyTiles, bgGetGfxPtr(sky), skyTilesLen);
  dmaCopy(@skyMap, bgGetMapPtr(sky),  skyMapLen);

  dmaCopy(@cloudsTiles, bgGetGfxPtr(clouds), cloudsTilesLen);
  dmaCopy(@cloudsMap, bgGetMapPtr(clouds),  cloudsMapLen);

  while true do
  begin
    swiWaitForVBlank();
    bgScrollf(mounts, 1 shl 8 {256}, 0);
    bgScrollf(clouds, -(1 shl 6){-64}, 0);
    bgUpdate();
  end;
end.

Vediamo di commentare il codice dell'esempio. Come prima cosa sono stati linkati i file oggetto contenenti la grafica e le mappe, come già visto nell'esempio precedente.

Sono state quindi dichiarate variabili e costanti generate da GRIT.

Alle righe 43-45 sono stati creati i tre background. Da prestare particolare attenzione ai valori assegnati alle tre base map e alle tre tile map.

Le righe 47-55 si occupano di copiare i dati nelle giuste locazioni di memoria. Notare che la palette è unica per tutti e tre i background, quindi viene copiata soltanto una volta. La palette infatti è stata ottimizzata per utilizzare 256 colori, suddivisi in 16 palette di 16 colori ciascuna. In questo caso stiamo utilizzando soltanto le prime 3 minipalette.

Il codice nella linea 60 si occupa di eseguire lo scroll del background "mounts" di (1 shl 8), cioè un pixel per frame; il background "clouds" (riga 61) invece verrà spostato di un valore pari a -(1 shl 6), che equivale a un pixel ogni 4 frames, ma nella direzione opposta rispetto al movimento delle montagne (tramite il segno "-").

A questo punto non ci rimane che compilare l'esempio e ammirare la scena, di cui potete ammirare una cattura in figura.


Un'ultima cosa da dire sui background riguarda la possibilità di leggere la loro priorità e cambiarla al volo, nel caso in cui si volesse spostarne uno in primo o secondo piano:

function bgGetPriority(id: cint): cint;
procedure bgSetPriority(id: cint; priority: cuint);

dove id è il valore restituito dalla funzione bgInit(). Nel nostro codice di esempio, le nuvole sono visualizzate davanti alle montagne; nel caso in cui volessimo spostarle dietro, basterebbe inserire questa riga di codice appena dopo la creazione dei background (dopo le righe 43-45):

bgSetPriority(clouds, 2);


Per commenti e chiarimenti: http://www.lazaruspascal.it/index.php?topic=78.0


Scarica i sorgenti



SMF 2.0.8 | SMF © 2011, Simple Machines
Privacy Policy
SMFAds for Free Forums
TinyPortal © 2005-2012

Go back to article