PongC: Clone of the classic Pong in mode 2 (C & ASM with SDCC)

Pincha aquí para verlo en español

In this tutorial we will try to make a clone of the classic game Pong in our Amstrad in Mode 2 (2-color 640x200).

pong

As we saw in the tutorial Painting pixels: Introduction to video memory, in mode 2 each byte of video memory stores 8 pixels in a very specific order. To set to 0 or 1 a pixel in concrete we have to do at the bit level. Let's start preparing a function for paint a pixel in mode 2, which looks like this:

////////////////////////////////////////////////////////////////////////
//PutPixelMode2
////////////////////////////////////////////////////////////////////////
void PutPixelMode2(unsigned int nX, unsigned int nY, unsigned char nColor)
{
  unsigned char *pAddress = (unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8);
  unsigned char nBit = 7 - nX % 8;
  unsigned char nAux = 0;
  
  if(nColor == 0)
  {
    nAux = ~(1 << nBit);
    *pAddress = *pAddress & nAux;
  }
  else
  {
    nAux = (1 << nBit);
    *pAddress = *pAddress | nAux;
  }
}
////////////////////////////////////////////////////////////////////////

Let's see how it works doing our first program, that simply paint the game ball. The entire program would look like this:

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


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char 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;
}
////////////////////////////////////////////////////////////////////////


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


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//PutPixelMode2
////////////////////////////////////////////////////////////////////////
void PutPixelMode2(unsigned int nX, unsigned int nY, unsigned char nColor)
{
  unsigned char *pAddress = (unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8);
  unsigned char nBit = 7 - nX % 8;
  unsigned char nAux = 0;
  
  if(nColor == 0)
  {
    nAux = ~(1 << nBit);
    *pAddress = *pAddress & nAux;
  }
  else
  {
    nAux = (1 << nBit);
    *pAddress = *pAddress | nAux;
  }
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define BALL_WIDTH 20
#define BALL_HEIGHT 10



////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned int nX = 0;
  unsigned int nY = 0;
  
  SetMode(2);
  SetBorder(0);
  SetColor(0, 0);
  SetColor(1, 26);
  
  nTimeLast = GetTime();

  while(1)
  {
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(1, 1);
      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
    
    for(nX = 0; nX < BALL_WIDTH; nX++)
      for(nY = 0; nY < BALL_HEIGHT; nY++)
        PutPixelMode2(100 + nX, 100 + nY, 1);
  }
}
////////////////////////////////////////////////////////////////////////

If you compile and run on the emulator get the following:

 

As can be seen, paint pixel by pixel is very slow and we barely reached 17 frames per second, not enough to make the game ... As the ball is square, we better paint it by horizontal lines to see if we improve the speed. For it we programmed a new function to paint horizontal lines in mode 2, which looks like this:

////////////////////////////////////////////////////////////////////////
//LineHMode2
////////////////////////////////////////////////////////////////////////
void LineHMode2(unsigned int nX, unsigned int nY, unsigned char nColor, unsigned int nWidth)
{
  unsigned char *pAddress = (unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8);
  
  if(nX % 8 == 0 && nWidth % 8 == 0)
  {
    memset(pAddress, nColor ? 0xFF : 0x00, nWidth / 8);
  }
  else
  {
    unsigned int nPixels = 0;
    unsigned int nX2 = 0;
    
    //first byte
    for(nX2 = nX; (nX2 % 8) && (nX2 < nX + nWidth); nX2++)
    {
      unsigned char nBit = 7 - nX2 % 8;
      unsigned char nAux = 0;
      
      if(nColor == 0)
      {
        nAux = ~(1 << nBit);
        *pAddress = *pAddress & nAux;
      }
      else
      {
        nAux = (1 << nBit);
        *pAddress = *pAddress | nAux;
      }
      
      nPixels++;
    }
    
    if(nPixels > 0)
      pAddress++;
    
    //intermediate bytes
    nX2 = (nWidth - nPixels) / 8;
    memset(pAddress, nColor ? 0xFF : 0x00, nX2);
    nPixels += nX2 * 8;
    pAddress += nX2;
    
    //last byte
    for(nX2 = nX + nPixels; (nX2 < nX + nWidth); nX2++)
    {
      unsigned char nBit = 7 - nX2 % 8;
      unsigned char nAux = 0;
      
      if(nColor == 0)
      {
        nAux = ~(1 << nBit);
        *pAddress = *pAddress & nAux;
      }
      else
      {
        nAux = (1 << nBit);
        *pAddress = *pAddress | nAux;
      }
      
      nPixels++;
    }   
  }
}
////////////////////////////////////////////////////////////////////////

As we see if the position x is a multiple of 8 and the number of pixels you want to paint are also multiple of 8 is as simple as doing a memset, otherwise the function paint the line bit by bit in first and last bytes and use memset in central bytes to save time. Modify the previous program to paint our ball with this new function, being as follows:

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


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char 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;
}
////////////////////////////////////////////////////////////////////////


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


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//LineHMode2
////////////////////////////////////////////////////////////////////////
void LineHMode2(unsigned int nX, unsigned int nY, unsigned char nColor, unsigned int nWidth)
{
  unsigned char *pAddress = (unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8);
  
  if(nX % 8 == 0 && nWidth % 8 == 0)
  {
    memset(pAddress, nColor ? 0xFF : 0x00, nWidth / 8);
  }
  else
  {
    unsigned int nPixels = 0;
    unsigned int nX2 = 0;
    
    //first byte
    for(nX2 = nX; (nX2 % 8) && (nX2 < nX + nWidth); nX2++)
    {
      unsigned char nBit = 7 - nX2 % 8;
      unsigned char nAux = 0;
      
      if(nColor == 0)
      {
        nAux = ~(1 << nBit);
        *pAddress = *pAddress & nAux;
      }
      else
      {
        nAux = (1 << nBit);
        *pAddress = *pAddress | nAux;
      }
      
      nPixels++;
    }
    
    if(nPixels > 0)
      pAddress++;
    
    //intermediate bytes
    nX2 = (nWidth - nPixels) / 8;
    memset(pAddress, nColor ? 0xFF : 0x00, nX2);
    nPixels += nX2 * 8;
    pAddress += nX2;
    
    //last byte
    for(nX2 = nX + nPixels; (nX2 < nX + nWidth); nX2++)
    {
      unsigned char nBit = 7 - nX2 % 8;
      unsigned char nAux = 0;
      
      if(nColor == 0)
      {
        nAux = ~(1 << nBit);
        *pAddress = *pAddress & nAux;
      }
      else
      {
        nAux = (1 << nBit);
        *pAddress = *pAddress | nAux;
      }
      
      nPixels++;
    }   
  }
}
////////////////////////////////////////////////////////////////////////



////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define BALL_WIDTH 20
#define BALL_HEIGHT 10

////////////////////////////////////////////////////////////////////////
//DrawBall
////////////////////////////////////////////////////////////////////////
void DrawBall(unsigned int nX, unsigned int nY)
{
  unsigned int nAux = 0;
  
  for(nAux = 0; nAux < BALL_HEIGHT; nAux++)
    LineHMode2(nX, nY + nAux, 1, BALL_WIDTH); 
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned int nX = 0;
  unsigned int nY = 0;
  
  SetMode(2);
  SetBorder(0);
  SetColor(0, 0);
  SetColor(1, 26);
  
  nTimeLast = GetTime();

  while(1)
  {
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(1, 1);
      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }

    nX = (nX + 1) % (640 - BALL_WIDTH);
    nY = (nY + 1) % (200 - BALL_HEIGHT);
    DrawBall(nX, nY);
  }
}
////////////////////////////////////////////////////////////////////////

If you compile and run on the emulator get the following:

 

We have improved a lot, has increased from 17 to 56 frames per second, but still quite insufficient since we lack all the rest of the game... Let's do an optimization to the function of paint lines, so that instead of doing at the bit level operations we already have precalculated the 8 possible bytes of left and right end of our line, the source code of the program is the following:

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


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char 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;
}
////////////////////////////////////////////////////////////////////////


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


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


const unsigned char aMode2Left[8] = { 0xFF, 0x7F, 0x3F, 0x1F, 0x0F, 0x07, 0x03, 0x01};
const unsigned char aMode2Right[8] = { 0xFF, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE};

////////////////////////////////////////////////////////////////////////
//LineHMode2
////////////////////////////////////////////////////////////////////////
void LineHMode2(unsigned int nX, unsigned int nY, unsigned char nColor, unsigned int nWidth)
{
  unsigned char *pAddress = (unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8);
  
  if(nColor == 0 || (nX % 8 == 0 && nWidth % 8 == 0))
  {
    memset(pAddress, nColor ? 0xFF : 0x00, nWidth / 8 + (nWidth % 8 ? 1 : 0));
  }
  else
  {
    unsigned int nPixels = 0;
    unsigned int nAux = 0;
    
    //first byte
    nAux = nX % 8;
    
    if(nAux)
    {
      *pAddress++ = *(aMode2Left + nAux);
      nPixels += 8 - nAux;
    }
    
    //intermediate bytes
    nAux = (nWidth - nPixels) / 8;
    memset(pAddress, 0xFF, nAux);
    nPixels += nAux * 8;
    pAddress += nAux;
    
    //last byte
    nAux = nWidth - nPixels;

    if(nAux)
      *pAddress++ = *(aMode2Right + nAux);
  }
}
////////////////////////////////////////////////////////////////////////



////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define BALL_WIDTH 20
#define BALL_HEIGHT 10

////////////////////////////////////////////////////////////////////////
//DrawBall
////////////////////////////////////////////////////////////////////////
void DrawBall(unsigned int nX, unsigned int nY)
{
  unsigned int nAux = 0;
  
  for(nAux = 0; nAux < BALL_HEIGHT; nAux++)
    LineHMode2(nX, nY + nAux, 1, BALL_WIDTH); 
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned int nX = 0;
  unsigned int nY = 0;
  
  SetMode(2);
  SetBorder(0);
  SetColor(0, 0);
  SetColor(1, 26);
  
  nTimeLast = GetTime();

  while(1)
  {
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(1, 1);
      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }

    nX = (nX + 1) % (640 - BALL_WIDTH);
    nY = (nY + 1) % (200 - BALL_HEIGHT);
    DrawBall(nX, nY);
  }
}
////////////////////////////////////////////////////////////////////////

If you compile and run on the emulator get the following:

 

Greatly improved speed, from 56 to 119 frames per second. Could we improve more? Well, we could use sprites and paint on screen with assembler to go faster, but clear, as we are moving the ball pixel by pixel and each byte's 8 pixels we can not have sprite for the ball... To solve it we will use 8 sprites for the ball, one for each possible combination of the ball on its first and last byte. This is the program if we apply these changes:

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


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char 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;
}
////////////////////////////////////////////////////////////////////////


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


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//PutSprite()
////////////////////////////////////////////////////////////////////////
void PutSprite(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;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define BALL_WIDTH 20
#define BALL_HEIGHT 10

#define BALL_WIDTH_BYTES 4
const unsigned char aBallSprite[9][BALL_WIDTH_BYTES * BALL_HEIGHT] = 
{
                        { 0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00 },
                          
                        { 0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00 },
                          
                        { 0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00 },
                          
                        { 0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00 },
                          
                        { 0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00 },
                          
                        { 0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80 },
                          
                        { 0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0 },
                          
                        { 0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0 },
                          
                        { 0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00 },
};


////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned int nX = 0;
  unsigned int nY = 0;
  
  SetMode(2);
  SetBorder(0);
  SetColor(0, 0);
  SetColor(1, 26);
  
  nTimeLast = GetTime();

  while(1)
  {
    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(1, 1);
      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }

    nX = (nX + 1) % (640 - BALL_WIDTH);
    nY = (nY + 1) % (200 - BALL_HEIGHT);
  
    PutSprite((unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8), BALL_WIDTH_BYTES, BALL_HEIGHT, aBallSprite[nX % 8]);
  }
}
////////////////////////////////////////////////////////////////////////

If you compile and run on the emulator get the following:

 

Now that's a big improvement, we have gone from 119 to 525 frames per second. Now that we have a good speed, we will see how to move it and paint well. To not let the ball trail what we do is that each time the ball moves first delete its previous position and then we paint at the new position. As we're painting directly in video memory, this will cause terrible flashing ball, so to avoid flicker we will synchronize with the screen refreshing and for this we will use a function of CPCWiki and we will adapt to the assembler of SDCC. With all these changes, the program would look like:

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


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char 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;
}
////////////////////////////////////////////////////////////////////////


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


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//PutSprite()
////////////////////////////////////////////////////////////////////////
void PutSprite(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;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//WaitVsync
////////////////////////////////////////////////////////////////////////
void WaitVsync()
{
  __asm
    ld b,#0xf5          ;; PPI port B input
    _wait_vsync:
    in a,(c)            ;; [4] read PPI port B input
                        ;; (bit 0 = "1" if vsync is active,
                        ;;  or bit 0 = "0" if vsync is in-active)
    rra                 ;; [1] put bit 0 into carry flag
    jp nc,_wait_vsync   ;; [3] if carry not set, loop, otherwise continue
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define BALL_WIDTH 20
#define BALL_HEIGHT 10

#define BALL_WIDTH_BYTES 4
const unsigned char aBallSprite[9][BALL_WIDTH_BYTES * BALL_HEIGHT] = 
{
                        { 0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00 },
                          
                        { 0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00 },
                          
                        { 0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00 },
                          
                        { 0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00 },
                          
                        { 0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00 },
                          
                        { 0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80 },
                          
                        { 0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0 },
                          
                        { 0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0 },
                          
                        { 0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00 },
};


////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned int nX = 0;
  unsigned int nY = 0;
  
  SetMode(2);
  SetBorder(0);
  SetColor(0, 0);
  SetColor(1, 26);
  
  nTimeLast = GetTime();

  while(1)
  {
    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(1, 1);
      printf("%u  ", nFPS);

      nTimeLast += 300;
      nFPS = 0;
    }

    WaitVsync();
    PutSprite((unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8), BALL_WIDTH_BYTES, BALL_HEIGHT, aBallSprite[8]);
    nX = (nX + 1) % (640 - BALL_WIDTH);
    nY = (nY + 1) % (200 - BALL_HEIGHT);
    PutSprite((unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8), BALL_WIDTH_BYTES, BALL_HEIGHT, aBallSprite[nX % 8]);

    nFPS++;
  }
}
////////////////////////////////////////////////////////////////////////

If you compile and run on the emulator get the following:

 

Using synchronization we avoid flicker, but we stuck to 50fps, so finally the ball will have to move several pixels at a time, so even though we are in sync with the screen repainting the ball will give a sense of flickering. Now let's add the players and for that we will not complicate, we use the function of painting horizontal lines, but always adjusted to 8 pixels to be quick. Once painted the players, what we will do when moving up or down is to paint the new lines that has moved and will delete the old to work fast. The program would look like this:

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


////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char 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;
}
////////////////////////////////////////////////////////////////////////


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


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//PutSprite()
////////////////////////////////////////////////////////////////////////
void PutSprite(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;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//WaitVsync
////////////////////////////////////////////////////////////////////////
void WaitVsync()
{
  __asm
    ld b,#0xf5          ;; PPI port B input
    _wait_vsync:
    in a,(c)            ;; [4] read PPI port B input
                        ;; (bit 0 = "1" if vsync is active,
                        ;;  or bit 0 = "0" if vsync is in-active)
    rra                 ;; [1] put bit 0 into carry flag
    jp nc,_wait_vsync   ;; [3] if carry not set, loop, otherwise continue
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//LineHMode2Byte
////////////////////////////////////////////////////////////////////////
void LineHMode2Byte(unsigned int nX, unsigned int nY, unsigned char nColor, unsigned char nBytes)
{
  unsigned char *pAddress = (unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8);
  
  if(nX % 8 == 0)
  {
    memset(pAddress, nColor ? 0xFF : 0x00, nBytes);
  }
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define BALL_WIDTH 20
#define BALL_HEIGHT 10

#define BALL_WIDTH_BYTES 4
const unsigned char aBallSprite[9][BALL_WIDTH_BYTES * BALL_HEIGHT] = 
{
                        { 0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00,
                          0xFF, 0xFF, 0xF0, 0x00 },
                          
                        { 0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00,
                          0x7F, 0xFF, 0xF8, 0x00 },
                          
                        { 0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00,
                          0x3F, 0xFF, 0xFC, 0x00 },
                          
                        { 0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00,
                          0x1F, 0xFF, 0xFE, 0x00 },
                          
                        { 0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00,
                          0x0F, 0xFF, 0xFF, 0x00 },
                          
                        { 0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80,
                          0x07, 0xFF, 0xFF, 0x80 },
                          
                        { 0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0,
                          0x03, 0xFF, 0xFF, 0xC0 },
                          
                        { 0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0,
                          0x01, 0xFF, 0xFF, 0xE0 },
                          
                        { 0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00,
                          0x00, 0x00, 0x00, 0x00 },
};

#define NUM_PLAYERS 2
#define SCREEN_WIDTH 640
#define SCREEN_HEIGHT 200

#define PLAYER_WIDTH 16
#define PLAYER_HEIGHT 30
#define PLAYER_WIDTH_BYTES 2

struct _tPlayer
{
  unsigned char nY;
  unsigned int nX;
  char nYDir;
}_tPlayer;

struct _tPlayer aPlayer[NUM_PLAYERS];

////////////////////////////////////////////////////////////////////////
//DrawPlayer
////////////////////////////////////////////////////////////////////////
void DrawPlayer(unsigned char nPlayer)
{
  unsigned char nY = 0;
  for(nY = 0; nY < PLAYER_HEIGHT; nY++)
    LineHMode2Byte(aPlayer[nPlayer].nX, aPlayer[nPlayer].nY+ nY, 1, PLAYER_WIDTH_BYTES);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//MovePlayer
////////////////////////////////////////////////////////////////////////
void MovePlayer(unsigned char nPlayer)
{
  unsigned char nY = 0;
  unsigned char nYToDelete = 0;
  unsigned char nYToDraw = 0;

  int nYNew = aPlayer[nPlayer].nY + aPlayer[nPlayer].nYDir;
  
  if(aPlayer[nPlayer].nYDir > 0)
  {
    if(nYNew + PLAYER_HEIGHT >= SCREEN_HEIGHT)
    {
      nYNew = SCREEN_HEIGHT - PLAYER_HEIGHT - 1;
      aPlayer[nPlayer].nYDir = -4;
    }

    aPlayer[nPlayer].nY = nYNew;    
    nYToDelete = aPlayer[nPlayer].nY - 4;
    nYToDraw = aPlayer[nPlayer].nY + PLAYER_HEIGHT - 4;
  }
  else
  {
    if(nYNew <= 0)
    {
      nYNew = 0;
      aPlayer[nPlayer].nYDir = 4;
    }
    
    aPlayer[nPlayer].nY = nYNew;
    nYToDelete = aPlayer[nPlayer].nY + PLAYER_HEIGHT;
    nYToDraw = aPlayer[nPlayer].nY;
  }
  
  for(nY = 0; nY < 4; nY++)
    LineHMode2Byte(aPlayer[nPlayer].nX, nYToDelete + nY, 0, PLAYER_WIDTH_BYTES);
  
  for(nY = 0; nY < 4; nY++)
    LineHMode2Byte(aPlayer[nPlayer].nX, nYToDraw + nY, 1, PLAYER_WIDTH_BYTES);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned int nX = 0;
  unsigned int nY = 0;
  
  memset(aPlayer, 0, sizeof(aPlayer));
  
  aPlayer[0].nY =  100 - PLAYER_HEIGHT / 2;
  aPlayer[1].nY =  100 - PLAYER_HEIGHT / 2;
  aPlayer[0].nX = 8;
  aPlayer[1].nX = (SCREEN_WIDTH - 8) - PLAYER_WIDTH;
  aPlayer[0].nYDir = 4;
  aPlayer[1].nYDir = -4;

  SetMode(2);
  SetBorder(26);
  SetColor(0, 0);
  SetColor(1, 26);
  
  DrawPlayer(0);
  DrawPlayer(1);
  
  
  nTimeLast = GetTime();

  while(1)
  {
    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(5, 1);
      printf("%u  ", nFPS);

      nTimeLast += 300;
      nFPS = 0;
    }

    WaitVsync();
    PutSprite((unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8), BALL_WIDTH_BYTES, BALL_HEIGHT, aBallSprite[8]);
    nX = (nX + 8) % (640 - BALL_WIDTH);
    nY = (nY + 4) % (200 - BALL_HEIGHT);
    PutSprite((unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8), BALL_WIDTH_BYTES, BALL_HEIGHT, aBallSprite[nX % 8]);

    MovePlayer(0);
    MovePlayer(1);
    
    nFPS++;
  }
}
////////////////////////////////////////////////////////////////////////

If you compile and run on the emulator get the following:

 

We lack scoring markers, as are just numbers from 0 to 9 I made the sprites by hand in text editor. As the ball moves finally 8 pixels each time, ie byte aligned, we do not need the 8 possible sprites of the ball, one is enough. I moved the sprites of the ball and scoring markers to a file .h for convenience:

////////////////////////////////////////////////////////////////////////
// Sprites.h
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////

#define BALL_WIDTH 16
#define BALL_HEIGHT 8

#define BALL_WIDTH_BYTES 2
const unsigned char aBallSprite[2][BALL_WIDTH_BYTES * BALL_HEIGHT] = 
{
                        { 0xFF, 0xFF,
                          0xFF, 0xFF,
                          0xFF, 0xFF,
                          0xFF, 0xFF,
                          0xFF, 0xFF,
                          0xFF, 0xFF,
                          0xFF, 0xFF,
                          0xFF, 0xFF,},
                          
                        { 0x00, 0x00,
                          0x00, 0x00,
                          0x00, 0x00,
                          0x00, 0x00,
                          0x00, 0x00,
                          0x00, 0x00,
                          0x00, 0x00,
                          0x00, 0x00},
};


#define NUMBER_HEIGHT 20
#define NUMBER_WIDTH_BYTES 4
#define NUMBER_WIDTH 32
const unsigned char aNumberSprite[10][NUMBER_HEIGHT * NUMBER_WIDTH_BYTES] = 
{
                        { //0
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                        },
                        
                        { //1
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                        },

                        { //2
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                        },                        

                        { //3
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                        },

                        { //4
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                        },

                        { //5
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                        },

                        { //6
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0x00, 0x00, 0x00,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                        },

                        { //7
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                        },
        
                        { //8
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                        },

                        { //9
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0x00, 0x00, 0x00, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                          0xFF, 0xFF, 0xFF, 0xFF,
                        },

};


////////////////////////////////////////////////////////////////////////

The modified program using scoring markers and showing what would be the entire game engine in motion is as follows:

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

#include "Sprites.h"

////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char 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;
}
////////////////////////////////////////////////////////////////////////


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


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//PutSprite()
////////////////////////////////////////////////////////////////////////
void PutSprite(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;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//WaitVsync
////////////////////////////////////////////////////////////////////////
void WaitVsync()
{
  __asm
    ld b,#0xf5          ;; PPI port B input
    _wait_vsync:
    in a,(c)            ;; [4] read PPI port B input
                        ;; (bit 0 = "1" if vsync is active,
                        ;;  or bit 0 = "0" if vsync is in-active)
    rra                 ;; [1] put bit 0 into carry flag
    jp nc,_wait_vsync   ;; [3] if carry not set, loop, otherwise continue
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//LineHMode2Byte
////////////////////////////////////////////////////////////////////////
void LineHMode2Byte(unsigned int nX, unsigned int nY, unsigned char nColor, unsigned char nBytes)
{
  unsigned char *pAddress = (unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8);
  
  if(nX % 8 == 0)
  {
    memset(pAddress, nColor ? 0xFF : 0x00, nBytes);
  }
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define NUM_PLAYERS 2
#define SCREEN_WIDTH 640
#define SCREEN_HEIGHT 200

#define SCORE_Y 20
#define SCORE_X_1 ((SCREEN_WIDTH / 2) - 50 - NUMBER_WIDTH / 2)
#define SCORE_X_2 ((SCREEN_WIDTH / 2) + 50 - NUMBER_WIDTH / 2)

#define PLAYER_WIDTH 16
#define PLAYER_HEIGHT 30
#define PLAYER_WIDTH_BYTES 2

struct _tPlayer
{
  unsigned char nY;
  unsigned int nX;
  char nYDir;
}_tPlayer;

struct _tPlayer aPlayer[NUM_PLAYERS];

////////////////////////////////////////////////////////////////////////
//DrawPlayer
////////////////////////////////////////////////////////////////////////
void DrawPlayer(unsigned char nPlayer)
{
  unsigned char nY = 0;
  for(nY = 0; nY < PLAYER_HEIGHT; nY++)
    LineHMode2Byte(aPlayer[nPlayer].nX, aPlayer[nPlayer].nY+ nY, 1, PLAYER_WIDTH_BYTES);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//MovePlayer
////////////////////////////////////////////////////////////////////////
void MovePlayer(unsigned char nPlayer)
{
  unsigned char nY = 0;
  unsigned char nYToDelete = 0;
  unsigned char nYToDraw = 0;

  int nYNew = aPlayer[nPlayer].nY + aPlayer[nPlayer].nYDir;
  
  if(aPlayer[nPlayer].nYDir > 0)
  {
    if(nYNew + PLAYER_HEIGHT >= SCREEN_HEIGHT)
    {
      nYNew = SCREEN_HEIGHT - PLAYER_HEIGHT - 1;
      aPlayer[nPlayer].nYDir = -4;
    }

    aPlayer[nPlayer].nY = nYNew;    
    nYToDelete = aPlayer[nPlayer].nY - 4;
    nYToDraw = aPlayer[nPlayer].nY + PLAYER_HEIGHT - 4;
  }
  else
  {
    if(nYNew <= 0)
    {
      nYNew = 0;
      aPlayer[nPlayer].nYDir = 4;
    }
    
    aPlayer[nPlayer].nY = nYNew;
    nYToDelete = aPlayer[nPlayer].nY + PLAYER_HEIGHT;
    nYToDraw = aPlayer[nPlayer].nY;
  }
  
  for(nY = 0; nY < 4; nY++)
    LineHMode2Byte(aPlayer[nPlayer].nX, nYToDelete + nY, 0, PLAYER_WIDTH_BYTES);
  
  for(nY = 0; nY < 4; nY++)
    LineHMode2Byte(aPlayer[nPlayer].nX, nYToDraw + nY, 1, PLAYER_WIDTH_BYTES);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned int nX = 0;
  unsigned int nY = 0;
  unsigned char *pScore[NUM_PLAYERS];
  unsigned int nCounter = 0;

  pScore[0] = (unsigned char *)0xC000 + ((SCORE_Y / 8) * 80) + ((SCORE_Y % 8) * 2048) + (SCORE_X_1 / 8);
  pScore[1] = (unsigned char *)0xC000 + ((SCORE_Y / 8) * 80) + ((SCORE_Y % 8) * 2048) + (SCORE_X_2 / 8);

  memset(aPlayer, 0, sizeof(aPlayer));
  aPlayer[0].nY =  100 - PLAYER_HEIGHT / 2;
  aPlayer[1].nY =  100 - PLAYER_HEIGHT / 2;
  aPlayer[0].nX = 8;
  aPlayer[1].nX = (SCREEN_WIDTH - 8) - PLAYER_WIDTH;
  aPlayer[0].nYDir = 4;
  aPlayer[1].nYDir = -4;

  SetMode(2);
  SetBorder(26);
  SetColor(0, 0);
  SetColor(1, 26);
  
  DrawPlayer(0);
  DrawPlayer(1);
  
  
  nTimeLast = GetTime();

  while(1)
  {
    if(GetTime() - nTimeLast >= 300)
    {
      SetCursor(5, 1);
      printf("%u  ", nFPS);

      nTimeLast += 300;
      nFPS = 0;
    }

    WaitVsync();
    PutSprite((unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8), BALL_WIDTH_BYTES, BALL_HEIGHT, aBallSprite[1]);
    nX = (nX + 8) % (640 - BALL_WIDTH);
    nY = (nY + 4) % (200 - BALL_HEIGHT);
    PutSprite((unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8), BALL_WIDTH_BYTES, BALL_HEIGHT, aBallSprite[0]);

    MovePlayer(0);
    MovePlayer(1);
    
    nCounter++;
    if(nCounter >= 1000)
      nCounter = 0;
    
    PutSprite(pScore[0], NUMBER_WIDTH_BYTES, NUMBER_HEIGHT, aNumberSprite[nCounter / 100]);
    PutSprite(pScore[1], NUMBER_WIDTH_BYTES, NUMBER_HEIGHT, aNumberSprite[nCounter / 100]);

    nFPS++;
  }
}
////////////////////////////////////////////////////////////////////////

If you compile and run on the emulator get the following:

 

Now just missing to implement the game logic and the menu, which because of its simplicity I will not comment, you can read the code directly. This is the final program completed:

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

#include "Sprites.h"

////////////////////////////////////////////////////////////////////////
//------------------------ Generic functions -------------------------//
////////////////////////////////////////////////////////////////////////
#define MIN(a,b) (((a)<(b))?(a):(b))
#define MAX(a,b) (((a)>(b))?(a):(b))

////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char 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;
}
////////////////////////////////////////////////////////////////////////


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


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetBorder
////////////////////////////////////////////////////////////////////////
void SetBorder(unsigned char nColor)
{
  __asm
    ld b, 4 (ix)
    ld c, b
    call #0xBC38 ;SCR_SET_BORDER
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//PutSprite()
////////////////////////////////////////////////////////////////////////
void PutSprite(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;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//WaitVsync
////////////////////////////////////////////////////////////////////////
void WaitVsync()
{
  __asm
    ld b,#0xf5          ;; PPI port B input
    _wait_vsync:
    in a,(c)            ;; [4] read PPI port B input
                        ;; (bit 0 = "1" if vsync is active,
                        ;;  or bit 0 = "0" if vsync is in-active)
    rra                 ;; [1] put bit 0 into carry flag
    jp nc,_wait_vsync   ;; [3] if carry not set, loop, otherwise continue
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//LineHMode2Byte
////////////////////////////////////////////////////////////////////////
void LineHMode2Byte(unsigned int nX, unsigned int nY, unsigned char nColor, unsigned char nBytes)
{
  memset((unsigned char *)(0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 8)), nColor ? 0xFF : 0x00, nBytes);
}
////////////////////////////////////////////////////////////////////////


//Enumeration to identify each physical key
typedef enum _eKey
{
  Key_CursorUp = 0,
  Key_CursorRight,
  Key_CursorDown,
  Key_F9,
  Key_F6,
  Key_F3,
  Key_Enter,
  Key_FDot,
  Key_CursorLeft, //8
  Key_Copy,
  Key_F7,
  Key_F8,
  Key_F5,
  Key_F1,
  Key_F2,
  Key_F0,
  Key_Clr, //16
  Key_BraceOpen,
  Key_Return,
  Key_BraceClose,
  Key_F4,
  Key_Shift,
  Key_BackSlash,
  Key_Control,
  Key_Caret, //24
  Key_Hyphen,
  Key_At,
  Key_P,
  Key_SemiColon,
  Key_Colon,
  Key_Slash,
  Key_Dot,
  Key_0, //32
  Key_9,
  Key_O,
  Key_I,
  Key_L,
  Key_K,
  Key_M,
  Key_Comma,
  Key_8, //40
  Key_7,
  Key_U,
  Key_Y,
  Key_H,
  Key_J,
  Key_N,
  Key_Space,
  Key_6_Joy2Up, //48
  Key_5_Joy2Down,
  Key_R_Joy2Left,
  Key_T_Joy2Right,
  Key_G_Joy2Fire,
  Key_F,
  Key_B,
  Key_V,
  Key_4, //56
  Key_3,
  Key_E,
  Key_W,
  Key_S,
  Key_D,
  Key_C,
  Key_X,
  Key_1, //64
  Key_2,
  Key_Esc,
  Key_Q,
  Key_Tab,
  Key_A,
  Key_CapsLock,
  Key_Z,
  Key_Joy1Up, //72
  Key_Joy1Down,
  Key_Joy1Left,
  Key_Joy1Right,
  Key_Joy1Fire1,
  Key_Joy1Fire2,
  Key_Joy1Fire3,
  Key_Del,
  Key_Max //80
}_ekey;


////////////////////////////////////////////////////////////////////////
//IsKeyPressedFW
////////////////////////////////////////////////////////////////////////
char nKeyPressed;
unsigned char IsKeyPressedFW(unsigned char eKey)
{
  __asm
    LD HL, #_nKeyPressed
    LD (HL), #0
    LD A, 4 (IX)
    CALL #0xBB1E ;KM TEST KEY
    JP Z, _end_IsKeyPressed
    LD HL, #_nKeyPressed
    LD (HL), #1
    _end_IsKeyPressed:
  __endasm;
  
  return nKeyPressed;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//---------------- Specific variables and functions-------------------//
////////////////////////////////////////////////////////////////////////

#define NUM_PLAYERS 2
#define SCREEN_WIDTH 640
#define SCREEN_HEIGHT 200

#define SCORE_Y 20
#define SCORE_X_1 ((SCREEN_WIDTH / 2) - 50 - NUMBER_WIDTH / 2)
#define SCORE_X_2 ((SCREEN_WIDTH / 2) + 50 - NUMBER_WIDTH / 2)

#define PLAYER_WIDTH 16
#define PLAYER_HEIGHT 30
#define PLAYER_WIDTH_BYTES 2
#define PLAYER_Y_INC 4

struct _tPlayer
{
  unsigned char nY;
  unsigned int nX;
  _ekey ekeyUp;
  _ekey ekeyDown;
  unsigned char nScore;
  char nLastYMove;
}_tPlayer;

struct _tPlayer aPlayer[NUM_PLAYERS];

struct _tBall
{
  int nY;
  int nX;
  char nYDir;
  char nXDir;
}_tBall;

struct _tBall tBall;

unsigned char *pScoreScreen[NUM_PLAYERS];


////////////////////////////////////////////////////////////////////////
//DrawPlayer
////////////////////////////////////////////////////////////////////////
void DrawPlayer(unsigned char nPlayer)
{
  unsigned char nY = 0;
  for(nY = 0; nY < PLAYER_HEIGHT; nY++)
    LineHMode2Byte(aPlayer[nPlayer].nX, aPlayer[nPlayer].nY+ nY, 1, PLAYER_WIDTH_BYTES);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//MovePlayer
////////////////////////////////////////////////////////////////////////
void MovePlayer(unsigned char nPlayer)
{
  unsigned char nY = 0;
  unsigned char nYToDelete = 0;
  unsigned char nYToDraw = 0;
  int nYNew = 0;

  unsigned char bKeyUp = IsKeyPressedFW(aPlayer[nPlayer].ekeyUp);
  unsigned char bKeyDown = IsKeyPressedFW(aPlayer[nPlayer].ekeyDown);

  if(bKeyUp == bKeyDown)
  {
    aPlayer[nPlayer].nLastYMove = 0;
    return;
  }

  nYNew = bKeyUp ? aPlayer[nPlayer].nY - PLAYER_Y_INC : aPlayer[nPlayer].nY + PLAYER_Y_INC;
  
  if(bKeyDown)
  {
    if(nYNew + PLAYER_HEIGHT > SCREEN_HEIGHT)
      nYNew = SCREEN_HEIGHT - PLAYER_HEIGHT;

    aPlayer[nPlayer].nY = nYNew;    
    nYToDelete = aPlayer[nPlayer].nY - PLAYER_Y_INC;
    nYToDraw = aPlayer[nPlayer].nY + PLAYER_HEIGHT - PLAYER_Y_INC;
    aPlayer[nPlayer].nLastYMove = 1;
  }
  else
  {
    if(nYNew <= 0)
      nYNew = 0;
    
    aPlayer[nPlayer].nY = nYNew;
    nYToDelete = aPlayer[nPlayer].nY + PLAYER_HEIGHT;
    nYToDraw = aPlayer[nPlayer].nY;
    aPlayer[nPlayer].nLastYMove = -1;
  }
  
  for(nY = 0; nY < PLAYER_Y_INC; nY++)
    LineHMode2Byte(aPlayer[nPlayer].nX, nYToDelete + nY, 0, PLAYER_WIDTH_BYTES);
  
  for(nY = 0; nY < PLAYER_Y_INC; nY++)
    LineHMode2Byte(aPlayer[nPlayer].nX, nYToDraw + nY, 1, PLAYER_WIDTH_BYTES);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//MoveBall
////////////////////////////////////////////////////////////////////////
char MoveBall()
{
  char nReturn = -1;
  char bCollision = -1;

  //Delete ball
  PutSprite((unsigned char *)0xC000 + ((tBall.nY / 8) * 80) + ((tBall.nY % 8) * 2048) + (tBall.nX / 8), 
     BALL_WIDTH_BYTES, BALL_HEIGHT, aBallSprite[1]);
  
  tBall.nY = tBall.nY + tBall.nYDir;
  
  if(tBall.nYDir > 0 && tBall.nY >= (SCREEN_HEIGHT - BALL_HEIGHT))
  {
    tBall.nYDir *= -1;
    tBall.nY = (SCREEN_HEIGHT - BALL_HEIGHT);
  }
  
  if(tBall.nYDir < 0 && tBall.nY <= 0)
  {
    tBall.nYDir *= -1;
    tBall.nY = 0;
  }
  
  tBall.nX = tBall.nX + tBall.nXDir;

  if(tBall.nXDir < 0) //ball moving to left
  {
    if(tBall.nX == aPlayer[0].nX + PLAYER_WIDTH)
    {
      if((tBall.nY > aPlayer[0].nY && tBall.nY < aPlayer[0].nY + PLAYER_HEIGHT) ||
         (tBall.nY + BALL_HEIGHT > aPlayer[0].nY && tBall.nY + BALL_HEIGHT < aPlayer[0].nY + PLAYER_HEIGHT))
        {
          bCollision = 0;
        }
    }

    if(tBall.nX < 0)
    {
      tBall.nX = 0;
      nReturn = 1;
    }
  }
  else //ball moving to right
  {
    if(tBall.nX + BALL_WIDTH == aPlayer[1].nX)
    {
      if((tBall.nY > aPlayer[1].nY && tBall.nY < aPlayer[1].nY + PLAYER_HEIGHT) ||
         (tBall.nY + BALL_HEIGHT > aPlayer[1].nY && tBall.nY + BALL_HEIGHT < aPlayer[1].nY + PLAYER_HEIGHT))
        {
          bCollision = 1;
        }
    }
    
    if(tBall.nX >= SCREEN_WIDTH - BALL_WIDTH)
    {
      tBall.nX = SCREEN_WIDTH - BALL_WIDTH;
      nReturn = 0;
    }
  }

  if(bCollision != -1)
  {
    tBall.nXDir *= -1;
    
    if(aPlayer[bCollision].nLastYMove != 0)
    {
      tBall.nYDir = aPlayer[bCollision].nLastYMove > 0 ? tBall.nYDir - 1 : tBall.nYDir + 1;

      if(tBall.nYDir > 0)
      {
        tBall.nYDir = MIN(tBall.nYDir , 4);
        tBall.nYDir = MAX(tBall.nYDir , 1);
      }
      else
      {
        tBall.nYDir = MIN(tBall.nYDir , -1);
        tBall.nYDir = MAX(tBall.nYDir , -4);
      }
    }
  }

  //Draw ball on new position
  PutSprite((unsigned char *)0xC000 + ((tBall.nY / 8) * 80) + ((tBall.nY % 8) * 2048) + (tBall.nX / 8), 
     BALL_WIDTH_BYTES, BALL_HEIGHT, aBallSprite[0]);
  return nReturn;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//ShowMenu
////////////////////////////////////////////////////////////////////////
void ShowMenu()
{
  SetMode(1);

  SetCursor(17, 5);
  printf("PongC");

  SetCursor(1, 8);
  printf("Use 'qa' and 'pl' to move");

  SetCursor(8, 16);
  printf("Press Space to start game");

  SetCursor(3, 24);
  printf("Mochilote - www.cpcmania.com - 2013");

  while(!IsKeyPressedFW(Key_Space)) {}
}

////////////////////////////////////////////////////////////////////////
//ShowMenuWinner
////////////////////////////////////////////////////////////////////////
void ShowMenuWinner(unsigned char nPlayer)
{
  SetMode(1);

  SetCursor(17, 5);
  printf("PongC");

  SetCursor(10, 8);
  printf("Player %d (%s) won", nPlayer + 1, nPlayer == 0 ? "left" : "right");

  SetCursor(8, 16);
  printf("Press Space to Exit");

  while(!IsKeyPressedFW(Key_Space)) {}
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//InitGame
////////////////////////////////////////////////////////////////////////
void InitGame(char nLastWinner)
{
  unsigned char bLeft = 0;
  
  if(nLastWinner == -1)
    memset(aPlayer, 0, sizeof(aPlayer));

  aPlayer[0].nY =  100 - PLAYER_HEIGHT / 2;
  aPlayer[1].nY =  100 - PLAYER_HEIGHT / 2;
  aPlayer[0].nX = 8;
  aPlayer[1].nX = (SCREEN_WIDTH - 8) - PLAYER_WIDTH;
  aPlayer[0].ekeyUp = Key_Q;
  aPlayer[1].ekeyUp = Key_P;
  aPlayer[0].ekeyDown = Key_A;
  aPlayer[1].ekeyDown = Key_L;

  srand(GetTime());
  bLeft = (nLastWinner == -1) ? (rand() % 2) : (nLastWinner == 0);

  memset(&tBall, 0, sizeof(tBall));
  tBall.nX = bLeft ? aPlayer[1].nX - 24 : aPlayer[0].nX + 24;
  tBall.nY = SCREEN_HEIGHT / 2;
  tBall.nYDir = 2;
  tBall.nXDir = bLeft ? -8 : 8;

  SetMode(2);
  SetBorder(26);
  SetColor(0, 0);
  SetColor(1, 26);
  
  DrawPlayer(0);
  DrawPlayer(1);
  
  pScoreScreen[0] = (unsigned char *)0xC000 + ((SCORE_Y / 8) * 80) + ((SCORE_Y % 8) * 2048) + (SCORE_X_1 / 8);
  pScoreScreen[1] = (unsigned char *)0xC000 + ((SCORE_Y / 8) * 80) + ((SCORE_Y % 8) * 2048) + (SCORE_X_2 / 8);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//Game
////////////////////////////////////////////////////////////////////////
void Game()
{
  char nWinner = -1;

  InitGame(-1);

  while(1)
  {
    WaitVsync();
    
    nWinner = MoveBall();

    MovePlayer(0);
    MovePlayer(1);

    PutSprite(pScoreScreen[0], NUMBER_WIDTH_BYTES, NUMBER_HEIGHT, aNumberSprite[aPlayer[0].nScore]);
    PutSprite(pScoreScreen[1], NUMBER_WIDTH_BYTES, NUMBER_HEIGHT, aNumberSprite[aPlayer[1].nScore]);

    if(nWinner != -1)
    {
      InitGame(nWinner);
      aPlayer[nWinner].nScore++;
      
      if(aPlayer[nWinner].nScore >= 10)
      {
        ShowMenuWinner(nWinner);
        return;
      }
      
      nWinner = -1;
    }
  }
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  while(1)
  {
    ShowMenu();
    Game();
  }
}
////////////////////////////////////////////////////////////////////////

If you compile and run on the emulator get the following, I've played with myself with both hands (mode forever alone) to record this video:

 

Finally we have achieved a clone of Pong quite nice and playable, but especially educational, that's what were trying to get in this tutorials.

You could download a zip with all files (source code, bat to compile, binary and dsk's) here: PongC.zip

 

www.CPCMania.com 2013