Sprite Manipulations

Table of Contents:
Assumptions
Saving a sprite to disk
Loading a sprite off a disk
Copying a sprite in memory
Example program


Assumptions

Yet another installation of the graphics programming series... What'll we cover today? This short article covers some of the few fundemental things that are missing from our sprite unit. That is: Why bother drawing a sprite every time a program runs? Why not just load it from disk? Also needed is a function to save a sprite to disk (Useful for an editor) and a outine to copy a sprite in memory. All of these functions work on any of the sprite types, so you can compile a sprite and store it to the disk, for example, so that you don't have to wait for them to compile every time.


Saving a sprite to disk

We need some disk functions to save and load sprites to and from the disk! How else are we going to create the editor of our dreams? How many games do you think draws each sprite in memory before it uses it? None!

All we have to do is save and load everything in the sprite structure to disk. Here it is again:

  SpriteType = RECORD
    SType         : BYTE;        { The type of sprite         }
    Height, Width : INTEGER;     { Height and Width of sprite }
    Data          : ^ByteArray;  { Pointer to the color data  }
    DataLen       : WORD;        { Length of Data             }
  END;
The format that I propose looks something like the following:

Header
Data

That's simple enough! We just put the SpriteType record into the header, and the data block into the data section and we're done! I've added a little piece of code that adds an extension onto a filename if the one passed in doesn't have one, but other than that, it's pretty simple:

PROCEDURE SaveSprite(Sprite : SpriteType; Filename : STRING);
VAR
  F : FILE;                                   { The file             }
  Header : ARRAY[0..3] OF WORD;               { The file header      }
BEGIN
  IF Pos('.', Filename) = 0 THEN              { Add a default        }
    Filename := Filename + '.SPR';            { extension to filename}

  ASSIGN(F, Filename);                        { Set the filename     }
  REWRITE(F, 1);                              { Open the file        }
  IF IOResult <> 0 THEN EXIT;
  Header[0] := Sprite.SType;                  { Copy the variables   }
  Header[1] := Sprite.Height;                 { into the header      }
  Header[2] := Sprite.Width;
  Header[3] := Sprite.DataLen;
  BlockWrite(F, Header[0], 8);                { Write header to file }
  BlockWrite(F, Sprite.Data^, Sprite.DataLen);{ Write data portion   }
                                              { to a file            }
  CLOSE(F);                                   { Close file           }
END;
You simply call this function like this:
  SaveSprite(TestSprite, 'Test');
and a file named "Test.SPR" will be created. If you want a file without an extension, then just do this:
  SaveSprite(TestSprite, 'Test.');  { Notice the extra period }
And a file will be created named "Test".


Loading a sprite off a disk

Loading a sprite is very much like saving it, except, of course, the process is reversed. We also have to take a special precausion when overwriting a sprite in memory. Other than that, it looks very much the same as the saving routine does!

PROCEDURE LoadSprite(VAR Sprite : SpriteType; Filename : STRING);
VAR
  F : FILE;                                   { The file             }
  Header : ARRAY[0..3] OF WORD;               { The file header      }
BEGIN
  IF Sprite.Data <> NIL THEN                  { Replace an existing  }
    KillSprite(Sprite);                       { Sprite               }

  IF Pos('.', Filename) = 0 THEN              { Add a default        }
    Filename := Filename + '.SPR';            { extension to filename}

  ASSIGN(F, Filename);                        { Set the filename     }
  RESET(F, 1);                                { Open the file        }
  IF IOResult <> 0 THEN EXIT;
  BlockRead(F, Header[0], 8);                 { Write header to file }
  Sprite.SType   := Header[0];                { Copy the variables   }
  Sprite.Height  := Header[1];                { from the header      }
  Sprite.Width   := Header[2];
  Sprite.DataLen := Header[3];
  GetMem(Sprite.Data, Sprite.DataLen);        { Allocate memory      }
  BlockRead(F, Sprite.Data^, Sprite.DataLen); { Read data portion    }
                                              { from a file          }
  CLOSE(F);                                   { Close file           }
END;


Copying a sprite in memory

Wouldn't it be nice if to copy a sprite all we had to do was this?:
  SpriteDest := SpriteSource;
Unfortunately, this doesn't work. It will appear to look fine when it is used, but the data pointer in the sprite header just gets copied... This means that two sprites point to the same data block. Instead, what we want is a small routine that copies the data section as well as the header. Something like this:

PROCEDURE CopySprite(Source : SpriteType; VAR Dest : SpriteType);
BEGIN
  IF Dest.Data <> NIL THEN                    { Overwrite the sprite }
    KillSprite(Dest);

  Dest := Source;                             { Copy the header      }
  GetMem(Dest.Data, Dest.DataLen);            { Allocate some memory }
  ASM
    push DS
    les DI, Dest                              { ES:DI = Dest         }
    lds SI, Source.Data                       { DS:SI = Source.Data^ }
    mov CX, Source.DataLen                    { CX = Sprite length   }
    les DI, ES:[DI+SpriteType.Data]           { ES:DI = Dest.Data^   }

    shr CX, 1
    sbb BX, BX                                { BX = -1 if we need   }
    shr CX, 1                                 { to copy an extra byte}
    db $66; rep movsw                         { rep movsd            }
    adc CX, CX                                { CX = 1 for a word    }
    add CX, CX
    sub CX, BX                                { Add an extra byte    }
    rep movsb                                 { Finish last 0-3 pixs }

    pop DS
  END;
END;
As you can see, it follows four simple steps to get the job done:

  • Kill the old sprite if necessary
  • Copy the sprite header
  • Allocate memory for sprite
  • Copy the sprite data
The only tricky thing in it was the fact that I tried to use rep movsd as much as possible. This speeds up the copying of data between the sprites. This adds seven lines of code to the copy routine, because it has to keep track of the odd number of bytes that can lay at the end of the sprite. Hmmm, time for an example!


Example Program

This example program uses some RLE sprites, all of the routines in this text, and some disk space. It is interesting and serves the purpose. Don't forget to get the new
Sprite Unit! Enjoy!

-----------------] Example Starts here [-----------------
PROGRAM SpriteManipulations;
USES
  CRT,               { For the Keypressed function }
  GraphPro, Sprite;

PROCEDURE GenSprite(NumLines, X : INTEGER; VAR Sprite : SpriteType);
VAR
  I      : INTEGER;
  F      : FILE;
  NumStr : STRING;
BEGIN
  STR(NumLines, NumStr);
  ASSIGN(F, 'Test'+NumStr+'.SPR');
  Reset(F);                                  { Does the file already exist?  }
  IF IOResult <> 0 THEN                      { If no then create the sprite  }
  BEGIN
    FOR I := 0 TO NumLines DO                { Draw all of the lines         }
      Line(Random(64)+X, Random(64), Random(64)+X, Random(64), Random(256));
    GetSprite(Sprite, X, 0, 63+X, 63);
    ConvertSpriteToRLE(Sprite);              { Make it an RLE Sprite         }
    SaveSprite(Sprite, 'Test'+NumStr);       { Save the sprite for next time }
  END
  ELSE                                       { If yes then just load it      }
  BEGIN
    CLOSE(F);
    LoadSprite(Sprite, 'Test'+NumStr);
  END;
END;

VAR
  I, DI : INTEGER;
  CurSprite,
  Sprite20, Sprite200, Sprite2000 : SpriteType;
  Key   : Char;
BEGIN
  WRITELN('This program demonstrates some sprite manipulation techniques.  First, it');
  WRITELN('generates three sprites (or loads them off your hard drive).  Then it starts');
  WRITELN('animating them.  Press ESC to quit.  Press keys 1-3 to switch sprites.');
  WRITELN;
  WRITELN('Press ENTER to continue');
  READLN;

  InitGraph;
  GenSprite(20  ,   0, Sprite20  );   { Generate a sprite with 20 lines in it }
  GenSprite(200 ,  64, Sprite200 );   { This one has 200 lines in it          } 
  GenSprite(2000, 128, Sprite2000);   { This one has 2000 lines in it.        }
  ClrScr(1);

  CopySprite(Sprite20  , CurSprite);
  WHILE Key <> #27 DO
  BEGIN
    I := 0; DI := 1;             { Start at the left side, DeltaI = Right    }
    WHILE NOT KeyPressed DO 
    BEGIN
      DrawSprite(CurSprite, I, 0);
      INC(I, DI);
      IF (I = 0) OR (I = 319-63) THEN DI := -DI;
    END;
    Key := ReadKey;
    CASE Key OF
    '1' : CopySprite(Sprite20  , CurSprite);  { Use a different sprite  }
    '2' : CopySprite(Sprite200 , CurSprite);
    '3' : CopySprite(Sprite2000, CurSprite);
    END;
  END;
  CloseGraph;
  WRITELN('The first time this program runs, it creates a number of *.SPR files in');
  WRITELN('the current directory.  The next time you run it, it uses these files  ');
  WRITELN('instead of redrawing them.                                             ');
END.
-----------------] Example Ends here [-----------------


  • Created by Chris Lattner