Demo grafici classici con Allegro

Antonio Bonifati <http://monitor.deis.unical.it/ant>

WIP - Work in Progress - Ultima modifica 22 Feb 2004

Introduzione

Allegro (acronimo ricorsivo per "Allegro Low Level Game Routines") è una libreria per la programmazione di giochi e applicazioni multimediali multi-piattaforma.

Uso Allegro sotto Linux e FreeBSD, per cui sono disponibili pacchetti binari e l'installazione è molto semplice. I percorsi dei file che citerò sono relativi a Linux e FreeBSD. Altre piattaforme supportate sono Linux, DOS, Windows, MacOS, BeOS e QNX. Gli esempi di questo testo dovrebbero essere pienamente portabili. Non posso garantirlo non avendoli testati su altra piattaforma diversa da Linux Slackware/FreeBSD, comunque in caso di problemi fatemi sapere.

Settare il modo grafico

In ogni programma che usa la libreria Allegro occorrerà includere il file header principale /usr/include/allegro.h in Linux Slackware e /usr/local/include/allegro.h in FreeBSD. Poi si chiama la macro allegro_init che inizializza la libreria e va chiamata prima di ogni altra funzione. install_keyboard è una funzione che installa il gestore delle interruzioni della tastiera ed è necessario chiamarla prima di altre routine per la gestione dell'input da tastiera. Per andare in modo grafico si chiama la funzione set_gfx_mode per i cui dettagli rimando alla documentazione di Allegro in /usr/doc/allegro-*/ (Slackware) o /usr/local/share/doc/allegro/ (FreeBSD). Nell'esempio seguente il modo grafico scelto è il classico modo standard numero 0x13 della VGA; ha una risoluzione di 320x200 pixel con 256 colori, scelti da una palette di 218=262144 colori differenti. In caso di problemi a settare il modo grafico si ritorna in modo testo (negli ambienti che hanno un modo testo) e viene stampato un messaggio di errore. Altrimenti si attende la pressione di un tasto per poi tornare in modo testo e terminare il programma. Non dimenticate di chiamare la macro END_OF_MAIN subito dopo la funzione main().

#include <allegro.h>

int main(int argc, char *argv[]) 
{ allegro_init();
  install_keyboard();

  if (set_gfx_mode(GFX_AUTODETECT, 320,200, 0,0))
  { set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
    allegro_message("%s\n", allegro_error);
    exit(1);
  }
  set_color_depth(8);

  /* insert the drawing commands here ... */

  readkey();
  
  exit(0);
}
END_OF_MAIN();

Per compilare in FreeBSD uso un comando del tipo:

$ gcc -o execname source.c -I/usr/local/include `allegro-config --shared`

In Linux Slackware è sufficiente questo:

$ gcc -o execname source.c `allegro-config --shared`

Alcune primitive grafiche

Il sistema di coordinate cartesiane sullo schermo convenzionalmente utilizzato dalle primitive grafiche ha origine nell'angolo superiore sinistro e gli assi disposti come nella figura seguente (riferita alla risoluzione 320x200):

                  x
      (0,0)------------(319,0)
        |                  |
       y|  visible screen  |
        |                  |
      (0,199)----------(319,199)

Questo implica che la formula che deve essere utilizzata per calcolare l'offset nella memoria video che individua il byte corrispondente al punto di coordinate (x,y) è piuttosto semplice:

x + y*320

Per una architettura in cui due istruzioni di shifting e una somma costano meno cicli macchina rispetto ad una moltiplicazione, si può ottimizzare tenendo conto che 320=1010000002=26+28 e quindi y*320=y*(26+28)=(y<<6)+(y<<8):

x + (y<<6) + (y<<8)

La funzione set_gfx_mode crea una variabile globale screen tramite la quale si accede alla memoria video hardware della scheda audio:

extern BITMAP *screen;

Questa può essere passata come parametro alle varie primitive grafiche. Le principali sono:

/* writes a pixel to the specified position in the bitmap,
   using the current drawing mode and the bitmap's clipping rectangle */
void putpixel(BITMAP *bmp, int x, int y, int color);

/* draws a horizontal line onto the bitmap, from point (x1, y) to (x2, y) */
void hline(BITMAP *bmp, int x1, int y, int x2, int color);

/* draws a vertical line onto the bitmap, from point (x, y1) to (x, y2) */
void vline(BITMAP *bmp, int x, int y1, int y2, int color);

/* draws a line onto the bitmap, from point (x1, y1) to (x2, y2) */
void line(BITMAP *bmp, int x1, int y1, int x2, int y2, int color);

/* draws a circle with the specified centre and radius */
void circle(BITMAP *bmp, int x, int y, int radius, int color);

La funzione putpixel() può essere troppo lenta se il programma ha bisogno di farne uso frequente. Una versione più veloce implementata in assembler inline è la seguente, ma attenzione che provoca un crash del programma se si tenta di disegnare al di fuori della bitmap (la putpixel invece effettua il clipping):

void _putpixel(BITMAP *bmp, int x, int y, int color);

In realtà quando Allegro viene usato col sistema X Window, a causa del protocollo X, le prestazioni sono molto basse e addirittura _putpixel è meno performante di putpixel. Per maggiori informazioni su come ottimizzare sotto la X (a costo di perdere la possibilità di esecuzione remota e a costo di richiedere i permessi di root), consultare la sezione "Unix specifics" nella documentazione di Allegro. Alternativamente nella console Linux si può usare il dispositivo framebuffer, oppure la libreria SVGAlib o l'accesso diretto (per i soli modi standard VGA). Mentre il framebuffer non richiede i permessi di root gli altri due metodi sì.

Per pulire lo schermo (riempiendolo di pixel di colore 0) usate la seguente chiamata di funzione:

void clear_bitmap(BITMAP *bitmap);

clear_bitmap(screen);

La palette

Nel già citato modo 0x13 nella memoria video viene usato un byte per ogni pixel. Questo byte non rappresenta direttamente il colore del pixel, come avviene invece per le risoluzioni con maggior numero di colori, ma un indice nella palette corrente. Una palette (a volte scritto pallete) è una tabella che contiene i livelli dei colori primari (rosso, verde e blu) che definiscono ciascun colore. Nel file /usr/local/include/allegro/palette.h Allegro definisce le strutture dati che rappresentano una palette: le struttura RGB rappresenta un colore nel formato hardware usato dalla VGA (r,g,b possono assumere i valori nell'intervallo 0-63), mentre PALETTE non è altro che un array contenente 256 strutture RGB:

typedef struct RGB
{
   unsigned char r, g, b;
   unsigned char filler;
} RGB;

#define PAL_SIZE     256

typedef RGB PALETTE[PAL_SIZE];

Nel momento in cui si entra nel modo 0x13, viene settata la palette di default del BIOS IBM che è definita nella seguente variabile globale:

IBM BIOS default palette
extern PALETTE default_palette;

Per visualizzare la palette corrente in forma grafica come mostrato nella figura sopra utilizzo la seguente funzione:

#define SIZE 10
#define WIDTH 32
#define HEIGHT 8
void display_palette()
{ int i, x,y;

  i=0;
  for (y=0; y<HEIGHT*SIZE; y+=SIZE)
    for (x=0; x<WIDTH*SIZE; x+=SIZE)
      rectfill(screen, x, y, x + (SIZE-1), y + (SIZE-1), i++);
}

Per settare un singolo colore della palette ai livelli voluti, occorre usare la funzione di Allegro:

void set_color(int index, const RGB *p);

Ad esempio per settare il colore di indice 0 (quello con cui normalmente è "pulito" lo schermo) su un viola scuro:

RGB darkMagenta = {34, 0, 34};
set_color(0, &darkMagenta);

La funzione inversa di set_color è get_color, che legge i valori RGB corrispondi al colore numero index della palette salvandoli nel parametro p passato per riferimento:

void get_color(int index, RGB *p);

Per settare l'intera palette passando un array di 256 strutture RGB usare:

void set_palette(const PALETTE p);

Per leggere l'intera palette corrente in un array di 256 strutture RGB:

void get_palette(PALETTE p);

La variable globale black_palette contiene una palette di tutti neri:

extern PALETTE black_palette;

L'effetto dissolvenza

La dissolvenza può essere in entrata (fade in) o in uscita (fade out). Nel primo caso si passa gradualmente da una palette di tutti neri (schermo nero) alla palette p desiderata (lo schermo visualizza l'immagine disegnata prima di iniziare il fading in con tutti i suoi veri colori), nel secondo viceversa.

Le due principali funzioni di Allegro per la dissolvenza sono:

void fade_in(const PALETTE p, int speed);
void fade_out(int speed);

speed indica la velocità che va 1 (la più lenta) a 64 (transizione praticamente istantanea).

Se guardate i sorgenti di Allegro, in particolare nel file src/gfx.c, trovate l'implementazione di queste e altre funzioni, che sono istruttive da studiare.

Il problema del sincronismo

In un monitor CRT il fascio di elettroni che disegna l'immagine, arrivato alla fine dello schermo, impiega un certo tempo per risalire in cima allo schermo ed iniziare poi un'altra scansione. In questo breve intervallo di tempo in cui il raggio risale, la scheda grafica non manda dati al monitor. Questo è il momento giusto per aggiornare lo schermo o cambiare la palette senza che si verifichi il fastidioso fenomeno dello sfarfallamento (detto anche flickering o snow effect).

La funzione vsync di Allegro serve per aspettare il retrace verticale, ossia aspetta finché il fascio ha raggiunto la parte inferiore dello schermo.

void vsync();

Normalmente non occorre chiamare la vsync. Ad esempio fade_in, fade_out e set_palette la chiamano al loro interno quindi non c'è bisogno di chiamarla prima, ma set_color no, quindi occorre chiamare vsync prima di set_color per sincronizzarsi con il retrace ed evitare il problema del flickering. vsync usa un meccanismo di attesa attiva (polling) per sincronizzarsi; sarebbe meglio usare un meccanismo basato sulle interruzioni, ma sfortunatamente non c'è un modo standard per fare questo.

La notazione fixed point

Molte architetture sono dotate ormai di sottounità specializzate per i calcoli tra numeri con la virgola (FPU, Floating Point Unit). Tuttavia rimane ancora vero che i calcoli tra numeri interi sono eseguiti più velocemente rispetto a quelli tra numeri con la virgola.

Per questo fatto in alcune situazioni in cui non è necessaria una elevata precisione, è possibile sostituire ai calcoli tra numeri con la virgola dei calcoli tra numeri interi. Questo avviene tramite la notazione fissa (o in virgola fissa), che non è altro che un altro modo di interpretare un numero intero.

Nel file /usr/local/include/allegro/fixed.h Allegro definisce il tipo fixed come long, che significa che sarà un intero con segno lungo almeno 32 byte su tutte le piattaforme (e comunque solo gli ultimi 32 byte più bassi sono utilizzati).

typedef long fixed;

fixed rappresenta un numero in notazione fissa 16.16, nel senso che la prima parola (16 bit) rappresenta la parte intera e può quindi variare nell'intervallo [-32768, 32767], mentre la word meno significativa (i 16 bit più bassi) rappresentano la parte dopo la virgola, che fornisce una accuratezza di circa quattro o cinque cifre dopo la virgola. Questa riduzione dei limiti e dell'accuratezza rappresenta il costo che si paga passando dalla notazione a virgola mobile (tipi float e double) a quella a virgola fissa (tipo fixed), mentre si guadagna in velocità di calcolo.

Da notare che sono possibili diversi tipi di notazione fissa, ad es. 8.24 o 24.8, ecc. Allegro implementa solo la 16.16

Ad esempio per convertire in notazione fixed un intero x si usa la funzione:

fixed itofix(int x);

Questa in realtà è una macro che semplicemente ritorna il valore x << 16. Infatti essendo x intero, la parte decimale è nulla e quindi i 16 bit più bassi sono posti a zero, mentre i 16 bit più alti hanno un valore che coincide con x.

La funzione inversa si chiama naturalmente fixtoi e converte un numero fixed point in un intero, arrotondando:

int fixtoi(fixed x);

fixtoi è più complicata, per i dettagli implementativi rimando al file /usr/local/include/allegro/inline/fmaths.inl. Limitatamente al caso in cui x è un numero positivo e che si arrotonda per difetto, quello che fa è semplicemente uno shift a destra di 16 posizioni: x >> 16, vale a dire che divide per 216=65536.

Queste altre due funzioni convertono da un double in un fixed e viceversa:

fixed ftofix(double x);
double fixtof(fixed x);

Una versione semplificata di ftofix sarebbe una che semplicemente ritorna (long)(x * 65536.0), mentre per fixtof l'implementazione effettivamente è una macro che ritorna (double)x / 65536.0

Per quanto riguarda le operazioni tra numeri fissi limitiamoci alla sola somma/sottrazione che può essere fatta semplicemente con l'operatore +/- e produce, se non si verifica il problema dell'overflow, il risultato corretto in notazione a punto fisso. Se volete gestire l'overflow (al costo di qualche ciclo macchina in più naturalmente) Allegro mette a disposizione due funzioni che lo fanno, da usare al posto degli operatori + e -:

fixed fixadd(fixed x, fixed y);
fixed fixsub(fixed x, fixed y);

Consideriamo ora alcune delle operazioni che coinvolgono numeri fissi e numeri interi o float e che danno come risultato desiderato un numero in notazione fissa. Per dividere o moltiplicare un numero fisso per un intero normale si usano gli operatori * e / e il risultato è un numero fisso, che tramite fixtoi o fixtof può essere poi convertito in un intero o un double. Per addizionare o sottrarre un intero/float ad un numero fixed è sbagliato usare gli operatori +/-. Occorre prima convertire l'intero/float in notazione fissa tramite itofix/ftofix e poi sommare usando +/- e il risultato naturalmente è in notazione fissa.

Inoltre sui numeri fissi si possono usare col significato aspettato gli operatori = (assegnazione), <,>,<=,>= (confronto), - (meno unario o negazione o complemento a 2) e << >> (shifting, per moltiplicare o dividere per potenze di 2 efficientemente, non possibile con i float).

Le tabelle di lookup

TODO

Il double buffering

TODO

Il campo stellato

Il campo stellato o starfield è un effetto spesso utilizzato come screensaver o come sfondo di giochi.

Può essere orizzontale oppure tridimensionale, come quello che disegneremo e animeremo in questo paragrafo.

Ogni stella è rappresentata da un record contenete le sue coordinate cartesiane nello spazio (x,y,z) e le corrispondenti coordinate proiettate sullo schermo (xp,yp). Ci sono MAX_STARS stelle.

typedef struct
{ int x, y, z;
  int xp, yp;
} STAR;

STAR starfield[MAX_STARS];

Assumiamo un sistema di coordinate con l'origine nel centro dello schermo, gli assi x e y disposti come in quello bidimensionale assunto sullo schermo; l'asse z (quello della profondità) lo assumiamo entrante nello schermo.

                    ^ z
                   /
 -----------------------------
|                /            |
|               /             |
|           (0,0,0)-----> x   |
|              |              |
|              |              |
 --------------|--------------
               |
               v y

Ad ogni stella è associato un colore. Per semplificare, senza perdere troppo in realismo, supponiamo che la luminosità di una stella dipenda solo dalla distanza dall'osservatore.

Per semplificare ulteriormente facciamo sì che la funzione che lega distanza e colore sia proprio la funzione identità, ovvero l'indice del colore della stella nella palette sia dato dalla sua distanza dal piano del monitor; la funzione identità è infatti la più semplice funzione non costante da calcolare.

Poiché nella palette abbiamo 256 colori, indicizzati da 0 a 255, la distanza massima di una stella la poniamo a 255 e la minima a 1 (non consideriamo la distanza 0 in quanto questo produce una divisione per zero nelle formule della proiezione prospettica che vedremo in seguito); inoltre poiché nel modo 0x13 possiamo avere solo 64 sfumature di grigi differenti (compreso il nero che rappresenta il grigio più scuro di tutti), e visto che desideriamo una palette che abbia transizioni uniformi dei colori, dovremmo avere ogni sfumatura associata a 4 distanze contigue; la palette che ne risulta è la seguente, che, a parte l'indice 0 che rappresenta il colore di sfondo nero, passa gradualmente dal bianco al nero attraverso tutte le scale di grigio disponibili:

una palette di grigi
PALETTE gray_palette;

Essa viene generata tramite la seguente funzione:

void preparePalette(PALETTE pal)
{ int i;

  pal[0].r = pal[0].g = pal[0].b = 0;

  for (i=1; i<256; i++)
    pal[i].r = pal[i].g = pal[i].b = (256-i) >> 2;
}

Per creare una nuova stella (all'indice i dell'array starfield e con una distanza iniziale distance) usiamo la seguente funzione. Anche se non strettamente necessario, evitiamo di creare una stella con coordinate x=0 e y=0, ovvero nel punto di fuga della proiezione prospettica che rappresenta il centro dello schermo (coordinate (0,0,0)), perché altrimenti la vedremmo illuminarsi senza muoversi (in pratica vuol dire che ci stiamo andando addosso!)

void createRandStar(int i, int distance)
{ do
  { starfield[i].x = rand() % 320 - 160;
    starfield[i].y = rand() % 200 - 100;
  }
  while (starfield[i].x == 0 && starfield[i].y == 0);

  starfield[i].z = distance;
}

Inizialmente popoliamo il cielo creando MAX_STARS stelle, con distanze comprese tra 1 e 255 (estremi inclusi) scelte a caso.

void createRandStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
     createRandStar(i, rand() % 255 + 1);
}

La funzione seguente effettua la proiezione di tutte le stelle tramite le formule della proiezione prospettica. Da notare anche la traslazione necessaria per convertire dalle nostre coordinate a quelle canoniche sullo schermo e la presenza di un fattore di scala per la proiezione. La legge della proiezione prospettica tiene conto di un effetto prodotto dal nostro occhio: gli oggetti più sono lontani e più ci appaiono convergere verso un unico punto di fuga corrispondente con il centro della nostra visione (e da noi fissato col centro dello schermo). Ad esempio se guardiamo le rotaie di un treno in un lontananza, le vedremo toccarsi.

void projectStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
  { starfield[i].xp = ((starfield[i].x * SCALE) / starfield[i].z) + 160;
    starfield[i].yp = ((starfield[i].y * SCALE) / starfield[i].z) + 100;
  }
}

Il fattore di scala SCALE va scelto in modo che i punti proiettati (xp,yp) non vadano la maggior parte fuori schermo (fattore troppo grande) oppure non siano la maggior parte addensati al centro (fattore troppo piccolo). Se SCALE=1 allora sicuramente MAX_STARS stelle saranno visibili, maggiore è SCALE, più è probabile che qualche stella abbia coordinate proiettate non visibili.

Allo scopo di usare un solo shift al posto della moltiplicazione conviene scegliere un fattore di scala che sia una potenza di 2 e cambiare la funzione come segue:

void projectStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
  { starfield[i].xp = ((starfield[i].x << SCALE_EXP) / starfield[i].z) + 160;
    starfield[i].yp = ((starfield[i].y << SCALE_EXP) / starfield[i].z) + 100;
  }
}

Nella funzione che disegna il campo stellato, notate che non è necessario effettuare il clipping, in quanto lo fa già per noi la putpixel.

void drawStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
    putpixel(screen, starfield[i].xp, starfield[i].yp, starfield[i].z);
}

A questo punto possiamo scrivere una prima versione del programma che disegna un cielo stellato statico:

#include <allegro.h>

#define MAX_STARS 100
#define SCALE_EXP 5 /* as a power of 2 */

typedef struct
{ int x, y, z;
  int xp, yp;
} STAR;

STAR starfield[MAX_STARS];

void preparePalette(PALETTE pal)
{ int i;

  pal[0].r = pal[0].g = pal[0].b = 0;

  for (i=1; i<256; i++)
    pal[i].r = pal[i].g = pal[i].b = (256-i) >> 2;
}

void createRandStar(int i, int distance)
{ do
  { starfield[i].x = rand() % 320 - 160;
    starfield[i].y = rand() % 200 - 100;
  }
  while (starfield[i].x == 0 && starfield[i].y == 0);

  starfield[i].z = distance;
}

void createRandStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
     createRandStar(i, rand() % 255 + 1);
}

void projectStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
  { starfield[i].xp = ((starfield[i].x << SCALE_EXP) / starfield[i].z) + 160;
    starfield[i].yp = ((starfield[i].y << SCALE_EXP) / starfield[i].z) + 100;
  }
}

void drawStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
    putpixel(screen, starfield[i].xp, starfield[i].yp, starfield[i].z);
}

int main(int argc, char *argv[]) 
{ PALETTE gray_palette;

  allegro_init();
  install_keyboard();

  if (set_gfx_mode(GFX_AUTODETECT, 320,200, 0,0))
  { set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
    allegro_message("%s\n", allegro_error);
    exit(1);
  }
  set_color_depth(8);

  preparePalette(gray_palette);
  set_palette(gray_palette);

  srand(time(NULL));

  createRandStarfield();
  projectStarfield();
  drawStarfield();

  readkey();
  
  exit(0);
}
END_OF_MAIN();

Animare il campo stellato, dando l'impressione di viaggiare nello spazio in linea retta è a questo punto abbastanza semplice. Basta costruire un ciclo in cui ad ogni passo si proietta, si pulisce lo schermo, si stampa e si muove il campo stellato, finché non viene premuto un tasto:

  createRandStarfield();
  while (!keypressed())
  { projectStarfield();
    clear_bitmap(screen);
    drawStarfield();
    moveStarfield();
  }

Muovere il campo stellato è banale: per ogni stella si diminuisce z di un certo passo STEP. Se z diventa 0 o negativo, occorre ricreare la stella che è uscita fuori dalla visuale per evitare che il campo rimanga ben presto vuoto e si verifichi un errore di divisione per zero. Anche qui poi non è detto che la stella ricreata sarà visibile, a meno che SCALE_EXP=0. La stella la ricreiamo con coordinate x,y scelte a caso e distanza z massima (255).

void moveStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
    if ((starfield[i].z -= STEP) <= 0)
      createRandStar(i, 255);
}

Il programma diventa:

#include <allegro.h>

#define MAX_STARS 500
#define SCALE_EXP 8 /* as a power of 2 */
#define STEP 1

typedef struct
{ int x, y, z;
  int xp, yp;
} STAR;

STAR starfield[MAX_STARS];

void preparePalette(PALETTE pal)
{ int i;

  pal[0].r = pal[0].g = pal[0].b = 0;

  for (i=1; i<256; i++)
    pal[i].r = pal[i].g = pal[i].b = (256-i) >> 2;
}

void createRandStar(int i, int distance)
{ do
  { starfield[i].x = rand() % 320 - 160;
    starfield[i].y = rand() % 200 - 100;
  }
  while (starfield[i].x == 0 && starfield[i].y == 0);

  starfield[i].z = distance;
}

void createRandStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
     createRandStar(i, rand() % 255 + 1);
}

void projectStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
  { starfield[i].xp = ((starfield[i].x << SCALE_EXP) / starfield[i].z) + 160;
    starfield[i].yp = ((starfield[i].y << SCALE_EXP) / starfield[i].z) + 100;
  }
}

void drawStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
    putpixel(screen, starfield[i].xp, starfield[i].yp, starfield[i].z);
}

void moveStarfield()
{ int i;

  for (i=0; i<MAX_STARS; i++)
    if ((starfield[i].z -= STEP) <= 0) 
      createRandStar(i, 255);      
}


int main(int argc, char *argv[]) 
{ PALETTE gray_palette;

  allegro_init();
  install_keyboard();

  if (set_gfx_mode(GFX_AUTODETECT, 320,200, 0,0))
  { set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
    allegro_message("%s\n", allegro_error);
    exit(1);
  }
  set_color_depth(8);

  preparePalette(gray_palette);
  set_palette(gray_palette);

  srand(time(NULL));

  createRandStarfield();
  while (!keypressed())
  { projectStarfield();
    clear_bitmap(screen);
    drawStarfield();
    moveStarfield();
  }

  exit(0);
}
END_OF_MAIN();

Per evitare in ogni caso problemi di flickering occorre inserire una vsync() prima della clear_bitmap. Questo rallenta notevolmente l'animazione. Allora per aumentare la velocità occorre incrementare STEP.