Sprites I: Introducción al uso de sprites (C y ASM con SDCC) 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 |
www.CPCMania.com 2012 |