Sprites I: Introduction to using sprites (C & ASM with SDCC) Pincha aquí para verlo en español In this tutorial we will initiate us into the use of sprites, creation / export and painting on the screen and also we will compare the speed difference between using C and assembler on the routines to draw on the screen What is a sprite? In broad terms we could say that a sprite is a small graphic which is drawn anywhere on the screen. If you move it around the screen creates the illusion of movement and if we use several different interleaved sprites to draw we would generate an animation. Let's see an example, the sprite that we will use in this tutorial is the character of Snow Bros Nick: It's a sprite of 22x28 pixels extracted from the arcade game. Let's see it big and with grid: It's a great sprite, we will convert it to Mode 0, palette of 16 colors to use in the cpc. To do this we again use ConvImgCPC as we saw in the tutorial Converting and displaying an image on the screen, I have configured as follows: As you can see I have used the options "Keep original size", "Overscan" putting 28 lines and 11 colums (11 bytes in mode 0 = 22 pixels). We see that Nick has put on some weight due to the aspect ratio of the mode 0, but it does not worry us now. To export select the options "asm mode" and "Linear" and click "Save Picture". Save the file in asm and easily convert it to C as we saw in the tutorial Converting and displaying an image on the screen, finally we have a file Nick.h like this: /* ;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 }; To paint on the screen of the Amstrad CPC, we will implement a function in C PutSpriteMode0, applying what we learned in previous tutorials, being as follows: 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++; } } A simple and short function that calculates the position of each line in video memory and copies from the sprite. We will now implement a complete program that puts the sprite palette, paints it on the screen and moves it bouncing to the edges of the screen. We will also add to the program the calculation of the times we painted per second (as we saw in the tutorial Measuring times and optimizing 2D Starfield and laters). The final program would look like this: //////////////////////////////////////////////////////////////////////// // 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; } } } //////////////////////////////////////////////////////////////////////// If you compile and run on the emulator get the following: As you can see, the sprite is painted 27 times per second, which is pretty good for not using nothing of assembler. We also see that the sprite is leaving trail / dirty, as it does not erase anything, this for now does not worry us. If we take a look at assembler generated by SDCC for our function PutSpriteMode0 we see for example, that the call to memcpy is accompanied by three 'PUSH' a 'CALL' and their three 'POP' at the end, it "hurts" quite to 11 bytes (22 pixel) that copies at every call to memcpy. Let's try to manually implement the copy instead of calling memcpy to see if the resulting code is faster. The modified program would look like this: //////////////////////////////////////////////////////////////////////// // 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; } } } //////////////////////////////////////////////////////////////////////// If you compile and run on the emulator get the following: As you can see has improved a lot, increasing to 58 sprites drawn per second. Is there much difference between these results and the use of assembler? Let's check it turning into assembly the function PutSpriteMode0. To do this we will use a routine that is well known that MiguelSky included in his Assembler Course. I have adapted the routine to SDCC assembler syntax and parameters taking, leaving the entire program source code as follows: //////////////////////////////////////////////////////////////////////// // 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; } } } //////////////////////////////////////////////////////////////////////// If you compile and run on the emulator get the following: As shown, the difference is immense, rising to the 173 sprites per second, 3 times faster than the version in C. To finish we will modify the program to handle 4 sprites at a time and remove the timing measurements. The final source code would look like this: //////////////////////////////////////////////////////////////////////// // 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); } } } //////////////////////////////////////////////////////////////////////// If you compile and run on the emulator get the following: This concludes this initial tutorial of get started using sprites. You could download a zip with all files (source code, bat to compile, binary and dsk's) here: Sprites_I.zip |
www.CPCMania.com 2012 |