Getting and Putting Sprites

Table of Contents:
The how and why...
Capturing a sprite
An assembler get...
Setting a sprite
Example program


How and why...

A sprite is an array of data that holds a rectangular picture. This picture can be sent to the screen, rapidly, to display a bitmap. Common uses for it are in a menuing system, before the menu is drawn on the screen, a copy of the screen underneath the menu is saved, so that it can be restored without redrawing the entire screen.

There are many different kinds of sprites, but the kind we're going to tackle here are just a rectangular array, one byte for every pixel. The size of the array is simply XSize*YSize, where X and YSize are simply the size of the sprite.

The need to get and to set sprites is in the heart of animation. Lets say that you're programming a pinball game. The ball would be a sprite, along with the flippers and the obstacles on the table. This table would be "painted" with a theme corresponds to the whole machines theme.

When the ball moves, you would calculate the new position, and draw the sprite there. The next frame you do the same. But what happens to the cool table underneath the sprite? It is obscured and destroyed. To fix this, we change the loop to this:

  • Draw over the sprite with the stored background.
  • Calculate the new position of the ball.
  • Capture (Get) the table underneath the new ball position.
  • Draw the ball in the new position.
  • Go back to the first step.
For this to work, we need facilities to both set AND get sprites... Fast ones if we have anything to say about it!


Capturing a sprite

To get a sprite, we need to capture an area of the screen and store it in memory as the sprite. For this task, we'll use the mighty GetPixel!! Here it is:

PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER);
VAR
  X, Y : INTEGER;
BEGIN
  CreateSprite(Sprite, X2-X1+1, Y2-Y1+1);
  FOR Y := Y1 TO Y2 DO
    FOR X := X1 TO X2 DO
      Sprite.Data^[Y*Sprite.Width+X] := GetPixel(X, Y);
END;
This is a reasonably fast Get routine, but it could be made faster by not calculating the index into the sprite data array each time. Since we access sequential index elements, we can simply increment it as we go...

PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER);
VAR
  Index, X, Y : INTEGER;
BEGIN
  CreateSprite(Sprite, X2-X1+1, Y2-Y1+1);
  Index := 0;
  FOR Y := Y1 TO Y2 DO
    FOR X := X1 TO X2 DO
    BEGIN
      Sprite.Data^[Index] := GetPixel(X, Y);
      INC(Index);
    END;
END;
This is as fast as we can go in plain Pascal... It's time to switch gears into assembler!


An assembler Get...

The first step to converting this is to change the inner loop... Therefore:

PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER);
VAR
  SprIndex, X, Y : INTEGER;
  ScrIndex       : WORD;
BEGIN
  CreateSprite(Sprite, X2-X1+1, Y2-Y1+1);
  SprIndex := 0;
  FOR Y := Y1 TO Y2 DO
  BEGIN
    ScrIndex := Y*Screen.Width+X1;
    FOR X := X1 TO X2 DO
    ASM
      push DS
      les DI, Sprite             { VAR parameters are actually passed as    }
      les DI, ES:[DI+SpriteType.Data] { pointers to the variable...         }
      add DI, SprIndex
      lds SI, Screen.Buffer
      add SI, ScrIndex
      movsb                      { Sprite.Data^[SprIndex] := GetPixel(X, Y) }
      pop DS
      inc ScrIndex
      inc SprIndex               { INC(SprIndex) }
    END;
  END;
END;
Obviously, we can eliminate a number of things from this loop if we include the FOR X loop in the assembler block. Many segment register loads and memory references can be removed... Here we go!

PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER);
VAR
  SprIndex, Y : INTEGER;
  ScrIndex    : WORD;
BEGIN
  CreateSprite(Sprite, X2-X1+1, Y2-Y1+1);
  SprIndex := 0;
  ScrIndex := Y1*Screen.Width+X1;
  FOR Y := Y1 TO Y2 DO
  ASM
    mov CX, X2
    sub CX, X1
    inc CX                     { Calculate DeltaX -> X2-X1+1              }
    push DS
    les DI, Sprite             { VAR parameters are actually passed as    }
    les DI, ES:[DI+SpriteType.Data] { pointers to the variable...         }
    add DI, SprIndex
    lds SI, Screen.Buffer
    add SI, ScrIndex
    rep movsb
    mov SprIndex, DI          { Auto-incremented by the movsb instruction }
    pop DS
    mov AX, Screen.Width
    add ScrIndex, AX
  END;
END;
That very inner loop can easily converted to a rep movsb instruction... Cool. Now we move in one level and optimize the YLoop...

PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER); ASSEMBLER;
VAR ScrIndex : WORD;
ASM
  mov DX, X2
  sub DX, X1                 { Calculate DeltaX -> X2-X1+1              }
  inc DX
  mov BX, Y2
  sub BX, Y1                 { Calculate DeltaY -> Y2-Y1+1              }
  inc BX
  push DX                    { Save a copy for later.                   }
  push BX
  db $66; mov AX, WORD [Sprite]
  db $66; push AX
  push DX
  push BX
  call CreateSprite          { CreateSprite(Sprite, X2-X1+1, Y2-Y1+1)   }

  mov AX, X1
  mov BX, Y1              { Y1 is an index into the ScreenY array       }
  add BX, BX              { Multipy by two because each entry is a word }
  add AX, DS:[BX+OFFSET Screen.YTable]
  mov ScrIndex, AX           { ScrIndex := Y1*Screen.Width+X1           }

  pop BX                     { Restore YDelta }
  pop DX                     { Restore XDelta }
  mov AX, Screen.Width       { Can't get this with DS mangled...        }
  sub AX, DX                 { Calculate Screen.Width-Sprite.Width      }
  push DS
  les DI, Sprite             { VAR parameters are actually passed as    }
  les DI, ES:[DI+SpriteType.Data] { pointers to the variable...         }
  lds SI, Screen.Buffer
  add ScrIndex, SI
@YLoop:                      { FOR Y := Y1 TO Y2 DO                     }
  mov CX, DX                 { Copy the correct number of pixels!       }
  rep movsb                  { Do one scan line.                        }
  add SI, AX                 { Increment the ScrIndex to the next line  }
  dec BX
  jnz @YLoop                 { Next YLoop...                            }
  pop DS
END;
That's it. Notice that there is a complete lack of multiply instructions... This routine is fast, although the call to CreateSprite makes it not as fast as DrawSprite...


Setting a sprite

Okay, now we can transfer a block of data from the screen to memory... What good does that do us? None, unless we have a routine that can transfer it back! This DrawSprite routine needs to be fast and furious, and it has to be easy to use.

Here is the Pascal only version. I've already converted it to a incrementally index version...

PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
  Index, XI, YI : INTEGER;
BEGIN
  Index := 0;
  FOR YI := 0 TO Sprite.Height-1 DO
    FOR XI := 0 TO Sprite.Width-1 DO
    BEGIN
      SetPixel(XI+X, YI+Y, Sprite.Data^[Index]);
      INC(Index);
    END;
END;
As you can see, the only differance between GetSprite and DrawSprite is the one line that calls SetPixel. To optimize the inner loop, we would to the following:

PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
  ScrIndex : WORD;
  SprIndex, XI, YI : INTEGER;
BEGIN
  SprIndex := 0;
  FOR YI := 0 TO Sprite.Height-1 DO
  BEGIN
    ScrIndex := (Y+YI)*Screen.Width+X;
    FOR XI := 0 TO Sprite.Width-1 DO
    ASM
      push DS
      les DI, Screen.Buffer
      add DI, ScrIndex
      lds SI, Sprite
      lds SI, DS:[SI+SpriteType.Data]
      add SI, SprIndex
      movsb                    { SetPixel(XI+X, YI+Y, Sprite.Data^[SprIndex] }
      pop DS
      inc ScrIndex
      inc SprIndex             { INC(SprIndex) }
    END;
  END;
END;
Does this look remarkably familiar? It's very like the first optimization of GetSprite! Hmmm... Here's the next loop optimized...

PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
  ScrIndex : WORD;
  SprIndex, YI : INTEGER;
BEGIN
  SprIndex := 0;
  ScrIndex := Y*Screen.Width+X;
  FOR YI := 0 TO Sprite.Height-1 DO
  ASM
    push DS
    les DI, Screen.Buffer
    add DI, ScrIndex
    lds SI, Sprite
    mov CX, DS:[SI+SpriteType.Width]
    lds SI, DS:[SI+SpriteType.Data]
    add SI, SprIndex
  @XLoop:
    movsb
    dec CX
    jnz @XLoop
    pop DS

    mov SprIndex, SI           { INC(SprIndex) }
    mov AX, Screen.Width
    add ScrIndex, AX
  END;
END;
Now when we convert it all to assembler, we see the true power of assembler. Doing it yourself. When Turbo Pascal compiled to original code, it required a pointer load for every pixel. It also required that there was a call to SetPixel for every pixel... This was sloooowww... To see the improvement, follow the suggestion in the example.

PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER); ASSEMBLER;
ASM
  mov AX, X
  mov BX, Y               { Y1 is an index into the ScreenY array       }
  add BX, BX              { Multipy by two because each entry is a word }
  add AX, DS:[BX+OFFSET Screen.YTable]
  mov BX, AX              { ScrIndex := Y*Screen.Width+X                }

  mov AX, Screen.Width       { Save this for later when we can't get to DS }
  push DS
  les DI, Screen.Buffer
  add DI, BX                 { Add DI, ScrIndex...                      }
  lds SI, Sprite
  mov DX, DS:[SI+SpriteType.Width]  { Grab XDelta }
  mov BX, DS:[SI+SpriteType.Height] { Grab YDelta }
  sub AX, DX                 { Calculate Screen.Width-Sprite.Width      }
  lds SI, DS:[SI+SpriteType.Data]
@YLoop:                      { FOR YI := 0 TO Sprite.Height-1 DO }
  mov CX, DX                 { Copy the correct number of pixels!       }
  rep movsb                  { Do one scan line.                        }
  add DI, AX                 { Inc ScrIndex to the next scanline        }
  dec BX
  jnz @YLoop                 { Next YLoop...                            }
  pop DS
END;
Both of these examples are very efficient, and they are compact. They also use six of the eight general purpose registers... They don't touch BP and SP. These routines are the basis for the sprite series, so it's a good thing they go fast! :)


Example Program

This example program demostrates the basics of handling sprites... If you want to see the differance that optimization makes, just set SlowSprite to true. It then uses the Pascal version of the sprite routines instead of the assembler one... It's a huge differance!

{{$DEFINE SlowSprite   }   { Just remove a curly bracket to enable...       }
{{$DEFINE WaitRetrace  }   { Remove this to see it nice and slow...         }

USES
  CRT,        { KeyPressed routine    }
  GraphPro;   { GetPixel and SetPixel }
TYPE
  ByteArrayType = ARRAY[0..65534] OF BYTE;
  ByteArrayPtr = ^ByteArrayType;
  SpriteType = RECORD            { Each record is convieniently 8 bytes long }
    Height, Width : INTEGER;     { Height and Width of sprite }
    Data          : ByteArrayPtr;{ Pointer to the color data  }
  END;

PROCEDURE KillSprite(VAR Sprite : SpriteType);
BEGIN
  WITH Sprite DO
  BEGIN
    FREEMEM(Data, Width*Height);
    Width := 0; Height := 0; Data := NIL;
  END;
END;

PROCEDURE CreateSprite(VAR Sprite : SpriteType; Width, Height : INTEGER);
BEGIN
  IF Sprite.Data <> NIL THEN KillSprite(Sprite);
  Sprite.Width := Width;
  Sprite.Height := Height;
  GETMEM(Sprite.Data, Width*Height);
END;

{$IFDEF SlowSprite}        { Use the Pascal version.... }
PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER);
VAR
  Index, X, Y : INTEGER;
BEGIN
  CreateSprite(Sprite, X2-X1+1, Y2-Y1+1);
  Index := 0;
  FOR Y := Y1 TO Y2 DO
    FOR X := X1 TO X2 DO
    BEGIN
      Sprite.Data^[Index] := GetPixel(X, Y);
      INC(Index);
    END;
END;

PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
  Index, XI, YI : INTEGER;
BEGIN
  Index := 0;
  FOR YI := 0 TO Sprite.Height-1 DO
    FOR XI := 0 TO Sprite.Width-1 DO
    BEGIN
      SetPixel(XI+X, YI+Y, Sprite.Data^[Index]);
      INC(Index);
    END;
END;

{$ELSE}                    { Use the Assembler version... }
PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER); ASSEMBLER;
VAR ScrIndex : INTEGER;
ASM
  mov DX, X2
  sub DX, X1                 { Calculate DeltaX -> X2-X1+1              }
  inc DX
  mov BX, Y2
  sub BX, Y1                 { Calculate DeltaY -> Y2-Y1+1              }
  inc BX
  push BX                    { Save a copy for later.                   }
  push DX
  db $66; mov AX, WORD [Sprite]
  db $66; push AX
  push DX
  push BX
  call CreateSprite          { CreateSprite(Sprite, X2-X1+1, Y2-Y1+1)   }

  mov AX, X1
  mov BX, Y1              { Y1 is an index into the ScreenY array       }
  add BX, BX              { Multipy by two because each entry is a word }
  add AX, DS:[BX+OFFSET Screen.YTable]
  mov ScrIndex, AX           { ScrIndex := Y1*Screen.Width+X1           }

  pop DX                     { Restore XDelta }
  pop BX                     { Restore YDelta }

  mov AX, Screen.Width       { Can't get this with DS mangled...        }
  push DS
  les DI, Sprite             { VAR parameters are actually passed as    }
  les DI, ES:[DI+SpriteType.Data] { pointers to the variable...         }
  lds SI, Screen.Buffer
  add SI, ScrIndex
  sub AX, DX                 { Calculate Screen.Width-Sprite.Width      }
@YLoop:                      { FOR Y := Y1 TO Y2 DO                     }
  mov CX, DX                 { Copy the correct number of pixels!       }
  rep movsb                  { Do one scan line.                        }
  add SI, AX                 { Increment the ScrIndex to the next line  }
  dec BX
  jnz @YLoop                 { Next YLoop...                            }
  pop DS
END;

PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER); ASSEMBLER;
ASM
  mov AX, X
  mov BX, Y               { Y1 is an index into the ScreenY array       }
  add BX, BX              { Multipy by two because each entry is a word }
  add AX, DS:[BX+OFFSET Screen.YTable]
  mov BX, AX              { ScrIndex := Y*Screen.Width+X                }

  mov AX, Screen.Width       { Save this for later when we can't get to DS }
  push DS
  les DI, Screen.Buffer
  add DI, BX                 { Add DI, ScrIndex...                      }
  lds SI, Sprite
  mov DX, DS:[SI+SpriteType.Width]  { Grab XDelta }
  mov BX, DS:[SI+SpriteType.Height] { Grab YDelta }
  sub AX, DX                 { Calculate Screen.Width-Sprite.Width      }
  lds SI, DS:[SI+SpriteType.Data]
@YLoop:                      { FOR YI := 0 TO Sprite.Height-1 DO }
  mov CX, DX                 { Copy the correct number of pixels!       }
  rep movsb                  { Do one scan line.                        }
  add DI, AX                 { Inc ScrIndex to the next scanline        }
  dec BX
  jnz @YLoop                 { Next YLoop...                            }
  pop DS
END;
{$ENDIF}

VAR
  BackgroundSprite,
  TestSprite : SpriteType;
  XD, YD,                   { X and Y Directions }
  OX, OY,                   { Old X and Y's      }
  I, X, Y    : INTEGER;
BEGIN
  WRITE('This is a test of the sprite system... The current mode is: ');
  {$IFDEF SlowSprite} WRITELN('Pascal Routines');
  {$ELSE            } WRITELN('Assembler Routines'); {$ENDIF}
  WRITELN;
  WRITELN('The sections are in the following order: ');
  WRITELN;
  WRITELN('1. Large Sprite moving across the screen.');
  WRITELN('2. Sprites that don''t clean up after themselves.');
  WRITELN('3. Sprites that DO clean up....');
  WRITELN;
  WRITELN('Press  after each section.');
  READLN;
  InitGraph;
  Line(31, 16, 31, 47, 9);
  Line(16, 31, 47, 31, 10);
  Line(0, 0, 63, 63, 15);
  Line(63, 0, 0, 63, 12);               { Draw a pattern for the sprite }
  GetSprite(TestSprite, 0, 0, 63, 63);

  FOR I := 0 TO 319-64 DO
    DrawSprite(TestSprite, I, 0);
  KillSprite(TestSprite);
  READLN;

  FOR I := 0 TO 30 DO
    Line(I, 0, I, 30, 7);
  Line( 0,  0, 30,  0, 15);
  Line( 0,  0,  0, 30, 15);
  Line(30,  1, 30, 30,  8);
  Line( 1, 30, 30, 30,  8);
  GetSprite(TestSprite, 0, 0, 30, 30);

  FOR I := 0 TO 10000 DO             { Generate a quick background }
    Line(RANDOM(320), RANDOM(200), RANDOM(320), RANDOM(200), RANDOM(256));

  X  := 0; Y  := 0;
  XD := 1; YD := 1;
  WHILE NOT KeyPressed DO
  BEGIN
    DrawSprite(TestSprite, X, Y);
    INC(X, XD); INC(Y, YD);  { Update position }
    IF (X = 319-30) OR (X = 0) THEN XD := -XD; { Bounce }
    IF (Y = 199-30) OR (Y = 0) THEN YD := -YD; { Bounce }
    {$IFDEF WaitRetrace}
    WHILE (Port[$3DA] AND 8) = 0 DO; { Syncronize with the vertical retrace }
    WHILE (Port[$3DA] AND 8) = 8 DO;
    WHILE (Port[$3DA] AND 8) = 0 DO;
    {$ENDIF}
  END;
  ReadLn;

  FOR I := 0 TO 10000 DO             { Generate a quick background }
    Line(RANDOM(320), RANDOM(200), RANDOM(320), RANDOM(200), RANDOM(256));

  X  := 0; Y  := 0;
  XD := 1; YD := 1;
  WHILE NOT KeyPressed DO
  BEGIN
    GetSprite(BackgroundSprite, X, Y, X+30, Y+30);  { Save background }
    DrawSprite(TestSprite, X, Y);                    { Draw Sprite     }
    OX := X; OY := Y;
    INC(X, XD); INC(Y, YD);  { Update position }
    IF (X = 319-30) OR (X = 0) THEN XD := -XD; { Bounce }
    IF (Y = 199-30) OR (Y = 0) THEN YD := -YD; { Bounce }
    {$IFDEF WaitRetrace}
    WHILE (Port[$3DA] AND 8) = 0 DO; { Syncronize with the vertical retrace }
    WHILE (Port[$3DA] AND 8) = 8 DO;
    WHILE (Port[$3DA] AND 8) = 0 DO;
    {$ENDIF}
    DrawSprite(BackgroundSprite, OX, OY);            { Restore background }
  END;
  DrawSprite(TestSprite, X, Y);
  ReadLn;
  CloseGraph;
  WRITELN('Have fun! See ya!');
END.

  • Created by Chris Lattner