Download as pdf or txt
Download as pdf or txt
You are on page 1of 10

Delphi Sprite Engine – Part 5 – Streaming resources.

chapmanworld.com/2015/03/09/delphi-sprite-engine-part-5-streaming-resources/

Craig Chapman March 9,


2015

[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.

1/10
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().

2/10
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.

3/10
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

4/10
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
5/10
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));
6/10
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));
7/10
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

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
8/10
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.

9/10
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!

10/10

You might also like