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: nick It's a sprite of 22x28 pixels extracted from the arcade game. Let's see it big and with grid:

Nick

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:

ConvImgCpc

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:

NICK

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:

NICK

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:

NICK

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:

NICK

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