Fuego: Intento de efecto fuego y optimización (C con SDCC)

Click here to see in English

En este tutorial vamos a intentar hacer un efecto de fuego en nuestro Amstrad. El efecto de fuego es un efecto más o menos sencillo de programar pero tiene el inconveniente de que tiene mucho calculo por pixel, con lo que la velocidad esperada es baja, ademas vamos a hacerlo enteramente en C con lo que el resultado será más lento que si lo hiciéramos completamente en ensamblador.

He programado infinidad de veces este efecto en PC, desde los tiempos de la VGA, pasando por GDI, Allegro, DirectX y algunos otros que ni recuerdo, pero desde luego en esos casos no tuve las limitaciones a las que el CPC nos enfrenta. Para empezar tenemos la paleta, para hacer un efecto de fuego se suele especificar una amplia paleta que va desde el blanco, pasando por el amarillo, naranja, rojo, morado hasta negro para que quede chulo, en el caso del CPC he decidido usar una paleta de 6 colores. El tamaño también lo he limitado mucho, ya que requiere mucho calculo, dejándolo en apenas 32x16.

El efecto de fuego es muy sencillo de programar, básicamente se aplican unos valores aleatorios en la base del fuego y después se programa una interpolación para simular que el fuego sube hacia arriba. Esta interpolación no es más que una media de 4 puntos de la siguiente manera:

pixel(x,y) = (pixel(x, y) + pixel(x-1, y-1) + pixel(x, y-1) + pixel(x+1, y-1) - 1) / 4;

Dado que nosotros estamos limitados a 6 colores y el fuego no subiría, vamos a usar un buffer intermedio para calcular el efecto de fuego, con valores de 0 a 60 por pixel que luego al pintar reduciremos a nuestros 6 colores.

Sin mucha explicación más vamos a ver el primer intento:

////////////////////////////////////////////////////////////////////////
// Fire01.c
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetColor
////////////////////////////////////////////////////////////////////////
void SetColor(unsigned char nColorIndex, unsigned char nPaletteIndex)
{
  __asm
    ld a, 4 (ix)
    ld b, 5 (ix)
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetPalette
////////////////////////////////////////////////////////////////////////
void SetPalette(const unsigned char *pPalette, unsigned char nNumColors)
{
  unsigned char nColor = 0;

  for(nColor = 0; nColor < nNumColors; nColor++)
    SetColor(nColor, pPalette[nColor]);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetMode0PixelColor
////////////////////////////////////////////////////////////////////////
void SetMode0PixelColor(unsigned char *pByteAddress, unsigned char nColor, unsigned char nPixel)
{
  unsigned char nByte = *pByteAddress;

  if(nPixel == 0)
  {
    nByte &= 85;

    if(nColor & 1)
      nByte |= 128;

    if(nColor & 2)
      nByte |= 8;

    if(nColor & 4)
      nByte |= 32;

    if(nColor & 8)
      nByte |= 2;
  }
  else
  {
    nByte &= 170;

    if(nColor & 1)
      nByte |= 64;

    if(nColor & 2)
      nByte |= 4;

    if(nColor & 4)
      nByte |= 16;

    if(nColor & 8)
      nByte |= 1;
  }

  *pByteAddress = nByte;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//PutPixelMode0
////////////////////////////////////////////////////////////////////////
void PutPixelMode0(unsigned char nX, unsigned char nY, unsigned char nColor)
{
  unsigned char nPixel = 0;
  unsigned int nAddress = 0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 2);
  nPixel = nX % 2;

  SetMode0PixelColor((unsigned char *)nAddress, nColor, nPixel);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define NUM_COLORS 6
const unsigned char FirePalette[NUM_COLORS] = {0, 3, 6, 15, 24, 26};

#define FIRE_WIDTH 32
#define FIRE_HEIGHT 16
unsigned char aFire[FIRE_WIDTH][FIRE_HEIGHT];

#define FIRE_COLOR_RANGE 60 // 6 colors

#define FIRE_X_ORIGIN 80 - (FIRE_WIDTH / 2)
#define FIRE_Y_ORIGIN 200

////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned char nX = 0;
  unsigned char nY = 0;
  
  SetMode(0);
  SetBorder(0);
  SetPalette(FirePalette, NUM_COLORS);
  
  memset(aFire, 0, sizeof(aFire));
  
  nTimeLast = GetTime();

  while(1)
  {
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(1, 1);
      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
    
    //fills the botton line with random values
    for(nX = 1; nX < (FIRE_WIDTH - 1); nX++)
     aFire[nX][0] = rand() % FIRE_COLOR_RANGE;
     
    //interpolate colors from bottom to top
    for(nY = 1; nY < FIRE_HEIGHT; nY++)
    {
      for(nX = 0; nX < FIRE_WIDTH; nX++)
      {
        aFire[nX][nY] = (aFire[nX][nY] + aFire[nX][nY - 1] +
                         (nX > 0 ? aFire[nX - 1][nY - 1] : 0) +
                         (nX < FIRE_WIDTH - 1 ? aFire[nX + 1][nY - 1] : 0) -
                         (FIRE_COLOR_RANGE / FIRE_HEIGHT)) / 4;
      }
    }
    
    //screen
    for(nY = 1; nY < FIRE_HEIGHT; nY++)
    {
      for(nX = 0; nX < FIRE_WIDTH; nX++)
      {
        PutPixelMode0(FIRE_X_ORIGIN + nX, FIRE_Y_ORIGIN - nY, aFire[nX][nY] / 10 + (aFire[nX][nY] % 10 >= 5 ? 1 : 0));
      }
    }

  }
}
////////////////////////////////////////////////////////////////////////

Si probáis Fire01.dsk en el emulador, veréis que el fuego es muy pequeño y alcanza una penosa velocidad de entre 1 y 2 frames por segundo...

Cuando llegué a este punto, pensé en abandonar el tutorial y buscar otra cosa, pero finalmente me decidí a intentar mejorarlo y así podría servir para ilustrar como optimizar un programa.

Lo primero que salta a la vista es que utilizar la función PutPixelMode0 y SetMode0PixelColor es un precio muy alto por cada pixel, ya que cada vez calcula la dirección de memoria de video así como la mascara de bits para poner el color que queremos, así que directamente las borramos. Para no tener que calcular cada vez que pintemos las direcciones de cada línea de la memoria de video, vamos a precalcularlas y almacenarlas en una array, de la siguiente manera:

unsigned int aScreenAddress[FIRE_HEIGHT * 2];


for(nY = 0; nY < FIRE_HEIGHT * 2; nY++)
  aScreenAddress[nY] = 0xC000 + (((FIRE_Y_ORIGIN - nY) / 8) * 80) +
                       (((FIRE_Y_ORIGIN - nY) % 8) * 2048) + FIRE_X_ORIGIN;

Ya vimos en otro tutorial que la función rand() de C es muy lenta (al menos la implementación de SDCC), por lo que para no tener que usarla constantemente vamos a precalcular un grupo de numeros aleatorios:

#define RANDOM_VALUES 1024
unsigned char aRandom[RANDOM_VALUES];


for(nRandom = 0; nRandom < RANDOM_VALUES; nRandom++)
  aRandom[nRandom] = FIRE_COLOR_RANGE / 4 + rand() % (FIRE_COLOR_RANGE * 3 / 4);

Al pintar en Fire01.c tenemos que convertir de nuestro rango de 60 a nuestra paleta de 6 colores, haciendo dos divisiones por cada pixel... para evitar esto y además poder distribuir la paleta de una manera no uniforme, vamos a hacer un array de 60, que contiene ya el valor a poner en un byte en modo 0, para poner los colores de nuestra paleta, de esta forma es muy simple pasar un pixel de fuego a pantalla:

#define FIRE_COLOR_RANGE 60 // 6 colors
const unsigned char aFireColors[FIRE_COLOR_RANGE] = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
    0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x30, 0x30, 0x30, 0x30, 0x30,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0};

Otra optimización es modificar el array donde calculamos el fuego, para que tenga una sola dimensión, ya que es mucho más optimo. También se ha modificado el código para recorrer los arrays mediante punteros, en vez de acceder siempre directamente al array[índice].

Con todos estos cambios y haciendo que cada punto del fuego ocupe 4 pixeles en la pantalla (lo trucamos al pintar) tenemos Fire02.c:

////////////////////////////////////////////////////////////////////////
// Fire02.c
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetColor
////////////////////////////////////////////////////////////////////////
void SetColor(unsigned char nColorIndex, unsigned char nPaletteIndex)
{
  __asm
    ld a, 4 (ix)
    ld b, 5 (ix)
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetPalette
////////////////////////////////////////////////////////////////////////
void SetPalette(const unsigned char *pPalette, unsigned char nNumColors)
{
  unsigned char nColor = 0;

  for(nColor = 0; nColor < nNumColors; nColor++)
    SetColor(nColor, pPalette[nColor]);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define NUM_COLORS 6
const unsigned char FirePalette[NUM_COLORS] = {0, 3, 6, 15, 24, 26};

#define FIRE_WIDTH 32
#define FIRE_HEIGHT 16
unsigned char aFire[FIRE_WIDTH * FIRE_HEIGHT];

#define FIRE_COLOR_RANGE 60 // 6 colors
const unsigned char aFireColors[FIRE_COLOR_RANGE] = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
    0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x30, 0x30, 0x30, 0x30, 0x30,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0};

#define FIRE_X_ORIGIN 40 - (FIRE_WIDTH / 2)
#define FIRE_Y_ORIGIN 199

unsigned int aScreenAddress[FIRE_HEIGHT * 2];

#define RANDOM_VALUES 1024
unsigned char aRandom[RANDOM_VALUES];

////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned char nX = 0;
  unsigned char nY = 0;
  unsigned char *pFire1 = NULL;
  unsigned char *pFire2 = NULL;
  unsigned int *pScreenAddress = NULL;
  unsigned char *pScreen1 = NULL;
  unsigned char *pScreen2 = NULL;
  unsigned int nRandom = 0;
  unsigned char *pArray = NULL;
  
  SetMode(0);
  SetBorder(0);
  SetPalette(FirePalette, NUM_COLORS);
  
  memset(aFire, 0, sizeof(aFire));
  
  for(nY = 0; nY < FIRE_HEIGHT * 2; nY++)
    aScreenAddress[nY] = 0xC000 + (((FIRE_Y_ORIGIN - nY) / 8) * 80) +
                                (((FIRE_Y_ORIGIN - nY) % 8) * 2048) + FIRE_X_ORIGIN;
  
  for(nRandom = 0; nRandom < RANDOM_VALUES; nRandom++)
    aRandom[nRandom] = FIRE_COLOR_RANGE / 4 + rand() % (FIRE_COLOR_RANGE * 3 / 4);
  
  nTimeLast = GetTime();

  while(1)
  {
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(1, 1);
      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
    
    //fills the botton line with random values
    pArray = (unsigned char *)aFire;
    
    for(nX = 1; nX < (FIRE_WIDTH - 1); nX++)
    {
     *pArray++ = aRandom[nRandom++];

     if(nRandom >= RANDOM_VALUES)
      nRandom = 0;
    }

    //interpolate colors from bottom to top
    pFire1 = aFire + FIRE_WIDTH; //current row
    pFire2 = aFire; //previous row
     
    for(nY = 1; nY < FIRE_HEIGHT; nY++)
    {
      *pFire1 = (*pFire1 + *pFire2 + *(pFire2 + 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)) / 4;
        
      pFire1++;
      pFire2++;
        
      for(nX = 1; nX < FIRE_WIDTH - 1; nX++)
      {
        
        *pFire1 = (*pFire1 + *pFire2 + *(pFire2 - 1) + *(pFire2 + 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)) / 4;
        
        pFire1++;
        pFire2++;
      }

      *pFire1 = (*pFire1 + *pFire2 + *(pFire2 - 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)) / 4;
        
      pFire1++;
      pFire2++;
    }
  
    //screen
    pFire1 = aFire + FIRE_WIDTH;
    pScreenAddress = aScreenAddress;
    for(nY = 1; nY < FIRE_HEIGHT; nY++)
    {
      pScreen1 = (unsigned char *)*pScreenAddress;
      pScreen2 = (unsigned char *)*(pScreenAddress + 1);
      pScreenAddress += 2;

      for(nX = 0; nX < FIRE_WIDTH; nX++)
      {
        *pScreen1++ = *pScreen2++ = aFireColors[*pFire1++];
      }
    }

  }
}
////////////////////////////////////////////////////////////////////////

Si probamos Fire02.dsk en el emulador, veremos que ha mejorado bastante, siendo un poco más grande y alcanzando unos 8 frames por segundo:

Llegados a este punto no queda mucho por optimizar, pero siempre se puede arañar un poco más. Por ejemplo, para evitar la división entre 4 por cada pixel vamos dejarla también, precalculada en un array. Por otro lado, tras varias pruebas de medida descubrí que al menos en SDCC para acceder a un valor de un array es más rápido hacerlo mediante *(aArrayName + index) que mediante el clásico aArrayName[index]. Además de los cambios anteriores, también he unificado el bucle de interpolación con el de pintado, quedando un código fuente mucho menos fácil de entender :-(

////////////////////////////////////////////////////////////////////////
// Fire03.c
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetColor
////////////////////////////////////////////////////////////////////////
void SetColor(unsigned char nColorIndex, unsigned char nPaletteIndex)
{
  __asm
    ld a, 4 (ix)
    ld b, 5 (ix)
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetPalette
////////////////////////////////////////////////////////////////////////
void SetPalette(const unsigned char *pPalette, unsigned char nNumColors)
{
  unsigned char nColor = 0;

  for(nColor = 0; nColor < nNumColors; nColor++)
    SetColor(nColor, pPalette[nColor]);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define NUM_COLORS 6
const unsigned char FirePalette[NUM_COLORS] = {0, 3, 6, 15, 24, 26};

#define FIRE_WIDTH 32
#define FIRE_HEIGHT 16
unsigned char aFire[FIRE_WIDTH * FIRE_HEIGHT];

#define FIRE_COLOR_RANGE 60 // 6 colors
const unsigned char aFireColors[FIRE_COLOR_RANGE] = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
    0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x30, 0x30, 0x30, 0x30, 0x30,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0};

#define FIRE_X_ORIGIN 40 - (FIRE_WIDTH / 2)
#define FIRE_Y_ORIGIN 199

unsigned int aScreenAddress[FIRE_HEIGHT * 2];

#define RANDOM_VALUES 1024
unsigned char aRandom[RANDOM_VALUES];

unsigned char aDiv4[256];

////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned char nX = 0;
  unsigned char nY = 0;
  unsigned char *pFire1 = NULL;
  unsigned char *pFire2 = NULL;
  unsigned int *pScreenAddress = NULL;
  unsigned char *pScreen1 = NULL;
  unsigned char *pScreen2 = NULL;
  unsigned int nRandom = 0;
  unsigned char *pArray = NULL;
  
  SetMode(0);
  SetBorder(0);
  SetPalette(FirePalette, NUM_COLORS);
  
  memset(aFire, 0, sizeof(aFire));
  
  for(nY = 0; nY < FIRE_HEIGHT * 2; nY++)
    aScreenAddress[nY] = 0xC000 + (((FIRE_Y_ORIGIN - nY) / 8) * 80) +
                      (((FIRE_Y_ORIGIN - nY) % 8) * 2048) + FIRE_X_ORIGIN;
  
  for(nRandom = 0; nRandom < RANDOM_VALUES; nRandom++)
    aRandom[nRandom] = FIRE_COLOR_RANGE / 4 + rand() % (FIRE_COLOR_RANGE * 3 / 4);
  
  memset(aDiv4, 0, sizeof(aDiv4));

  for(nRandom = 0; nRandom <= 240; nRandom++)
    aDiv4[nRandom] = nRandom / 4;
  
  nRandom = 0;
  nTimeLast = GetTime();

  while(1)
  {
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(1, 1);
      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
    
    //fills the botton line with random values
    pArray = (unsigned char *)aFire;
    
    for(nX = 1; nX < (FIRE_WIDTH - 1); nX++)
    {
     *pArray++ = *(aRandom + nRandom++);
     
     if(nRandom >= RANDOM_VALUES)
      nRandom = 0;
    }

    //interpolate colors from bottom to top
    pFire1 = aFire + FIRE_WIDTH; //current row
    pFire2 = aFire; //previous row
    
    pScreenAddress = aScreenAddress;

    for(nY = 1; nY < FIRE_HEIGHT; nY++)
    {
      pScreen1 = (unsigned char *)*pScreenAddress;
      pScreen2 = (unsigned char *)*(pScreenAddress + 1);
      pScreenAddress += 2;

      *pFire1 = *(aDiv4 + (*pFire1 + *pFire2 + *(pFire2 + 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)));
        
      *pScreen1++ = *pScreen2++ = *(aFireColors + *pFire1++);
      pFire2++;
        
      for(nX = 1; nX < FIRE_WIDTH - 1; nX++)
      {
        
        *pFire1 = *(aDiv4 + (*pFire1 + *pFire2 + *(pFire2 - 1) + *(pFire2 + 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)));
        
        *pScreen1++ = *pScreen2++ = *(aFireColors + *pFire1++);
        pFire2++;
      }

      *pFire1 = *(aDiv4 + (*pFire1 + *pFire2 + *(pFire2 - 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)));
        
      *pScreen1++ = *pScreen2++ = *(aFireColors + *pFire1++);
      pFire2++;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Si ejecutamos Fire03.dsk en el emulador vemos que con todos estos cambios apenas hemos aumentado a 10 frames por segundo, pero bueno, algo es algo.

Para hacer algo un poco más chulo, he hecho unos ultimos cambios para que haya 3 efectos distintos, el fuego sin más, una cerilla moviéndose y fogonazos:

////////////////////////////////////////////////////////////////////////
// Fire04.c
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetColor
////////////////////////////////////////////////////////////////////////
void SetColor(unsigned char nColorIndex, unsigned char nPaletteIndex)
{
  __asm
    ld a, 4 (ix)
    ld b, 5 (ix)
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetPalette
////////////////////////////////////////////////////////////////////////
void SetPalette(const unsigned char *pPalette, unsigned char nNumColors)
{
  unsigned char nColor = 0;

  for(nColor = 0; nColor < nNumColors; nColor++)
    SetColor(nColor, pPalette[nColor]);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define NUM_COLORS 6
const unsigned char FirePalette[NUM_COLORS] = {0, 3, 6, 15, 24, 26};

#define FIRE_WIDTH 32
#define FIRE_HEIGHT 16
unsigned char aFire[FIRE_WIDTH * FIRE_HEIGHT];

#define FIRE_COLOR_RANGE 60 // 6 colors
const unsigned char aFireColors[FIRE_COLOR_RANGE] = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
    0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x30, 0x30, 0x30, 0x30, 0x30,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0};

#define FIRE_X_ORIGIN 40 - (FIRE_WIDTH / 2)
#define FIRE_Y_ORIGIN 199

unsigned int aScreenAddress[FIRE_HEIGHT * 2];

#define RANDOM_VALUES 1024
unsigned char aRandom[RANDOM_VALUES];

unsigned char aDiv4[256];

enum _eState
{
  State_Fire,
  State_Splash,
  State_Match,
  State_Max
}_eState;

#define STATE_CHANGE_TICKS 100
#define MATCH_WIDTH 10

////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned char nX = 0;
  unsigned char nY = 0;
  unsigned char *pFire1 = NULL;
  unsigned char *pFire2 = NULL;
  unsigned int *pScreenAddress = NULL;
  unsigned char *pScreen1 = NULL;
  unsigned char *pScreen2 = NULL;
  unsigned int nRandom = 0;
  unsigned char *pArray = NULL;
  enum _eState eState = State_Splash;
  unsigned int nStateTick = 0;
  char nXMatch = 0;
  char nXMatchDir = 1;
  
  SetMode(0);
  SetBorder(0);
  SetPalette(FirePalette, NUM_COLORS);
  
  memset(aFire, 0, sizeof(aFire));
  
  for(nY = 0; nY < FIRE_HEIGHT * 2; nY++)
    aScreenAddress[nY] = 0xC000 + (((FIRE_Y_ORIGIN - nY) / 8) * 80) +
                    (((FIRE_Y_ORIGIN - nY) % 8) * 2048) + FIRE_X_ORIGIN;
  
  for(nRandom = 0; nRandom < RANDOM_VALUES; nRandom++)
    aRandom[nRandom] = FIRE_COLOR_RANGE / 4 + rand() % (FIRE_COLOR_RANGE * 3 / 4);
  
  memset(aDiv4, 0, sizeof(aDiv4));

  for(nRandom = 0; nRandom <= 240; nRandom++)
    aDiv4[nRandom] = nRandom / 4;
  
  nRandom = 0;
  nTimeLast = GetTime();

  while(1)
  {
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(1, 1);
      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
    
    nStateTick++;
    if(nStateTick >= STATE_CHANGE_TICKS)
    {
      nStateTick = 0;
      eState = (eState + 1) % State_Max;
      memset(aFire, 0, sizeof(aFire));
    }
    
    if(eState == State_Fire)
    {
      //fills the botton line with random values
      pArray = (unsigned char *)aFire + 1;
      
      for(nX = 1; nX < (FIRE_WIDTH - 1); nX++)
      {
       *pArray++ = *(aRandom + nRandom++);
       
       if(nRandom >= RANDOM_VALUES)
        nRandom = 0;
      }
    }
    else if(eState == State_Splash)
    {
      if(nStateTick % 10 < 3)
      {
        memset(aFire, 0, FIRE_WIDTH);
      }
      else
      {
        pArray = (unsigned char *)aFire + 1;
        
        for(nX = 1; nX < (FIRE_WIDTH - 1); nX++)
        {
         *pArray++ = *(aRandom + nRandom++);
         
         if(nRandom >= RANDOM_VALUES)
          nRandom = 0;
        }
      }
    }
    else if(eState == State_Match)
    {
      memset(aFire, 0, FIRE_WIDTH);
      //memset(aFire + nXMatch, 40 + rand()%20, MATCH_WIDTH);
      
      pArray = (unsigned char *)aFire + nXMatch;
      
      for(nX = 0; nX < MATCH_WIDTH; nX++)
      {
       *pArray++ = 10 + *(aRandom + nRandom++);
       
       if(nRandom >= RANDOM_VALUES)
        nRandom = 0;
      }     
      
      nXMatch = nXMatch + nXMatchDir;
      
      if(nXMatch > (FIRE_WIDTH - MATCH_WIDTH))
      {
        nXMatch--;
        nXMatchDir = -1;
      }
      
      if(nXMatch < 0)
      {
        nXMatch = 0;
        nXMatchDir = 1;
      }     
    }

    //interpolate colors from bottom to top
    pFire1 = aFire + FIRE_WIDTH; //current row
    pFire2 = aFire; //previous row
    
    pScreenAddress = aScreenAddress;

    for(nY = 1; nY < FIRE_HEIGHT; nY++)
    {
      pScreen1 = (unsigned char *)*pScreenAddress;
      pScreen2 = (unsigned char *)*(pScreenAddress + 1);
      pScreenAddress += 2;

      *pFire1 = *(aDiv4 + (*pFire1 + *pFire2 + *(pFire2 + 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)));
        
      *pScreen1++ = *pScreen2++ = *(aFireColors + *pFire1++);
      pFire2++;
        
      for(nX = 1; nX < FIRE_WIDTH - 1; nX++)
      {
        
        *pFire1 = *(aDiv4 + (*pFire1 + *pFire2 + *(pFire2 - 1) + *(pFire2 + 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)));
        
        *pScreen1++ = *pScreen2++ = *(aFireColors + *pFire1++);
        pFire2++;
      }

      *pFire1 = *(aDiv4 + (*pFire1 + *pFire2 + *(pFire2 - 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)));
        
      *pScreen1++ = *pScreen2++ = *(aFireColors + *pFire1++);
      pFire2++;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Si ejecutamos Fire04.dsk en un emulador vemos lo siguiente:

Pensaba terminar aquí el tutorial, pero no estaba muy contento con el resultado final así que se me ocurrió otra manera de hacerlo para conseguir aumentar la velocidad y el tamaño del fuego. Hasta hora todo lo que hemos visto podía decirse que se calculaba en 'tiempo real', lo que vamos a hacer ahora es precalcular un número concreto de frames del fuego y luego únicamente dedicarnos a pintarlos en pantalla, pudiendo así además aumentar el tamaño. No voy a entrar en muchos detalles, ya que no hay ningún misterio, el código fuente final quedaría así:

////////////////////////////////////////////////////////////////////////
// Fire05.c
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetColor
////////////////////////////////////////////////////////////////////////
void SetColor(unsigned char nColorIndex, unsigned char nPaletteIndex)
{
  __asm
    ld a, 4 (ix)
    ld b, 5 (ix)
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetPalette
////////////////////////////////////////////////////////////////////////
void SetPalette(const unsigned char *pPalette, unsigned char nNumColors)
{
  unsigned char nColor = 0;

  for(nColor = 0; nColor < nNumColors; nColor++)
    SetColor(nColor, pPalette[nColor]);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define NUM_COLORS 6
const unsigned char FirePalette[NUM_COLORS] = {0, 3, 6, 15, 24, 26};

#define FIRE_WIDTH 32
#define FIRE_HEIGHT 16

#define FIRE_COLOR_RANGE 60 // 6 colors
const unsigned char aFireColors[FIRE_COLOR_RANGE] = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
    0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x30, 0x30, 0x30, 0x30, 0x30,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0};

#define FIRE_X_MARGIN 4
#define FIRE_X_ORIGIN 40 - (FIRE_WIDTH - FIRE_X_MARGIN)
#define FIRE_Y_ORIGIN 199

#define NUM_ADDRESS FIRE_HEIGHT * 4
unsigned int aScreenAddress[NUM_ADDRESS];

#define NUM_FRAMES 65
unsigned char aFireFrames[NUM_FRAMES][FIRE_WIDTH * FIRE_HEIGHT];

////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned char nX = 0;
  unsigned char nY = 0;
  unsigned char nFrame = 0;
  unsigned char *pFire1 = NULL;
  unsigned char *pFire2 = NULL;
  unsigned int nPixel = 0;
  unsigned int *pScreenAddress = NULL;
  unsigned char *pScreen1 = NULL;
  unsigned char *pScreen2 = NULL;
  unsigned int nPreframes = 9;
  
  SetMode(0);
  SetBorder(0);
  SetPalette(FirePalette, NUM_COLORS);
  
  memset(aFireFrames, 0, sizeof(aFireFrames));
  
  for(nY = 0; nY < NUM_ADDRESS; nY++)
    aScreenAddress[nY] = 0xC000 + (((FIRE_Y_ORIGIN - nY) / 8) * 80) +
                  (((FIRE_Y_ORIGIN - nY) % 8) * 2048) + FIRE_X_ORIGIN;
  
  SetCursor(1, 1);
  printf("Calculating...");
  
  for(nFrame = 0; nFrame < NUM_FRAMES; nFrame++)
  {
    if(nPreframes > 0)
      nFrame = 0;
 
    SetCursor(8, 5);
    printf("%u / %u  ", nFrame + 1, NUM_FRAMES);
    
    //fills the botton line with random values
    for(nX = 1; nX < (FIRE_WIDTH - 1); nX++)
     aFireFrames[nFrame][nX] = FIRE_COLOR_RANGE / 4 + rand() % (FIRE_COLOR_RANGE * 3 / 4);

    //interpolate colors from bottom to top
    pFire1 = aFireFrames[nFrame] + FIRE_WIDTH; //current row
    pFire2 = aFireFrames[nFrame]; //previous row
     
    for(nY = 1; nY < FIRE_HEIGHT; nY++)
    {
      *pFire1 = (*pFire1 + *pFire2 + *(pFire2 + 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)) / 4;
        
      pFire1++;
      pFire2++;
        
      for(nX = 1; nX < FIRE_WIDTH - 1; nX++)
      {
        
        *pFire1 = (*pFire1 + *pFire2 + *(pFire2 - 1) + *(pFire2 + 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)) / 4;
        
        pFire1++;
        pFire2++;
      }

      *pFire1 = (*pFire1 + *pFire2 + *(pFire2 - 1) -
                  (FIRE_COLOR_RANGE / FIRE_HEIGHT)) / 4;
        
      pFire1++;
      pFire2++;
    }

    if(nPreframes > 0)
      nPreframes--;
    
    if(nPreframes == 0)
    {
      if(nFrame < NUM_FRAMES - 1)
        memcpy(aFireFrames[nFrame + 1], aFireFrames[nFrame], FIRE_WIDTH * FIRE_HEIGHT);
  
      for(nPixel = 0; nPixel < FIRE_WIDTH * FIRE_HEIGHT; nPixel++)
        aFireFrames[nFrame][nPixel] = aFireColors[aFireFrames[nFrame][nPixel]];
    }
  }
  
  SetMode(0);
  nTimeLast = GetTime();
  nFrame = 0;

  while(1)
  {
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(1, 1);
      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
    
    //screen
    pFire1 = aFireFrames[nFrame++] + FIRE_WIDTH;

    if(nFrame >= NUM_FRAMES)
      nFrame = 0;

    pScreenAddress = aScreenAddress;
    for(nY = 1; nY < FIRE_HEIGHT; nY++)
    {
      for(nX = 0; nX < 4; nX++)
      {
        pScreen1 = (unsigned char *)*pScreenAddress++;
        memcpy(pScreen1, pFire1, FIRE_WIDTH - FIRE_X_MARGIN);
        memcpy(pScreen1 + FIRE_WIDTH - FIRE_X_MARGIN, pFire1 + FIRE_X_MARGIN,
               FIRE_WIDTH - FIRE_X_MARGIN);
      }

      pFire1 += FIRE_WIDTH;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Si ejecutamos Fire05.dsk en un emulador vemos lo siguiente:

Ahora sí, ya tiene un buen tamaño y se mueve a unos 32 frames por segundo, que comparado con como hemos empezado está muy bien :-)

 

Podéis bajar un zip con todos ficheros (código fuente, bat's para compilar, binarios y dsk's) aquí: Fire.zip

 

www.CPCMania.com 2013