[7] Free Pascal per NDS: gli sprites

Nel gergo videoludico è chiamato sprite ogni elemento grafico che si muove attraverso lo schermo: i fantasmini di Pac-Man, le navicelle spaziali di Space Invaders, gli omini di Prince of Persia, i boss di fine livello di Metal Slug sono tutti esempi di sprites. Il Nintendo DS è equipaggiato con un sistema (in realtà sono due, lo vedremo tra poco) che li gestisce via hardware, chiamato sprite engine, che risparmia al programmatore il compito di scrivere il codice per disegnarli e muoverli; come rovescio della medaglia, questo sistema non gestisce le collisioni, che vanno perciò controllate via software; è inoltre limitato ad un numero massimo di 128 sprites per motore, dei quali soltanto 32, sempre per motore, possono essere ruotati e scalati.

Gli sprites possono avere una dimensione variabile da 8x8 a 64x64 pixel e forma quadrata o rettangolare, sia con il lato lungo disposto orizzontalmente che verticalmente:

CONSTANT VALUE       SPRITE SIZE
SpriteSize_8x8
 8 x  8
SpriteSize_16x16
16 x 16
SpriteSize_32x32
32 x 32
SpriteSize_64x64
64 x 64
SpriteSize_16x8
16 x  8
SpriteSize_32x8
32 x  8
SpriteSize_32x16
32 x 16
SpriteSize_64x32
64 x 32
SpriteSize_8x16
 8 x 16
SpriteSize_8x32
 8 x 32
SpriteSize_16x32
16 x 32
SpriteSize_32x64
32 x 64


Essi sono composti da una serie di tiles di dimensione 8x8 pixel che vanno immagazzinate in memoria con un procedimento del tutto analogo a quello già visto per i background e, come per i background, possono disporre di 16 palette da 16 colori, oppure di una da 256 colori, oltre alla possibilità di utilizzare una palette estesa.


CONSTANT VALUE       COLOR FORMAT
SpriteColorFormat_16Color
16 colori per sprite
SpriteColorFormat_256Color
256 colori per sprite
SpriteColorFormat_Bmp
16 bit di colore per sprite


La tile di partenza di ogni sprite ha bisogno di essere indicizzata per permettere allo sprite engine di ricomporne l'immagine sullo schermo:


CONSTANT VALUE     
BOUNDARY BETWEEN OFFSETS
SpriteMapping_1D_32
32 bytes
SpriteMapping_1D_64
64 bytes
SpriteMapping_1D_128
128 bytes
SpriteMapping_1D_256
256 bytes (main engine only)
SpriteMapping_2D
32 bytes
SpriteMapping_Bmp_1D_128
128 bytes
SpriteMapping_Bmp_1D_256
256 bytes (main engine only)
SpriteMapping_Bmp_2D_128
128 bytes
SpriteMapping_Bmp_2D_256
256 bytes (main engine only)


Il sistema di indicizzazione adottato nel DS presenta delle limitazioni che vengono aggirate mediante degli artifici. Infatti ogni sprite engine ha a disposizione un registro di 10 bit, ovvero di 1024 "indici" (o offset) da utilizzare per gli sprites. Ad esempio, SpriteMapping_1D_32 stabilisce che i vari indici siano separati da 32 bytes l'uno dall'altro, quindi è in grado di indicizzare un massimo di 32kb (32 bytes * 1024 offset) di memoria video per gli sprites; allo stesso modo SpriteMapping_1D_128 è in grado di indicizzare un massimo di 128kb per gli sprites. Posso anticipare fin da ora che è perfettamente inutile perdere tempo dietro a questo concetto in quanto, salvo complicazioni dovute a un numero elevato di sprites molto piccoli, conviene sempre impostare un valore di offset pari alla memoria assegnata allo sprite engine. Ad esempio, assegnando allo sprite engine 64kb di memoria video, è conveniente impostare un offset di 64 bytes tramite SpriteMapping_1D_64.


Inizializzazione

Prima di potere utilizzare gli sprite engine del DS, essi vanno inizializzati tramite la procedura oamInit:

 procedure oamInit(var oam: OamState; mapping: SpriteMapping; extPalette: cbool); cdecl; external;

Essa richiede tre parametri; il primo indica lo sprite engine che intendiamo utilizzare (oamMain per l'engine principale e oamSub per il secondario); il secondo fa riferimento al tipo di layout che gli sprites assumono nella memoria video e all'offset utilizzato per la loro indicizzazione; il terzo fa riferimento all'utilizzo della palette estesa. Vediamo in dettaglio questi tre parametri.

Dicevamo di una distinzione tra due sprite engine, il principale e il secondario, associati rispettivamente al motore 2D principale e al motore 2D secondario. Essi sono rappresentati internamente da due record, chiamati OAM (Object Attribute Memory), che occupano una specifica posizione di memoria, situata a partire dagli indirizzi $07000000 (oamMain) e $07000400 (oamSub). I bit di queste locazioni di memoria funzionano da "interruttori" per attivare e disattivare le caratteristiche degli sprite; così, ad esempio, impostare un bit a 1 invece che a 0 può significare l'attivazione o la disattivazione dell'effetto mosaico, oppure l'impostazione dello sprite alla dimensione 16x16, o ancora il flipping verticale dello stesso, e così via. Altri gruppi di bit rappresentano invece la posizione dello sprite sullo schermo. Non ci complicheremo la vita manipolando direttamente i bit; ci avvarremo piuttosto delle comode funzioni di alto livello che vengono messe a disposizione dalla libreria libnds. Nei prossimi paragrafi vedremo le più importanti e il loro utilizzo.

Torniamo ai parametri della procedura oamInit, analizzando il secondo. Abbiamo parlato in precedenza di layout degli sprites nella memoria video. Esistono due layout utilizzabili: il layout 1D e il layout 2D. Senza farsi intimorire - e fuorviare - dalla definizione, basti sapere che gli sprites 1D sono organizzati nella memoria in maniera consecutiva, uno di seguito all'altro, suddivisi in blocchi da 8x8 pixel; gli sprites 2D sono invece organizzati come se si trovassero in un array, quindi sono disposti in memoria esattamente come vengono disegnati con un programma di grafica. A differenza dei background, gli sprites non hanno bisogno di una mappa per ricomporre sullo schermo le tiles in cui sono suddivisi. È sufficiente indicare all'engine la tile 8x8 di partenza, la forma dello sprite, la sua dimensione e il layout assunto in memoria dall'immagine (1D o 2D), e il gioco è fatto. Le immagini qui in basso mostrano i layout che gli sprites assumono in memoria, rispettivamente nella modalità 1D e in quella 2D:


Layout 1D
Layout 1D



Layout 2D
Layout 2D


I "quadratini" rappresentano le tiles 8x8 che compongono gli sprites; le tiles, da sinistra a destra, dall'alto verso il basso, occupano posizioni consecutive nella memoria video. È facile capire che nella modalità 1D ogni sprite è contenuto in blocchi di memoria contigui; nell'esempio incontreremo prima tutte le tiles dell'uovo, poi tutte le tiles della stella. Personalmente ritengo la modalità 2D poco attraente perché, anche se a prima vista potrebbe sembrare più semplice da capire, limita l'utilizzo degli sprites ad un massimo di 32 kb (più altri 32 kb per il motore secondario), contro i 256 kb utilizzabili nella modalità 1D (più altri 128 kb per il motore secondario).


Gestione degli sprites

La caratteristica interessante degli sprites nel DS è la possibilità di muoverli attraverso gli schermi senza doversi preoccupare di ridisegnarli di frame in frame. La funzione-mostro che se ne occupa è oamSet, definita come segue:

procedure oamSet(var oam: OamState; id, x, y, priority, palette_alpha: cint; size:
SpriteSize; format: SpriteColorFormat; const gfxOffset: pointer; affineIndex: cint;
sizeDouble, hide, hflip, vflip, mosaic: cbool); cdecl; external;

Prima di vedere come utilizzarla, diamo un'occhiata ai tanti parametri che la compongono:

Un esempio vale più di mille parole, perciò proviamo a caricare uno sprite in memoria, a disegnarlo e ad animarlo.


  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
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
program Sprites_a;

{$L build/sprite.o}
{$mode objfpc}

uses
  ctypes, nds9;

type
  TSprite = record 
    x, y: cint;
    gfx_mem: pcuint16;
    gfx_frame: pcuint8;
    anim_frame: cint;
    anim_delay: cint;
    mirror: boolean
  end;            

const
  FRAMES_PER_ANIMATION = 6;
  SCREEN_TOP    = 0;
  SCREEN_BOTTOM = 192;
  SCREEN_LEFT   = 0;
  SCREEN_RIGHT  = 256; 

var
  counter: cint = 0;
  spriteTiles: array [0..0] of cuint; cvar; external;
  spritePal: array [0..0] of cushort; cvar; external;
  MySprite: TSprite = (x: (256 - 64) div 2; y: (192 - 64) div 2; 
                       gfx_mem: nil; gfx_frame: nil;
                       anim_frame: 0;
                       anim_delay: 10;
                       mirror: true;);
  keys: cint;

procedure animSprite(var sprite: TSprite);
var
  offset: pcuint8;
begin
  if (counter mod sprite.anim_delay) = 0 then 
  begin
    inc(sprite.anim_frame);
    if Sprite.anim_frame >= FRAMES_PER_ANIMATION then
      Sprite.anim_frame := 0;
    offset := sprite.gfx_frame + (sprite.anim_frame * 64*64);
    dmaCopy(offset, sprite.gfx_mem, 64*64); 
  end;
end;

begin
  videoSetMode(MODE_0_2D);
  videoSetModeSub(MODE_0_2D);

  vramSetBankA(VRAM_A_MAIN_SPRITE);

  oamInit(oamMain, SpriteMapping_1D_128, false);

  MySprite.gfx_mem := oamAllocateGfx(oamMain, SpriteSize_64x64, SpriteColorFormat_256Color); 
  MySprite.gfx_frame := @spriteTiles;

  dmaCopy(@spritePal, SPRITE_PALETTE, 512);

  while true do
  begin
    scanKeys();

    keys := keysHeld();

    if keys <> 0 then
    begin
      if (keys and KEY_UP) <> 0 then
      begin
        if MySprite.y >= SCREEN_TOP - 16 then dec(MySprite.y);
      end;
      if (keys and KEY_LEFT) <> 0 then
      begin
        if MySprite.x >= SCREEN_LEFT - 16 then dec(MySprite.x);
        MySprite.mirror := false;
      end;
      if (keys and KEY_RIGHT) <> 0 then
      begin
        if MySprite.x <= SCREEN_RIGHT - 48 then inc(MySprite.x);
        MySprite.mirror := true;
      end;
      if (keys and KEY_DOWN) <> 0 then
      begin
        if MySprite.y <= SCREEN_BOTTOM - 48 then inc(MySprite.y);
      end;
    end; 

    animSprite(MySprite); 

    oamSet(oamMain, 0, MySprite.x, MySprite.y, 0, 0,
           SpriteSize_64x64, SpriteColorFormat_256Color,
           MySprite.gfx_mem, -1, false, false, MySprite.mirror, false, false);

    swiWaitForVBlank();
    oamUpdate(oamMain); 
    inc(counter);   
  end;

end.


10-17: Definiamo un record con le caratteristiche dello sprite: le coordinate della posizione sullo schermo (x, y), l'indirizzo in memoria dove è memorizzato lo sprite (gfx_mem) e quello dove è memorizzato il primo frame (gfx_frame), il frame corrente dell'animazione (anim_frame) e la pausa tra un frame e l'altro (anim_delay), il flag per stabilire se lo sprite vada disegnato ribaltato orizzontalmente (mirror).
20-24: Definiamo una serie di costanti che ci serviranno durante l'animazione e nella gestione del movimento dello sprite.
30-34: Dichiariamo una variabile di tipo TSprite, inizializzandone i valori.
55: Riserviamo agli sprites l'intero banco A di memoria.
57: Inizializziamo lo sprite engine primario (oamMain) in modo tale che contenga sprites 1D con un offset di 128 bytes e che non utilizzi la palette estesa.
59-60: Allochiamo memoria per lo sprite tramite oamAllocateGfx() sull'engine principale (oamMain), in modo tale che lo sprite abbia dimensione 64x64 pixel (SpriteSize_64x64) di 256 colori (SpriteColorFormat_256Color). Facciamo quindi puntare il primo frame dell'animazione all'indirizzo in memoria delle tiles che compongono lo sprite.
62: Copiamo l'intera palette dello sprite.
66-90: In questo blocco di codice gestiamo l'input del giocatore e il movimento dello sprite. Niente di eccessivamente complicato, ma per maggiori informazioni vi rimando a un futuro articolo dedicato interamente alla gestione dell'input.
92: Chiamiamo la funzione animSprite() (41-47) che gestisce l'animazione dello sprite: per prima cosa viene eseguito un controllo per vedere se è passato il tempo necessario affinché si debba disegnare il frame successivo. In caso affermativo viene incrementato l'indice dell'animazione oppure, se si è superato l'ultimo frame, viene azzerato. Viene quindi copiato in memoria (offset) l'indirizzo dei dati del frame da disegnare, che viene infine fisicamente copiato nella memoria video (dmaCopy()).
94-96: Effettuiamo una chiamata alla funzione oamSet(), che si occuperà di posizionare e disegnare lo sprite sullo schermo.
99: Aggiorniamo lo stato dello sprite engine.
100: Incrementiamo il contatore che servirà per l'animazione.



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