Delphi Sprite Engine – Part 5 – Streaming resources.

pacman

[If you’re new to this series, you may wish to skip ahead to part-7, where I’ve done a partial ‘start-over’]

In part 5 of this series, as promised, we’re going to add streaming capabilities to our TStriteSheet class.

Adding streaming to the TSpriteSheet class will enable us to save sprite sheets in files, with their animation and static image data. Or perhaps to save them into a database to be fetched as required by a mobile device. When combined with some kind of streaming for the scene classes, this could be used to package all of the data required to represent a game level, so we’ll be able to load levels rather than hard code them.

Before anyone asks, I did give serious consideration to serializing our sprite sheet to a common format such as XML or JSON. Using these formats have merit, however, doing so increases code complexity a little, and requires more work. For the purposes of this series of blog posts, I decided there was little benefit in targeting these formats. If you’d like to, please consider that an exercise for the reader.

Lets get started.

We’ll need two methods with the following content…

  • SaveToStream()
    • Save an identifying signature to identify this as a sprite sheet.
    • Save the source image.
    • Save the animation data.
    • Save the static image data.
  • LoadFromStream()
    • Load the identifying signature and confirm this is a sprite sheet.
    • Load the source image.
    • Load the animation data.
    • Load the static image data.

So lets start with saving to stream.

const
  cSig = 'MAGIC_SPRITES';

procedure TSpriteSheet.SaveToStream(aStream: TStream);
var
  l: int32;
  idx: int32;
begin
  // Start by writing some kind of signiture.
  // We use this to confirm a valid stream when reading back.
  StringToStream(cSig,aStream);
  // Save the source image to stream.
  BitmapToStream(fSourceImage,aStream);
  // Save the animations list to stream.
  l := AnimCount;
  aStream.Write(l,sizeof(l));
  for idx := 0 to pred(l) do begin
    AnimationToStream(Animation[idx],aStream);
  end;
  // Save static images to stream.
  l := ImageCount;
  aStream.Write(l,sizeof(l));
  for idx := 0 to pred(l) do begin
    ImageToStream(Image[idx],aStream);
  end;
end;

Note the constant in the above code, this can be placed anywhere in the implementation section, so long as it comes before the SaveToStream() and LoadFromStream() methods.

There are four new functions introduced in SaveToStream()…

  1. StringToStream(), The standard TStream class does not have a method for saving strings to a stream, so here we supply our own.
  2. BitmapToStream(), TBitmap does have a method enabling it to be saved to a stream, however, it’s not suitable for our needs so we wrap it with our own method.
  3. AnimationToStream(), Allows us to save our TAnimation class, and subsequently TAnimationFrame classes to stream.
  4. ImageToStream(), This method allows us to save our TStaticImage class to stream.

Each of these methods has a method which does the reverse and loads data from the stream. We’re going to take a closer look at each of the saving methods, and then I’ll simply include the loading methods for you to study. Lets start with StringToStream().

procedure TSpriteSheet.StringToStream( S: string; aStream: TStream );
var
  l: int32;
  idx: int32;
  c: char;
begin
  // write the length of the string in code points (characters)
  l := length(S);
  aStream.Write(l,sizeof(l));
  // loop the code-points to write each one-at-a-time
  for idx := 1 to l do begin
    c := S[idx];
    aStream.Write(c,sizeof(c));
  end;
end;

This method is quite inefficient. The default string type when using modern Delphi compilers is unicode UTF-16 Little Endian. Although UTF-16 has variable length code points, the majority of languages will only ever use 16-bits, and those that require more space always use 32-bits. In order to save the string, we’re saving each 16-bit code-point, or partial code-point, one at a time within a loop. I’ve written it this way because it’s pretty clear what it does, and, I’m lazy, this is the easy way. LoadFromStream() does the reverse.

I’ve done something similar with the BitmapToStream() method.

procedure TSpriteSheet.BitmapToStream( aBitmap: TBitmap; aStream: TStream );
var
  MS: TMemoryStream;
  S: int32;
begin
  // We can't simply use TBitmap.SaveToStream because it does nothing to
  // mashal the size of the data. When loading back, it'll over-run. So we
  // need an intermediate buffer.
  MS := TMemoryStream.Create;
  try
    // Copy bitmap to memory stream
    aBitmap.SaveToStream(MS);
    MS.Position := 0;
    // Write size of memory stream to target stream
    if MS.Size>MaxInt then begin
      raise Exception.Create('TSpriteSheet.BitmapToStream: Image data too large.');
    end;
    S := MS.Size;
    aStream.Write(S,Sizeof(S));
    // Write content of memory stream to target stream
    MS.SaveToStream(aStream);
  finally
    MS.Free;
  end;
end;

The TBitmap class actually has a .SaveToStream() method, as well as a .LoadFromStream(). Unfortunately they don’t behave the way we’d like them to. TBitmap.SaveToStream() will save all of the bitmap data to a stream, but without saving any indication of the size of that data. When we attempt to load the bitmap back in using TBitmap.LoadFromStream() it assumes that the remainder of the stream is the data to load. In our case, we want to save more information to the stream after the bitmap (our animation data), so we wrap the TBitmap.SaveToStream() method in our BitmapToStream() method, which, stores the size of the bitmap data first.

Now we’ll skip the order a little and look at ImageToStream().

procedure TSpriteSheet.ImageToStream( anImage: TStaticImage; aStream: TStream );
begin
  // Write the image name
  StringToStream(anImage.Name, aStream);
  // Write the pixel coordinates
  aStream.Write(anImage.PixCoords,sizeof(TRect));
end;

There’s nothing too special going on here. We’re simply saving the name of the image to the stream, and then, we’re saving the pixel coordinates of the image. When we load this back, we’ll create a new instance of TStaticImage and give these pixel coordinates to the constructor so that the texture coordinates are recalculated.

procedure TSpriteSheet.AnimationToStream( anAnimation: TAnimation; aStream: TStream );
var
  FrameCount: int32;
  idx: int32;
begin
  // Write the animation name to the stream
  StringToStream( anAnimation.Name, aStream );
  // write the frame count to the stream
  FrameCount := anAnimation.Count;
  aStream.Write(FrameCount,Sizeof(FrameCount));
  // write the frames to the stream
  for idx := 0 to pred(FrameCount) do begin
    // we only need to save the pixel coords.
    aStream.Write(anAnimation.Frames[idx].PixCoords, sizeof(TRect));
  end;
end;

AnimationToStream() starts by saving it’s name to the stream, and then it saves the number of frames of animation, followed by the data for each animation frame. Again, we only save the pixel coordinates because we can recalculate the texture coordinates when we load this data back.

Lets see all of the new streaming methods:

procedure TSpriteSheet.StringToStream( S: string; aStream: TStream );
var
  l: int32;
  idx: int32;
  c: char;
begin
  // write the length of the string in code points (characters)
  l := length(S);
  aStream.Write(l,sizeof(l));
  // loop the code-points to write each one-at-a-time
  for idx := 1 to l do begin
    c := S[idx];
    aStream.Write(c,sizeof(c));
  end;
end;

function TSpriteSheet.StringFromStream( aStream: TStream ): string;
var
  l: int32;
  idx: int32;
  c: char;
begin
  Result := '';
  // load string length
  aStream.Read(l,sizeof(l));
  // loop and load code-points
  for idx := 1 to l do begin
    aStream.Read(c,sizeof(c));
    Result := Result + C; //
  end;
end;

procedure TSpriteSheet.BitmapToStream( aBitmap: TBitmap; aStream: TStream );
var
  MS: TMemoryStream;
  S: int32;
begin
  // We can't simply use TBitmap.SaveToStream because it does nothing to
  // mashal the size of the data. When loading back, it'll over-run. So we
  // need an intermediate buffer.
  MS := TMemoryStream.Create;
  try
    // Copy bitmap to memory stream
    aBitmap.SaveToStream(MS);
    MS.Position := 0;
    // Write size of memory stream to target stream
    if MS.Size>MaxInt then begin
      raise Exception.Create('TSpriteSheet.BitmapToStream: Image data too large.');
    end;
    S := MS.Size;
    aStream.Write(S,Sizeof(S));
    // Write content of memory stream to target stream
    MS.SaveToStream(aStream);
  finally
    MS.Free;
  end;
end;

procedure TSpriteSheet.BitmapFromStream( aBitmap: TBitmap; aStream: TStream );
var
  MS: TMemoryStream;
  S: int32;
  idx: int32;
  b: byte;
begin
  // Get size of image data.
  aStream.Read(S,Sizeof(S));
  // create buffer for the bitmap data.
  MS := TMemoryStream.Create;
  try
    // Read in the data from the stream one byte at a time!
    // could optimize this to read in chuncks...
    for idx := 1 to S do begin
      aStream.Read(b,sizeof(b));
      MS.Write(b,sizeof(b));
    end;
    // Load the data into the bitmap
    MS.Position := 0;
    aBitmap.LoadFromStream(MS);
  finally
    MS.Free;
  end;
end;

procedure TSpriteSheet.AnimationToStream( anAnimation: TAnimation; aStream: TStream );
var
  FrameCount: int32;
  idx: int32;
begin
  // Write the animation name to the stream
  StringToStream( anAnimation.Name, aStream );
  // write the frame count to the stream
  FrameCount := anAnimation.Count;
  aStream.Write(FrameCount,Sizeof(FrameCount));
  // write the frames to the stream
  for idx := 0 to pred(FrameCount) do begin
    // we only need to save the pixel coords.
    aStream.Write(anAnimation.Frames[idx].PixCoords, sizeof(TRect));
  end;
end;

function TSpriteSheet.AnimationFromStream( aStream: TStream ): TAnimation;
var
  AnimNameStr: string;
  aCount: int32;
  idx: int32;
  aRect: TRect;
begin
  // get the animation name first
  AnimNameStr := StringFromStream( aStream );
  Result := TAnimation.Create(Self,AnimNameStr);
  // Load the frames
  aStream.Read(aCount,Sizeof(aCount));
  for idx := 0 to pred(aCount) do begin
    // read pixel coords
    aStream.Read(aRect,Sizeof(aRect));
    Result.AddFrame(aRect.Left,aRect.Top,aRect.Width,aRect.Height);
  end;
end;

procedure TSpriteSheet.ImageToStream( anImage: TStaticImage; aStream: TStream );
begin
  // Write the image name
  StringToStream(anImage.Name, aStream);
  // Write the pixel coordinates
  aStream.Write(anImage.PixCoords,sizeof(TRect));
end;

function TSpriteSheet.ImageFromStream( aStream: TStream ): TStaticImage;
var
  ImageNameStr: string;
  ImageRect: TRect;
begin
  // Read the image name
  ImageNameStr := StringFromStream( aStream );
  // Read the image pixel coords
  aStream.Read(ImageRect,Sizeof(ImageRect));
  // Return
  Result := TStaticImage.Create(ImageNameStr,fSourceImage.Width,fSourceImage.Height,ImageRect.Left,ImageRect.Top,ImageRect.Width,ImageRect.Height);
end;

procedure TSpriteSheet.LoadFromStream(aStream: TStream);
var
  SigStr: string;
  aCount: int32;
  idx: int32;
  RefAnim: TAnimation;
  RefImage: TStaticImage;
begin
  // We could do without having any previous data in the sprite sheet...
  Self.Clear;
  // check that this is a sprite sheet stream.
  SigStr := StringFromStream(aStream);
  if SigStr<>cSig then begin
    raise Exception.Create('TSpriteSheet.LoadFromStream:: Signiture not found in stream.');
  end;
  // load the bitmap image
  BitmapFromStream(fSourceImage,aStream);
  // load the animations from stream
  aStream.Read(aCount,sizeof(aCount));
  for idx := 0 to pred(aCount) do begin
    RefAnim := AnimationFromStream( aStream );
    fAnimations.Add(RefAnim);
  end;
  // Load the images from stream
  aStream.Read(aCount,Sizeof(aCount));
  for idx := 0 to pred(aCount) do begin
     // read the image name
     RefImage := ImageFromStream( aStream );
     fImages.Add(RefImage)
  end;
end;

procedure TSpriteSheet.SaveToStream(aStream: TStream);
var
  l: int32;
  idx: int32;
begin
  // Start by writing some kind of signiture.
  // We use this to confirm a valid stream when reading back.
  StringToStream(cSig,aStream);
  // Save the source image to stream.
  BitmapToStream(fSourceImage,aStream);
  // Save the animations list to stream.
  l := AnimCount;
  aStream.Write(l,sizeof(l));
  for idx := 0 to pred(l) do begin
    AnimationToStream(Animation[idx],aStream);
  end;
  // Save static images to stream.
  l := ImageCount;
  aStream.Write(l,sizeof(l));
  for idx := 0 to pred(l) do begin
    ImageToStream(Image[idx],aStream);
  end;
end;

Okay, we can now save our TSpriteSheet class to a stream and load it back. So if we create a sprite sheet in code as we have done, we can save it to a file, and then load it back into the engine from file rather than from the hard-coding.

Why is this important? Well, this gives us the ability to supply a sprite based application with limited artwork, and to then supply additional artwork later without having to release an updated application binary. If you extend this out to games, with a few additional streaming functions we could save entire levels into streams (or files via file streams), and deliver those levels as updates to the game, without having to actually deliver an updated game application.  This was not one of our original goals, but it’ll become useful later, consider it an added extra for now.

Restoring cross platform

Back in part-1 I promised that this sprite engine would be cross platform, but it isn’t! I tested a proof of concept, and have since merely asserted that the sprite engine is cross platform, when this is actually not true. A few issues have crept in which have broken the code for cross platform deployment.

1) My use of TList for storing lists of classes has lead to the code being incompatible with ARC (automatic reference counting) which is how all of the mobile compilers for Delphi work.

2) The sprite isn’t actually rendering on mobile devices.

The solution to the fist of these is to swap TList for a generic TList<> everywhere.
The second problem is also due to ARC. The FMX framework TMaterial class has a weak reference to the texture which we’re assigning…

So I’ve gone ahead and fixed that problems in the code which you can download here – : (Replaced, please skip ahead and take the sources from part 5.5: Delphi Sprite Engine part 5.5 )

Thanks for reading!

Print Friendly, PDF & Email
Facebooktwittergoogle_plusredditpinterestlinkedintumblrmail

Leave a Reply