Sprites I: Introducción al uso de sprites (C y ASM con SDCC)

Click here to see in English

En este tutorial vamos a iniciarnos en el uso de sprites, tanto su creación/exportación como su pintado en pantalla y además comprobaremos la diferencia de velocidad entre usar C y ensamblador en las rutinas de pintado.

¿Que es un sprite? A grandes rasgos podríamos decir que un sprite es un grafico pequeño que se dibuja en cualquier parte de la pantalla. Si lo movemos por la pantalla crea la sensación de movimiento y si utilizamos varios sprites diferentes intercalándolos al dibujarlos generaríamos una animación. Vamos a ver un ejemplo, el sprite que vamos a usar en este tutorial es el personaje Nick de Snow Bros: Nick Es un sprite de 22x28 pixels extraido del juego de recreativa. Vamos a verlo en grande y con cuadricula:

Nick

Es un sprite estupendo, vamos a convertirlo a Modo 0, paleta de 16 colores para poder usarlo en el cpc. Para ello volvemos a usar ConvImgCPC como ya vimos en el tutorial Convirtiendo y mostrando una imagen en pantalla, yo lo he configurado de la siguiente manera:

ConvImgCpc

Como se puede ver he utilizado las opciones "Keep original size", "Overscan" poniendo 28 de alto y 11 de ancho (11 bytes en modo 0 = 22 pixels). Vemos que Nick ha engordado un poco debido al aspect ratio del modo 0, pero bueno eso no nos preocupa ahora mismo. Para exportarlo seleccionamos las opciones "asm mode" y "Linear" y pulsamos "Save Picture". Guardamos el fichero en asm y lo convertimos a C fácilmente como ya vimos en el tutorial Convirtiendo y mostrando una imagen en pantalla, finalmente nos queda un fichero Nick.h como este:

/*
;Généré par ConvImgCpc Version 0.16
; Mode 0
; 11x28
; Linear
*/
#define NICK_WIDTH 11
#define NICK_HEIGHT 28
#define NUM_COLORS 16

const unsigned char NickPalette[NUM_COLORS] = {26, 13, 0, 11, 2, 1, 10, 20, 3, 14, 15, 0, 0, 0, 0, 0};

const char NickSprite[] = 
{
               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
        ,      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
        ,      0x00, 0x84, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00
        ,      0x00, 0x00, 0x00, 0x40, 0x5C, 0x48, 0x00, 0x00
        ,      0x00, 0x00, 0x00, 0x00, 0x40, 0x0C, 0x0C, 0xFC
        ,      0xAC, 0x80, 0x00, 0x00, 0x00, 0x00, 0x40, 0x0C
        ,      0x80, 0xC0, 0xD4, 0x3C, 0x08, 0x00, 0x00, 0x00
        ,      0x00, 0x84, 0x80, 0x00, 0x00, 0x04, 0x2C, 0x80
        ,      0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00
        ,      0x40, 0x48, 0x00, 0x00, 0x00, 0x00, 0x40, 0x08
        ,      0x00, 0x00, 0x00, 0x40, 0x84, 0x00, 0x00, 0x00
        ,      0x00, 0x04, 0x84, 0x04, 0x08, 0x00, 0x00, 0x84
        ,      0x00, 0x00, 0x00, 0x00, 0x04, 0x84, 0x04, 0x08
        ,      0x00, 0x00, 0x84, 0x0C, 0x0C, 0x80, 0x00, 0x04
        ,      0x04, 0x04, 0x08, 0x00, 0x40, 0xC4, 0xC0, 0xC0
        ,      0x48, 0x00, 0x04, 0x80, 0x00, 0x01, 0x03, 0x40
        ,      0x30, 0x80, 0x40, 0x84, 0x00, 0x84, 0x03, 0x03
        ,      0x07, 0x4A, 0x40, 0xC8, 0x00, 0x40, 0x84, 0x40
        ,      0x48, 0x81, 0x0F, 0x0A, 0x00, 0xC4, 0xE0, 0x00
        ,      0x00, 0x84, 0x04, 0xC0, 0x80, 0x00, 0x00, 0xC0
        ,      0x64, 0xE0, 0xC0, 0x00, 0x84, 0x04, 0x40, 0xA4
        ,      0x00, 0xC0, 0x48, 0x92, 0xC0, 0x08, 0x40, 0x84
        ,      0x04, 0x40, 0x60, 0x48, 0x0C, 0xC0, 0x98, 0x84
        ,      0x00, 0x40, 0x48, 0x04, 0xC0, 0x60, 0x40, 0x80
        ,      0x41, 0x98, 0xC0, 0x00, 0xC0, 0x08, 0x04, 0xC0
        ,      0x60, 0x00, 0x00, 0xC4, 0xC6, 0x08, 0x40, 0x84
        ,      0x80, 0x40, 0x48, 0xB0, 0xCC, 0xCC, 0xC9, 0xC6
        ,      0xE0, 0xC0, 0x48, 0x00, 0x00, 0x84, 0xB0, 0x07
        ,      0x4B, 0xC3, 0xCC, 0x0C, 0x84, 0x80, 0x00, 0x00
        ,      0x04, 0xB0, 0xCC, 0xCC, 0xC3, 0x98, 0x24, 0x0C
        ,      0x00, 0x00, 0x00, 0x40, 0x58, 0x98, 0x18, 0xCC
        ,      0x98, 0x70, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x58
        ,      0x70, 0x18, 0x64, 0x30, 0xF0, 0x48, 0x00, 0x00
        ,      0x00, 0x00, 0x84, 0xF0, 0x0C, 0xB0, 0xF0, 0xA4
        ,      0x80, 0x00, 0x00, 0x00, 0x84, 0x1C, 0x3C, 0x2C
        ,      0xFC, 0x3C, 0x2C, 0x80, 0x00, 0x00, 0x00, 0x1C
        ,      0x3C, 0x3C, 0xFC, 0xFC, 0xFC, 0x3C, 0x08, 0x00
        ,      0x00, 0x00, 0x84, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C
        ,      0x0C, 0x80, 0x00, 0x00
};

Para pintarlo en la pantalla del Amstrad CPC, vamos a implementar una función PutSpriteMode0 en C, aplicando lo que hemos aprendido en los tutoriales anteriores, quedando de la siguiente manera:

void PutSpriteMode0(unsigned char *pSprite, unsigned char nX, unsigned char nY, unsigned char nWidth, unsigned char nHeight)
{
    unsigned char nYPos = 0;
    unsigned char *pAddress = 0;
    
    for(nYPos = 0; nYPos < nHeight; nYPos++)
    {
        pAddress = (unsigned char *)(0xC000 + ((nY / 8u) * 80u) + ((nY % 8u) * 2048u) + nX);

        memcpy(pAddress, pSprite, nWidth);
        pSprite += nWidth;
        nY++;
    }
}

Una sencilla y corta función que calcula la posición en memoria de cada línea del sprite y la copia en pantalla. Vamos ahora a implementar un programa completo, que pone la paleta del sprite, lo pinta en pantalla y lo mueve rebotando con los bordes de la pantalla. Vamos también a añadirle al programa el calculo de las veces que pintamos por segundo (como ya vimos en el tutorial Midiendo tiempos y optimizando Campo de estrellas 2D y posteriores). El programa final quedaría así:

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

#define MAX_X 79
#define MAX_Y 199

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;
}

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

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

void PutSpriteMode0(unsigned char *pSprite, unsigned char nX, unsigned char nY, unsigned char nWidth, unsigned char nHeight)
{
    unsigned char nYPos = 0;
    unsigned char *pAddress = 0;
    
    for(nYPos = 0; nYPos < nHeight; nYPos++)
    {
        pAddress = (unsigned char *)(0xC000 + ((nY / 8u) * 80u) + ((nY % 8u) * 2048u) + nX);

        memcpy(pAddress, pSprite, nWidth);
        pSprite += nWidth;
        nY++;
    }
}


////////////////////////////////////////////////////////////////////////
unsigned char char1,char2,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;
}
////////////////////////////////////////////////////////////////////////

void main()
{
    unsigned int nFPS = 0;
    unsigned int nTimeLast = 0;
    
    int nX = 40;
    int nY = 100;
    char nXDir = 1;
    char nYDir = 2;
    
    //SCR_SET_MODE 0
    __asm
        ld a, #0
        call #0xBC0E
    __endasm;

    //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;
  
  SetPalette(NickPalette);
  
    nTimeLast = GetTime();

    while(1)
    {
        //move
        nX += nXDir;
        nY += nYDir;
        
        if(nX <= 0)
        {
            nX = 0;
            nXDir = 1;
        }

        if(nY <= 0)
        {
            nY = 0;
            nYDir = 2;
        }
        
        if(nX >= (MAX_X - NICK_WIDTH))
        {
            nX = MAX_X - NICK_WIDTH;
            nXDir = -1;
        }

        if(nY >= (MAX_Y - NICK_HEIGHT))
        {
            nY = MAX_Y - NICK_HEIGHT;
            nYDir = -2;
        }

        //paint
        PutSpriteMode0(NickSprite, nX, nY, NICK_WIDTH, NICK_HEIGHT);
        
        
        nFPS++;

        if(GetTime() - nTimeLast >= 300)
        {
            //TXT SET CURSOR 0,0
            __asm
                ld h, #1
                ld l, #1
                call #0xBB75
            __endasm;

            printf("%u  ", nFPS);

            nTimeLast = GetTime();
            nFPS = 0;
        }
    }
}
////////////////////////////////////////////////////////////////////////

Si compilamos y ejecutamos en el emulador obtenemos lo siguiente:

NICK

Como puede verse, el sprite se pinta unas 27 veces por segundo, que está bastante bien para no haber usado nada de ensamblador en el pintado. También vemos que el sprite va dejando rastro/estela, ya que no borramos ni nada, esto por ahora no nos preocupa tampoco. Si echamos un ojo al ensamblador que genera SDCC para nuestra función PutSpriteMode0 vemos por ejemplo que la llamada a memcpy va acompañada de tres 'PUSH' un 'CALL' y sus correspondientes tres 'POP' al terminar, esto "le duele" bastante, para 11 bytes (22 pixel) que copia cada vez. Vamos a probar a implementar manualmente la copia en vez de llamar a memcpy para ver si el código resultante es más rápido. El programa modificado quedaría así:

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

#define MAX_X 79
#define MAX_Y 199

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;
}

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

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

void PutSpriteMode0(unsigned char *pSprite, unsigned char nX, unsigned char nY, unsigned char nWidth, unsigned char nHeight)
{
  unsigned char nXPos = 0;
  unsigned char nYPos = 0;
  unsigned char *pAddress = 0;
  
  for(nYPos = 0; nYPos < nHeight; nYPos++)
  {
    pAddress = (unsigned char *)(0xC000 + ((nY / 8u) * 80u) + ((nY % 8u) * 2048u) + nX);

    for(nXPos = 0; nXPos < nWidth; nXPos++)
      *pAddress++ = *pSprite++;

    nY++;
  }
}


////////////////////////////////////////////////////////////////////////
unsigned char char1,char2,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;
}
////////////////////////////////////////////////////////////////////////

void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  
  int nX = 40;
  int nY = 100;
  char nXDir = 1;
  char nYDir = 2;
  
  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;
  
  SetPalette(NickPalette);
  
  nTimeLast = GetTime();

  while(1)
  {
    //move
    nX += nXDir;
    nY += nYDir;
    
    if(nX <= 0)
    {
      nX = 0;
      nXDir = 1;
    }

    if(nY <= 0)
    {
      nY = 0;
      nYDir = 2;
    }
    
    if(nX >= (MAX_X - NICK_WIDTH))
    {
      nX = MAX_X - NICK_WIDTH;
      nXDir = -1;
    }

    if(nY >= (MAX_Y - NICK_HEIGHT))
    {
      nY = MAX_Y - NICK_HEIGHT;
      nYDir = -2;
    }

    //paint
    PutSpriteMode0(NickSprite, nX, nY, NICK_WIDTH, NICK_HEIGHT);
    
    
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      //TXT SET CURSOR 0,0
      __asm
        ld h, #1
        ld l, #1
        call #0xBB75
      __endasm;

      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Si compilamos y ejecutamos en el emulador obtenemos lo siguiente:

NICK

Como puede verse ha mejorado mucho, aumentando hasta 58 los sprites dibujados por segundo. En C ya poco más podemos hacer, pero ¿hay mucha diferencia entre estos resultados y el uso de ensamblador? Vamos a comprobarlo convirtiendo la función PutSpriteMode0 a ensamblador. Para ello vamos a usar una rutina muy conocida que es la que incluyó MiguelSky en su Curso de Ensamblador. He adaptado la rutina a la sintaxis de ensamblador de SDCC así como la toma de parámetros, quedando el código fuente entero del programa de la siguiente manera:

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

#define MAX_X 79
#define MAX_Y 199

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;
}

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

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

void PutSpriteMode0(unsigned char *pAddress, unsigned char nWidth, unsigned char nHeight, unsigned char *pSprite)
{
  __asm
    LD L, 4(IX) 
    LD H, 5(IX) 
    LD C, 6(IX) 
    LD B, 7(IX)            
    LD E, 8(IX) 
    LD D, 9(IX) 

    _loop_alto:
       PUSH BC
       LD B,C
       PUSH HL
    _loop_ancho:
       LD A,(DE)
       LD (HL),A
       INC DE
       INC HL
       DJNZ _loop_ancho
       POP HL
       LD A,H
       ADD #0x08
       LD H,A
       SUB #0xC0
       JP NC, _sig_linea
       LD BC, #0xC050
       ADD HL,BC
    _sig_linea:
       POP BC
       DJNZ _loop_alto
  __endasm;
}


////////////////////////////////////////////////////////////////////////
unsigned char char1,char2,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;
}
////////////////////////////////////////////////////////////////////////

void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  
  int nX = 40;
  int nY = 100;
  char nXDir = 1;
  char nYDir = 2;
  
  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;
  
  SetPalette(NickPalette);
  
  nTimeLast = GetTime();

  while(1)
  {
    //move
    nX += nXDir;
    nY += nYDir;
    
    if(nX <= 0)
    {
      nX = 0;
      nXDir = 1;
    }

    if(nY <= 0)
    {
      nY = 0;
      nYDir = 2;
    }
    
    if(nX >= (MAX_X - NICK_WIDTH))
    {
      nX = MAX_X - NICK_WIDTH;
      nXDir = -1;
    }

    if(nY >= (MAX_Y - NICK_HEIGHT))
    {
      nY = MAX_Y - NICK_HEIGHT;
      nYDir = -2;
    }

    //paint
    PutSpriteMode0((unsigned char *)(0xC000 + ((nY / 8u) * 80u) + ((nY % 8u) * 2048u) + nX),
                    NICK_WIDTH, NICK_HEIGHT, NickSprite);
    
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      //TXT SET CURSOR 0,0
      __asm
        ld h, #1
        ld l, #1
        call #0xBB75
      __endasm;

      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Si compilamos y ejecutamos en el emulador obtenemos lo siguiente:

NICK

Como puede verse, la diferencia es inmensa, llegando a los 173 sprites por segundo, 3 veces más rápido que la versión en C. Para finalizar vamos a modificar el programa para que maneje 4 sprites simultáneamente y quitamos las mediciones de tiempo y los carteles. El código fuente final quedaría así:

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

#define MAX_X 79
#define MAX_Y 199

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;
}

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

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

void PutSpriteMode0(unsigned char *pAddress, unsigned char nWidth, unsigned char nHeight, unsigned char *pSprite)
{
  __asm
    LD L, 4(IX) 
    LD H, 5(IX) 
    LD C, 6(IX) 
    LD B, 7(IX)            
    LD E, 8(IX) 
    LD D, 9(IX) 

    _loop_alto:
       PUSH BC
       LD B,C
       PUSH HL
    _loop_ancho:
       LD A,(DE)
       LD (HL),A
       INC DE
       INC HL
       DJNZ _loop_ancho
       POP HL
       LD A,H
       ADD #0x08
       LD H,A
       SUB #0xC0
       JP NC, _sig_linea
       LD BC, #0xC050
       ADD HL,BC
    _sig_linea:
       POP BC
       DJNZ _loop_alto
  __endasm;
}


////////////////////////////////////////////////////////////////////////
unsigned char char1,char2,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;
}
////////////////////////////////////////////////////////////////////////

#define NUM_SPRITES 4

void main()
{
  unsigned char nSprite = 0;
  int nX[NUM_SPRITES];
  int nY[NUM_SPRITES];
  char nXDir[NUM_SPRITES];
  char nYDir[NUM_SPRITES];
  
  for(nSprite = 0; nSprite < NUM_SPRITES; nSprite++)
  {
    nX[nSprite] = rand() % (MAX_X - NICK_WIDTH);
    nY[nSprite] = rand() % (MAX_Y - NICK_HEIGHT);
    nXDir[nSprite] = (rand() % 2) == 0 ? 1 : -1;
    nYDir[nSprite] = (rand() % 2) == 0 ? 2 : -2;
  }
  
  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;
  
  SetPalette(NickPalette);
  
  while(1)
  {
    for(nSprite = 0; nSprite < NUM_SPRITES; nSprite++)
    {
      //move
      nX[nSprite] += nXDir[nSprite];
      nY[nSprite] += nYDir[nSprite];
      
      if(nX[nSprite] <= 0)
      {
        nX[nSprite] = 0;
        nXDir[nSprite] = 1;
      }
  
      if(nY[nSprite] <= 0)
      {
        nY[nSprite] = 0;
        nYDir[nSprite] = 2;
      }
      
      if(nX[nSprite] >= (MAX_X - NICK_WIDTH))
      {
        nX[nSprite] = MAX_X - NICK_WIDTH;
        nXDir[nSprite] = -1;
      }
  
      if(nY[nSprite] >= (MAX_Y - NICK_HEIGHT))
      {
        nY[nSprite] = MAX_Y - NICK_HEIGHT;
        nYDir[nSprite] = -2;
      }
  
      //paint
      PutSpriteMode0((unsigned char *)(0xC000 + ((nY[nSprite] / 8u) * 80u) + ((nY[nSprite] % 8u) * 2048u) + nX[nSprite]),
                     NICK_WIDTH, NICK_HEIGHT, NickSprite);
    }
  }
}
////////////////////////////////////////////////////////////////////////

Si compilamos y ejecutamos en el emulador obtenemos lo siguiente:

NICK

Con esto concluye este primer tutorial de introducción al uso de sprites. Podéis bajar un zip con todos ficheros (código fuente, bat's para compilar, binarios y dsk's) aquí: Sprites_I.zip

 

www.CPCMania.com 2012