Sprites

Gestione degli sprite con SDL


Non mi assumo nessuna responsabilita' per danneggiamenti, perdita di dati o danni personali come risultato diretto o indiretto dell'uso delle informazioni contenute in queste pagine. Questo materiale e' fornito cosi' com'e' senza nessuna garanzia implicita o esplicita.


Home
Hardware
Software

Il codice
Sprite
Download

Tempo fa comprai un libro stupendo intitolato "Linux game programming", da allora sono passati diversi anni e di giochi ne ho realizzati ben pochi. Confesso di essere ancora per strada, malgrado i mille impegni che la vita ci mette davanti sto ancora cercando di non abbandonare questo sogno.

In questa pagina vorrei presentare almeno una parte del lavoro e delle scoperte fatte fin'ora. Tra le altre cose, proprio mentre scrivo queste righe, la SDLlib 2 comincia a prendere piede; non me ne frega niente: rimarro' fedele alla prima SDL fin che potro'.

Veniamo a noi: lo scopo del gioco e' quello di realizzare una libreria per la gestione degli sprite. Libreria potrebbe essere un nome un po' forte, diciamo una collezione di funzioni che semplifichino la creazione e la manipolazione degli sprite.

Il tutto comincia con una piccola demo, giusto per collaudare tutto quello che mi sono inventato. Chi vuole cominciare subito a smanettare puo' andare sotto (nella sezione download) e scaricare il codice sorgente; un rapido make clean seguito da un agile make dovrebbe risolvere i problemi di compilazione; ovviamente dovete avere installata la SDLlib (e magari anche il pacchetto dev).

Una volta superata la paura (e di solito anche i problemi) della compilazione un bel ./demo avvia la demo. Qui sopra si puo' vedere come si presenta: la caravella di centro naviga da sola e non c'e' modo di modificare il suo percorso, premendo i tasti a, b, x, y, r, l e' possibile muovere le altre 2 caravelle e il pappagallo; premere q per terminare la demo.

I tasti, che sembrano scelti a casaccio, sono invece gli stessi tasti che presenta la GP2X, ho scelto questi pensando che un giorno sarei riuscito a portare il tutto su GP2X.

Noterete che i tasti a e b fanno saltare lo sprite 1 da un punto all'altro; questa cosa e' voluta, non e' un difetto, serve a verificare che la libreria funzioni correttamente anche se utilizzata in questo modo.

I tasti x ed y muovono lo sprite 2 lungo una linea retta tracciata tra il punto di partenza e il punto di arrivo. In realta' il movimento non e' arrestabile a meta' strada, il tasto x dice devi andare a destra, mentre il tasto y dice devi andare a sisnistra, i punti di arresto sono fissi e non possono essere modificati. Noterete che lo sprite 2 (questa caravella) ha un movimento leggermente diverso dallo sprite 4 (la caravella che naviga da sola al centro della scena). Questo e' dovuto al fatto che i due sprite hanno 2 gestori di movimento differenti: la prima ha un gestore in grado di muoversi in linea retta, la seconda ha un gestore semplificato che muove solo verticalmente, orizzontalmente o a 45 gradi.

In fine i tasti r ed l muovono lo sprite 3 (il pappagallo) verso destra e verso sinistra rispettivamente. Questo sprite, che si muove solo in orizzontale, ha la caratteristica di sparire una volta raggiunta la posizione all'estrema destra. L'effetto grafico non e' gradevole, ma non e' questo lo scopo, il punto e' far sparire lo sprite quando non serve (e collaudare il fatto che sparisca veramente).

Organizzazione del codice

Vediamo alcune parti salienti del codice, l'inizio della demo assomiglia a un qualunque programma che faccia uso della SDL, di fatto quello e': dalla SDL non ci svincola nessuno, semplicemente noi facciamo un uso piu' creativo delle SDL_Surface.

Cominciamo col creare la finestra base e caricare delle immagini da disco:

  // crea la finestra base
  if((Video=SDL_SetVideoMode(VIDEO_W,VIDEO_H,16,VIDEO_MODE))==NULL)
  {
    // termina causa errore creazione finestra
    fprintf(stderr,"SDL_SetVideoMode: %s\n",SDL_GetError());
    return 1;					// termina con fallimento
  }
  
  // carica tutte le immagini da disco
  LoadSetVideo(Video);				// finestra di riferimento
  if((BackGround=Load16bitPng("backgnd.png")) == NULL)
    puts("Unable to load background");
  Tile_1=Load16bitPng("ship_1.png");		// nave verso destra
  Tile_2=Load16bitPng("ship_2.png");		// nave verso sinistra
  Tile_3=Load16bitPng("pappa_1.png");		// pappagallo verso destra
  Tile_4=Load16bitPng("pappa_2.png");		// pappagallo verso sinistra
  SDL_SetColorKey(Tile_1,SDL_SRCCOLORKEY|SDL_RLEACCEL,
  			*((unsigned short int*)Tile_1->pixels));
  SDL_SetColorKey(Tile_2,SDL_SRCCOLORKEY|SDL_RLEACCEL,
  			*((unsigned short int*)Tile_2->pixels));
  SDL_SetColorKey(Tile_3,SDL_SRCCOLORKEY|SDL_RLEACCEL,
  			*((unsigned short int*)Tile_3->pixels));
  SDL_SetColorKey(Tile_4,SDL_SRCCOLORKEY|SDL_RLEACCEL,
  			*((unsigned short int*)Tile_4->pixels));
      

Fin qui e' quasi SDL pura, ho solo utilizzato delle funzioni piu' comode (contenute almeno per il momento in loadpng.c per caricare le immagini da disco. Subito dopo il caricamento attiva il croma key e rende trasparente il colore che si trova nel pixel in alto a sinistra dell'immagine stessa: il magenta di sfondo.

sprite image sprite image sprite image sprite image

A questo punto si passa alla creazione vera e propria degli sprite:

  // crea tutti gli sprite
  PDEBUG("%s: creating sprites...\n",__FUNCTION__);
  SpriteSetVideo(Video,RefreshCallback);		// finestra base
  
  SpriteSetBackground(BackGround,BackgroundUpdate);	// imposta lo sfondo
  Sprite_1=SpriteNew(Tile_2,NULL, 10, 10);		// crea gli sprite
  Sprite_2=SpriteNew(Tile_1,NULL, 10,160);
  Sprite_3=SpriteNew(Tile_3,NULL,290, 80);
  Sprite_4=SpriteNew(Tile_1,NULL,Corner[1].x,Corner[1].y);
  
  SpriteSetOnOff(Sprite_1,1);				// sprite ON
  SpriteSetOnOff(Sprite_2,1);				// sprite ON
  SpriteSetOnOff(Sprite_4,1);				// sprite ON
      

La funzione SpriteSetVideo() indica al gestore delle sprite qual'e' la finestra in cui saranno visualizzate e segnala inoltre la funzione RefreshCallback() come callback da chiamare durante il refresh del video. In questo esempio la funzione RefreshCallback() serve a ben poco, potremmo passare alla SpriteSetVideo() un NULL al suo posto che non cambierebbe molto. Tecnicamente la callback RefreshCallback() viene chiamata dopo ogni refresh del video e potrebbe aggiornare valori o apportare modifiche con la cadenza del refresh stesso.

La funzione SpriteSetBackground() imposta l'immagine (tecnicamente la SDL_Surface) utilizzata come sfondo della scena; lo sfondo viene gestito in un modo leggermente differente dagli sprite e di fatto non e' uno sprite. La funzione SpriteSetBackground() imposta anche la callback BackgroundUpdate(), in questo esempio non serve a niente, questa callback viene chiamata durante il refresh del video in un momento strategico in cui e' possibile modificare l'immagine di sfondo senza creare problemi al refresh della scena, la callback stessa termina con 0 se non ha alterato lo sfondo, deve terminare con un valore non nullo se ha apportato modifiche allo sfondo. Questa tecnica permette di poter modificare lo sfondo stesso mentre gli sprite si muovono sopra di esso; questo potrebbe essere utile per implementare uno scrolling dello sfondo o una variazione del colore del cielo.

Per finire ecco la creazione delle sprite tramite la funzione SpriteNew(), per il momento ignorate il secondo parametro. Quando uno sprite viene creato gli viene assegnata una posizione di partenza, lo sprite pero' e' invisibile, ecco perche' il codice che segue rende visibili tutti gli sprite tranne il pappagallo.

Il codice che segue e' il piu' criptico e il piu' carico di effetti speciali, nell'evoluzione della libreria per la gestione delle sprite questo codice, molto probabilmente, subira' diverse modifiche.

  SpriteMoveLinearCfg(Sprite_2,NULL,&Linear_2);		// linear move config
  SpriteSetMoveHandler(Sprite_4,NULL,TurnAround);	// stop callback
  SpriteSetDest(Sprite_4,Corner[0].x,Corner[0].y);	// start trip
  SpriteUpdateAll();					// refresh totale
      

Ad ogni sprite, nel momento in cui viene creato, viene assegnato un gestore di movimento standard; il gestore di movimento ha bisogno di gestire dei dati propri che non sono solitamente compresi nella definizione dello sprite stesso (la cosa ha poco senso e in futuro sara' molto differente). Perche' il gestore del movimento possa funzionare la prima riga associa allo sprite 2 la struttura Linear_2, da notare che lo sprite 2 si muove in linea retta.

La seconda e la terza riga modificano il gestore di movimento dello sprite 4 (la caravella che naviga da sola) e impostano la prima destinazione di questo sprite (il punto sulla destra, visto che la caravella viene creata sulla sinistra). Per finire un bel refresh di tutta la finestra.

Ora che abbiamo creato la scena, l'abbimo popolata con delle sprite e abbiamo impostato le linee generali del discorso, serve la cosa piu' importante: l'update della scena. Creaiamo un timer che ad intervalli fissi generi il refresh della scena, per fare questo ricorriamo alla SDL:

  // crea il timer aggiornamento video
  PDEBUG("%s: creating screen update timer...\n",__FUNCTION__);
  if((SerialTimer=SDL_AddTimer(ReadInterval,TimerCallback,NULL))==NULL)
    fprintf(stderr,"SDL_AddTimer: %s\n",SDL_GetError());
      

Ora siamo pronti per tuffarci in un loop infinito in cui fare poco niente e rimanere a guardare. la caravella al centro della scena naviga da sola, quindi come minimo ci aspettiamo che questa si muova. Il movimento di questa caravella viene completamente gestito da una serie di alchimie create ad arte dallo stesso gestore delle sprite: la callback TurnAround() viene chiamata automaticamente tutte le volte che lo sprite ha raggiunto la propria destinazione, la funzione non fa altro che girare la nave (modifica lo sprite stesso) e impostare una nuova destinazione, sara' il gestore del movimento dello sprite a spostare lo sprite stesso fino a raggiungere la nuova destinazione.

Il loop infinito perdura fin tanto che la variabile Run non viene posta a zero (quale immensa fantasia), nel frattempo ad ogni giro legge lo stato dei tasti e interpreta i tasti premuti, vediamo questa parte per svelare i segreti del movimento degli altri sprite.

	  case SDLK_a:
	    SpriteSetPos(Sprite_1,200,10);		// movimento secco
	    SpriteUpdateAll();				// forza il refresh
	    break;
	  case SDLK_b:
	    SpriteSetPos(Sprite_1,10,10);		// movimento secco
	    SpriteUpdateAll();				// forza il refresh
	    break;
	  case SDLK_x:
	    SpriteMoveLinear(Sprite_2,200,120,3);	// linear move
	    break;
	  case SDLK_y:
	    SpriteMoveLinear(Sprite_2,10,160,3);	// linear move
	    break;
	  case SDLK_l:
	    SpriteSetOnOff(Sprite_3,1);			// sicuramente visibile
	    SpriteSetMoveHandler(Sprite_3,NULL,NULL);	// rimuove la callback
	    SpriteSetDest(Sprite_3,200,80);		// slide move
	    break;
	  case SDLK_r:
	    SpriteSetMoveHandler(Sprite_3,NULL,Invisibile);	// end callback
	    SpriteSetDest(Sprite_3,290,80);			// slide move
	    break;
      

Alla pressione dei tasti a e b corrisponde una chiamata secca alla funzione SpriteSetPos(), questa, come suggerisce il nome, sposta lo sprite nella nuova posizione. L'uso diretto di questa funzione al di fuori di un gestore di movimento dello sprite stesso e' un po' strano, di fatto il risultato e' altrettanto strano: la caravella salta da una posizione all'altra; come ho gia' detto e' solo una prova. La chiamata alla SpriteUpdateAll() serve a segnalare al gestore delle sprite che c'e' stato movimento, l'uso diretto della SpriteSetPos() non e' sufficiente.

I tasti x ed y generano una chiamata alla funzione SpriteMoveLinear() che avvia un movimento lineare dalla posizione attuale dello sprite fino alla posizione indicata. Una nuova chiamata a meta' strada puo' cambiare la meta.

I tasti l ed r modificano la destinazione dello sprite attraverso la funzione SpriteSetDest(); il tasto l prima di fare questo rimuove la stop callback, cioe' la funzione che sara' chiamata una volta raggiunta la destinazione. Al contrario il tasto r prima di modificare la destinazione dello sprite imposta la stop callback a Invisibile(), questa funzione rendera' invisibile lo sprite una volta raggiunta la destinazione, ecco' perche' il pappagallo sparisce una volta raggiunto il bordo destro della finestra.

Preparazione degli sprite

Ora facciamo un piccolo esempio di preparazione degli sprite: supponiamo di voler animare il seguente sprite.


Gif animata

Questa gif e' scaricabile da questo sito.

Per integrare lo sprite nel nostro codice avremmo bisogno di organizzare i vari frames in una tavola, possibilmante dove tutti i frame hanno la stessa dimensione, senza canale alfa e magari con uno sfondo differente per ogni frame che faccia da corma key. Tutto questo puo' essere fatto quasi automaticamente col pacchetto ImageMagick.

  $ convert mummy.gif -trim trimmed.gif
  $ convert -coalesce trimmed.gif mummy_%03d.png
  $ file mummy_000.png
  $ convert -size 37x45 xc:'#0099ff' canvas_a.png
  $ convert -size 37x45 xc:'#00ccff' canvas_b.png
      

Per fortuna o per sfiga, non so, ma partiamo da una gif animata. Questo per certi versi semplifica alcuni passaggi, potrebbe pero' complicarne degli altri. Vediamo il significato delle righe qui sopra.
La prima riga serve a ritagliare il rettangolo piu' piccolo in grado di contenere l'intera animazione, nel caso specifico non serve visto che l'animazione e' gia' ridotta ai minimi termini.
La seconda riga estrae tutti i frame dalla gif animata e salva ciascuno in un file png.
La terza riga serve solo ad avere la dimensione del singolo frame, nel nostro caso sono tutti 37x45 pixel.
La quarta e la quinta riga creano delle nuove immagini monocrome della stessa dimensione dei nostri frame, e' molto importante che il colore scelto non sia contenuto nei frame stessi. Ne creiamo due di due colori leggermente diversi per essere piu' comodi a verificare la tavola finale prodotta, in realta' in un processo automatizzato di composizione delle immagini questa accortezza non dovrebbe essere necessaria.

  $ convert canvas_a.png mummy_000.png -geometry +0+0 -composite mummy_000_c.png
  $ convert canvas_b.png mummy_001.png -geometry +0+0 -composite mummy_001_c.png
  $ convert canvas_a.png mummy_002.png -geometry +0+0 -composite mummy_002_c.png
  $ convert canvas_b.png mummy_003.png -geometry +0+0 -composite mummy_003_c.png
  $ convert canvas_a.png mummy_004.png -geometry +0+0 -composite mummy_004_c.png
  $ convert canvas_b.png mummy_005.png -geometry +0+0 -composite mummy_005_c.png
  $ convert canvas_a.png mummy_006.png -geometry +0+0 -composite mummy_006_c.png
  $ convert canvas_b.png mummy_007.png -geometry +0+0 -composite mummy_007_c.png
  $ convert canvas_a.png mummy_008.png -geometry +0+0 -composite mummy_008_c.png
  $ convert canvas_b.png mummy_009.png -geometry +0+0 -composite mummy_009_c.png
  $ convert canvas_a.png mummy_010.png -geometry +0+0 -composite mummy_010_c.png
  $ convert canvas_b.png mummy_011.png -geometry +0+0 -composite mummy_011_c.png
  $ convert canvas_a.png mummy_012.png -geometry +0+0 -composite mummy_012_c.png
  $ convert canvas_b.png mummy_013.png -geometry +0+0 -composite mummy_013_c.png
  $ convert canvas_a.png mummy_014.png -geometry +0+0 -composite mummy_014_c.png
  $ convert canvas_b.png mummy_015.png -geometry +0+0 -composite mummy_015_c.png
  $ convert canvas_a.png mummy_016.png -geometry +0+0 -composite mummy_016_c.png
  $ convert canvas_b.png mummy_017.png -geometry +0+0 -composite mummy_017_c.png
      

Ora incolliamo un frame sopra ad ogni sfondo nonocromo che abbiamo preparato e salviamo il risultato in un nuovo file.
A questo punto non rimane che incollare tutte queste immagini una fianco all'altra e salvare il risultato in un nuovo file.

  $ convert mummy_???_c.png +append mummy_02.png
      


Tavola dei frame prodotta

Qui sopra si puo' vedere il risultato finale.
Questa e' un'immagine in formato png, senza canale alfa, in cui ciascun frame ha le stesse dimensioni ed il proprio sfondo croma key.

Un'altro trucchetto basato su ImageMagick potrebbe essere questo:

  $ convert -size 666x45 xc:'#0099ff' -draw "image over 0,0 0,0 mummy_000.png" mummy_02.png
      

Questa tecnica usa il comando draw per incollare i frame sopra alla nuova tela appena generata, questo dovrebbe ridurre il numero di passaggi necessari. Una precisazione sui parametri della riga di sopra: la prima coppia 0,0 indica la posizione in cui sara' incollata l'immagine, la seconda coppia e' un'eventuale resize dell'immagine incollata, 0,0 per disabilitare il resize.

Note sulla musica

Importanti note sulla gestione della musica si trovano qui.

Per convertire i file MIDI tramite timidity usare il comando:

  $ timidity file.mid -M file.wav
      

Links

http://lazyfoo.net/SDL_tutorials/

Download

sdldemo-0.03.tar.gz
sdldemo-0.04.tar.gz online documentation

Questo sito e' stato realizzato interamente con vim.
Grazie a tutta la comunita' open source, alla free software foundation e chiunque scriva software libero.