Sei sulla pagina 1di 13

CAPITOLO 6

Gestione degli sprite


- 6.1 Generalità sugli sprites

Il termine sprite venne coniato negli anni ’70 dallo staff della Apple e dell’Atari, per indicare un
piccolo oggetto in movimento sullo schermo (difatti la traduzione italiana di sprite è spettro o
folletto).
Ad esempio uno sprite può rappresentare un giocatore in un videogioco, in grado di muoversi sul
monitor, animarsi, cambiare la sua forma dimensione e colore, senza molte difficoltà.
Solitamente, la gestione degli sprites è implementata tramite hardware, come succede nell’Amiga,
Atari, Commodore ed Apples; purtroppo il PC IBM (e compatibili), sono sprovvisti di tale
caratteristica, per cui bisognerà sopperire a tale mancanza, implementando tramite software un così
detto sprite engine, cioè una serie di routines che ci permetteranno di gestire le animazioni.
Successivamente, nelle nostre demo, considereremo uno sprite come un oggetto ad alto livello che
ci permetterà di simulare il movimento di oggetti o addirittura scritte in modalità grafica.
Per ora, nel prossimo paragrafo, ci occuperemo della sua implementazione ad un livello più basso.

- 6.2 Gestione low-level degli sprites

Nel momento in cui, desideriamo visualizzare uno sprite in movimento su un’immagine di fondo,
dobbiamo simulare la netta distinzione tra l’oggetto in movimento ed il suo campo d’azione.
La prima problematica che sorge, è quella di disegnare la nostra entità da animare e di come
proporla sul monitor del nostro PC.
La risposta è semplice, infatti, come già accennato, si può utilizzare un’immagine creata con un
qualsiasi programma di elaborazione grafica (come ad esempio Paint Shop Pro, o lo arcinoto Paint
Brush di Windows) come un contenitore di sprites.
Così, dopo aver opportunamente disegnato i nostri soggetti, ed averli successivamente salvati in uno
dei formati grafici a noi noti (BMP, TGA o, preferibilmente, PCX), dobbiamo, in qualche modo,
riuscire ad estrarli per ricopiarli nella memoria video.
L’operazione mediante la quale, una porzione di memoria viene trasferita in un altro blocco, viene
detta, da parte dei programmatori di videogiochi, Bit Blitting, che sta per Binary Block Image
Transfer, e sfruttata da essi per spostare un pezzo di immagine da una parte ad un’altra della stessa.
Per meglio comprendere tale concetto, realizzeremo una routine, in grado di estrarre una porzione
rettangolare di immagine salvata in un formato tra quelli conosciuti e di ricopiarla in una porzione
di memoria, come mostrato nel seguente listato :

/*******************************************************************************/
/* Estrae una porzione rettangolare (box) di immagine da un buffer 320x200 */
/* e la copia in un'altra di dimensioni identiche al box */
/*******************************************************************************/
void Image_Grab(unsigned char far *sorg,unsigned char far *dest,int x,int y,int b,int h){
unsigned int i;
sorg+=(y<<6)+(y<<8)+x; /* Calcolo vertice superiore sinistro del box sorgente */
for(i=0;i<h;i++){ /* Trasferimento nel buffer puntato da dest */
memcpy(dest,sorg,(size_t)b); /* Trasferimento di una singola riga */
dest+=b; /* Salto alla riga successiva */
sorg+=320;
}
}

A questo punto, dobbiamo trasferire lo sprite contenuto nel nostro box, nella memoria video.
1
Per fare ciò, utilizzeremo la seguente routine :

/***************************************************************************/
/* Disegna lo sprite su un buffer senza tenere conto delle trasparenze. */
/***************************************************************************/
void Draw_Sprite_Raw(unsigned char far *spriteptr,unsigned char far *buff,int x,int y,
int b,int h){
int i,j; /* Indici di colonna e di riga */
buff+=(y<<6)+(y<<8)+x; /* Calcolo vertice superiore sinistro del buffer destinazione */

for(j=0;j<h;j++){
for(i=0;i<b;i++){ /* Copia una riga del box sul buffer destinazione */
*buff = *spriteptr;
spriteptr++;
buff++;
}
buff+=(320-b); /* Salta una riga dello schermo sul buffer */
}
}

#include<stdio.h>
#include<conio.h>
#include<alloc.h>
#include"vga.h"
#include"formati.h"

#define BASE_SPRITE 20 /* Dimensioni orizzontali e verticali dello sprite */


#define ALTEZZA_SPRITE 20

void main(void) {
PALETTE pal[256]; /* La palette utilizzata */
char *image; /* Puntatore inizio immagine sfondo */
char *sprites; /* Immagine contenente gli sprites */
char *app; /* Box dello sprite */
int x,y; /* Coordinate dello sprite */

if((image=malloc(64000))==NULL) { /* Allocazione memoria per sfondo */


printf("Memoria insufficiente");
exit(-1);
}
if((sprites=malloc(64000))==NULL) { /* Allocazione memoria per sprites */
printf("Memoria insufficiente");
exit(-1);
}
if((app=malloc(BASE_SPRITE*ALTEZZA_SPRITE))==NULL) { /* Allocazione memoria per il box*/
printf("Memoria insufficiente");
exit(-1);
}
if(Load_Pcx(“sfondo.pcx”,image,pal)==NULL) { /* Caricamento immagine sfondo */
printf("Immagine non trovata");
exit(-1);
}
if(Load_Pcx(“sprites.pcx”,sprites,pal)==NULL) { /* Caricamento immagine sprite */
printf("Immagine non trovata");
exit(-1);
}

Set_Vga_Mode(); /* Setta la modalità video */


Set_Palette(pal); /* Imposta i colori */
memcpy(VGAPTR,image,64000); /* Visualizzazione dello sfondo */

/* Estrae lo sprite dall’angolo alto sx dell’immagine contenitore e lo pone nel box */


Image_Grab(sprites,app,0,0,BASE_SPRITE,ALTEZZA_SPRITE);

y=150;
for(x=0;x<250;x++){ /* Muove lo sprite orizzontalmente sullo schermo */
Wait_Vertical_Retrace(); /* Evita il flickering */
Draw_Sprite_Raw(app,VGAPTR,x,y,BASE_SPRITE,ALTEZZA_SPRITE);
}
2
getch();
Set_Text_Mode(); /* Ritorno alla modalità testuale */
free(image); /* Rilascio della memoria allocata per lo sfondo */
free(sprites); /* Rilascio della memoria allocata per gli sprites */
}

Mandando in esecuzione il precedente esempio, ci si rende conto che l’oggetto in movimento


comprende l’intero box ed ha praticamente distrutto lo sfondo sul quale è passato.
Bisogna in qualche modo riparare a tale inconveniente introducendo il concetto di trasparenza,
trattato esaurientemente nel prossimo paragrafo.

- 6.3 Sprites e trasparenza

Abbiamo detto che uno sprite deve rappresentare un’entità autonoma, completamente distaccata
dallo sfondo, e che quest’ultimo non deve essere coinvolto dalle variazioni che gli oggetti in
movimento subiscono.
Per far sì che tale definizione sia verificata, bisogna introdurre il concetto di trasparenza.
Supponiamo che il nostro soggetto da animare, sia contenuto all’interno di un ipotetico box,
allocato mediante la funzione Image_Grab(…); poiché tale involucro è caratterizzato da una forma
rettangolare, e non è detto che il nostro sprite debba avere per forza la stessa forma, l’ipotetica
scatola, conterrà oltre all’oggetto, anche dei pixel che non dovranno essere coinvolti nelle
operazioni di Bit Blitting.
Bisogna quindi progettare un meccanismo che consenta di distinguere i punti del box costituenti lo
sprite da quelli estranei ad esso.
Un semplice ed efficace metodo consiste nell’assegnare ai punti esterni un valore costante e noto,
ad esempio zero, allo scopo di indicare che in corrispondenza dei bytes estranei (di valore zero) lo
sfondo non deve essere sostituito.
Ecco un esempio di rappresentazione matriciale di uno sprite :

0 0 3 0 0 0 = Parte trasparente dello sprite


0 3 2 3 0 1,2,3,4 = Colori dello sprite
4 2 1 2 4
0 3 2 3 0
0 0 3 0 0

Ed ora la routine per visualizzare uno sprite tenendo conto delle trasparenze eliminando così gli
elementi del box non interessati nell’animazione :

/****************************************************************************/
/* Disegna lo sprite su un buffer tenendo conto delle trasparenze. */
/****************************************************************************/
void Draw_Sprite(BYTE far *spriteptr,BYTE far *buff,int x,int y,int b,int h){
int i,j;
buff+=(y<<6)+(y<<8)+x;
for(j=0;j<h;j++){
for(i=0;i<b;i++){
if(*spriteptr) *buff = *spriteptr; /* Se il valore del box è diverso da zero */
spriteptr++; /* modifica lo sfondo */
buff++;
}
buff+=(320-b);
}
}

3
Se sostituissimo quest’ultima funzione al posto della precedente Draw_Sprite_Raw(…), avremmo
ottenuto l’effetto di eliminare gli elementi estranei allo sprite, ma non avremo ancora risolto il
problema del mantenimento dell'immagine sottostante.
Per eliminare tale imprevisto, bisogna ricorrere ad uno stratagemma : salvare in un’area di memoria
la porzione di sfondo sopra la quale sarà disegnato lo sprite per poi ripristinare l’immagine
originale, prima di effettuare un ulteriore spostamento dell’oggetto animato.
Così operando, si simula la separazione dello sprite rispetto all’immagine di fondo.
Abbiamo in questa maniera ottenuto lo scopo desiderato, la cui implementazione pratica è riportata
nel prossimo listato :

#include<stdio.h>
#include<conio.h>
#include<alloc.h>
#include"vga.h"
#include"formati.h"

#define BASE_SPRITE 20 /* Dimensioni orizzontali e verticali dello sprite */


#define ALTEZZA_SPRITE 20

void main(void) {
PALETTE pal[256]; /* La palette utilizzata */
char *image; /* Puntatore inizio immagine sfondo */
char *sprites; /* Immagine contenente gli sprites */
char *app, /* Box dello sprite */
*back; /* Buffer per memorizzazione sfondo */
int x,y; /* Coordinate dello sprite */

if((image=malloc(64000))==NULL) { /* Allocazione memoria per sfondo */


printf("Memoria insufficiente");
exit(-1);
}

if((sprites=malloc(64000))==NULL) { /* Allocazione memoria per sprites */


printf("Memoria insufficiente");
exit(-1);
}

if((app=malloc(BASE_SPRITE*ALTEZZA_SPRITE))==NULL) { /* Allocazione memoria per il box*/


printf("Memoria insufficiente");
exit(-1);
}

if((back=malloc(BASE_SPRITE*ALTEZZA_SPRITE))==NULL) { /* Memoria per backup sfondo */


printf("Memoria insufficiente");
exit(-1);
}

if(Load_Pcx(“sfondo.pcx”,image,pal)==NULL) { /* Caricamento immagine sfondo */


printf("Immagine non trovata");
exit(-1);
}

if(Load_Pcx(“sprites.pcx”,sprites,pal)==NULL) { /* Caricamento immagine sprite */


printf("Immagine non trovata");
exit(-1);
}

Set_Vga_Mode(); /* Setta la modalità video */


Set_Palette(pal); /* Imposta i colori */
memcpy(VGAPTR,image,64000); /* Visualizzazione dello sfondo */

4
/* Estrae lo sprite dall’angolo alto sx dell’immagine contenitore e lo pone nel box */
Image_Grab(sprites,app,0,0,BASE_SPRITE,ALTEZZA_SPRITE);
y=150;
for(x=0;x<250;x++){ /* Muove lo sprite orizzontalmente senza rovinare lo sfondo */
Image_Grab(image,back,x,y,BASE_SPRITE,ALTEZZA_SPRITE); /* Salva lo sfondo */
Wait_Vertical_Retrace(); /* Evita il flickering */
Draw_Sprite_Raw(back,VGAPTR,x,y,BASE_SPRITE,ALTEZZA_SPRITE);/*Ripristina lo sfondo*/
Draw_Sprite(app,VGAPTR,x+1,y,BASE_SPRITE,ALTEZZA_SPRITE); /* Disegna lo sprite */
}

getch();
Set_Text_Mode(); /* Ritorno alla modalità testuale */
free(image); /* Rilascio della memoria allocata per lo sfondo */
free(sprites); /* Rilascio della memoria allocata per gli sprites */
free(back); /* Rilascio della memoria allocata per backup sfondo */
}

La metodologia descritta risulta valida solamente nel caso in cui si abbia a che fare con un numero
limitato si sprites in azione contemporaneamente sullo schermo.
Al contrario se il numero di oggetti in movimento contemporaneamente diviene consistente, si
noterà un eccessivo rallentamento dell’animazione ed il movimento in ordine sequenziale degli
sprites, contrariamente a quanto ci si aspettava.
Fortunatamente ci viene in aiuto una diffusa tecnica, nota con il nome di Double Buffering.

- 6.4 Tecnica del Double Buffering

Nonostante l’aver adottato molteplici tecniche al fine di migliorare la nostra animazione, come
sincronizzarsi col pennello elettronico, attendendo la fine del ritracciamento verticale ed
orizzontale, non siamo ancora riusciti ad eliminare del tutto quel fastidioso disturbo di sfarfallio o
meglio noto come flickering, che senz’altro compromette la buona riuscita dell’ipotetica demo.
Lo sgradevole inconveniente è dovuto all’elevato tempo di accesso alla RAM video, che
aggireremo con la sopra citata tecnica conosciuta come double buffering, consistente nell’utilizzo
di un’area di memoria avente dimensioni esattamente uguali a quelle della memoria video da noi
utilizzata, ma allocata sulla RAM del PC ed utilizzata come un vero e proprio “schermo virtuale”,
non visibile, nel quale effettuare tutte le modifiche in maniera temporanea, per poi riversare alla
fine di un fotogramma (frame) l’intero contenuto del buffer nella memoria della VGA, rendendo
visibili le modifiche.
Del resto, si tratta della stessa operazione effettuata per l’apertura di un file grafico, nel momento in
cui utilizzavamo un buffer d’appoggio per l’eventuale decodifica dell’immagine prima di mostrarla
a video.
In analogia con la definizione di memoria video, che in utilizzando il linguaggio C è visibile come
un array di 64000 elementi allocato a partire dall’indirizzo lineare A0000h, mediante questa tecnica,
non facciamo altro che definire un altro schermo immaginario allocato a partire da un indirizzo
differente, avente la caratteristica di celare all’occhio dell’utente le eventuali modifiche apportate su
di esso.
Il guadagno sta nel fatto che l’accesso alla RAM di sistema risulta notevolmente più rapido nei
confronti di quella video, conferendo alla nostra demo una maggiore fluidità, e allo stesso tempo
introducendo un alto grado di parallelismo garantendo il movimento simultaneo degli sprites.
La copiatura del contenuto del buffer d’appoggio nella VRAM, sarà effettuata mediante una
funzione di copia ad alta velocità, implementabile direttamente in assembler, in quanto le CPU Intel
80x86, possiedono nel loro set di istruzioni degli appositi comandi.

5
Segue un esempio :

/************************************************************************/
/* Copia lo schermo puntato da screen nella RAM video */
/************************************************************************/
void Fast_Copy (void far *screen){
asm {
push ds /* Salva il contenuto del Data Segment */
cld /* Reset del flag di direzione */
les di,VGAPTR /* Carica l’indirizzo iniziale della VRAM in es:di */
lds si,screen /* Carica l’indirizzo puntato da screen in ds:si */
mov cx,32000 /* Registro contatore di iterazioni */
rep movsw /* Copia 32000 Word (2 bytes) da ds:si a es:di */
pop ds /* Ripristina il contenuto del segmento dati */
}
}

Possedendo un processore 386 o superiore, ed un compilatore capace di produrre codice eseguibile


a 32 bit (come quello della Watcom tanto per fare un esempio), la precedente funzione può essere
realizzata adottando l’istruzione di trasferimento movsd, capace di copiare una doubleword alla
volta (4 bytes) impiegando 7 cicli di clock (386 e 486), limitando la durata del ciclo iterativo.

/************************************************************************/
/* Copia lo schermo puntato da screen nella RAM video utilizzando movsd */
/************************************************************************/
void Fast_Copy (void far *screen){
asm {
push ds /* Salva il contenuto del Data Segment */
cld /* Reset del flag di direzione */
les edi,VGAPTR /* Carica l’indirizzo iniziale della VRAM in es:edi */
lds esi,screen /* Carica l’indirizzo puntato da screen in ds:esi */
mov ecx,16000 /* Registro contatore di iterazioni */
rep movsd /* Copia 16000 DWord (4 bytes) da ds:esi a es:edi */
pop ds /* Ripristina il contenuto del segmento dati */
}
}

Operando in questo modo, abbiamo ottenuto l’indubbio vantaggio di non dover attendere per lo
spostamento di ogni sprite il ritracciamento verticale, ma una sola volta prima della visualizzazione
di un frame completo.

#include<stdio.h>
#include<conio.h>
#include<alloc.h>
#include"vga.h"
#include"formati.h"

#define BASE_SPRITE 20 /* Dimensioni orizzontali e verticali dello sprite */


#define ALTEZZA_SPRITE 20

void main(void) {
PALETTE pal[256]; /* La palette utilizzata */
BYTE *image, /* Puntatore inizio immagine sfondo */
*sprites, /* Immagine contenente gli sprites */
*app, /* Box dello sprite */
*back, /* Buffer per memorizzazione sfondo */
*workarea; /* Il nostro schermo virtuale */
int x,y; /* Coordinate dello sprite */

if((image=malloc(64000))==NULL) { /* Allocazione memoria per sfondo */


printf("Memoria insufficiente");
exit(-1);
}

6
if((sprites=malloc(64000))==NULL) { /* Allocazione memoria per sprites */
printf("Memoria insufficiente");
exit(-1);
}

if((workarea=malloc(64000))==NULL) { /* Allocazione double buffer */


printf("Memoria insufficiente");
exit(-1);
}

if((app=malloc(BASE_SPRITE*ALTEZZA_SPRITE))==NULL) { /* Allocazione memoria per il box*/


printf("Memoria insufficiente");
exit(-1);
}

if((back=malloc(BASE_SPRITE*ALTEZZA_SPRITE))==NULL) { /* Memoria per backup sfondo */


printf("Memoria insufficiente");
exit(-1);
}

if(Load_Pcx(“sfondo.pcx”,image,pal)==NULL) { /* Caricamento immagine sfondo */


printf("Immagine non trovata");
exit(-1);
}

if(Load_Pcx(“sprites.pcx”,sprites,pal)==NULL) { /* Caricamento immagine sprite */


printf("Immagine non trovata");
exit(-1);
}

Set_Vga_Mode(); /* Setta la modalità video */


Set_Palette(pal); /* Imposta i colori */
memcpy(workarea,image,64000); /* Visualizzazione dello sfondo */

/* Estrae lo sprite dall’angolo alto sx dell’immagine contenitore e lo pone nel box */


Image_Grab(sprites,app,0,0,BASE_SPRITE,ALTEZZA_SPRITE);
y=150;
for(x=0;x<250;x++){ /* Muove gli sprites orizzontalmente senza rovinare lo sfondo */
/* Disegna due sprites uguali che si incrociano durante la loro traiettoria */
Draw_Sprite(app,workarea,x,y,BASE_SPRITE,ALTEZZA_SPRITE);
Draw_Sprite(app,workarea,320-x,y,BASE_SPRITE,ALTEZZA_SPRITE);
Wait_Vertical_Retrace(); /* Evita il flickering */
Fast_Copy(workarea); /* Visualizza i dati nell’area di lavoro */

/* Preleva gli sfondi e li ripristina */


Image_Grab(image,back,x,y,BASE_SPRITE,ALTEZZA_SPRITE);
Draw_Sprite_Raw(back,workarea,x,y,BASE_SPRITE,ALTEZZA_SPRITE);
Image_Grab(image,back,320-x,y,BASE_SPRITE,ALTEZZA_SPRITE);
Draw_Sprite_Raw(back,workarea,320-x,y,BASE_SPRITE,ALTEZZA_SPRITE);
}

getch();
Set_Text_Mode(); /* Ritorno alla modalità testuale */
free(image); /* Rilascio della memoria allocata per lo sfondo */
free(sprites); /* Rilascio della memoria allocata per gli sprites */
free(back); /* Rilascio della memoria allocata per backup sfondo */
}

Siamo finalmente riusciti ad animare due sprites contemporaneamente sullo schermo, eliminando
tutti i disturbi, ci rimane solamente di modificare la routine Draw_Sprite(…) al fine di controllare il
movimento degli oggetti in prossimità dei bordi dello schermo.
Di ciò ci occuperemo nel prossimo paragrafo.

7
- 6.5 Regioni di clipping

Per clipping s’intende l’operazione mediante la quale, si effettua la visualizzazione parziale di uno
sprite, ad esempio in prossimità dei bordi dello schermo, al fine di non alterare il contenuto di aree
di memoria esterne all’area di lavoro, oppure per limitare la visibilità di un oggetto in movimento
all’interno di una regione limitata (si immagini, un pupazzetto che si muove all’interno di
un’abitazione nel momento in cui passa davanti ad una finestra).
Nell’esempio che riporteremo, saranno presi in considerazione solamente i bordi dello schermo,
considerando per semplicità quest’ultimo come un’unica regione, semplicemente controllando le
coordinate di ogni punto in cui dovrà essere disegnato lo sprite.
Se cadranno all’interno della regione di schermo visibile, i pixel corrispondenti saranno ricopiati nel
buffer di lavoro, altrimenti, in caso contrario no.

/****************************************************************************/
/* Disegna lo sprite su un buffer controlla le trasparenze ed il clipping */
/****************************************************************************/
void draw_sprite_clip(unsigned char far *spriteptr,unsigned char far *buff,int x,
int y,int b,int h){
int i,j;
buff+=(y<<6)+(y<<8)+x;
for(j=0;j<h;j++){
for(i=0;i<b;i++){ /* Controllo trasparenza e clipping */
if(*spriteptr&&((x+i)<320)&&((y+j)<200)) *buff = *spriteptr;
spriteptr++;
buff++;
}
buff+=(320-b);
}
}

L’esempio di utilizzo è semplicemente identico al precedente, salvo che occorre sostituire la


funzione Draw_Sprite(…) con Draw_Sprite_Clip(…).

- 6.6 Gestione delle collisioni

Con il termine collisione, si intende l’evento rappresentato dall’incontro di due o più sprites aventi
traiettorie che si incrociano.
Tratteremo l’argomento in maniera piuttosto sintetica e generale, poiché le routines che si occupano
della gestione di tali eventi, devono essere progettate in maniera differente, a seconda dello
specifico caso.
Le collisioni, innanzitutto, devono essere rilevate, o secondo tecniche algoritmiche facenti uso delle
operazioni logiche su numeri binari, oppure semplicemente per confronto di coordinate.
Quest’ultima tecnica, controlla le coordinata dei box degli sprites, tenendo conto delle eventuali
dimensioni, riuscendo a determinare il verificarsi o meno di una sovrapposizione, come mostrato in
questo pseudocodice :

/************************************************************************************/
/* Supponiamo che ogni box degli sprites abbia dimensioni BASE x ALTEZZA e che */
/* siano rispettivamente posizionati alle coordinate (x1,y1),(x2,y2) corrispondenti */
/* all’angolo superiore sinistro di ogni box. */
/************************************************************************************/

if(x1>x2 && x1<x2+BASE && y1>y2 && y1<y2+ALTEZZA) {


Gestione_Collisione();
}
else {
Nessuna_Collisione();
}

8
Se si desidera conseguire una maggiore precisione nel rilevamento delle collisioni, dopo aver
applicato il precedente metodo, si può procedere con l’operazione logica AND bit a bit, tra le
maschere precalcolate rappresentanti le frazioni di box che si sono sovrapposte, nelle quali un bit
settato sta ad indicare la presenza dello sprite, uno resettato, la trasparenza.
Se il risultato di tale operazione è pari a zero, la collisione non è verificata, altrimenti se è diverso
da zero, dovrà essere eventualmente gestita.
Questo procedimento può essere utilizzato, nei casi in cui, si ha la necessità di effettuare un
controllo preciso sull’intersezione dei profili degli sprites, ad esempio in un picchia duro, per il
rilevamento degli impatti corpo a corpo, oppure nell’incastonamento di oggetti.
Poiché la trattazione specifica di tale argomento esula dallo scopo del testo, ci siamo limitati alla
sola descrizione delle tecniche da adottare.
Lasciamo al lettore l’implementazione pratica, ora che è in possesso delle adeguate basi.

- 6.7 Text blitting

Un argomento molto importante nella programmazione di demo è rappresentato dalla


visualizzazione del testo in modalità grafica.
I singoli caratteri, possono essere disegnati da noi oppure si può sfruttare il font di default del nostro
PC, comunque in entrambe i casi, i singoli simboli devono essere trattati come sprites.
Scopriremo ora, dove sono nascosti questi caratteri, per poterli rappresentare alla grandezza e con il
colore che più desideriamo.
Essi sono rappresentati all’interno di un’ipotetica matrice di 8x8 elementi, allocati sequenzialmente
dall’indirizzo di partenza F000:FA6E, rispettando l’ordine di sequenza del codice ASCII, così il
carattere ‘A’ si trova nella posizione n° 65.
Ogni carattere è composto da otto bytes ognuno dei quali rappresenta una singola linea del bitmap
associato, così per calcolare l’offset del carattere ‘A’, rispetto all’indirizzo iniziale, bisognerà
sommare 65*8 all’indirizzo base.
Prima di procedere alla visualizzazione, abbiamo bisogno di convertire i singoli bits in bytes,
associando così ad ogni pixel un colore a nostro piacimento, magari riuscendo ad ottenere un font
multi-color; ciò può essere ottenuto scandendo il set di caratteri della ROM linea per linea, e per
ognuna di esse, assegnando al rispettivo pixel della RAM video, il colore scelto per la
rappresentazione grafica del carattere in corrispondenza dei bit settati; per i bit di valore zero, si
adotta la stessa tecnica degli sprite, cioè, si lascia inalterato lo sfondo.
Set caratteri ROM 8 x 8

Byte numero :

0
1
2
3
0
4
5
6
7

.
.
.
520
521
522 Codice ASCII 'A'
523 Elemento n° 65
524 Inizia al 520° byte dall'inizio del set di caratteri ROM 8x8
525
526
527
.
.
.

255

9
Continuiamo sviluppando la routine che effettua la visualizzazione di un singolo carattere sullo
schermo e di seguito, la funzione per la stampa di una intera stringa :

#define CHAR_HEIGHT 8
#define CHAR_WIDHT 8
#define SCREEN_WIDHT 320

unsigned char far *rom_char_set = (unsigned char far *)0x0F000FA6E;

/***************************************************/
/* Stampa un carattere di sistema c partendo dalle */
/* coordinate (xc,yc) utilizzando il colore color. */
/***************************************************/
void Blit_Char(int xc, int yc, char c, int color){
unsigned int offset,x,y;
unsigned char far *work_char;
unsigned char bit_mask = 0x080;

/* Calcolo indirizzo iniziale del carattere di sistema */


work_char = rom_char_set + c * CHAR_HEIGHT;
/* Calcolo indirizzo iniziale del carattere nella memoria video */
offset= (yc<<8) + (yc<<6) + xc;

for(y=0;y<CHAR_HEIGHT;y++){ /* Text Blitting */


bit_mask = 0x080; /* Maschera per scansione riga */
for(x=0;x<CHAR_WIDHT;x++){
if(*work_char & bit_mask) *(VGAPTR+x+offset) = color;
bit_mask>>=1;
}
offset+=SCREEN_WIDHT; /* Salto alla riga successiva nella VRAM */
work_char++; /* Salto alla riga successiva nella memoria RAM di sistema */

}
}

/**************************************************************/
/* Stampa una stringa puntata da string con i font di sistema */
/**************************************************************/
void Blit_String(int x,int y,int color, char *string){
int i;
for(i=0;string[i]!=0;i++) Blit_Char(x+(i<<3),y,string[i],color);
}

- 6.8 Realizzazione di un text scroller

Dal paragrafo precedente sono state dettagliatamente spiegate le tecniche per la rappresentazione
del testo in modalità grafica, utilizzando il font di default; ora, unendo le procedure alla base della
gestione degli sprites, con le precedenti funzioni per il text blitting, spiegheremo la metodologia che
ci permetterà di adoperare un nostro set di caratteri personalizzato, realizzato mediante l’impiego
dei soliti programmi di disegno elettronico.
Bisogna premettere che il nostro insieme di caratteri, contenuto in un’area di 320 x 200 bytes, sarà
costituito da un numero fisso di simboli per riga, racchiusi all’interno di box perfettamente
coincidenti di dimensione prestabilita (CHAR_HEIGHT e CHAR_WIDHT), disegnati a partire
dall’angolo in alto a sinistra :

10
Essi saranno trattati come sprites, come mostrato negli esempi successivi :

#define CHAR_HEIGHT 30 /* Dimensioni dei singoli carattari */


#define CHAR_WIDHT 30
#define FIRST_CHAR 65 /* Codice ASCII del primo carattere */
#define NUM_CHARS 26 /* Numero di caratteri totali */
#define CPL 10 /* Caratteri per linea */

/*********************************************************************************/
/* Questa funzione estrae uno sprite carattere da un’immagine appositamente */
/* predisposta, e lo copia in un buffer, controllando clipping e trasparenze. */
/*********************************************************************************/
void Char_Graph(BYTE far *imageptr,BYTE far *buff,int x,int y, char car){
int i,j;
buff+=(y<<6)+(y<<8)+x; /* Angolo superiore sinistro nel buffer di lavoro */
/* Calcolo angolo superiore sinistro del box contenente il carattere */
imageptr+=((car-FIRST_CHAR)/CPL)*320*CHAR_HEIGHT+((car-FIRST_CHAR)%CPL)*CHAR_WIDHT;
for(j=0;j<CHAR_HEIGHT;j++){
for(i=0;i<CHAR_WIDHT;i++){
if(*imageptr&&((x+i)<320)&&((y+j)<200)) *buff = *imageptr;
imageptr++;
buff++;
}
buff+=(320-CHAR_WIDHT);
imageptr+=(320-CHAR_WIDHT);
}
}

/***************************************************************************************/
/* Questa funzione stampa una stringa in modalità grafica nell’area di memoria puntata */
/* da buff, estraendo i font dal buffer puntato da imageptr. */
/* E’ simile alla precedente, salvo che scorre tutta la stringa e ricalcola i relativi */
/* spiazzamenti. */
/***************************************************************************************/
void text_graph(BYTE far *imageptr,BYTE far *buff,int x,int y,char *str){
int i,j;
unsigned char far *app=imageptr;
unsigned char far *app2;
buff+=(y<<6)+(y<<8)+x;
app2=buff;
while(*str){ /*Scorre la stringa fino al carattere NULL */
buff=app2;
imageptr=app+((*str-FIRST_CHAR)/CPL)*320*CHAR_HEIGHT+((*str-FIRST_CHAR)%CPL)*CHAR_WIDHT;
for(j=0;j<CHAR_HEIGHT;j++){
for(i=0;i<CHAR_WIDHT;i++){
if(*imageptr&&((x+i)<320)&&((y+j)<200)) *buff = *imageptr;
imageptr++;
buff++;
}
buff+=(320-CHAR_WIDHT);
imageptr+=(320-CHAR_WIDHT);
}
str++; /* Punta al prossimo carattere */
app2+=CHAR_WIDHT;
}
}

11
/*********************************************************************************/
/* Questo programma effettua un simpatico text scrolling */
/*********************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<alloc.h>
#include<mem.h>
#include<conio.h>
#include<math.h>
#include”vga.h”
#include”sprite.h”
#include”formati.h”

float t1[320],t2[320],t3[320],t4[320];

void main(void){
int i;
BYTE *source;
BYTE *work_area;
BYTE *sfondo;
char flag=1;
PALETTE pal[256];
double alfa;
for(i=0;i<320;i++){
alfa=i*3.14159265/160;
t1[i]=sin(alfa);
t2[i]=sin(2*alfa);
t3[i]=sin(3*alfa);
t4[i]=cos(2*alfa);
}
if((work_area=(BYTE *)malloc(64000))==NULL){
printf(“Errore memoria”);
exit(-1);
}
if((source=malloc(64000))==NULL){
printf(“Errore memoria”);
exit(-1);
}
if((sfondo=malloc(64000))==NULL){
printf(“Errore memoria”);
exit(-1);
}

Set_Vga_Mode();
Load_Pcx("charfont.pcx",source,pal);
Load_Pcx("image.pcx",sfondo,pal);
Set_Palette(pal);

while(flag){
for(i=0;i<320;i+=3){
memcpy((void *)work_area,(void *)sfondo,(size_t)64000);
Text_Graph(source,work_area,30+t3[i]*20,10,"ITALYSOFT");
Text_Graph(source,work_area,40+t4[i]*20,50,"PRESENTS");
Text_Graph(source,work_area,10+t2[i]*10,90,"AN ITALIAN");
Char_Graph(source,work_area,i,t4[i]*30+150,'D');
Char_Graph(source,work_area,i+30,t4[(i+15)%320]*30+150,'E');
Char_Graph(source,work_area,i+60,t4[(i+30)%320]*30+150,'M');
Char_Graph(source,work_area,i+90,t4[(i+45)%320]*30+150,'O');
Wait_Vertical_Retrace();
Fast_Copy(work_area);
}
if(kbhit())flag=0;
}
getch();

free(source);
free(work_area);
free(sfondo);
Set_Text_Mode();
}
12
- 6.9 Animazione (tecniche di base)

Sostituendo al posto delle lettere del precedente esempio, la sequenza di fotogrammi sufficienti a
descrivere le varie fasi del movimento di uno sprite, si possono realizzare semplici animazioni.
Basta rispettare il giusto ordine cronologico della sequenza, per veder realizzato il movimento di un
pupazzetto, o di qualsiasi altro soggetto.
Un’ulteriore accortezza da prendere, consiste nel numerare in maniera intelligente la sequenza di
fotogrammi, al fine di semplificare la stesura del listato.
Omettiamo per brevità lo sviluppo di tale programma, che risulta molto simile al text scroller.

Nello spazio sottostante sono rappresentati i principali fotogrammi (frames) del famigerato Mario
Bross :

- 6.10 Temporizzazione delle animazioni

Solitamente, la presenza di un elevato numero di sprite, comporta notevoli rallentamenti


nell’animazione, la quale aumenta man mano di velocità al diminuire di essi.
Per rendere l’animazione più realistica, oppure un videogame più giocabile, si cerca di mantenere
pressoché costante la rapidità con cui vengono scambiati i frames.
A tale scopo si utilizza un meccanismo comandato dall’orologio di sistema : il timer.
Questo chip genera 18.2 interruzioni al secondo dette timer tick, il cui numero progressivo relativo
all’accensione del sistema, viene mantenuto aggiornato nella Doubleword allocata all’indirizzo
0000:046Ch.
Con le prossime funzioni, preleveremo il contenuto di tale contatore, riusciremo a calcolare
intervalli di tempo espressi in timer ticks ed a controllare se un determinato lasso di tempo è
trascorso a partire da un prefissato istante :

unsigned long far *clock=(unsigned long far *)0x0000046C;

unsigned long Get_Time(void){


return(*clock);
}

void Timer(unsigned long start, unsigned long ticks){


while(*clock-start<ticks);
}

La soluzione più semplice, ma meno ottimizzata, è quella di attendere valori di tempo fissi.
In alternativa si può effettuare un testing della velocità della macchina, adattando eventualmente i
tempi al valore più duraturo rilevato.

13