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: Es un sprite de 22x28 pixels extraido del juego de recreativa. Vamos a verlo en grande y con cuadricula:

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:

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:

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:

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:

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:

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
|